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.

Create an API key

Available on every plan, including free workspaces.

How it works

API access, step by step.

  1. 1

    Create a key

    Developer API in the dashboard sidebar. Shown once — store it somewhere safe.

  2. 2

    Send it as a header

    x-api-key on every request. The key already binds the workspace.

  3. 3

    Read the reference

    The API reference renders live from /api/v1/openapi.json.

  4. 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

Create an API key

Available on every plan, including free workspaces.