Documentation

Five minutes to a published post, then the concepts underneath it. Full endpoint detail lives in the API reference.

Quickstart

From nothing to a published post. Every step is a real call.

1. Get a key

Create a workspace, then Settings → API keys → Reveal. The key is wf_live_… and it is scoped to that one workspace.

export POSTWHARF_KEY=wf_live_...

2. Connect a channel

Bluesky needs no review — an app password from your own account settings is the whole credential.

curl -X POST https://api.postwharf.com/v1/channels \
  -H "Authorization: Bearer $POSTWHARF_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "bluesky",
    "handle": "you.bsky.social",
    "credentials": { "identifier": "you.bsky.social", "appPassword": "xxxx-xxxx-xxxx-xxxx" }
  }'

The credential is sealed with AES-256-GCM before it is stored and is never returned by any endpoint.

3. Publish

curl -X POST https://api.postwharf.com/v1/posts \
  -H "Authorization: Bearer $POSTWHARF_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Shipping today.", "channels": ["ch_..."] }'

Omit publish_at and it goes now. Include an ISO-8601 instant and it queues.

4. Check it landed

curl https://api.postwharf.com/v1/posts/post_xxx \
  -H "Authorization: Bearer $POSTWHARF_KEY"

The response carries one delivery per channel, each with a status, the live URL once published, and the error text if it failed.

Four ways in, one queue

The dashboard, the REST API, the CLI and the MCP server are four front doors onto the same queue, with the same retry policy and the same receipts. Nothing is exclusive to one of them.

Workspaces

A workspace is one brand. It owns channels, media, posts and webhooks, and it holds the API key. Isolation is enforced at the query, not in the route handler: a key for one workspace cannot read or publish to another, and a post cannot reference media it does not own.

Pricing is per workspace and channels inside it are unmetered, so adding a brand's sixth network costs nothing.

Channels

A channel is one connected account. Two kinds exist and the difference is worth understanding before you plan a rollout:

  • Self-serve: Bluesky and Telegram. You create the credential yourself, there is no review, and the channel publishes immediately.
  • Managed: Instagram, TikTok, X, LinkedIn, YouTube, Facebook, Threads and Pinterest. PostWharf holds the platform relationship, so there is no review for you to file and no audit to wait out — add the account and it publishes.
  • Request-only: Reddit, Discord, Snapchat and WordPress. They stay visible, but create no channel and cannot be selected for publishing. One request per workspace records demand.

Ten of the fourteen networks publish today. What each approval actually takes covers the queue per platform.

Post lifecycle

One post fans out to one delivery per channel, and the two have separate statuses. That is the whole design: a post to six accounts where one fails is partial, not “published”.

Post statusMeans
scheduledWaiting for its slot. Cancellable.
publishingDeliveries are being attempted. Cancellable.
publishedEvery delivery live.
partialSome live, some failed. Terminal.
failedNo delivery succeeded. Terminal.
cancelledPulled before publishing. Terminal.

Workers claim delivery rows with FOR UPDATE SKIP LOCKED, so several workers can run concurrently and the same delivery can never publish twice.

Retries and receipts

Transport failures retry with backoff. Rejections do not — a 401 will fail identically on the third attempt, so retrying it only delays the moment you find out.

Every attempt emits an event you can read at /v1/events or receive on a webhook. A failure carries an action field saying what to do about it, and channel.token_expiring fires before a channel goes dark.

channel.disconnected fires when you remove a channel. Disconnecting does not delete anything: the channel's delivery history stays readable, and any post still queued to it is settled as cancelled rather than quietly dropped. The payload carries cancelled_deliveries and the posts that lost a leg of their fan-out, which are the ones worth queueing somewhere else.

Published deliveries are re-checked afterwards. A post removed by moderation an hour later stops reporting as live, which is the difference between a receipt and a log line.

Plan limits

Posts are counted per workspace per calendar month, and one post counts once no matter how many channels it reaches. Media is the live sum of stored bytes, so deleting a file frees the space.

Over the limit you get a 402 naming the limit, the plan, what was used and what is allowed — not a silent drop and not an overage charge. Read your current position any time at /v1/usage.