API & MCP reference

Build on Postclay

Everything the dashboard does, the API does. The technical reference below is maintained in English — code is code in every locale.

Full API & MCP access is included on every plan — no separate add-on.

Authentication

Every request is authenticated with an API key in the standard Authorization header. A key belongs to exactly one workspace, and every call is scoped to that workspace.

Authorization: Bearer pm_live_<your-key>

Base URL: https://postclay.com/api. Missing key → 401 MISSING_API_KEY; unknown or revoked → 401 INVALID_API_KEY.

Create an API key

Create a key from the dashboard, or via the API. The secret is shown once — store it now.

POST https://postclay.com/api/api-keys
Authorization: Bearer <session>
Content-Type: application/json

{ "name": "My integration" }

# 201 Created — the raw key appears only in this response
{ "key": "pm_live_ab12cd34…" }

GET /api-keys lists keys (never the secret): each has id, name, prefix, lastUsedAt, revokedAt, createdAt, createdByName (name, or failing that email, of whoever minted the key; null for keys minted before this field existed, or by an API-key caller). DELETE /api-keys/:id revokes a key and returns 204.

List accounts

Returns the social accounts connected to your workspace — this is how you obtain a real socialAccountId for create's targets below, in place of a placeholder string.

GET https://postclay.com/api/accounts

200 OK
[
  { "id": "account-uuid", "platform": "X", "handle": "@yourbrand",
    "status": "ACTIVE", "isMock": false }
]

Disconnected accounts are excluded by default; pass ?includeDisconnected=true to include them.

Upload media

Multipart upload into the workspace media library. Returns a MediaAsset whose id can be passed to create_post's mediaIds.

POST https://postclay.com/api/media
Content-Type: multipart/form-data

[email protected]

# 201 Created — { "id": "media-uuid", "kind": "IMAGE", ... }

REST accepts images and video up to 150MB. MCP's upload_media tool is a separate, lower ceiling — 20MB decoded — because MCP has no multipart channel and carries bytes as base64 inside the JSON-RPC request body; an agent uploading a larger file should have a human (or a REST-capable step) use this endpoint instead.

Create a post

Creates a post in DRAFT. targets is required — one entry per connected account you want to publish to (see List accounts for a real socialAccountId). A scheduledAt passed here is ignored and discarded, not stored — every post is created with no schedule; call schedule afterwards to actually set a time.

POST https://postclay.com/api/posts

{
  "content": "Hello from the Postclay API",
  "kind": "SINGLE",                       // SINGLE | CAROUSEL | THREAD
  "mediaIds": ["media-uuid"],             // optional, ordered, ≤ 50
  "labels": ["launch"],                   // optional
  "targets": [
    { "socialAccountId": "account-uuid",  // from GET /accounts, see above
      "contentOverride": null,            // null = inherit "content"
      "mediaIds": null }                  // null = inherit; [] = no media
  ]
}

Response — the created post (truncated):

201 Created
{
  "id": "post-uuid",
  "status": "DRAFT",
  "kind": "SINGLE",
  "content": "Hello from the Postclay API",
  "mediaIds": ["media-uuid"],
  "targets": [
    { "id": "target-uuid", "socialAccountId": "account-uuid",
      "platform": "X", "status": "PENDING", "attempts": 0,
      "externalPostId": null, "permalink": null }
  ],
  "createdAt": "2026-08-20T09:00:00.000Z"
}

curl

curl https://postclay.com/api/posts \
  -H "Authorization: Bearer pm_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hello from the Postclay API",
    "kind": "SINGLE",
    "targets": [{ "socialAccountId": "account-uuid" }]
  }'

Schedule a post

Moves a draft to SCHEDULED and enqueues each target. Both fields are required. scheduledAt is absolute when it carries a Z or offset, otherwise it is wall-clock time in timezone.

POST https://postclay.com/api/posts/:id/schedule

{ "scheduledAt": "2026-08-20T18:30:00Z", "timezone": "Asia/Riyadh" }

# 200 OK — the full post, status now "SCHEDULED"

List posts

Returns a paginated envelope. Filter by status (repeatable — pass a comma-separated list to match any of several statuses in one request), free-text search, label, or date range.

GET https://postclay.com/api/posts?status=scheduled,failed&q=launch&page=1&pageSize=20

# status: scheduled | published | draft | pending | failed
#         | publishing | partially_failed | cancelled
#         (comma-separated = match any, up to 8)
# q:       free text across content, first comment, thread segments,
#          and per-target content overrides
# label:   string   from,to: ISO-8601   pageSize: 1–100 (default 20)

{ "data": [ /* posts */ ], "total": 42, "page": 1, "pageSize": 20 }

Delete a post

Cancels any pending targets and deletes the post.

DELETE https://postclay.com/api/posts/:id

# 204 No Content

Idempotency

Postclay does not accept a client Idempotency-Key header today — treat POST /posts as create-once and store the returned id. Retries are safe downstream: each target carries an internal, server-generated idempotency value so the publish workers never double-post to a platform on retry. (A client-facing idempotency key is on the roadmap; this note will change when it ships.)

Webhooks

Subscribe a URL to publish events. Two events fire: POST_PUBLISHED and POST_FAILED.

POST https://postclay.com/api/webhooks
{ "url": "https://you.example/hook",
  "events": ["POST_PUBLISHED", "POST_FAILED"] }

# 201 — response includes "secret" once (used to verify signatures)

Delivered payloads:

// POST_PUBLISHED
{ "event": "POST_PUBLISHED", "occurredAt": "2026-08-20T12:00:00.000Z",
  "data": { "postId": "…", "targetId": "…", "platform": "X",
            "externalId": "…", "permalink": "https://…" } }

// POST_FAILED
{ "event": "POST_FAILED", "occurredAt": "2026-08-20T12:00:00.000Z",
  "data": { "postId": "…", "targetId": "…", "platform": "X",
            "error": "…" } }

Every delivery is signed. Verify the HMAC before trusting a payload:

Content-Type: application/json
X-Postclay-Signature: sha256=<hex hmac of the raw body, keyed by your secret>
X-Postclay-Event: POST_PUBLISHED
X-Postclay-Delivery: <delivery-uuid>

# verify (Node):
const mac = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const ok  = "sha256=" + mac === req.headers["x-postclay-signature"];

Deliveries retry up to 5 times with exponential backoff (10s per-request timeout); internal/loopback URLs are refused. POST /webhooks/:id/test sends a test delivery (202); POST /webhooks/:id/rotate-secret issues a new secret.

Rate limits & quotas

There are no per-plan API rate limits. Two real limits apply:

Per-IP limits guard unauthenticated and AI endpoints (e.g. signup, login, caption/hashtag generation). Two authenticated routes carry their own per-workspace budgets instead: POST /webhooks (30/hour), POST /webhooks/:id/test (10/hour), and POST /mcp (120/hour, covering every MCP tool call over that connection). The authenticated /posts, /media and /api-keys endpoints remain unthrottled. On exhaustion you get:

429 { "error": { "code": "RATE_LIMITED", "messageKey": "errors.rateLimited" } }

Per-plan quotas cap connected accounts, team seats, and media storage (posts are unlimited on every tier):

PlanAccountsSeatsWorkspacesStoragePostsAI captionsAI imagesAI videos
Trial251011GBUnlimited50010010
Builder8315GBUnlimited100203
Team2510120GBUnlimited50010010
ScaleUnlimitedUnlimited150GBUnlimitedUnlimited50050

Storage is the one quota that is never unlimited, even on Scale — disk is a real, finite host cost.

A “connected account” is one social identity linked to your workspace (a specific X handle, Instagram business account, Facebook page, and so on). Reconnecting the same identity reuses its slot.

MCP — connect your agent

Postclay is an MCP server, so any MCP client can create, schedule, list and cancel posts as a first-class user. The endpoint is stateless Streamable-HTTP and takes the same API key:

POST https://postclay.com/api/mcp
Authorization: Bearer pm_live_<your-key>

Claude Code

Add Postclay to your MCP config:

{
  "mcpServers": {
    "postclay": {
      "url": "https://postclay.com/api/mcp",
      "headers": { "Authorization": "Bearer pm_live_<your-key>" }
    }
  }
}

Cursor

Add the same server block to Cursor’s MCP settings (~/.cursor/mcp.json or Settings → MCP). The url and Authorization header are identical to the snippet above.

Tools

The server exposes twelve tools. Platforms never run in a mock-publish mode — an unconfigured platform is simply not connectable; check configured from list_platforms before targeting one.

list_platforms()
list_accounts()

create_post({ content, kind: "SINGLE"|"CAROUSEL"|"THREAD", segments?,
              targets: [{ socialAccountId, contentOverride?, mediaIds? }],
              mediaIds?, timezone?, recurrence? })
              // recurrence: { freq, interval, byDay?, timeOfDay, until?, count? }

schedule_post({ postId, scheduledAt, timezone })

list_posts({ status?, q?, from?, to?, page?, pageSize? })
  // status: scheduled | published | draft | pending | failed
  //         | publishing | partially_failed | cancelled

get_post({ postId })

upload_media({ filename, mimeType, dataBase64 })
  // 20MB decoded max — larger files go through REST POST /media instead

cancel_post({ postId })

search_media({ query?, kind?: "IMAGE"|"VIDEO", page? })

get_next_slots({ count? })
  // the queue's own next free slots — use instead of inventing scheduledAt

update_post({ postId, content?, kind?, segments?, labels?,
              firstComment?, mediaIds? })

get_analytics({ postId? })
  // workspace overview, or one post's per-target metrics with postId

A ready-made skill file is published at postclay.com/skill.md — point OpenClaw / Hermes-style agents at it and they pick up these tools natively.

Questions? [email protected] · Back to home