Skip to content

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. 1. In Postbito, open API & agents and create a key with the permissions it needs.
  2. 2. Send it as a Bearer token:
curl https://postbito.com/api/v1/channels \
  -H "Authorization: Bearer pbt_your_key" \
  -H "Accept: application/json"
  1. 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.

  1. 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

  2. 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

  3. Step 03

    Check

    Every caption and file is checked against each network’s rules before anything is saved.

    POST /posts/validate
    check_post

  4. Step 04

    Upload

    Hand over finished files — uploaded, or by a public link Postbito fetches.

    POST /media
    upload_media_from_url

  5. Step 05

    Draft

    One post, a caption (and media, and options) per channel where they differ.

    POST /posts
    create_post

  6. 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

  7. Step 07

    Publish

    Postbito posts each channel’s copy on schedule and records the link.

    GET /posts/{id}
    get_post

  8. Step 08

    Clean up

    Files are deleted once every post using them is live.

    GET /media/{id}
    —

  9. 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; submit asks for approval; queue puts each channel’s copy in its next free posting-schedule slot; schedule sends it at scheduled_at; now sends 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

GET /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

GET /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.

GET /channels/{id} read

One channel, with its next five free queue slots.

Ideas

GET /ideas read

Requests for posts, soonest first. ?status=new,in_progress (also drafted, done) &per_page=25

POST /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"
}
GET /ideas/{id} read

One idea, with the posts made from it.

PATCH /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"
}
DELETE /ideas/{id} delete

Delete an idea. Its posts stay.

Media

POST /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
}
GET /media read

The library, newest first. ?kind=image|video &purged=0|1

GET /media/{id} read

One file (by id or uuid): a signed url, thumbnail, and pending_uses — posts still waiting to go out with it.

PATCH /media/{id} write

Change alt_text, filename or purge_after_publish.

{
    "purge_after_publish": false
}
DELETE /media/{id} delete

Delete a file. Refused (409) while a post that hasn’t gone out uses it.

Posts

POST /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
    ]
}
POST /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"
}
GET /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

GET /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.

PATCH /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
            ]
        }
    ]
}
POST /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.

POST /posts/{id}/drafts write

Take it off the calendar (or out of the approval queue) and keep it as a draft.

POST /posts/{id}/duplicate write

Copy it as a new draft.

POST /posts/{id}/retry publish

Send the channels that failed again, now.

DELETE /posts/{id} delete

Delete it from Postbito. Anything already live stays on the networks.

Analytics

GET /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
Facebook 63,206 Text is enough Up to 10 JPEG, PNG, GIF, WEBP · 10 MB 1 video 10,240 MB · up to 4:00:00
Instagram 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
LinkedIn 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).

Instagram

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).

LinkedIn

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.