PostPost

Threads

Post to Threads (by Meta) programmatically using the PostPost REST API. A simpler alternative to the official Threads API or Meta Graph API for Threads.

⚠️ Temporary Restriction: Multi-threaded nested posts (content >500 characters that would be split into multiple connected replies) are temporarily unavailable due to Threads app reconnection status. Single posts, carousel posts, and standalone threads continue to work normally. Contact [email protected] for updates on when this feature will be restored.

Threads API Overview

PostPost provides a unified REST API for publishing text posts, images, videos, carousels, and automatic thread splitting for long-form content on Threads. No need to manage Meta OAuth flows, handle the Threads Publishing API complexity, or wait for Threads API access approval.

Why Use PostPost Instead of Threads API / Meta Graph API?

FeaturePostPost APIThreads API (Meta)
AuthenticationSingle API keyMeta OAuth 2.0 flow
API accessInstantRequires Meta app review
Thread creationAutomatic splittingManual implementation
Multi-platformPost to 11 platformsThreads only
Setup time5 minutesDays to weeks
Carousel supportYesYes

Keywords: Threads API, Threads posting API, Meta Threads API, post to Threads programmatically, Threads REST API, Threads developer API, Threads automation API, Threads bot API, Instagram Threads API, publish to Threads API

Platform ID Format

threads-{accountId}

Where {accountId} is your Threads account ID assigned during connection via Meta OAuth.

Requirements

  • A Threads account connected via Meta OAuth through the PostPost dashboard
  • API key from PostPost

Supported Content

TypeSupportedLimits
TextYes500 characters
ImagesYesUp to 10 per carousel, WebP auto-converted
VideosYesMP4, MOV formats, 1 per post (video carousels not supported by PostPost)
CarouselsYes2-10 items; images supported, video support in carousels is limited (see Platform Quirks)
ThreadsYesAuto-split for long content
HashtagsYesMaximum 1 hashtag per post

Threading

When your content exceeds the 500-character limit, PostPost automatically splits it into a thread (multiple connected posts):

How It Works

PostPost uses the official Threads API reply_to_id parameter to chain posts together. Each subsequent post is posted as a reply to the previous one, creating a connected thread visible on Threads.

Technical flow:

  1. First post is published normally
  2. Each subsequent post is published with reply_to_id set to the previous post's ID
  3. All posts appear as a connected thread on Threads

Automatic Splitting

When content exceeds 500 characters, PostPost automatically splits it:

  • Content is split at paragraph breaks (\n\n) when possible
  • Falls back to sentence boundaries (. , ! , ? )
  • Falls back to word boundaries if needed
  • Each part respects the 500-character limit
  • Adds (1/N) markers at the end of each post by default (e.g., (1/3), (2/3), (3/3))

Manual Thread Parts

You can manually define where thread breaks should occur using either method:

Method 1: Triple dash separator

This is my first post in the thread.

---

This is my second post in the thread.

---

And this is my third post!

Method 2: Explicit markers

First part of the thread [1/3]

Second part of the thread [2/3]

Third and final part [3/3]

When explicit markers are detected, PostPost preserves them exactly as written and splits at those points. Use square brackets [n/m], which is distinct from the auto-added numbering format (1/N) that uses parentheses.

Media in Threads

  • Carousel/Images: Attached to the first post only
  • Video: Attached to the first post only
  • Subsequent posts in the thread are text-only

Platform-Specific Settings

Threads supports a replyControl setting that controls who can reply to your posts:

{
  "platformSettings": {
    "threads": {
      "replyControl": ""
    }
  }
}

Reply Control

ValueDescription
"everyone"Anyone can reply. This is a valid enum value in the schema but is distinct from the default "". When explicitly set, it is sent to the Threads API.
"" (empty string)Default. No replyControl value is sent to the Threads API — the platform's own default behavior applies (anyone can reply). This is NOT included in the API's default platform settings.
"accounts_you_follow"Only accounts you follow can reply
"mentioned_only"Only accounts mentioned in the post can reply

Reply Management

Threads supports reply management through the PostPost dashboard. The getReplies endpoint retrieves replies to a Threads post, and the manageReply endpoint allows you to hide or unhide individual replies. These endpoints are accessible via the dashboard API routes.

Note: Reply management requires the threads_manage_replies OAuth scope. The current OAuth flow requests this scope automatically, but older connections established before this scope was added may lack it. If reply management returns permission errors, disconnect and reconnect your Threads account to obtain the updated scopes.

Examples

Post a Text Update

JavaScript (fetch)

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: 'Just shipped a major update to our API. Faster response times, better error messages, and new endpoints for batch operations.',
    platforms: ['threads-55667788']
  })
});

const data = await response.json();
console.log(data);
// Response: { "success": true, "postGroupId": "abc123..." }

Python (requests)

import requests

response = requests.post(
    'https://api.postpost.dev/api/v1/create-post',
    headers={
        'Content-Type': 'application/json',
        'x-api-key': 'YOUR_API_KEY'
    },
    json={
        'content': 'Just shipped a major update to our API. Faster response times, better error messages, and new endpoints for batch operations.',
        'platforms': ['threads-55667788']
    }
)

data = response.json()
print(data)
# Response: { "success": true, "postGroupId": "abc123..." }

cURL

curl -X POST https://api.postpost.dev/api/v1/create-post \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "content": "Just shipped a major update to our API. Faster response times, better error messages, and new endpoints for batch operations.",
    "platforms": ["threads-55667788"]
  }'
# Response: { "success": true, "postGroupId": "abc123..." }

Node.js (axios)

const axios = require('axios');

const response = await axios.post('https://api.postpost.dev/api/v1/create-post', {
  content: 'Just shipped a major update to our API. Faster response times, better error messages, and new endpoints for batch operations.',
  platforms: ['threads-55667788']
}, {
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'YOUR_API_KEY'
  }
});

console.log(response.data);
// Response: { "success": true, "postGroupId": "abc123..." }

Post with an Image Carousel

Carousels require 2-10 images or videos. The workflow is: create a draft post, upload images, then schedule.

JavaScript (fetch)

const API_KEY = 'YOUR_API_KEY';
const BASE_URL = 'https://api.postpost.dev/api/v1';

// Step 1: Create a 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: 'Our product evolution over the past year. #buildinpublic',
    platforms: ['threads-55667788']
    // No scheduledTime = draft
  })
});

const { postGroupId } = await postResponse.json();

// Step 2: Upload each image (2-10 images supported)
const images = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'];

for (const fileName of images) {
  // Get upload URL
  const uploadUrlResponse = await fetch(`${BASE_URL}/get-upload-url`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY
    },
    body: JSON.stringify({
      fileName,
      contentType: 'image/jpeg',
      type: 'image',
      postGroupId
    })
  });

  const { uploadUrl } = await uploadUrlResponse.json();

  // Upload to S3
  const fileBuffer = await fs.promises.readFile(`./${fileName}`);
  await fetch(uploadUrl, {
    method: 'PUT',
    headers: { 'Content-Type': 'image/jpeg' },
    body: fileBuffer
  });
}

// Step 3: Schedule the post
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-15T14:00:00.000Z'
  })
});

console.log('Carousel scheduled!');

Python (requests)

import requests

API_KEY = 'YOUR_API_KEY'
BASE_URL = 'https://api.postpost.dev/api/v1'
HEADERS = {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY
}

# Step 1: Create a draft post (no scheduledTime)
post_response = requests.post(
    f'{BASE_URL}/create-post',
    headers=HEADERS,
    json={
        'content': 'Our product evolution over the past year. #buildinpublic',
        'platforms': ['threads-55667788']
        # No scheduledTime = draft
    }
)

post_group_id = post_response.json()['postGroupId']

# Step 2: Upload each image (2-10 images supported)
images = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg']

for file_name in images:
    # Get upload URL
    upload_response = requests.post(
        f'{BASE_URL}/get-upload-url',
        headers=HEADERS,
        json={
            'fileName': file_name,
            'contentType': 'image/jpeg',
            'type': 'image',
            'postGroupId': post_group_id
        }
    )

    upload_url = upload_response.json()['uploadUrl']

    # Upload to S3
    with open(f'./{file_name}', 'rb') as f:
        requests.put(upload_url, headers={'Content-Type': 'image/jpeg'}, data=f.read())

# Step 3: Schedule the post
requests.put(
    f'{BASE_URL}/update-post/{post_group_id}',
    headers=HEADERS,
    json={
        'status': 'scheduled',
        'scheduledTime': '2026-03-15T14:00:00.000Z'
    }
)

print('Carousel scheduled!')

cURL

API_KEY="YOUR_API_KEY"

# Step 1: Create a draft post (no scheduledTime)
POST_RESPONSE=$(curl -s -X POST https://api.postpost.dev/api/v1/create-post \
  -H "Content-Type: application/json" \
  -H "x-api-key: $API_KEY" \
  -d '{
    "content": "Our product evolution over the past year. #buildinpublic",
    "platforms": ["threads-55667788"]
  }')

POST_GROUP_ID=$(echo "$POST_RESPONSE" | jq -r '.postGroupId')

# Step 2: Upload each image (2-10 images supported)
for FILE in photo1.jpg photo2.jpg photo3.jpg; do
  UPLOAD_RESPONSE=$(curl -s -X POST https://api.postpost.dev/api/v1/get-upload-url \
    -H "Content-Type: application/json" \
    -H "x-api-key: $API_KEY" \
    -d "{
      \"fileName\": \"$FILE\",
      \"contentType\": \"image/jpeg\",
      \"type\": \"image\",
      \"postGroupId\": \"$POST_GROUP_ID\"
    }")

  UPLOAD_URL=$(echo "$UPLOAD_RESPONSE" | jq -r '.uploadUrl')

  curl -s -X PUT "$UPLOAD_URL" \
    -H "Content-Type: image/jpeg" \
    --data-binary @"./$FILE"
done

# Step 3: Schedule the post
curl -X PUT "https://api.postpost.dev/api/v1/update-post/$POST_GROUP_ID" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $API_KEY" \
  -d '{
    "status": "scheduled",
    "scheduledTime": "2026-03-15T14:00:00.000Z"
  }'

Note: For immediate publishing, set scheduledTime to the current time or a few seconds in the future. For more details, see the media upload workflow.

Post a Thread (Long Content)

JavaScript (fetch)

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: `We just completed a major infrastructure migration and I want to share what we learned. Moving from a monolithic architecture to microservices is not as straightforward as the blog posts make it sound.

First, we had to map every single dependency between our services. This alone took two weeks. We discovered circular dependencies we never knew existed and had to refactor several core modules before we could even begin the migration.

The actual migration took three months. We ran both systems in parallel, comparing outputs in real-time. When we finally cut over, we had 99.97% uptime throughout the process. The key was incremental rollout and comprehensive monitoring at every step.`,
    platforms: ['threads-55667788']
  })
});

const data = await response.json();
console.log(data);
// Response: { "success": true, "postGroupId": "abc123..." }

Python (requests)

import requests

content = """We just completed a major infrastructure migration and I want to share what we learned. Moving from a monolithic architecture to microservices is not as straightforward as the blog posts make it sound.

First, we had to map every single dependency between our services. This alone took two weeks. We discovered circular dependencies we never knew existed and had to refactor several core modules before we could even begin the migration.

The actual migration took three months. We ran both systems in parallel, comparing outputs in real-time. When we finally cut over, we had 99.97% uptime throughout the process. The key was incremental rollout and comprehensive monitoring at every step."""

response = requests.post(
    'https://api.postpost.dev/api/v1/create-post',
    headers={
        'Content-Type': 'application/json',
        'x-api-key': 'YOUR_API_KEY'
    },
    json={
        'content': content,
        'platforms': ['threads-55667788']
    }
)

data = response.json()
print(data)
# Response: { "success": true, "postGroupId": "abc123..." }

cURL

curl -X POST https://api.postpost.dev/api/v1/create-post \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "content": "We just completed a major infrastructure migration and I want to share what we learned. Moving from a monolithic architecture to microservices is not as straightforward as the blog posts make it sound.\n\nFirst, we had to map every single dependency between our services. This alone took two weeks. We discovered circular dependencies we never knew existed and had to refactor several core modules before we could even begin the migration.\n\nThe actual migration took three months. We ran both systems in parallel, comparing outputs in real-time. When we finally cut over, we had 99.97% uptime throughout the process. The key was incremental rollout and comprehensive monitoring at every step.",
    "platforms": ["threads-55667788"]
  }'
# Response: { "success": true, "postGroupId": "abc123..." }

Node.js (axios)

const axios = require('axios');

const content = `We just completed a major infrastructure migration and I want to share what we learned. Moving from a monolithic architecture to microservices is not as straightforward as the blog posts make it sound.

First, we had to map every single dependency between our services. This alone took two weeks. We discovered circular dependencies we never knew existed and had to refactor several core modules before we could even begin the migration.

The actual migration took three months. We ran both systems in parallel, comparing outputs in real-time. When we finally cut over, we had 99.97% uptime throughout the process. The key was incremental rollout and comprehensive monitoring at every step.`;

const response = await axios.post('https://api.postpost.dev/api/v1/create-post', {
  content,
  platforms: ['threads-55667788']
}, {
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'YOUR_API_KEY'
  }
});

console.log(response.data);
// Response: { "success": true, "postGroupId": "abc123..." }

PostPost will automatically split this into multiple thread posts, each staying within the 500-character limit, with (1/N) numbering markers added to each part.

Platform Quirks

  • Single hashtag limit: Threads allows a maximum of 1 hashtag per post. If your content includes more than one hashtag, only the first will be recognized by the platform.
  • WebP auto-conversion: If you provide WebP images, PostPost automatically converts them to JPEG before uploading to Threads.
  • Auto-threading: Content exceeding 500 characters is automatically split into a thread. PostPost adds (1/N) numbering markers to the end of each post (e.g., (1/3), (2/3), (3/3)) and splits at sentence boundaries to keep posts readable.
  • Manual thread parts: You can use --- as a separator in your content to explicitly define where thread breaks should occur.
  • No edit support: Once posted, Threads posts cannot be edited via the API. You would need to delete and repost.
  • MP4 and MOV for videos: MP4 and MOV video formats are supported. Other formats will be rejected.
  • Carousel video support is limited: While the Threads API supports videos in carousels, PostPost's current implementation only supports IMAGE type items in carousels. Standalone video posts work normally, but VIDEO items within carousels are not yet supported by PostPost.

Character Limits

ElementLimit
Post body500 characters
Hashtags1 per post
Carousel items2-10 items (images supported; video in carousels not yet supported by PostPost)
Thread partsNo fixed limit on number of parts

API Limits

Text Limits

ElementLimit
Post body500 characters
Links per post5
Hashtags1 per post

Media Limits

Media TypeMax SizeMax CountSupported Formats
Images8 MB10 per carouselJPEG, PNG
Videos500 MB1 per post (video carousels not supported by PostPost)MP4, MOV
Video ConstraintLimit
Duration5 minutes

Rate Limits

Limit TypeValue
Posts per 24 hours250
Replies per 24 hours1,000

Additional Notes

  • Threading is supported for long-form content
  • PostPost handles rate limiting automatically and will return appropriate errors if limits are exceeded

On this page