Check the result

Understand post statuses and find out if a post was published.

Publishing happens in the background. When you create a post, Bulkit answers right away, before the social networks have received anything. The real result comes later — minutes later, or days later for a scheduled post.

You can get the result in two ways:

  1. Webhooks (recommended) — Bulkit calls your server when something happens. See Webhooks.
  2. Polling — you call GET /posts/{id} from time to time.

Get one post

curl "$BULKIT_URL/posts/POST_ID" -H "x-api-key: $BULKIT_API_KEY"
Response (shortened)
{
  "id": "clx3k2p9q0001abcd1234efgh",
  "status": "partially_published",
  "scheduledAt": "2026-10-15T09:00:00.000Z",
  "publishedAt": null,
  "channels": [
    {
      "channelId": "chn_x",
      "platform": "x",
      "status": "published",
      "error": null,
      "remoteUrl": "https://x.com/acme/status/123",
      "blocks": [{ "content": "Shipping today.", "remoteUrl": "https://x.com/acme/status/123" }]
    },
    {
      "channelId": "chn_li",
      "platform": "linkedin",
      "status": "failed",
      "error": "LinkedIn refused the post: the access token has expired.",
      "remoteUrl": null,
      "blocks": [{ "content": "A longer version, for LinkedIn.", "remoteUrl": null }]
    }
  ]
}

The top status is a summary. The real result is in each channel. Look at channels[].status and channels[].error.

What each status means

StatusMeaning
draftSaved. Nothing will be published until you publish it.
scheduledWaiting for scheduledAt.
publishingBeing sent to the platform right now.
publishedLive. remoteUrl has the link.
partially_published(top level only) Some channels worked, some failed.
failedDid not go out. error says why.
  • The top-level publishedAt is only set when every channel is published. So if it is not null, the whole post is done.
  • In a thread, each block has its own remoteUrl.

Fix a failed post

  1. Read channels[].error to see what went wrong.
  2. Fix the cause (for example, reconnect the account in the app).
  3. Call POST /posts/{id}/publish.
curl -X POST "$BULKIT_URL/posts/POST_ID/publish" \
  -H "x-api-key: $BULKIT_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

This only retries the channels that are not published yet. The ones that worked are not posted again.

List your posts

curl "$BULKIT_URL/posts?status=failed&limit=50" -H "x-api-key: $BULKIT_API_KEY"
QueryWhat it does
statusFilter by status. Repeat it for more: ?status=scheduled&status=failed.
tagFilter by tag id. Repeat it for more.
from, toOnly posts scheduled in this time range. A date (2026-10-15) or a full ISO 8601 time both work.
limitPosts per page. Default 25, max 100.
cursorThe nextCursor from the previous page.

The response is { "data": [...], "nextCursor": "..." }. To get the next page, send ?cursor= with the nextCursor value. Stop when nextCursor is null.

Get every page
let cursor = null;
do {
  const url = new URL(`${process.env.BULKIT_URL}/posts`);
  if (cursor) url.searchParams.set("cursor", cursor);
  const res = await fetch(url, { headers: { "x-api-key": process.env.BULKIT_API_KEY } });
  const page = await res.json();
  for (const post of page.data) console.log(post.id, post.status);
  cursor = page.nextCursor;
} while (cursor);

Engagement numbers

GET /posts/{id}/metrics returns likes, comments, views and more, as the platform reports them over time.

  • Numbers are running totals, so they only go up (unless people unlike).
  • Readings are hourly on day one, then daily for a week, then weekly up to 30 days. After that they stop.
  • If a metric is missing, the platform did not report it. It does not mean zero.
  • Some channels report nothing. Check metricsStatus on GET /channels.

For totals across many posts, use GET /stats?from=…&to=….

On this page