Create posts

Save drafts, schedule posts, publish now, post to many channels and write threads.

You create every post with one route: POST /posts.

The shape of a post

{
  "intent": "schedule",
  "scheduledAt": "2026-10-15T09:00:00Z",
  "tagIds": ["tag_launch"],
  "channels": [
    {
      "channelId": "CHANNEL_ID",
      "blocks": [{ "content": "Your text here", "mediaIds": ["MEDIA_ID"] }]
    }
  ]
}
FieldRequiredWhat it does
intentYesdraft, schedule or now. See below.
scheduledAtFor scheduleWhen to publish, in ISO 8601 with a timezone (Z or +02:00). A time with no timezone, or a date that doesn't exist, is refused.
channelsYesWhere the post goes. At least one.
channels[].channelIdYesAn id from GET /channels.
channels[].blocksYesThe content. One block = one normal post.
blocks[].contentNoThe text. You can leave it out if the block has media.
blocks[].mediaIdsNoMedia ids from POST /media.
channels[].titleSome platformsA title, for platforms that have one (like YouTube or Reddit).
channels[].postTypeNoA special kind of post, like an Instagram story.
channels[].optionsSome platformsFields only one platform has, like Pinterest's link.
tagIdsNoYour own labels, from GET /tags. Tags are never published.

Choose an intent

Bulkit saves the post and publishes it at scheduledAt.

curl -X POST "$BULKIT_URL/posts" \
  -H "x-api-key: $BULKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "intent": "schedule",
    "scheduledAt": "2026-10-15T09:00:00Z",
    "channels": [
      { "channelId": "CHANNEL_ID", "blocks": [{ "content": "See you on Thursday!" }] }
    ]
  }'

The time must be in the future, or you get schedule_in_past.

Bulkit starts publishing right away.

curl -X POST "$BULKIT_URL/posts" \
  -H "x-api-key: $BULKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "intent": "now",
    "channels": [
      { "channelId": "CHANNEL_ID", "blocks": [{ "content": "We just shipped!" }] }
    ]
  }'

The response comes back with status publishing, not published. Publishing happens in the background. See Check the result.

Bulkit saves the post and publishes nothing. You can publish it later with POST /posts/{id}/publish.

curl -X POST "$BULKIT_URL/posts" \
  -H "x-api-key: $BULKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "intent": "draft",
    "channels": [
      { "channelId": "CHANNEL_ID", "blocks": [{ "content": "Idea for next week" }] }
    ]
  }'

Post to many channels at once

Put all channels in one request. Each channel can have its own text.

{
  "intent": "schedule",
  "scheduledAt": "2026-10-15T09:00:00Z",
  "channels": [
    { "channelId": "CHANNEL_X", "blocks": [{ "content": "Short version for X." }] },
    { "channelId": "CHANNEL_LINKEDIN", "blocks": [{ "content": "A longer version, written for LinkedIn." }] }
  ]
}

Do not call POST /posts once per channel. You would get several separate posts, each with its own id and schedule.

Write a thread

Add more than one block. They are published in order: on X each block is a reply to the one before; on LinkedIn the extra blocks become comments.

{
  "channelId": "CHANNEL_X",
  "blocks": [
    { "content": "1/ We rebuilt our app from scratch. Here is what we learned." },
    { "content": "2/ First: keep things simple." },
    { "content": "3/ Second: measure before you optimise." }
  ]
}

Before you send a thread, check two things:

  • limits.maxBlocks on GET /platforms — 1 means that platform has no threads.
  • continuationStatus on GET /channels — must be available for that account.

If a thread stops halfway, a retry continues from the block that failed. It does not post the first blocks again.

Titles, post types and platform options

Some platforms need more than text. GET /platforms tells you what each one needs:

  • Title — if limits.title is not null, send channels[].title.
  • Post type — if postTypes is not empty, you can send one of its key values as channels[].postType (for example "story").
  • Options — limits.options lists extra fields. Send them in channels[].options.
Example: a Pinterest pin with a link
{
  "channelId": "CHANNEL_PINTEREST",
  "title": "Easy pesto in 10 minutes",
  "options": { "link": "https://example.com/recipes/pesto" },
  "blocks": [{ "content": "Fresh basil, garlic, pine nuts.", "mediaIds": ["MEDIA_ID"] }]
}

Always read limits from GET /platforms instead of writing them in your code. Platforms change their rules, and this route always has the current ones.

If the request fails

Bulkit checks every channel against its platform's rules before it saves anything. So if you get a 4xx error, nothing was created. Fix the problem shown in error.field and send the request again.

{
  "error": {
    "code": "content_too_long",
    "message": "X (Twitter) allows 280 characters — this one is 300",
    "field": "channels[1].blocks[0].content",
    "requestId": "req_a8b0c1d2-…"
  }
}

See Errors for all codes.

On this page