HTTP API
One contract, generated from the same source the app uses. Not a second implementation that drifts.
Every operation is declared once and served at /api/v1, with an OpenAPI document generated from those declarations. Authenticate with a workspace-scoped API key, get the resource itself back rather than a wrapper, and narrow on error codes that are part of the contract instead of parsing messages.
Available on every plan, including free workspaces.
How it works
API access, step by step.
- 1
Create a key
Developer API in the dashboard sidebar. Shown once — store it somewhere safe.
- 2
Send it as a header
x-api-key on every request. The key already binds the workspace.
- 3
Read the reference
The API reference renders live from /api/v1/openapi.json.
- 4
Ship it
Same limits as the dashboard, so nothing surprises you in production.
Capabilities
What you get with API access.
- No response envelope
- A handler returns the resource, or { items, nextCursor } where a list is already that shape. The HTTP status carries success, so you do not unwrap twice on every call.
- Errors are part of the contract
- Each operation declares its error codes, so a code reaches the OpenAPI document and your generated client from one declaration. You narrow on a typed code rather than string-matching a message.
- Keys are scoped to one workspace
- A key is bound to the workspace that created it and cannot reach another. Removing someone from a workspace revokes their keys for it, and revocation takes effect immediately.
- Versioned in the mount, not the paths
- The version lives only in the /api/v1 prefix. A future v2 is a second handler over a second contract version rather than an edit to every path you have hardcoded.
The shape of it
Resources are at /{resource} and /{resource}/{id}. GET reads, POST creates, PATCH partially updates, PUT replaces, DELETE deletes. An action that is not CRUD gets a sub-resource verb:
GET /api/v1/posts
POST /api/v1/posts
GET /api/v1/posts/{id}
PATCH /api/v1/posts/{id}
POST /api/v1/posts/{id}/duplicate
The active workspace comes from your key, never from the path. That is a deliberate choice: a key already binds one workspace, so putting the id in the URL would let a caller ask for a workspace their key cannot reach and get a confusing answer instead of a clean one.
Pagination
Lists return the items and a cursor:
{
"items": [{ "id": "pst_8f21c4", "status": "SCHEDULED" }],
"nextCursor": "eyJpZCI6..."
}
Pass nextCursor back to continue. When it comes back null, you are at the end.
Errors
Every operation declares the codes it can return, with a status attached. So a 404 is a 404, and the body carries a code you can switch on:
{ "code": "NOT_FOUND", "message": "Post not found" }
A row that belongs to a different workspace returns NOT_FOUND rather than FORBIDDEN. That is on purpose — FORBIDDEN would confirm the row exists, which is an existence oracle for anyone probing ids.
Aggregate fields come back as numbers, not strings, even though Postgres returns them as text on the wire. The output schema coerces them, so you do not have to guess which numeric fields need parsing.
Limits and quotas
The API is not a bypass. Scheduling a post spends the same monthly per-platform quota as the composer, and uploading media spends the same storage allowance, because those checks live inside the services rather than being re-implemented per surface. When you hit one, the error names the limit and your plan.
X is metered separately and more tightly than everything else, because X charges roughly a dollar per hundred posts through their API. That cap is a plan fact, not an API fact — it applies identically in the dashboard.
Keeping keys safe
- Revoke from the same settings page; revocation is immediate.
- Keys cannot create other keys.
- Removing a member from a workspace revokes their keys for it.
- Set an expiry at creation time for a one-off job.
If you would rather not write HTTP at all, the CLI reads POSTONUS_API_KEY and covers the same ground, and the MCP server accepts the same key.
Found this useful? Pass it on.
Questions
Frequently asked questions Everything the first evaluation usually needs.
- Where is the API reference?
The dashboard serves a live reference rendered from /api/v1/openapi.json. Because the document is generated from the same declarations the server routes on, it cannot describe an endpoint that does not exist.
- How do I authenticate?
Create an API key under Developer API in the dashboard and send it as an x-api-key header. The key identifies both the user and the workspace, so there is no separate workspace parameter to pass.
- Can one key reach several workspaces?
No, by design. A key is bound to the workspace that created it. Agencies should create one key per client workspace, which also means revoking one does not interrupt the others.
- What are the rate limits?
Requests per minute, set by your plan. Programmatic traffic (API key, CLI, MCP) is counted in a separate bucket from browser sessions, so a script in a loop cannot rate-limit the dashboard of the same workspace.
- Is the API on paid plans only?
It is available on every plan. Plans differ on volume, not on which surfaces you can reach. Creating a live schedule still needs a subscription, so a free workspace can read and write drafts but not publish.
- Does the API key work with the MCP server too?
Yes. The MCP endpoint accepts the same key as an x-api-key header, which suits a server-to-server caller that would rather speak MCP than REST.
Keep reading
- MCP serverConnect Claude, Cursor, or ChatGPT to your social media calendar over MCP. Five tools to read your queue, check posting windows, and stage scheduled posts.Read
- Team collaborationInvite teammates with owner, admin, or member roles, leave threaded comments on a draft, and route posts through approve, reject, or request-changes first.Read