API reference
Base URL https://postpipe.dev. Everything is JSON over HTTPS and scoped to one workspace. The dashboard uses these same endpoints — there is no private API behind it.
Authentication
Two interchangeable mechanisms.
API key, for programs: Authorization: Bearer pp_live_…. Create one in the dashboard or via POST /v1/api-keys. Scopes are read (GETs), write (posts and media, includes read) and admin (keys, webhooks, connections; includes write). Limited to 240 requests per minute per key.
Session cookie, for the dashboard: POST /auth/signup, POST /auth/login, POST /auth/logout, GET /auth/me. Sessions carry admin scope.
Only a SHA-256 digest of an API key is ever stored, so a leaked database does not yield working keys. The secret is shown once, at creation.
Errors
{ "error": { "code": "validation_failed", "message": "…", "details": { } } }
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request |
| 401 | unauthorized | Missing or invalid credentials |
| 403 | forbidden | Authenticated, but the scope doesn't allow it |
| 404 | not_found | Unknown id — also returned for ids in another workspace, so existence never leaks |
| 409 | conflict | State conflict, e.g. deleting a post mid-publish |
| 422 | validation_failed | Well-formed but invalid; details says why |
| 429 | rate_limited | Slow down |
Idempotency
Send Idempotency-Key on POST /v1/posts (header, or idempotency_key in the body). A retransmission returns the original post rather than publishing twice. Reusing a key with a different body is rejected with 422 — that combination almost always means a bug rather than a retry.
Platforms
GET /v1/platforms → [{platform, display_name, configured}]
GET /v1/platforms/:platform/capabilities → text limits, media counts, types, sizes, features
GET /v1/capabilities → every capability record at once
Capability records exist so you don't hardcode platform rules. Character limits, accepted MIME types, maximum file sizes, video duration bounds and carousel counts are all reported here and change when the platform changes.
Connections and social accounts
POST /v1/connections/:platform {redirect_to?} → 201 {authorization_url, platform}
GET /v1/connections → tokens are never included
POST /v1/connections/:id/refresh
DELETE /v1/connections/:id → revokes; tokens deleted immediately
GET /v1/social-accounts
GET /v1/social-accounts/:id
Request an authorization_url, send the person through it, and the platform redirects back to Postpipe. Accounts then appear under /v1/social-accounts. One authorization can expose several publishable accounts.
Media
POST /v1/media/uploads {filename, mime_type, size_bytes}
→ 201 {media, upload: {method, url, headers, expires_in}, complete_url}
PUT <upload.url> raw bytes
POST /v1/media/uploads/:id/complete → validates, extracts dimensions/duration
GET /v1/media GET /v1/media/:id DELETE /v1/media/:id
Accepted: image/jpeg, image/png, image/webp, image/gif, video/mp4, video/quicktime, video/webm. On completion the file is checked by its actual magic bytes rather than its extension, and width, height and duration are read from the file itself. Per-platform limits are enforced later, per destination.
Posts
POST /v1/posts
GET /v1/posts?status=&limit=&cursor=
GET /v1/posts/:id?include=resolved
PATCH /v1/posts/:id edit, or {action:"publish"|"cancel"}
DELETE /v1/posts/:id
GET /v1/posts/:id/destinations
GET /v1/posts/:id/destinations/:destinationId full attempt history
POST /v1/posts/:id/retry {destination_ids?}
{
"text": "New spot in Lisbon",
"link_url": "https://example.com",
"media": ["media_…"],
"destinations": ["acct_…", "acct_…"],
"scheduled_at": "2026-09-01T09:00:00Z",
"timezone": "Europe/Lisbon",
"overrides": { "bluesky": { "text": "…" } },
"account_overrides": { "acct_…": { "text": "…" } },
"idempotency_key": "launch-day-1"
}
Override semantics
Resolved field by field, account beating platform beating the default. An omitted field inherits; "text": "" means empty text; "media": [] means no media; "link_url": "" removes the link.
Status
A post's status is a rollup — draft, scheduled, in_progress, published, partial, failed, canceled — derived from its destinations, which are the source of truth. Each destination moves through queued, validating, uploading, processing, publishing, published, or retrying / failed / canceled.
Failure codes
Every platform failure is normalized to one vocabulary, with the platform's own response preserved alongside it.
| Classification | Codes | What happens |
|---|---|---|
| retryable | RATE_LIMITED PLATFORM_UNAVAILABLE NETWORK_ERROR INTERNAL | Retried automatically with backoff, up to 5 attempts |
| non_retryable | INVALID_MEDIA MEDIA_TOO_LARGE UNSUPPORTED_MEDIA INVALID_CONTENT PLATFORM_REJECTED | Stops; the content has to change |
| requires_user_action | AUTH_EXPIRED AUTH_REVOKED INSUFFICIENT_SCOPE AMBIGUOUS_RESULT | Waits for you. AMBIGUOUS_RESULT means the platform may already have published — check before retrying |
API keys
POST /v1/api-keys {name, scopes, expires_at?} → 201 {api_key, secret}
GET /v1/api-keys → prefix and last_used_at only
DELETE /v1/api-keys/:id → revoke
POST /v1/api-keys/:id/rotate → new secret; the old key dies immediately
secret is returned exactly once, at creation or rotation. There is no way to recover it afterwards — rotate instead.
Webhooks
POST /v1/webhooks {url, events, description?} → 201 {webhook, secret}
GET /v1/webhooks PATCH /v1/webhooks/:id DELETE /v1/webhooks/:id
GET /v1/webhooks/:id/deliveries
POST /v1/webhooks/:id/test
Events: post.created, post.scheduled, post.completed, destination.queued, destination.processing, destination.published, destination.failed.
Each delivery carries Postpipe-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256(secret, "<t>.<raw body>"). Verify it, reject anything more than 300 seconds old, and deduplicate on the payload's id — it stays identical across retries and across endpoints. Respond 2xx within 15 seconds; failures retry with backoff up to 8 attempts.
{
"id": "evt_…",
"type": "destination.published",
"created_at": "…",
"workspace_id": "ws_…",
"data": {
"destination_id": "dest_…", "post_id": "post_…",
"platform": "bluesky", "status": "published",
"remote_post_id": "…", "remote_url": "…"
}
}