Developers
Postbito API
Everything the composer does, your scripts and agents can do over HTTP or MCP: pick up ideas, upload media, check a post against every network, draft it with a caption per channel, send it for approval, and read back how it did. People stay in charge of what goes out.
Quick start
- 1. In Postbito, open API & agents and create a key with the permissions it needs.
- 2. Send it as a Bearer token:
curl https://postbito.com/api/v1/channels \ -H "Authorization: Bearer pbt_your_key" \ -H "Accept: application/json"
- 3. Draft a post for two channels, with its own caption on one of them, and send it for approval:
curl -X POST https://postbito.com/api/v1/posts \
-H "Authorization: Bearer pbt_your_key" \
-H "Content-Type: application/json" \
-d '{"text": "Doors at 9 tonight", "channels": [3],
"targets": [{"channel_id": 4, "text": "Doors at 9 🎶 see you there"}],
"action": "submit"}'
The pipeline
Postbito is built to sit in the middle of an agent-made content loop. Each step has an endpoint and an MCP tool; the one step without either — approving — is a person’s.
-
Step 01
Request
People (or agents) file ideas: a topic, the channels it’s for, the day it goes out.
GET /ideas · PATCH /ideas/{id}
list_ideas · update_idea -
Step 02
Build
Your agent writes the captions and makes the photos and videos — a cut per network where it helps.
Your tools
Your tools -
Step 03
Check
Every caption and file is checked against each network’s rules before anything is saved.
POST /posts/validate
check_post -
Step 04
Upload
Hand over finished files — uploaded, or by a public link Postbito fetches.
POST /media
upload_media_from_url -
Step 05
Draft
One post, a caption (and media, and options) per channel where they differ.
POST /posts
create_post -
Step 06
Approve
A person watches every post in Postbito and signs it off — or sends it back with a note.
GET /posts?status=changes_requested
list_posts · update_post · submit_post -
Step 07
Publish
Postbito posts each channel’s copy on schedule and records the link.
GET /posts/{id}
get_post -
Step 08
Clean up
Files are deleted once every post using them is live.
GET /media/{id}
— -
Step 09
Learn
Weekly results — against the week before, by channel, best posts and hours — shape the next ideas.
GET /analytics
get_analytics
When a reviewer sends a post back it becomes changes_requested with their review_note — back to Build. Fix it, PATCH it, and submit it again.
Keys & permissions
Keys belong to the team, not a person, so integrations keep working when people come and go. Owners and admins create them; a key is shown once. Each has up to four permissions:
| read | Read channels, posts, ideas, media and analytics |
| write | Upload media, write ideas and drafts, and submit posts for approval |
| publish | Queue, schedule and publish posts without approval |
| delete | Delete posts, ideas and media |
Approvals
A key without publish — or any key, when the team requires approval (Team plan) — can still ask to queue, schedule or post now: the post is submitted for approval instead, and the response says so. Keys never approve. GET /me tells you which applies (key.can_publish).
MCP server
Postbito runs a Streamable HTTP MCP server at https://postbito.com/mcp. It takes the same keys and permissions — tools a key can’t use are hidden — and its instructions teach the pipeline above.
Claude Code
claude mcp add --transport http postbito https://postbito.com/mcp \ --header "Authorization: Bearer pbt_your_key"
Claude Desktop, Cursor, VS Code and other clients
{
"mcpServers": {
"postbito": {
"type": "http",
"url": "https://postbito.com/mcp",
"headers": {
"Authorization": "Bearer pbt_your_key"
}
}
}
}
whoami
The account, your key’s permissions, and whether what you queue goes out or waits for approval.
list_channels
Channels with ids, next free slots and each network’s rules.
list_ideas
Requests still to do (new, in progress), soonest first.
create_idea
File a request for a post (write).
update_idea
Change an idea — usually its status (write).
upload_media_from_url
Hand over a finished file by its public link; get a media id (write).
check_post
Run a post past every network’s rules without saving.
create_post
Write a post with a caption per channel; draft, submit, queue, schedule or now (write).
update_post
Change a post that hasn’t gone out — e.g. after a change request (write).
get_post
One post in full, with each channel’s result and “next”.
list_posts
Find posts; status “changes_requested” is your rebuild list.
submit_post
Send a post to a person for approval (write).
get_analytics
How the week went: totals, channels, best hours, best posts.
Conventions
- Write once, per channel where it differs. A post has a caption ("text") and media every channel gets. targets[] gives one channel its own caption, media (a vertical cut for TikTok, say) or network options. A target’s text or media left out (or null, or []) uses the post’s. Shorthand: a channels entry can be an object too — {"id": 4, "text": "…"}.
- Actions —
draft(the default) keeps it to finish later;submitasks for approval;queueputs each channel’s copy in its next free posting-schedule slot;schedulesends it at scheduled_at;nowsends it right away. With submit, scheduled_at or publish_mode (queue, now, at) says what happens once it’s approved. - PATCH changes only what you send. targets edits channels one at a time; channels replaces the list. Without an action a post keeps its state — a draft stays a draft, a scheduled post stays scheduled.
- Statuses — post:
draft,pending_approval,changes_requested,scheduled,published,partial,failed. Each channel (target):draft,scheduled,publishing,published,failed. - next on every post says, in a sentence, what happens now and what you can do about it.
- Media is cleaned up. Once every post using a file is live, the file is deleted (its record and thumbnail stay; url becomes null). Turn it off per file with purge_after_publish, or for the team in Publishing settings.
- Times are ISO 8601; send an offset (2026-11-02T09:00:00-05:00) or Z. Dates (publish_on, analytics from/to) are YYYY-MM-DD in the team’s time zone.
- Responses wrap resources in "data"; lists add "links" and "meta" for paging (per_page up to 100). Each post has a url that opens it in Postbito.
Endpoints
Base URL: https://postbito.com/api/v1 · PUT works wherever PATCH does.
Account
/me
read
The team, its time zone and plan limits, the key’s permissions, and whether what it queues goes straight out (can_publish) or waits for approval.
Channels
/channels
read
Connected accounts: network, name, status, posting schedule and the network’s rules (caption length, the media it needs and takes). People connect channels in Postbito.
/channels/{id}
read
One channel, with its next five free queue slots.
Ideas
/ideas
read
Requests for posts, soonest first. ?status=new,in_progress (also drafted, done) &per_page=25
/ideas
write
File a request: the topic, notes, the channels it’s for and the day it should go out.
{
"title": "Halloween weekend on the rooftop",
"body": "Costume contest Saturday, 2-for-1 cocktails.",
"channel_ids": [
3,
4,
7
],
"publish_on": "2026-10-30"
}
/ideas/{id}
read
One idea, with the posts made from it.
/ideas/{id}
write
Change any field — usually status: in_progress when you start building it, done when it’s out. Saving a post with its idea_id marks it drafted.
{
"status": "in_progress"
}
/ideas/{id}
delete
Delete an idea. Its posts stay.
Media
/media
write
Upload a photo or video as multipart "file" — or send a public "url" and Postbito fetches it. Returns the media id to use in posts, with its size, dimensions and length.
{
"url": "https://bucket.example.com/videos/halloween-vertical.mp4",
"alt_text": "Costumes on the rooftop",
"purge_after_publish": true
}
/media
read
The library, newest first. ?kind=image|video &purged=0|1
/media/{id}
read
One file (by id or uuid): a signed url, thumbnail, and pending_uses — posts still waiting to go out with it.
/media/{id}
write
Change alt_text, filename or purge_after_publish.
{
"purge_after_publish": false
}
/media/{id}
delete
Delete a file. Refused (409) while a post that hasn’t gone out uses it.
Posts
/posts/validate
read
The check step: run a post past every channel’s network rules without saving. Returns ok, problems per channel id, and each channel’s caption length against its limit.
{
"text": "Costume contest Saturday at 10pm 🎃",
"media": [
41
],
"channels": [
3,
4
]
}
/posts
write
Write a post once for several channels, with a caption, media and options per channel where they differ. action: draft (default), submit, queue, now or schedule (with scheduled_at).
{
"text": "Costume contest Saturday at 10pm 🎃",
"media": [
41
],
"channels": [
3
],
"targets": [
{
"channel_id": 4,
"text": "Costume contest Sat 10pm. 2-for-1 if you dress up 🎃"
},
{
"channel_id": 7,
"media": [
42
],
"options": {
"privacy_level": "PUBLIC_TO_EVERYONE"
}
}
],
"idea_id": 12,
"action": "submit"
}
/posts
read
Posts, newest first. ?status=changes_requested (comma-separate several) &channel_id= &idea_id= &from= &to= (ISO 8601; a channel’s scheduled or published time) &sort=newest|schedule &per_page=25
/posts/{id}
read
One post: each channel’s caption, media, time, result (remote_url or error) and metrics, the review_note, and next — what happens now.
/posts/{id}
write
Change a post that hasn’t gone out. Only what you send changes: targets edits channels one at a time, channels replaces the list. Without an action it keeps its state.
{
"targets": [
{
"channel_id": 7,
"media": [
43
]
}
]
}
/posts/{id}/submit
write
Ask an owner or admin to approve it (it’s checked first). Approved posts go out as asked; sent-back ones become changes_requested with a review_note.
/posts/{id}/drafts
write
Take it off the calendar (or out of the approval queue) and keep it as a draft.
/posts/{id}/duplicate
write
Copy it as a new draft.
/posts/{id}/retry
publish
Send the channels that failed again, now.
/posts/{id}
delete
Delete it from Postbito. Anything already live stays on the networks.
Analytics
/analytics
read
The learn step: totals for a period (default: the last 7 days) against the period before, each channel’s numbers, the best hours, and posts best first with their metrics. ?from=YYYY-MM-DD &to=YYYY-MM-DD &channel_id= &limit=25 (Essentials and Team plans).
The post object
What POST /posts takes:
{
"text": "Costume contest Saturday at 10pm 🎃",
"media": [
41
],
"channels": [
3
],
"targets": [
{
"channel_id": 4,
"text": "Costume contest Sat 10pm. 2-for-1 if you dress up 🎃"
},
{
"channel_id": 7,
"media": [
42
],
"options": {
"privacy_level": "PUBLIC_TO_EVERYONE"
}
}
],
"idea_id": 12,
"action": "submit"
}
And what comes back (shortened):
{
"data": {
"id": 88,
"status": "changes_requested",
"text": "Costume contest Saturday at 10pm 🎃",
"media": [{ "id": 41, "kind": "video", "mime_type": "video/mp4", "duration": 24.5, "url": "https://postbito.com/media/9f1c…/halloween.mp4?signature=…", "purged_at": null }],
"targets": [
{
"id": 210,
"channel": { "id": 4, "network": "x", "name": "Harbor House", "handle": "@harborhouse" },
"status": "draft",
"text": "Costume contest Sat 10pm. 2-for-1 if you dress up 🎃",
"own_text": true,
"media": [],
"options": {},
"scheduled_at": null,
"published_at": null,
"remote_url": null,
"error": null,
"metrics": {}
}
],
"publish_mode": "queue",
"idea_id": 12,
"author": "Claude Code",
"created_via": "api",
"review_note": "Say the contest starts at 10pm sharp, and use the vertical cut on X too.",
"reviewed_by": "Chris",
"next": "A reviewer asked for changes: \"Say the contest starts at 10pm sharp…\". Rebuild what they asked for, update the post (PATCH /api/v1/posts/{id}, MCP: update_post) and submit it again.",
"url": "https://postbito.com/your-team/posts/88/edit"
}
}
Network rules
What each network takes. POST /posts/validate checks all of it; GET /channels returns it per channel as "rules". X counts every link as 23 characters and emoji as 2.
| Network | Caption | Needs | Photos | Video |
|---|---|---|---|---|
| 63,206 | Text is enough | Up to 10 JPEG, PNG, GIF, WEBP · 10 MB | 1 video 10,240 MB · up to 4:00:00 | |
| 2,200 30 hashtags max | A photo or video | Up to 20 JPEG, PNG · 8 MB | Up to 20, mixed with photos 300 MB · up to 15:00 | |
| YouTube | 5,000 + a title (100) | A video | — | 1 video Any size |
| TikTok | 2,200 | A photo or video | Up to 35 JPEG, WEBP · 20 MB | 1 video 4,096 MB · up to 10:00 |
| X | 280 | Text is enough | Up to 4 JPEG, PNG, GIF, WEBP · 5 MB | 1 video 512 MB · up to 2:20 |
| 3,000 | Text is enough | Up to 20 JPEG, PNG, GIF · 36 MB | 1 video 5,120 MB · up to 30:00 |
Network options targets[].options
YouTube
- title
- — Required by YouTube, up to 100 characters. Defaults to the caption’s first line.
- privacy
- — public, unlisted or private (default public).
- made_for_kids
- — true or false (default false).
- tags
- — A list of tags.
- category_id
- — YouTube’s video category id (default 22, People & Blogs).
TikTok
- privacy_level
- — PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR or SELF_ONLY. Apps TikTok hasn’t audited can only post SELF_ONLY.
- disable_comment
- — true to turn comments off.
- disable_duet
- — true to turn duets off (videos).
- disable_stitch
- — true to turn stitches off (videos).
- title
- — Photo posts: a title (up to 90 characters).
- brand_content_toggle
- — true when it’s a paid partnership (promoting another brand).
- brand_organic_toggle
- — true when it promotes your own business.
- cover_timestamp_ms
- — Videos: the frame to use as the cover, in milliseconds (default 1000).
- auto_add_music
- — Photo posts: let TikTok add music (default true).
- type
- — post (a photo, carousel or video — videos go out as Reels), reel, or story.
- share_to_feed
- — Reels: also show it in the feed (default true).
- visibility
- — PUBLIC (default) or CONNECTIONS.
Errors & limits
| 401 | Missing, revoked or expired key. |
| 403 | The key lacks a permission, or the plan doesn’t include it (analytics on Free). The message says which. |
| 404 | No such post, channel, idea or file on this account. |
| 409 | A conflict: deleting a file a waiting post still uses, or a post that’s publishing right now. |
| 422 | Validation failed. "errors" names each field; a network’s complaints are under "targets.{channel id}", whole-post ones under "post". |
| 429 | Too many requests — 300 per minute per key. Retry after the Retry-After header. |
{
"message": "X allows 280 characters; this is 312 (32 over). (and 1 more error)",
"errors": {
"targets.4": ["X allows 280 characters; this is 312 (32 over)."],
"targets.7": ["Instagram needs a photo or a video."]
}
}
Uploads can be up to 250 MB. The Free plan holds 10 scheduled posts per channel; Essentials and Team are unlimited.