Threading
Learn how to post threads (multiple connected posts) to Twitter/X and Threads using the PostPost API. This guide covers automatic thread splitting, manual thread creation, and best practices.
⚠️ Threads Platform Notice: Multi-part nested threads on Threads are temporarily unavailable due to API access requirements. Twitter/X threading works normally. Single posts and carousels on Threads continue to work. Contact support@postpost.dev for updates.
What is a Thread?
A thread is a series of connected posts that appear as a single conversation. On X/Twitter, these are called "tweet threads" or "tweetstorms." On Threads (by Meta), they appear as connected replies to your own posts.
Keywords: post thread API, Twitter thread API, Threads multi-post API, tweet thread programmatically, create thread API, multi-tweet API, thread posting automation, social media thread API
Quick Start
Post a thread by simply providing content that exceeds the platform's character limit:
const response = await fetch('https://api.postpost.dev/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
content: `This is a long post that will be automatically split into multiple parts.
When your content exceeds the character limit (280 for X/Twitter, 500 for Threads), PostPost automatically creates a thread.
Each part is posted as a reply to the previous one, creating a connected conversation that your followers can read through.`,
platforms: ['twitter-123', 'threads-456']
})
});PostPost will:
- Detect content exceeds the limit
- Split at natural break points (paragraphs, sentences)
- Post each part as a reply to the previous
- Return the head post ID
How Threading Works
Automatic Thread Creation
When your content exceeds platform limits, PostPost automatically:
-
Splits content intelligently:
- First tries paragraph breaks (
\n\n) - Falls back to sentence endings (
.,!,?) - Falls back to word boundaries
- Hard splits only as last resort
- First tries paragraph breaks (
-
Posts sequentially:
- First post published normally
- Each subsequent post replies to the previous
- Uses
reply_to_id(Threads) orin_reply_to_tweet_id(X)
-
Adds numbering (X/Twitter and Threads):
- Appends
(1/N),(2/N), etc. to each post - Reserves 10 characters for the marker
- Appends
Platform Limits
| Platform | Character Limit | Thread Numbering |
|---|---|---|
| X/Twitter (Standard) | 280 | Yes (1/N) |
| X/Twitter (Premium) | 25,000 | Yes (1/N) |
| Threads | 500 | Yes (default) |
Manual Thread Control
Method 1: Triple Dash Separator
Use --- to explicitly define where thread breaks should occur:
const content = `First part of my thread - the hook that grabs attention.
---
Second part with the main content and details.
---
Final part with the call to action!`;
const response = await fetch('https://api.postpost.dev/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
content,
platforms: ['twitter-123']
})
});Method 2: Explicit Markers
Use [n/m] markers to define parts:
const content = `The hook that grabs attention [1/3]
The main content with all the details you want to share [2/3]
The conclusion with a call to action! [3/3]`;
const response = await fetch('https://api.postpost.dev/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
content,
platforms: ['twitter-123', 'threads-456']
})
});When explicit markers are detected:
- PostPost preserves them exactly as written
- Content is split at marker positions
- No additional numbering is added
Note: The
[n/m]explicit marker and---separator behavior is handled by the content splitting service and cannot be independently verified from the core backend source. Thread numbering reserves approximately 10 characters for the(n/m)marker when calculating available space per part.
Media in Threads
Media (images, videos) are attached to the first post only. Use the standard upload workflow: create draft, upload media, then schedule.
const API_KEY = 'YOUR_API_KEY';
const BASE_URL = 'https://api.postpost.dev/api/v1';
// Step 1: Create draft post (no scheduledTime)
const postResponse = await fetch(`${BASE_URL}/create-post`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY
},
body: JSON.stringify({
content: `Check out these results from our latest experiment!
---
The methodology was straightforward but the results surprised us.
---
What do you think? Drop your thoughts below!`,
platforms: ['twitter-123']
// No scheduledTime = draft
})
});
const { postGroupId } = await postResponse.json();
// Step 2: Get pre-signed upload URL
const urlResponse = await fetch(`${BASE_URL}/get-upload-url`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY
},
body: JSON.stringify({
fileName: 'experiment-results.jpg',
contentType: 'image/jpeg',
type: 'image',
postGroupId
})
});
const { uploadUrl, fileUrl } = await urlResponse.json();
// Step 3: Upload file to S3
const fileBuffer = await fs.promises.readFile('./experiment-results.jpg');
await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'image/jpeg' },
body: fileBuffer
});
console.log(`Uploaded: ${fileUrl}`);
// Step 4: Schedule the thread
await fetch(`${BASE_URL}/update-post/${postGroupId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY
},
body: JSON.stringify({
status: 'scheduled',
scheduledTime: '2026-03-01T14:00:00.000Z'
})
});Media Limits per Platform
| Platform | Images | Video | Notes |
|---|---|---|---|
| X/Twitter | Up to 4 | 1 | Cannot mix images and video |
| Threads | Up to 10 (carousel) | 1 | WebP auto-converted |
Cross-Platform Threading
Post the same thread to multiple platforms simultaneously:
const response = await fetch('https://api.postpost.dev/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
content: `A long piece of content that will become a thread...`,
platforms: ['twitter-123', 'threads-456']
})
});PostPost handles platform differences:
- X/Twitter: 280 char limit, adds
(1/N)markers - Threads: 500 char limit, adds
(1/N)markers by default
Scheduling Threads
Schedule a thread for future publication:
const response = await fetch('https://api.postpost.dev/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
content: `Your long-form thread content here...`,
platforms: ['twitter-123'],
scheduledTime: '2026-03-15T14:30:00.000Z'
})
});Error Handling
Partial Thread Failures
If a thread partially publishes (some posts succeed, others fail):
{
"status": "partially_published",
"posts": [
{ "platform": "twitter", "platformId": "123", "content": "Part 1 (1/5)", "status": "published", "postedId": "123" },
{ "platform": "twitter", "platformId": "123", "content": "Part 2 (2/5)", "status": "published", "postedId": "456" },
{ "platform": "twitter", "platformId": "123", "content": "Part 3 (3/5)", "status": "failed", "error": { "code": "RATE_LIMIT", "message": "Rate limit exceeded", "platformStatusCode": 429, "platformError": null, "failedAt": "2026-03-15T14:01:12.000Z", "retryable": true } },
{ "platform": "twitter", "platformId": "123", "content": "Part 4 (4/5)", "status": "failed", "error": { "code": "RATE_LIMIT", "message": "Rate limit exceeded", "platformStatusCode": 429, "platformError": null, "failedAt": "2026-03-15T14:01:12.000Z", "retryable": true } },
{ "platform": "twitter", "platformId": "123", "content": "Part 5 (5/5)", "status": "failed", "error": { "code": "RATE_LIMIT", "message": "Rate limit exceeded", "platformStatusCode": 429, "platformError": null, "failedAt": "2026-03-15T14:01:12.000Z", "retryable": true } }
]
}The successfully posted parts remain live. You may need to:
- Wait for rate limits to reset
- Manually post remaining content as replies
Rate Limits
| Platform | Limit | Notes |
|---|---|---|
| X/Twitter (Free) | 500/month | Each tweet in thread counts |
| X/Twitter (Basic) | 10,000/month | 100 per 15 min per user |
| Threads | 250/day | ~25/hour |
Best Practices
- Keep threads focused - Each thread should have one main topic
- Hook in first post - The first post should grab attention
- Natural breaks - Use paragraphs to help automatic splitting
- Preview before posting - Long threads are hard to edit once live
- Consider your audience - Some followers prefer single posts over threads
API Response
The create-post endpoint returns only the post group reference, not the individual thread parts:
{
"success": true,
"postGroupId": "664f1a2b3c4d5e6f7a8b9c0d"
}To see the individual thread parts and their statuses, fetch the post group after it has been processed using GET /api/v1/get-post/:postGroupId. Each thread part appears as a separate entry in the posts array:
{
"success": true,
"postGroupId": "664f1a2b3c4d5e6f7a8b9c0d",
"posts": [
{
"platform": "twitter",
"platformId": "123",
"content": "First part (1/3)",
"status": "published",
"postedId": "1234567890"
},
{
"platform": "twitter",
"platformId": "123",
"content": "Second part (2/3)",
"status": "published",
"postedId": "1234567891"
},
{
"platform": "twitter",
"platformId": "123",
"content": "Third part (3/3)",
"status": "published",
"postedId": "1234567892"
}
]
}The postedId field contains the platform-native post ID (not a full URL). If a thread partially publishes, some entries will have status: "failed" with an error field.
Related Guides
PostPost - Post threads to X/Twitter and Threads via a simple REST API.
Validation
This guide covers how PostPost validates posts before scheduling to prevent publish failures, including what gets validated, the validation response format, error codes, and best practices for avoidin
Twitter Threads
Post tweet threads (tweetstorms) to X/Twitter programmatically using the PostPost REST API. A simpler alternative to manually chaining tweets with the Twitter API v2.