PostPost

X (Twitter)

Post to Twitter (X) programmatically using the PostPost REST API. A simpler alternative to the official Twitter API v2, Tweepy, node-twitter-api-v2, or Twitter Java SDK.

Twitter API Overview

PostPost provides a unified REST API for posting tweets to X (formerly Twitter), including text posts, media attachments, images, videos, and automatic thread splitting for long-form content. No need to manage OAuth tokens, API rate limits, or complex Twitter API authentication flows.

Why Use PostPost Instead of Twitter API v2 / Tweepy / node-twitter-api-v2?

FeaturePostPost APITwitter API v2 / Tweepy
AuthenticationSingle API keyComplex OAuth 2.0 flow
Rate limit handlingAutomaticManual implementation
Thread creationAutomatic splittingManual tweet chaining
Multi-platformPost to 11 platformsTwitter only
Setup time5 minutesHours to days
PricingFree tier available; paid plan required for XFree tier + paid tiers

Keywords: Twitter API, X API, post tweet API, Twitter posting API, tweet programmatically, Twitter bot API, Twitter automation API, send tweet API, Twitter REST API, Twitter developer API

Platform ID Format

twitter-{userId}

Where {userId} is your X/Twitter numeric user ID assigned during account connection.

Requirements

  • An X/Twitter account connected via OAuth through the PostPost dashboard
  • API key from PostPost
  • Paid PostPost plan (Pro or Premium) — Twitter/X is excluded from the Starter plan

Supported Content

TypeSupportedLimits
TextYes280 characters
ImagesYesUp to 4 per post, auto-converted to PNG (max 1000px width)
VideosYesMP4, MOV format
ThreadsYesAuto-split with (1/N) markers

Character Counting

X has specific rules for character counting that PostPost handles automatically:

  • Standard characters count as 1
  • Emojis count as 2 characters
  • URLs are counted by their literal length (PostPost does NOT apply Twitter's 23-character URL shortening rule)
  • PostPost calculates the character count before posting

Threading

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

How It Works

PostPost uses the official X API v2 reply.in_reply_to_tweet_id parameter to chain tweets together. Each subsequent tweet is posted as a reply to the previous one, creating a connected thread.

Technical flow:

  1. First tweet is published normally via POST /2/tweets
  2. Each subsequent tweet is published with reply.in_reply_to_tweet_id set to the previous tweet's ID
  3. All tweets share the same conversation_id (equals the first tweet's ID)

Automatic Splitting

When content exceeds the 280-character limit, PostPost automatically:

  • Splits at paragraph breaks (\n\n) when possible
  • Falls back to sentence boundaries (. , ! , ? )
  • Falls back to word boundaries if needed
  • Adds (1/N) markers at the end of each tweet (e.g., (1/3), (2/3), (3/3))
  • Reserves 10 characters per tweet for the marker

Note: PostPost auto-converts all images to PNG and resizes them to a maximum of 1000px width using sharp before uploading.

Manual Thread Parts

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

Method 1: Triple dash separator

This is my first tweet in the thread.

---

This is my second tweet in the thread.

---

And this is my third tweet!

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

  • Images: Up to 4 images attached to the first tweet only
  • Video: Single video attached to the first tweet only
  • Subsequent tweets in the thread are text-only
  • Images and video cannot be combined in the same tweet

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: 'Hello from PostPost! Posting to X has never been easier.',
    platforms: ['twitter-12345678']
  })
});

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': 'Hello from PostPost! Posting to X has never been easier.',
        'platforms': ['twitter-12345678']
    }
)

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": "Hello from PostPost! Posting to X has never been easier.",
    "platforms": ["twitter-12345678"]
  }'
# 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: 'Hello from PostPost! Posting to X has never been easier.',
  platforms: ['twitter-12345678']
}, {
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'YOUR_API_KEY'
  }
});

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

Post with an Image

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: 'Check out this screenshot of our new dashboard!',
    platforms: ['twitter-12345678']
  })
});

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': 'Check out this screenshot of our new dashboard!',
        'platforms': ['twitter-12345678']
    }
)

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": "Check out this screenshot of our new dashboard!",
    "platforms": ["twitter-12345678"]
  }'
# 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: 'Check out this screenshot of our new dashboard!',
  platforms: ['twitter-12345678']
}, {
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'YOUR_API_KEY'
  }
});

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

Note: To attach media to a post, first create the post, then use the media upload workflow with the returned postGroupId.

Post a Thread

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: `Here is a deep dive into our new feature release and what it means for developers building on our platform.

We have completely redesigned the API layer to support batch operations, real-time webhooks, and granular rate limiting. This means you can now process up to 1000 requests per minute with predictable throughput.

The new SDK is available for JavaScript, Python, and Go. Each SDK includes full TypeScript definitions, async support, and built-in retry logic. Check our docs for migration guides.`,
    platforms: ['twitter-12345678']
  })
});

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

Python (requests)

import requests

content = """Here is a deep dive into our new feature release and what it means for developers building on our platform.

We have completely redesigned the API layer to support batch operations, real-time webhooks, and granular rate limiting. This means you can now process up to 1000 requests per minute with predictable throughput.

The new SDK is available for JavaScript, Python, and Go. Each SDK includes full TypeScript definitions, async support, and built-in retry logic. Check our docs for migration guides."""

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': ['twitter-12345678']
    }
)

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": "Here is a deep dive into our new feature release and what it means for developers building on our platform.\n\nWe have completely redesigned the API layer to support batch operations, real-time webhooks, and granular rate limiting. This means you can now process up to 1000 requests per minute with predictable throughput.\n\nThe new SDK is available for JavaScript, Python, and Go. Each SDK includes full TypeScript definitions, async support, and built-in retry logic. Check our docs for migration guides.",
    "platforms": ["twitter-12345678"]
  }'
# Response: { "success": true, "postGroupId": "abc123..." }

Node.js (axios)

const axios = require('axios');

const content = `Here is a deep dive into our new feature release and what it means for developers building on our platform.

We have completely redesigned the API layer to support batch operations, real-time webhooks, and granular rate limiting. This means you can now process up to 1000 requests per minute with predictable throughput.

The new SDK is available for JavaScript, Python, and Go. Each SDK includes full TypeScript definitions, async support, and built-in retry logic. Check our docs for migration guides.`;

const response = await axios.post('https://api.postpost.dev/api/v1/create-post', {
  content,
  platforms: ['twitter-12345678']
}, {
  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 a numbered thread (e.g., (1/3), (2/3), (3/3)) at sentence boundaries.

Platform Quirks

  • Emoji character counting: Each emoji counts as 2 characters toward the 280-character limit. PostPost accounts for this automatically.
  • PNG preferred for images: While JPEG works, PNG images tend to render with higher quality on X due to their compression algorithm.
  • Thread numbering: PostPost adds (1/N) markers at the end of each tweet in a thread. This is appended after the content, so it reduces available character space by 10 characters per tweet.
  • Image auto-conversion: PostPost automatically converts all images to PNG format and resizes them to a maximum width of 1000px before uploading.
  • Rate limits: X enforces its own rate limits. If you hit them, PostPost will return the appropriate error from the X API.
  • Up to 4 images: You can attach a maximum of 4 images to a single tweet. Attempting to attach more will result in an error.
  • Video and images are mutually exclusive: A single tweet can contain either images or a video, but not both.

API Limits

Character Limit

  • All accounts: 280 characters (PostPost always uses the 280-character limit)
  • Threading: Supported - content over 280 characters will be split into a thread

Image Limits

PropertyLimit
Max size5 MB
Max count4 per tweet
FormatsJPEG, PNG, GIF, WebP

Note: All images are automatically converted to PNG (max 1000px width) via sharp before upload, regardless of the original format. This means animated GIFs will lose their animation.

Video Limits (API)

PropertyLimit
Max duration2 minutes (120 seconds)
Max size512 MB
FormatsMP4, MOV

Important: The X API has a 2-minute video limit even though the native X app allows videos up to 2:20 (140 seconds). If you attempt to upload a longer video via the API, you will receive the error: "This user is not allowed to post a video longer than 2 minutes"

Premium users via native app: Up to 4 hours (not available via API)

Character Limits

Account TypeLimit
All accounts280 characters
Thread tweet (each part)280 characters, minus (X/N) marker space (10 chars)

Rate Limits

The underlying X API v2 has the following publishing limits based on your account tier:

TierMonthly PostsPer 15 MinutesPer 24 Hours
Free500~17~500
Basic ($100/mo)10,000100 per user10,000 per app
Pro ($5,000/mo)1,000,000HigherHigher

PostPost returns the appropriate error from the X API if rate limits are exceeded. Each tweet in a thread counts as a separate post toward these limits.


On this page