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"] }]
}
]
}| Field | Required | What it does |
|---|---|---|
intent | Yes | draft, schedule or now. See below. |
scheduledAt | For schedule | When 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. |
channels | Yes | Where the post goes. At least one. |
channels[].channelId | Yes | An id from GET /channels. |
channels[].blocks | Yes | The content. One block = one normal post. |
blocks[].content | No | The text. You can leave it out if the block has media. |
blocks[].mediaIds | No | Media ids from POST /media. |
channels[].title | Some platforms | A title, for platforms that have one (like YouTube or Reddit). |
channels[].postType | No | A special kind of post, like an Instagram story. |
channels[].options | Some platforms | Fields only one platform has, like Pinterest's link. |
tagIds | No | Your 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.maxBlocksonGET /platforms—1means that platform has no threads.continuationStatusonGET /channels— must beavailablefor 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.titleis notnull, sendchannels[].title. - Post type — if
postTypesis not empty, you can send one of itskeyvalues aschannels[].postType(for example"story"). - Options —
limits.optionslists extra fields. Send them inchannels[].options.
{
"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.