Postato Docs
GuidesCookbook

Twitter thread

Post a multi-tweet thread in one API call.

Twitter thread

A thread is a single Postato post with an array content. Each element becomes one tweet, posted in order, with each tweet replying to the previous. The whole thread has one postId, one delivery lifecycle, and one idempotencyKey.

Shape

{
  "platform": "twitter",
  "accountId": "acc_01H...",
  "status": "publish",
  "postType": "thread",
  "content": [
    { "text": "We're rolling out a new release today. Here's what changed." },
    { "text": "1/ Faster post creation — down from 40 s to sub-second for external media." },
    { "text": "2/ New MCP tools for media management: upload_media, list_media, delete_media." },
    { "text": "3/ Schema-first REST API docs, auto-generated from server schemas." },
    { "text": "Give it a try: https://docs.postato.com.br" }
  ]
}

Full call

curl -X POST "https://api.postato.com.br/v1/workspaces/$WORKSPACE_ID/posts" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "platform": "twitter",
    "accountId": "acc_01H...",
    "status": "publish",
    "postType": "thread",
    "content": [
      { "text": "We are rolling out a new release today." },
      { "text": "1/ Faster post creation — down from 40 s to sub-second." },
      { "text": "2/ New MCP tools for media management." },
      { "text": "3/ Schema-first REST API docs, auto-generated." },
      { "text": "Give it a try: https://docs.postato.com.br" }
    ]
  }'
const response = await fetch(
  `https://api.postato.com.br/v1/workspaces/${WORKSPACE_ID}/posts`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': crypto.randomUUID(),
    },
    body: JSON.stringify({
      platform: 'twitter',
      accountId: 'acc_01H...',
      status: 'publish',
      postType: 'thread',
      content: [
        { text: "We're rolling out a new release today. Here's what changed." },
        { text: '1/ Faster post creation — down from 40 s to sub-second.' },
        { text: '2/ New MCP tools for media management.' },
        { text: '3/ Schema-first REST API docs, auto-generated.' },
        { text: 'Give it a try: https://docs.postato.com.br' },
      ],
    }),
  }
);

const { id, status } = await response.json();
console.log('Queued thread', id, 'status', status);
import os
import uuid
import httpx

response = httpx.post(
    f"https://api.postato.com.br/v1/workspaces/{WORKSPACE_ID}/posts",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "platform": "twitter",
        "accountId": "acc_01H...",
        "status": "publish",
        "postType": "thread",
        "content": [
            {"text": "We're rolling out a new release today."},
            {"text": "1/ Faster post creation — down from 40 s to sub-second."},
            {"text": "2/ New MCP tools for media management."},
            {"text": "3/ Schema-first REST API docs, auto-generated."},
            {"text": "Give it a try: https://docs.postato.com.br"},
        ],
    },
    timeout=30,
)
data = response.json()
print("Queued thread", data["id"], "status", data["status"])

Adding media

Attach per-item, not at the top level:

{
  "content": [
    {
      "text": "Launch photos from yesterday.",
      "media": [{ "id": "med_01H...photo1" }]
    },
    {
      "text": "Behind the scenes.",
      "media": [{ "id": "med_01H...photo2" }]
    }
  ]
}

Upload media first via POST /media/upload-url (see Media upload).

Constraints

  • Twitter caps each tweet at 280 characters (500 for Premium users). Postato does NOT auto-split long text; overflow fails at delivery.
  • Maximum 25 tweets per thread (Twitter's limit).
  • First tweet can have media; subsequent tweets can too. Each position is independent.
  • Thread posting is atomic: if tweet 3 fails, the thread does not auto-rollback. The first 2 tweets stay published. Plan for that with good Idempotency-Key hygiene so retries don't duplicate the successful tweets.

Polling outcome

async function waitForPublish(postId: string) {
  for (let i = 0; i < 30; i++) {
    const r = await fetch(
      `https://api.postato.com.br/v1/workspaces/${WORKSPACE_ID}/posts/${postId}`,
      { headers: { Authorization: `Bearer ${API_KEY}` } }
    );
    const { status, externalUrl, error } = await r.json();
    if (status === 'published') return externalUrl;
    if (status === 'failed') throw new Error(error?.message ?? 'failed');
    await new Promise((res) => setTimeout(res, 3000));
  }
  throw new Error('timeout');
}
import time
import httpx

def wait_for_publish(post_id: str, workspace_id: str, api_key: str) -> str:
    for _ in range(30):
        r = httpx.get(
            f"https://api.postato.com.br/v1/workspaces/{workspace_id}/posts/{post_id}",
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=15,
        )
        data = r.json()
        if data["status"] == "published":
            return data["externalUrl"]
        if data["status"] == "failed":
            raise RuntimeError(data.get("error", {}).get("message", "failed"))
        time.sleep(3)
    raise TimeoutError("timeout")

Or skip polling and use the post.published / post.failed webhook. See Webhook handler.

On this page