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