Threading Guide - Post Multi-Part Threads via API
Learn how to post threads (multiple connected posts) to Twitter/X and Threads using the Publora 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@publora.com 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.publora.com/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-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), Publora 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']
})
});Publora 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, Publora 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.publora.com/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-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.publora.com/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
content,
platforms: ['twitter-123', 'threads-456']
})
});When explicit markers are detected:
- Publora 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.publora.com/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-publora-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-publora-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-publora-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.publora.com/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
content: `A long piece of content that will become a thread...`,
platforms: ['twitter-123', 'threads-456']
})
});Publora 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.publora.com/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-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
Publora - Post threads to X/Twitter and Threads via a simple REST API.