Quickstart
Create an API key, check it, find a connected network, save a draft and schedule it, in under five minutes.
You need a Postel workspace with at least one network connected. Scheduling needs a paid plan; saving drafts works on every plan.
The JavaScript samples use fetch and need Node 18 or later. Save them as .mjs files so top-level await works. The Python samples use requests.
Create an API key
In Postel, open Settings → API & MCP and click Create API key. Give it a name you will recognise later, like "Zapier" or "CRM sync". Postel shows the key once, so copy it now. Only workspace owners and admins see this page.
Keys start with pstl_. Store yours in an environment variable:
export POSTEL_API_KEY="pstl_..."Check the key
GET /v1/me returns the person and workspace the key acts for.
curl https://www.postel.app/api/v1/me \
-H "Authorization: Bearer $POSTEL_API_KEY"{
"user": {
"id": "rL9xQ2mT7vK4pN8sW3cZ6hB1yF5dJ0aE",
"name": "Maya Chen",
"email": "maya@northwind.studio"
},
"workspace": {
"id": "o7Hk2LmQ9rT4vX1nB6sY3pC8wZ5dF0gJ",
"name": "Northwind",
"slug": "northwind",
"timezone": "Europe/Berlin"
},
"canPublish": true,
"label": "Northwind (maya@northwind.studio)"
}If canPublish is false, you can still save drafts in step 4, but step 5 answers 402 plan_required.
Find a connected network
GET /v1/channels lists the networks Postel posts to, connected ones first.
curl https://www.postel.app/api/v1/channels \
-H "Authorization: Bearer $POSTEL_API_KEY"{
"channels": [
{
"id": "x",
"name": "X",
"account": "@mayabuilds",
"connected": true,
"status": "ok",
"label": "X (@mayabuilds)"
},
{
"id": "linkedin",
"name": "LinkedIn",
"account": "Maya Chen",
"connected": true,
"status": "ok",
"label": "LinkedIn (Maya Chen)"
},
{
"id": "bluesky",
"name": "Bluesky",
"account": null,
"connected": false,
"status": "disconnected",
"label": "Bluesky (not connected)"
},
{
"id": "mastodon",
"name": "Mastodon",
"account": null,
"connected": false,
"status": "disconnected",
"label": "Mastodon (not connected)"
}
]
}A channel's id is the value you send as platform. This guide uses linkedin; swap in any channel that shows "connected": true.
Create a draft
POST /v1/posts with a platform and text saves a draft.
curl https://www.postel.app/api/v1/posts \
-H "Authorization: Bearer $POSTEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "linkedin",
"text": "We shipped our public API today. Three things we learned writing the docs first."
}'{
"id": "cmhjz4w2p0001l708xq3kd9vb",
"platform": "linkedin",
"status": "draft",
"text": "We shipped our public API today. Three things we learned writing the docs first.",
"thread": [
"We shipped our public API today. Three things we learned writing the docs first."
],
"firstComment": null,
"mediaCount": 0,
"scheduledAt": null,
"publishedAt": null,
"url": null,
"failure": null,
"createdAt": "2026-11-04T13:05:12.481Z",
"updatedAt": "2026-11-04T13:05:12.481Z"
}The draft now shows in Postel too. Keep its id for the next step:
export POST_ID="cmhjz4w2p0001l708xq3kd9vb"Schedule it
POST /v1/posts/{id}/schedule schedules a draft. scheduledAt is an ISO 8601 time with a timezone, and it must be in the future.
# Pick a time in the future
curl https://www.postel.app/api/v1/posts/$POST_ID/schedule \
-H "Authorization: Bearer $POSTEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "scheduledAt": "2026-12-01T09:30:00+01:00" }'{
"id": "cmhjz4w2p0001l708xq3kd9vb",
"platform": "linkedin",
"status": "scheduled",
"text": "We shipped our public API today. Three things we learned writing the docs first.",
"thread": [
"We shipped our public API today. Three things we learned writing the docs first."
],
"firstComment": null,
"mediaCount": 0,
"scheduledAt": "2026-12-01T08:30:00.000Z",
"publishedAt": null,
"url": null,
"failure": null,
"createdAt": "2026-11-04T13:05:12.481Z",
"updatedAt": "2026-11-04T13:06:40.118Z"
}Postel stores times in UTC, so 09:30+01:00 comes back as 08:30Z. If your workspace needs approval for posts, the status is pending_approval and a message says so. See Approvals and plans.