Skip to content

Twitter/X API

Schedule and automate Twitter/X posts with Zernio API - Tweets, threads, images, videos, and GIFs

Quick Reference

PropertyValue
Character limit280 (free) / 25,000 (Premium)
Images per post4 (or 1 GIF)
Videos per post1
Image formatsJPEG, PNG, WebP, GIF
Image max size5 MB (images), 15 MB (GIFs)
Video formatsMP4, MOV
Video max size512 MB
Video max duration140 seconds
ThreadsYes (via threadItems)
SchedulingYes
Inbox (DMs)Yes
Inbox (Comments)Yes
AnalyticsYes

Before You Start

Twitter has a strict 280 character limit for free accounts. URLs always count as 23 characters regardless of actual length. Emojis count as 2 characters. If you're cross-posting from platforms with higher limits (LinkedIn 3,000, Facebook 63,000), use customContent to provide a shorter Twitter version or your post WILL fail.

Additional requirements:

  • Duplicate tweets are rejected (even very similar content)
  • Free accounts: 280 characters, Premium accounts: 25,000 characters

X API costs are passed through at exact rate, with zero markup. Unlike other platforms, X (Twitter) operates on a pay-per-call API model. Every X API operation Zernio performs on your behalf — posting, reading analytics, sending DMs, fetching followers — is metered against your account at X's published price.

Common rates that appear on your invoice:

  • Posts: Read — $0.005 per resource (analytics, post lookups)
  • Content: Create — $0.015 per request (publishing a tweet)
  • Content: Create with URL — $0.200 per request (tweets containing http/https links — yes, X charges 13× more for these)
  • DM: Read / DM: Send — $0.010 / $0.015 per resource

These appear as itemized line items on your monthly invoice. Zernio's per-call price always equals X's published price, never higher. The full rate table is at docs.x.com/x-api/getting-started/pricing.

You can set a monthly X spend cap from the dashboard's Billing tab to prevent surprise bills. At 80% of the cap you get a warning email; at 100% Zernio automatically pauses X analytics + inbox polling for the rest of the period.

Quick Start

Post a tweet in under 60 seconds:

typescript
const { post } = await zernio.posts.createPost({
  content: 'Hello from Zernio API!',
  platforms: [
    { platform: 'twitter', accountId: 'YOUR_ACCOUNT_ID' }
  ],
  publishNow: true
});
console.log('Tweet posted!', post._id);
python
result = client.posts.create_post(
    content="Hello from Zernio API!",
    platforms=[
        {"platform": "twitter", "accountId": "YOUR_ACCOUNT_ID"}
    ],
    publish_now=True
)
post = result.post
print(f"Tweet posted! {post['_id']}")
bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hello from Zernio API!",
    "platforms": [
      {"platform": "twitter", "accountId": "YOUR_ACCOUNT_ID"}
    ],
    "publishNow": true
  }'

Content Types

Text Tweet

A simple text-only tweet. Keep it under 280 characters for free accounts.

typescript
const { post } = await zernio.posts.createPost({
  content: 'Just shipped a new feature. Check it out!',
  platforms: [
    { platform: 'twitter', accountId: 'YOUR_ACCOUNT_ID' }
  ],
  publishNow: true
});
console.log('Tweet posted!', post._id);
python
result = client.posts.create_post(
    content="Just shipped a new feature. Check it out!",
    platforms=[
        {"platform": "twitter", "accountId": "YOUR_ACCOUNT_ID"}
    ],
    publish_now=True
)
post = result.post
print(f"Tweet posted! {post['_id']}")
bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Just shipped a new feature. Check it out!",
    "platforms": [
      {"platform": "twitter", "accountId": "YOUR_ACCOUNT_ID"}
    ],
    "publishNow": true
  }'

Tweet with Image

Attach up to 4 images per tweet. JPEG, PNG, WebP, and GIF formats are supported.

typescript
const { post } = await zernio.posts.createPost({
  content: 'Check out this photo!',
  mediaItems: [
    { type: 'image', url: 'https://cdn.example.com/photo.jpg' }
  ],
  platforms: [
    { platform: 'twitter', accountId: 'YOUR_ACCOUNT_ID' }
  ],
  publishNow: true
});
console.log('Tweet with image posted!', post._id);
python
result = client.posts.create_post(
    content="Check out this photo!",
    media_items=[
        {"type": "image", "url": "https://cdn.example.com/photo.jpg"}
    ],
    platforms=[
        {"platform": "twitter", "accountId": "YOUR_ACCOUNT_ID"}
    ],
    publish_now=True
)
post = result.post
print(f"Tweet with image posted! {post['_id']}")
bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Check out this photo!",
    "mediaItems": [
      {"type": "image", "url": "https://cdn.example.com/photo.jpg"}
    ],
    "platforms": [
      {"platform": "twitter", "accountId": "YOUR_ACCOUNT_ID"}
    ],
    "publishNow": true
  }'

Tweet with Video

Attach a single video per tweet. MP4 and MOV formats, up to 512 MB, max 140 seconds (standard uploads).

typescript
const { post } = await zernio.posts.createPost({
  content: 'New product demo',
  mediaItems: [
    { type: 'video', url: 'https://cdn.example.com/demo.mp4' }
  ],
  platforms: [
    { platform: 'twitter', accountId: 'YOUR_ACCOUNT_ID' }
  ],
  publishNow: true
});
console.log('Tweet with video posted!', post._id);
python
result = client.posts.create_post(
    content="New product demo",
    media_items=[
        {"type": "video", "url": "https://cdn.example.com/demo.mp4"}
    ],
    platforms=[
        {"platform": "twitter", "accountId": "YOUR_ACCOUNT_ID"}
    ],
    publish_now=True
)
post = result.post
print(f"Tweet with video posted! {post['_id']}")
bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "New product demo",
    "mediaItems": [
      {"type": "video", "url": "https://cdn.example.com/demo.mp4"}
    ],
    "platforms": [
      {"platform": "twitter", "accountId": "YOUR_ACCOUNT_ID"}
    ],
    "publishNow": true
  }'

Tweet with GIF

Only 1 GIF per tweet (it consumes all 4 image slots). Max 15 MB, 1280 x 1080 px. Animated GIFs auto-play in the timeline.

typescript
const { post } = await zernio.posts.createPost({
  content: 'Check out this animation!',
  mediaItems: [
    { type: 'gif', url: 'https://cdn.example.com/animation.gif' }
  ],
  platforms: [
    { platform: 'twitter', accountId: 'YOUR_ACCOUNT_ID' }
  ],
  publishNow: true
});
console.log('Tweet with GIF posted!', post._id);
python
result = client.posts.create_post(
    content="Check out this animation!",
    media_items=[
        {"type": "gif", "url": "https://cdn.example.com/animation.gif"}
    ],
    platforms=[
        {"platform": "twitter", "accountId": "YOUR_ACCOUNT_ID"}
    ],
    publish_now=True
)
post = result.post
print(f"Tweet with GIF posted! {post['_id']}")
bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Check out this animation!",
    "mediaItems": [
      {"type": "gif", "url": "https://cdn.example.com/animation.gif"}
    ],
    "platforms": [
      {"platform": "twitter", "accountId": "YOUR_ACCOUNT_ID"}
    ],
    "publishNow": true
  }'

Thread (Multi-Tweet)

Create Twitter threads with multiple connected tweets using platformSpecificData.threadItems. Each item becomes a reply to the previous tweet and can have its own content and media.

> Note: When threadItems is provided, the top-level content field is used only for display and search purposes, it is NOT published. You must include your first tweet as threadItems[0].

Reply Tweets

Use platformSpecificData.replyToTweetId to publish a tweet as a reply to an existing tweet.

> Note: replyToTweetId cannot be combined with replySettings. For threads, only the first tweet replies to the target; subsequent tweets chain normally.

bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Replying via Zernio API",
    "platforms": [{
      "platform": "twitter",
      "accountId": "YOUR_ACCOUNT_ID",
      "platformSpecificData": {
        "replyToTweetId": "1748391029384756102"
      }
    }],
    "publishNow": true
  }'
typescript
const { post } = await zernio.posts.createPost({
  content: 'Replying via Zernio API',
  platforms: [{
    platform: 'twitter',
    accountId: 'YOUR_ACCOUNT_ID',
    platformSpecificData: {
      replyToTweetId: '1748391029384756102'
    }
  }],
  publishNow: true
});
console.log('Reply posted!', post._id);
python
result = client.posts.create_post(
    content="Replying via Zernio API",
    platforms=[{
        "platform": "twitter",
        "accountId": "YOUR_ACCOUNT_ID",
        "platformSpecificData": {
            "replyToTweetId": "1748391029384756102"
        }
    }],
    publish_now=True
)
post = result.post
print(f"Reply posted! {post['_id']}")

Reply Thread

To reply with a thread, combine replyToTweetId with threadItems. Only the first thread item replies to the target tweet.

bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platforms": [{
      "platform": "twitter",
      "accountId": "YOUR_ACCOUNT_ID",
      "platformSpecificData": {
        "replyToTweetId": "1748391029384756102",
        "threadItems": [
          {"content": "1/ Reply thread: first tweet replies to the target"},
          {"content": "2/ Follow-up tweet in the same thread"}
        ]
      }
    }],
    "publishNow": true
  }'
typescript
const { post } = await zernio.posts.createPost({
  platforms: [{
    platform: 'twitter',
    accountId: 'YOUR_ACCOUNT_ID',
    platformSpecificData: {
      replyToTweetId: '1748391029384756102',
      threadItems: [
        { content: '1/ Reply thread: first tweet replies to the target' },
        { content: '2/ Follow-up tweet in the same thread' }
      ]
    }
  }],
  publishNow: true
});
console.log('Reply thread posted!', post._id);
python
result = client.posts.create_post(
    platforms=[{
        "platform": "twitter",
        "accountId": "YOUR_ACCOUNT_ID",
        "platformSpecificData": {
            "replyToTweetId": "1748391029384756102",
            "threadItems": [
                {"content": "1/ Reply thread: first tweet replies to the target"},
                {"content": "2/ Follow-up tweet in the same thread"}
            ]
        }
    }],
    publish_now=True
)
post = result.post
print(f"Reply thread posted! {post['_id']}")
typescript
const { post } = await zernio.posts.createPost({
  platforms: [{
    platform: 'twitter',
    accountId: 'YOUR_ACCOUNT_ID',
    platformSpecificData: {
      threadItems: [
        {
          content: '1/ Starting a thread about API design',
          mediaItems: [{ type: 'image', url: 'https://cdn.example.com/image1.jpg' }]
        },
        { content: '2/ First, always use proper HTTP methods...' },
        { content: '3/ Second, version your APIs from day one...' },
        { content: '4/ Finally, document everything! /end' }
      ]
    }
  }],
  publishNow: true
});
console.log('Thread posted!', post._id);
python
result = client.posts.create_post(
    platforms=[{
        "platform": "twitter",
        "accountId": "YOUR_ACCOUNT_ID",
        "platformSpecificData": {
            "threadItems": [
                {
                    "content": "1/ Starting a thread about API design",
                    "mediaItems": [{"type": "image", "url": "https://cdn.example.com/image1.jpg"}]
                },
                {"content": "2/ First, always use proper HTTP methods..."},
                {"content": "3/ Second, version your APIs from day one..."},
                {"content": "4/ Finally, document everything! /end"}
            ]
        }
    }],
    publish_now=True
)
post = result.post
print(f"Thread posted! {post['_id']}")
bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platforms": [{
      "platform": "twitter",
      "accountId": "YOUR_ACCOUNT_ID",
      "platformSpecificData": {
        "threadItems": [
          {
            "content": "1/ Starting a thread about API design",
            "mediaItems": [{"type": "image", "url": "https://cdn.example.com/image1.jpg"}]
          },
          {
            "content": "2/ First, always use proper HTTP methods..."
          },
          {
            "content": "3/ Second, version your APIs from day one..."
          },
          {
            "content": "4/ Finally, document everything! /end"
          }
        ]
      }
    }],
    "publishNow": true
  }'

Media Requirements

Images

PropertyRequirement
Max images4 per tweet
FormatsJPEG, PNG, WebP, GIF
Max file size5 MB (images), 15 MB (GIFs)
Min dimensions4 x 4 px
Max dimensions8192 x 8192 px
Recommended1200 x 675 px (16:9)

Aspect Ratios

TypeRatioDimensions
Landscape16:91200 x 675 px
Square1:11200 x 1200 px
Portrait4:51080 x 1350 px

GIFs

PropertyRequirement
Max per tweet1 (consumes all 4 image slots)
Max file size15 MB
Max dimensions1280 x 1080 px
BehaviorAuto-plays in timeline

Videos

PropertyRequirement
Max videos1 per tweet
FormatsMP4, MOV
Max file size512 MB
Max duration140 seconds (2 min 20 sec) standard; up to ~10 minutes with Premium long video (see below)
Min duration0.5 seconds
Min dimensions32 x 32 px
Max dimensions1920 x 1200 px
Frame rate40 fps max
Bitrate25 Mbps max

Long Video Uploads (Premium)

By default, X limits API video uploads to 140 seconds. If the connected X account has an active X Premium subscription, you can enable long video uploads (over 140 seconds) by setting platformSpecificData.longVideo: true.

> Note: Not all Premium accounts have API long-video access, as X may require separate allowlisting.

bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Long video upload (Premium)",
    "mediaItems": [
      {"type": "video", "url": "https://cdn.example.com/long-video.mp4"}
    ],
    "platforms": [{
      "platform": "twitter",
      "accountId": "YOUR_ACCOUNT_ID",
      "platformSpecificData": {
        "longVideo": true
      }
    }],
    "publishNow": true
  }'
typescript
const { post } = await zernio.posts.createPost({
  content: 'Long video upload (Premium)',
  mediaItems: [{ type: 'video', url: 'https://cdn.example.com/long-video.mp4' }],
  platforms: [{
    platform: 'twitter',
    accountId: 'YOUR_ACCOUNT_ID',
    platformSpecificData: {
      longVideo: true
    }
  }],
  publishNow: true
});

console.log('Tweet posted!', post._id);
python
result = client.posts.create_post(
    content="Long video upload (Premium)",
    media_items=[{"type": "video", "url": "https://cdn.example.com/long-video.mp4"}],
    platforms=[{
        "platform": "twitter",
        "accountId": "YOUR_ACCOUNT_ID",
        "platformSpecificData": {"longVideo": True}
    }],
    publish_now=True
)

post = result.post
print(f"Tweet posted! {post['_id']}")
PropertyRecommended
Resolution1280 x 720 px (720p)
Aspect ratio16:9 (landscape) or 1:1 (square)
Frame rate30 fps
CodecH.264
AudioAAC, 128 kbps

Platform-Specific Fields

All fields go inside platformSpecificData on the Twitter platform entry.

FieldTypeDescription
replyToTweetIdstringID of an existing tweet to reply to. The published tweet will appear as a reply in that tweet's thread. For threads, only the first tweet replies to the target; subsequent tweets chain normally.
replySettings"following" | "mentionedUsers" | "subscribers" | "verified"Controls who can reply to the tweet. Omit for default (everyone can reply). For threads, applies to the first tweet only. Cannot be combined with replyToTweetId.
threadItemsArray<{content, mediaItems?}>Complete sequence of tweets in a thread. The first item becomes the root tweet and must be provided as threadItems[0]. When threadItems is provided, top-level content is for display/search only and is NOT published.
pollobjectCreate a poll with this tweet. Mutually exclusive with media attachments and threads.
poll.optionsstring[]Poll options (2-4 choices, max 25 characters each).
poll.duration_minutesnumberPoll duration in minutes (5 min to 7 days).
longVideobooleanEnable long video uploads (over 140 seconds) using amplify_video. Requires X Premium; may require allowlisting.
geoRestrictionobjectRestrict media visibility to specific countries. Only applies when media is attached (ignored for text-only tweets). geoRestriction.countries: array of uppercase ISO 3166-1 alpha-2 codes, max 25. The media is hidden in restricted countries; the tweet text remains visible globally.

Geo-Restriction

Restrict who can see your tweet's media by country. This applies at the media level: the media is hidden for users outside the specified countries, but the tweet text remains visible globally. Requires media to be attached.

json
{
  "platforms": [{
    "platform": "twitter",
    "accountId": "YOUR_ACCOUNT_ID",
    "platformSpecificData": {
      "geoRestriction": {
        "countries": ["US", "ES"]
      }
    }
  }],
  "mediaItems": [{"type": "image", "url": "https://example.com/photo.jpg"}]
}

Polls

Create a Twitter/X poll by providing platformSpecificData.poll.

> Note: Polls are mutually exclusive with mediaItems and platformSpecificData.threadItems.

bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Which feature should we ship next?",
    "platforms": [{
      "platform": "twitter",
      "accountId": "YOUR_ACCOUNT_ID",
      "platformSpecificData": {
        "poll": {
          "options": ["Dark mode", "New analytics", "More integrations"],
          "duration_minutes": 1440
        }
      }
    }],
    "publishNow": true
  }'
typescript
const { post } = await zernio.posts.createPost({
  content: 'Which feature should we ship next?',
  platforms: [{
    platform: 'twitter',
    accountId: 'YOUR_ACCOUNT_ID',
    platformSpecificData: {
      poll: {
        options: ['Dark mode', 'New analytics', 'More integrations'],
        duration_minutes: 1440
      }
    }
  }],
  publishNow: true
});
console.log('Poll posted!', post._id);
python
result = client.posts.create_post(
    content="Which feature should we ship next?",
    platforms=[{
        "platform": "twitter",
        "accountId": "YOUR_ACCOUNT_ID",
        "platformSpecificData": {
            "poll": {
                "options": ["Dark mode", "New analytics", "More integrations"],
                "duration_minutes": 1440
            }
        }
    }],
    publish_now=True
)
post = result.post
print(f"Poll posted! {post['_id']}")

Media URL Requirements

These do not work as media URLs:

  • Google Drive -- returns an HTML download page, not the file
  • Dropbox -- returns an HTML preview page
  • OneDrive / SharePoint -- returns HTML
  • iCloud -- returns HTML

Test your URL in an incognito browser window. If you see a webpage instead of the raw image or video, it will not work.

Media URLs must be:

  • Publicly accessible (no authentication required)
  • Returning actual media bytes with the correct Content-Type header
  • Not behind redirects that resolve to HTML pages
  • Hosted on a fast, reliable CDN

Supabase URLs: Zernio auto-proxies Supabase storage URLs, so they work without additional configuration.

Analytics

> Included — Analytics is bundled with every paid account on the Usage plan.

Available metrics via the Analytics API:

MetricAvailable
Impressions
Likes
Comments
Shares
Clicks
Views
typescript
const analytics = await zernio.analytics.getAnalytics({
  platform: 'twitter',
  fromDate: '2024-01-01',
  toDate: '2024-01-31'
});
console.log(analytics.posts);
python
analytics = client.analytics.get_analytics(
    platform="twitter",
    from_date="2024-01-01",
    to_date="2024-01-31"
)
print(analytics["posts"])
bash
curl "https://zernio.com/api/v1/analytics?platform=twitter&fromDate=2024-01-01&toDate=2024-01-31" \
  -H "Authorization: Bearer YOUR_API_KEY"

Engagement

Retweet, bookmark, and follow directly through the API. All engagement endpoints share a 50 requests per 15-min window rate limit. Retweets also share the 300/3hr creation limit with tweet creation.

typescript
// Retweet
await zernio.twitterengagement.retweetPost({
  accountId: 'YOUR_ACCOUNT_ID',
  tweetId: '1748391029384756102'
});

// Bookmark
await zernio.twitterengagement.bookmarkPost({
  accountId: 'YOUR_ACCOUNT_ID',
  tweetId: '1748391029384756102'
});

// Follow
await zernio.twitterengagement.followUser({
  accountId: 'YOUR_ACCOUNT_ID',
  targetUserId: '123456789'
});
python
# Retweet
client.twitter_engagement.retweet_post(
    account_id="YOUR_ACCOUNT_ID",
    tweet_id="1748391029384756102"
)

# Bookmark
client.twitter_engagement.bookmark_post(
    account_id="YOUR_ACCOUNT_ID",
    tweet_id="1748391029384756102"
)

# Follow
client.twitter_engagement.follow_user(
    account_id="YOUR_ACCOUNT_ID",
    target_user_id="123456789"
)
bash
# Retweet
curl -X POST https://zernio.com/api/v1/twitter/retweet \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"accountId": "YOUR_ACCOUNT_ID", "tweetId": "1748391029384756102"}'

# Bookmark
curl -X POST https://zernio.com/api/v1/twitter/bookmark \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"accountId": "YOUR_ACCOUNT_ID", "tweetId": "1748391029384756102"}'

# Follow
curl -X POST https://zernio.com/api/v1/twitter/follow \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"accountId": "YOUR_ACCOUNT_ID", "targetUserId": "123456789"}'
ActionEndpointUndo
RetweetPOST /v1/twitter/retweetDELETE /v1/twitter/retweet
BookmarkPOST /v1/twitter/bookmarkDELETE /v1/twitter/bookmark
FollowPOST /v1/twitter/followDELETE /v1/twitter/follow

See Twitter Engagement API Reference for full endpoint documentation.

Edit Published Tweets

Zernio supports editing published tweets via X's edit feature.

> Note: Editing is currently supported for X (Twitter) only, requires an active X Premium subscription on the connected account, must be within 1 hour of the original publish time, is limited to 5 edits per tweet (enforced by X), and supports text-only edits (media changes are not supported).

bash
curl -X POST https://zernio.com/api/v1/posts/YOUR_POST_ID/edit \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "twitter",
    "content": "Updated tweet text with corrected information"
  }'
typescript
const result = await zernio.posts.editPost({
  postId: 'YOUR_POST_ID',
  platform: 'twitter',
  content: 'Updated tweet text with corrected information'
});

console.log('Edited tweet:', result.id, result.url);
python
result = client.posts.edit_post(
    post_id="YOUR_POST_ID",
    platform="twitter",
    content="Updated tweet text with corrected information"
)

print("Edited tweet:", result["id"], result["url"])

What You Can't Do

These features are not available through Twitter's API:

  • Create Spaces
  • Post to Communities
  • Pin tweets to profile
  • Add Twitter Cards (must be configured on the destination URL via meta tags)
  • Post as a personal DM broadcast

Common Errors

Twitter/X has a 21.3% failure rate across Zernio's platform (17,385 failures out of 81,796 attempts). Here are the most frequent errors and how to fix them:

ErrorWhat it meansHow to fix
"Tweet text is too long (X characters). Twitter's limit is 280 characters. Note: URLs count as 23 characters."Exceeds 280 character limit for free accountsShorten text or use customContent for Twitter. Remember: URLs = 23 chars, emojis = 2 chars.
"X (Twitter) does not allow duplicate tweets"Same or very similar content was already postedModify the text, even slightly.
"Rate limit hit. Please wait 10 minutes"Zernio's velocity limit was triggeredReduce posting frequency. Space posts at least 4 minutes apart.
"Missing tweet.write scope" / "forbidden"OAuth token lacks required permissionsReconnect the account with all required scopes.
Token expiredOAuth access was revoked or expiredReconnect the account. Subscribe to the account.disconnected webhook to catch this proactively.

Inbox

> Included — Inbox (DMs, comments, reviews) is bundled with every paid account on the Usage plan.

Twitter/X supports DMs and comments through the unified Inbox API.

Direct Messages

FeatureSupported
List conversations
Fetch messages
Send text messages
Send attachments✅ (images, videos - max 25 MB)
Archive/unarchive

Create conversation (send a DM)

Use POST /v1/inbox/conversations to start a new DM conversation (or append to an existing thread).

> Note: X DM write endpoints require X API Pro tier ($5,000/month) or Enterprise access. This applies to BYOK users who provide their own X API credentials.

> Note: By default, Zernio checks DM eligibility (recipient receives_your_dm). If the recipient does not accept DMs, the API returns 422 with code DM_NOT_ALLOWED. You can skip this check with skipDmCheck: true.

bash
curl -X POST https://zernio.com/api/v1/inbox/conversations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "YOUR_ACCOUNT_ID",
    "participantUsername": "targetuser",
    "message": "Hello from Zernio Inbox API!"
  }'
typescript
const result = await zernio.messages.createInboxConversation({
  accountId: 'YOUR_ACCOUNT_ID',
  participantUsername: 'targetuser',
  message: 'Hello from Zernio Inbox API!'
});

console.log('DM sent:', result.data.conversationId, result.data.messageId);
python
result = client.messages.create_inbox_conversation(
    account_id="YOUR_ACCOUNT_ID",
    participant_username="targetuser",
    message="Hello from Zernio Inbox API!"
)

print("DM sent:", result["data"]["conversationId"], result["data"]["messageId"])

Comments

FeatureSupported
List comments on posts
Post new comment
Reply to comments
Delete comments
Like/unlike comments
Hide/unhide comments

Limitations

  • Encrypted X Chat DMs not accessible - X has replaced traditional DMs with end-to-end encrypted "X Chat" for many accounts. Messages sent or received through encrypted X Chat are not returned by X's API. This means some conversations may show only outgoing messages or appear empty. This is an X platform limitation that affects all third-party applications.
  • DM permissions - DMs require dm.read and dm.write scopes
  • Reply search - Uses cached conversation threads (2-min TTL) to manage rate limits
  • Cached DMs - Conversations are cached with a 15-min TTL

See Messages and Comments API Reference for endpoint details.

  • Connect Twitter Account - OAuth flow
  • Create Post - Post creation and scheduling
  • Upload Media - Image and video uploads
  • Analytics - Post performance metrics
  • Twitter Engagement - Retweet, bookmark, follow
  • Messages and Comments