OpenTweet X Poster
You can post to X (Twitter) using the OpenTweet REST API. All requests go to https://opentweet.io with the user's API key.
Authentication
Every request needs this header:
Authorization: Bearer $OPENTWEET_API_KEY
Content-Type: application/json
For file uploads, use Content-Type: multipart/form-data instead.
Before You Start
ALWAYS verify the connection first:
GET https://opentweet.io/api/v1/me
Returns subscription status, daily post limits, post counts, and connected X accounts. Check subscription.has_access is true and limits.remaining_posts_today > 0 before scheduling or publishing.
Multi-Account Support
Pro users get 1 X account, Advanced 3, Agency 10. Use the x_account_id parameter to target a specific account.
List connected accounts
GET https://opentweet.io/api/v1/accounts
Returns: { "accounts": [{ "id": "...", "x_handle": "@handle", "x_name": "Display Name", "is_primary": true, "nickname": null }] }
Using x_account_id
Add x_account_id to any POST/PUT body or GET query parameter to target a specific X account:
- Creating posts:
{ "text": "...", "x_account_id": "account_id_here" } - Listing posts:
GET /api/v1/posts?x_account_id=account_id_here - Batch schedule:
{ "schedules": [...], "x_account_id": "account_id_here" } - Analytics:
GET /api/v1/analytics/overview?x_account_id=account_id_here - Evergreen:
GET /api/v1/evergreen/posts?x_account_id=account_id_here - Best-times analyze:
POST /api/v1/analytics/best-times/analyzebody{ "x_account_id": "..." }
When x_account_id is omitted, the primary account is used. Single-account users never need to specify it.
Post Management
Create a tweet
POST https://opentweet.io/api/v1/posts
Body: { "text": "Your tweet text" }
Optionally add "scheduled_date": "2026-05-01T10:00:00Z" to schedule it (requires active subscription, date must be in the future).
Create and publish immediately (one step)
POST https://opentweet.io/api/v1/posts
Body: { "text": "Hello from the API!", "publish_now": true }
Creates the post AND publishes to X in one request. Cannot combine with scheduled_date or bulk posts. Response includes status: "posted", x_post_id, and url (the real X post URL) on success.
Create a tweet with media
POST https://opentweet.io/api/v1/posts
Body: {
"text": "Check out this screenshot!",
"media_urls": ["https://url-from-upload-endpoint"]
}
Upload media first via POST /api/v1/upload, then pass the returned URL(s) in media_urls.
Create a thread
POST https://opentweet.io/api/v1/posts
Body: {
"text": "First tweet of the thread",
"is_thread": true,
"thread_tweets": ["Second tweet", "Third tweet"]
}
Create a thread with per-tweet media
POST https://opentweet.io/api/v1/posts
Body: {
"text": "Thread intro with image",
"is_thread": true,
"thread_tweets": ["Second tweet", "Third tweet"],
"media_urls": ["https://intro-image-url"],
"thread_media": [["https://img-for-tweet-2"], []]
}
thread_media is an array of arrays. Each inner array contains media URLs for the corresponding tweet in thread_tweets. Use [] for tweets with no media.
Post to an X Community
POST https://opentweet.io/api/v1/posts
Body: {
"text": "Shared with the community!",
"community_id": "1234567890",
"share_with_followers": true
}
Auto-retweet a post
POST https://opentweet.io/api/v1/posts
Body: {
"text": "This will get a boost.",
"scheduled_date": "2026-05-01T10:00:00Z",
"auto_retweet_enabled": true,
"auto_retweet_offset_minutes": 240
}
After the post publishes, OpenTweet automatically retweets it from the same account auto_retweet_offset_minutes later. Works on PUT too. Range: 1–10080 minutes (up to 7 days). Both fields can also be set via PUT /api/v1/posts/{id}.
Bulk create (up to 50 posts)
POST https://opentweet.io/api/v1/posts
Body: {
"posts": [
{ "text": "Tweet 1", "scheduled_date": "2026-05-01T10:00:00Z" },
{ "text": "Tweet 2", "scheduled_date": "2026-05-01T14:00:00Z" }
]
}
Schedule a post
POST https://opentweet.io/api/v1/posts/{id}/schedule
Body: { "scheduled_date": "2026-05-01T10:00:00Z" }
The date must be in the future. Use ISO 8601 format.
Publish immediately
POST https://opentweet.io/api/v1/posts/{id}/publish
No body needed. Posts to X right now. Response includes status: "posted", x_post_id, and url (the real X post URL).
Batch schedule (up to 50 posts)
POST https://opentweet.io/api/v1/posts/batch-schedule
Body: {
"schedules": [
{ "post_id": "id1", "scheduled_date": "2026-05-02T09:00:00Z" },
{ "post_id": "id2", "scheduled_date": "2026-05-03T14:00:00Z" }
],
"community_id": "optional-community-id",
"share_with_followers": true,
"x_account_id": "optional-account-id"
}
List posts
GET https://opentweet.io/api/v1/posts?status=scheduled&page=1&limit=20
Status options: scheduled, posted, draft, failed, evergreen (returns evergreen pool source posts).
Get a post
GET https://opentweet.io/api/v1/posts/{id}
Update a post
PUT https://opentweet.io/api/v1/posts/{id}
Body: {
"text": "Updated text",
"media_urls": ["https://..."],
"scheduled_date": "2026-05-01T10:00:00Z",
"auto_retweet_enabled": true,
"auto_retweet_offset_minutes": 120
}
All fields optional. Cannot update already-published posts. Set scheduled_date to null to unschedule (convert back to draft).
Delete a post
DELETE https://opentweet.io/api/v1/posts/{id}
Default: if the post was already published, OpenTweet also deletes it from X. To delete only locally and leave the X post live, append ?delete_from_x=false. Response includes x_deleted and (if it failed) x_delete_error.
Media Upload
Upload an image or video
POST https://opentweet.io/api/v1/upload
Content-Type: multipart/form-data
Body: file=@your-image.png
Returns: { "url": "https://..." }
Supported formats: JPG, PNG, GIF, WebP (max 5MB), MP4, MOV (max 20MB).
Workflow: Upload first, then use the returned URL in media_urls or thread_media when creating/updating posts.
Evergreen Queue
The evergreen queue keeps a pool of timeless tweets and republishes them on a schedule with cooldown gaps so the same post doesn't repeat too often. Source posts stay as templates; the scheduler clones them as regular posts at the configured times. Requires an active paid subscription (not available on trial). Pro: 10 pool / 2 per day. Advanced: 999 pool / 10 per day.
Get queue settings + pool stats
GET https://opentweet.io/api/v1/evergreen/settings
Returns: enabled, posts_per_day, posting_times (["09:00","17:00"]), default_cooldown_days, plus pool counts and your plan limits.
Update queue settings
PUT https://opentweet.io/api/v1/evergreen/settings
Body: {
"enabled": true,
"posts_per_day": 2,
"posting_times": ["09:00", "17:00"],
"default_cooldown_days": 14
}
All fields optional. posting_times must be "HH:mm" strings. default_cooldown_days is 1–90. posts_per_day capped to your plan's daily limit.
List evergreen pool
GET https://opentweet.io/api/v1/evergreen/posts?page=1&limit=20&paused=false
Filter paused=true or paused=false. Each item includes cooldown_days, last_posted_at, times_posted, paused.
Add to evergreen pool
Mode 1 — convert an existing post:
POST https://opentweet.io/api/v1/evergreen/posts
Body: { "post_id": "507f1f77bcf86cd799439011", "cooldown_days": 14 }
Mode 2 — create a new evergreen post directly:
POST https://opentweet.io/api/v1/evergreen/posts
Body: {
"text": "Timeless tweet text",
"category": "Tips",
"cooldown_days": 21,
"is_thread": false,
"media_urls": ["https://..."]
}
Get / update / remove an evergreen post
GET https://opentweet.io/api/v1/evergreen/posts/{id}
PUT https://opentweet.io/api/v1/evergreen/posts/{id} # body: { "cooldown_days": 30, "paused": true }
DELETE https://opentweet.io/api/v1/evergreen/posts/{id} # converts back to a draft (does not hard-delete)
GET also returns recent_posts — the last 5 published clones with their X URLs.
Evergreen publish history
GET https://opentweet.io/api/v1/evergreen/history?page=1&limit=20&source_id=optional
Lists published clones. Filter by source_id to see the history of a single evergreen post.
Inspiration (Search + Repurpose)
Search X for tweets and have AI rewrite them in the user's voice. Both endpoints require an active subscription. Search has a daily cap (Pro: 50/day, Advanced: 200/day, trial: 2/day). Repurpose counts against the AI generation daily quota.
Search inspiration tweets
GET https://opentweet.io/api/v1/inspiration/search?q=AI%20agents&max_results=20&sort_order=relevancy&lang=en&has_media=true&min_likes=100&min_retweets=10
Required: q. Optional filters: max_results, sort_order (relevancy or recency), lang, has_media, min_likes, min_retweets. Response includes data (tweets), meta.result_count, and usage (searches_used / remaining / daily_limit).
Repurpose a tweet with AI
POST https://opentweet.io/api/v1/inspiration/repurpose
Body: {
"tweet_text": "Original tweet text to remix",
"tweet_author": "@someone",
"instructions": "Make it punchier and add a call to action",
"tone": "casual",
"save_as_draft": true
}
Returns repurposed.text, category, key_topics, plus draft.id when save_as_draft is true (default). Honors the user's voice profile and content pillars automatically. Optional x_account_id tags the saved draft.
Analytics
Account overview
GET https://opentweet.io/api/v1/analytics/overview
Returns posting stats (total posts, publishing rate, active days, avg posts/week, most active day/hour, threads, media posts), streaks (current, longest), trends (this week vs last, this month vs last, best month), category breakdown, and recent activity (daily counts for last 7 and 30 days).
Tweet engagement metrics (Advanced plan only)
GET https://opentweet.io/api/v1/analytics/tweets?period=30
Returns per-tweet engagement: likes, retweets, replies, quotes, impressions, bookmarks, engagement rate. Also includes top/worst performers, content type stats, engagement timeline, and best hours/days. Period: 7-365 days or "all".
Best posting times
GET https://opentweet.io/api/v1/analytics/best-times
Two analysis modes:
engagement_weighted— uses real per-tweet engagement to score every hour×day cell. Returnsheatmap,confidence,top_windows,best_day,best_hour,worst_day,worst_hour,insights. Only available after running an analysis.frequency_only— fallback based purely on when the user has posted. Returned when no engagement profile exists yet (needs ≥3 published posts).
Both modes also return legacy hour_distribution, day_distribution, best_hours, best_days keys for backward compatibility.
Trigger fresh best-times analysis
POST https://opentweet.io/api/v1/analytics/best-times/analyze
Body: {} # optional: { "x_account_id": "..." }
Pulls the user's recent published tweets from X, computes engagement-weighted windows, and stores the profile. Has a built-in cooldown — if a recent analysis is still fresh, returns 429 with next_available_at. Returns success, profile (status ready / analyzing / insufficient_posts).
Common Workflows
First: verify your connection works:
GET /api/v1/me— checkauthenticatedis true,subscription.has_accessis true
Post a tweet right now (one step):
GET /api/v1/me— checklimits.can_postis truePOST /api/v1/postswith{ "text": "...", "publish_now": true }
Post a tweet with an image:
GET /api/v1/me— check limits- Upload:
POST /api/v1/uploadwith the image file — get back a URL - Create + publish:
POST /api/v1/postswith{ "text": "...", "media_urls": ["<url>"], "publish_now": true }
Schedule a tweet:
GET /api/v1/me— checklimits.remaining_posts_today> 0POST /api/v1/postswith{ "text": "...", "scheduled_date": "2026-05-01T10:00:00Z" }— you MUST make this HTTP call- Read the response JSON — confirm
posts[0].status === "scheduled"and show the user theidandscheduled_datefrom the response
Schedule a tweet with auto-retweet boost:
GET /api/v1/me— checklimits.remaining_posts_today> 0POST /api/v1/postswith text,scheduled_date,auto_retweet_enabled: true,auto_retweet_offset_minutes: 240- Show the user the
idandscheduled_datefrom the response
Schedule a week of content:
GET /api/v1/me— check remaining limit- Bulk create:
POST /api/v1/postswith"posts": [...]array, each with a scheduled_date - Show the user the list of created post IDs and their scheduled dates from the response
Find inspiration and repurpose it:
GET /api/v1/inspiration/search?q=...&min_likes=500— pick a tweetPOST /api/v1/inspiration/repurposewithtweet_text,tweet_author,save_as_draft: true- The saved draft's
idcan then be scheduled withPOST /api/v1/posts/{id}/schedule
Set up an evergreen queue from existing drafts:
PUT /api/v1/evergreen/settingswith{ "enabled": true, "posts_per_day": 2, "posting_times": ["09:00","17:00"] }- For each draft to recycle:
POST /api/v1/evergreen/postswith{ "post_id": "...", "cooldown_days": 14 } GET /api/v1/evergreen/historylater to see what got published
Tune posting times based on engagement:
POST /api/v1/analytics/best-times/analyze— wait forprofile.status: "ready"(poll ifanalyzing)GET /api/v1/analytics/best-times— readtop_windowsandbest_hour/best_day- Schedule new posts at the suggested times
Important Rules
- ALWAYS call
GET /api/v1/mebefore scheduling or publishing to check limits and connected accounts. - For multi-account users, call
GET /api/v1/accountsand passx_account_idto target a specific account. - CRITICAL: You MUST make the actual HTTP API call for every operation. Never skip the call and generate a response from memory or context.
- CRITICAL: Always parse and use the ACTUAL JSON response from the API. Never fabricate or assume response values.
- CRITICAL: A 4xx or 5xx HTTP status means the operation FAILED — never report success to the user on an error response.
- CRITICAL: After scheduling a post, always show the user the
idfield from the API response. If you cannot show a real 24-character MongoDB ObjectId from the response, the call was not made. - Post IDs are always 24-character MongoDB ObjectIds (e.g. "507f1f77bcf86cd799439011"), never short strings.
- Every post response includes a
statusfield: "draft", "scheduled", "posted", or "failed". - Published posts include a
urlfield with the real X post URL. Always use this URL — never construct your own. - To verify a post was published, check:
statusis "posted" ANDurlis present. - To verify a post was scheduled, check:
statusis "scheduled" ANDscheduled_dateis present in the response. - Tweet max length: 280 characters (per tweet in a thread).
- Bulk limit: 50 posts per request (create or batch-schedule).
- Rate limit: 60 requests/minute, 1,000/day (Pro); 300/min, 10,000/day (Advanced).
- Dates must be ISO 8601 and in the future — past dates are rejected.
- Active subscription required to schedule, publish, use evergreen, search inspiration, or repurpose. Creating drafts is free.
- Including
scheduled_dateorpublish_nowinPOST /api/v1/postsrequires a subscription. - Upload media before creating posts — use the returned URL in
media_urlsorthread_media. - Media limits: 5MB for images (JPG, PNG, GIF, WebP), 20MB for videos (MP4, MOV).
- URL-containing posts have a separate, plan-based daily cap. A 429 with a
urlLimitpayload means the post was saved as a draft instead of published. - Tweet engagement analytics require the Advanced plan (returns 403 on Pro).
- Evergreen queue is not available during trial.
auto_retweet_offset_minutesmust be 1–10080 (up to 7 days) whenauto_retweet_enabledis true.- 403 = no subscription / X not connected, 429 = rate limit, daily post limit, URL post cap, or evergreen pool full.
- Check response status codes: 201=created, 200=success, 4xx=client error, 5xx=server error.
Safety Guardrails
Publishing is irreversible — once a tweet is posted to X it cannot be undone via the API (DELETE removes it locally and from X, but reposts are not the same tweet).
Confirm before publishing
- Before calling
/publishor usingpublish_now: true, always tell the user which post(s) you are about to publish and ask for confirmation. - Show the tweet text (truncated if long) and the post ID so the user can verify.
Scheduled posts ≠ ready to publish
- If a post has a
scheduled_datein the future, it is meant to be published at that time by the scheduler — not right now. - NEVER call
/publishon a post that has a futurescheduled_dateunless the user explicitly asks you to publish it immediately. - When the user asks to "publish" posts, clarify whether they want to publish NOW or schedule for later. Default to scheduling if dates are provided.
Evergreen sources are not regular drafts
- A post with
isEvergreen: trueis a recurring template. The scheduler publishes clones, not the source itself. - NEVER call
/publishdirectly on an evergreen source post (the API will reject it). Add it to the queue withPOST /api/v1/evergreen/postsand let the scheduler run.
Batch operations — go slow
- When creating or scheduling more than 5 posts, summarize the batch (count, date range, first/last tweet previews) and ask the user to confirm before proceeding.
- Never bulk-create AND immediately publish in one go. Create as drafts or scheduled posts first, let the user review, then publish only on confirmation.
- When using batch-schedule, show the user the list of dates before sending the request.
Don't loop publish calls
- Never loop through a list of posts calling
/publishon each one without explicit user approval for the full list. - If the user asks to "publish all my drafts" or similar, list them first and get confirmation.
AI-generated content needs a review pass
- Before saving repurposed tweets as auto-scheduled posts, show the user the AI output and let them edit. The repurpose endpoint already saves to drafts by default — keep
save_as_draft: trueunless the user has reviewed.
Full API docs
For complete documentation: https://opentweet.io/api/v1/docs