Install
openclaw skills install @petricbranko/opentweet-x-posterPost to X (Twitter) using the OpenTweet API. Create tweets, schedule posts, publish threads, write and schedule long-form X Articles from markdown, upload media, run an evergreen queue, search inspiration tweets, repurpose them with AI, run AI-qualified DM outreach campaigns with human-approved sending, and read engagement-weighted analytics, all autonomously.
openclaw skills install @petricbranko/opentweet-x-posterYou can post to X (Twitter) using the OpenTweet REST API. All requests go to https://opentweet.io with the user's API key.
Every request needs this header:
Authorization: Bearer $OPENTWEET_API_KEY
Content-Type: application/json
For file uploads, use Content-Type: multipart/form-data instead.
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.
Pro users get 1 X account, Advanced 3, Agency 10. Use the x_account_id parameter to target a specific account.
GET https://opentweet.io/api/v1/accounts
Returns: { "accounts": [{ "id": "...", "x_handle": "@handle", "x_name": "Display Name", "is_primary": true, "nickname": null }] }
Add x_account_id to any POST/PUT body or GET query parameter to target a specific X account:
{ "text": "...", "x_account_id": "account_id_here" }GET /api/v1/posts?x_account_id=account_id_here{ "schedules": [...], "x_account_id": "account_id_here" }GET /api/v1/analytics/overview?x_account_id=account_id_hereGET /api/v1/evergreen/posts?x_account_id=account_id_herePOST /api/v1/analytics/best-times/analyze body { "x_account_id": "..." }When x_account_id is omitted, the primary account is used. Single-account users never need to specify it.
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).
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.
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.
POST https://opentweet.io/api/v1/posts
Body: {
"text": "First tweet of the thread",
"is_thread": true,
"thread_tweets": ["Second tweet", "Third tweet"]
}
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 https://opentweet.io/api/v1/posts
Body: {
"text": "Shared with the community!",
"community_id": "1234567890",
"share_with_followers": true
}
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}.
POST https://opentweet.io/api/v1/posts
Body: {
"text": "Hot take about AI agents.",
"scheduled_date": "2026-05-01T10:00:00Z",
"auto_plug_enabled": true,
"auto_plug_threshold": 50,
"auto_plug_text": "Enjoyed this? I share more every week → link.com/newsletter"
}
After the post publishes, OpenTweet checks its like count every 5 minutes. When like_count >= auto_plug_threshold, it automatically posts auto_plug_text as a reply to the original tweet — turning viral reach into subscribers or leads.
auto_plug_threshold — likes needed to trigger (default: 20, no upper limit)auto_plug_text — the reply content, max 280 chars (required when auto_plug_enabled: true)auto_plug_done: true and auto_plug_tweet_id are set after sendingPUT /api/v1/posts/{id} too (set before or after publishing)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" }
]
}
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.
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).
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"
}
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 https://opentweet.io/api/v1/posts/{id}
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 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.
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.
Generate images and videos with Grok Imagine (xAI) directly from a prompt. The generated file is permanently stored and the URL can be used in media_urls when creating a tweet. Requires XAI_API_KEY to be configured on the server.
POST https://opentweet.io/api/v1/generate/image
Body: {
"prompt": "A vibrant product launch announcement graphic",
"aspect_ratio": "16:9",
"resolution": "1k"
}
prompt — required, max 1000 charsaspect_ratio — optional: "1:1" (default), "16:9", "9:16", "4:3", "3:4"resolution — optional: "1k" (default) or "2k"Returns: { "url": "https://...", "prompt": "...", "aspect_ratio": "16:9", "resolution": "1k" }
Response is synchronous — the URL is ready to use immediately.
POST https://opentweet.io/api/v1/generate/video
Body: {
"prompt": "A product spinning on a pedestal with dramatic lighting",
"aspect_ratio": "16:9",
"resolution": "480p",
"duration": 5
}
prompt — required, max 1000 charsaspect_ratio — optional: "16:9" (default), "9:16", "1:1", "4:3", "3:4"resolution — optional: "480p" (default) or "720p"duration — optional: 5 (default) to 10 secondsReturns (202): { "job_id": "...", "status": "processing", "message": "..." }
Video generation is asynchronous — poll the status endpoint until complete.
GET https://opentweet.io/api/v1/generate/video/{job_id}
Returns one of:
{ "job_id": "...", "status": "processing" } — still generating, poll again in 10 seconds{ "job_id": "...", "status": "completed", "url": "https://..." } — ready, use the URL{ "job_id": "...", "status": "failed", "error": "..." } — generation failedTypical generation time: 1–3 minutes. Poll every 10 seconds.
Write and publish long-form X Articles from markdown. OpenTweet converts markdown to X's rich-text article format at publish time: #/##/### headings, bold, italic, strikethrough, links, bullet/numbered lists, > quotes, inline images via  (uploaded to X automatically), a cover image, and embedded posts (an x.com status URL on its own line). Fenced code blocks are preserved as plain text.
Requires an active paid subscription (not trial). The connected X account must have X Premium (any tier since January 2026) to publish Articles — if not, publishing fails and failed_reason carries X's exact error.
Article statuses: draft, scheduled, publishing, published, failed.
POST https://opentweet.io/api/v1/articles
Body: {
"title": "How We Grew to 10k Users",
"content_markdown": "# The short version\n\nWe shipped **every week**...",
"cover_image_url": "https://url-from-upload-endpoint",
"scheduled_date": "2026-07-15T14:00:00Z"
}
title and content_markdown are required.scheduled_date schedules it; "publish_now": true publishes immediately (mutually exclusive with scheduled_date).x_account_id for multi-account users.Returns 201 with the article object. A published article includes article_url (the live X link) and x_post_id.
GET https://opentweet.io/api/v1/articles?status=scheduled&page=1&limit=20
Status filter options: draft, scheduled, publishing, published, failed.
GET https://opentweet.io/api/v1/articles/{id}
PUT https://opentweet.io/api/v1/articles/{id} # body: any of title, content_markdown, cover_image_url, scheduled_date, x_account_id
DELETE https://opentweet.io/api/v1/articles/{id}
Published articles cannot be edited. Set scheduled_date to null to unschedule back to draft. Updating a failed article resets it to draft/scheduled.
POST https://opentweet.io/api/v1/articles/{id}/publish
No body needed. Returns the article with status: "published" and article_url, or 502 with code: "publish_failed" and X's error in failed_reason.
Run outreach campaigns that find qualified X profiles, qualify them with AI, draft a personalized DM for each, and send only after a human approves. This is not a mass messenger: sending is human-in-the-loop, drip-scheduled, rate-limited, and honors opt-outs. Available on the Advanced and Agency plans only (403 on Pro). Requires a paid API key (not the free taste key).
How a campaign flows: you create it with an ideal-customer description and one or more lead sources, discovery finds and AI-qualifies candidates into leads, you review and approve the drafted messages, then the sender drips approved DMs from the connected X account within the campaign's caps. Replies are tracked and tagged, and opt-outs are auto-suppressed. Actual sending is gated server-side and paced, so approving a lead queues it, it does not fire instantly.
POST https://opentweet.io/api/v1/dm-campaigns
Body: {
"name": "Shopify founders Q3",
"x_account_id": "account_id_here",
"sources": [
{ "type": "search", "value": "shopify analytics", "max_candidates": 500 },
{ "type": "followers_of", "value": "somecompetitor", "max_candidates": 500 },
{ "type": "tweet_engagers", "value": "https://x.com/user/status/123", "max_candidates": 300 },
{ "type": "own_followers", "value": "", "max_candidates": 500 }
],
"icp_prompt": "Shopify store owners or ecommerce founders, 200-20k followers, English, active in the last 2 weeks, complaining about ad costs or analytics. Not agencies.",
"score_threshold": 60,
"message_brief": { "offer": "a $29/mo analytics tool for Shopify", "tone": "casual", "cta": "worth a look?", "follow_up_enabled": false },
"limits": { "dms_per_day": 25, "active_hours_start": 9, "active_hours_end": 18, "timezone": "America/New_York" },
"accept_policy": true
}
accept_policy must be true (it records acceptance of X's acceptable-use terms; the connected account bears the outreach risk). Returns { "campaign": {...} }. Source types: search (keyword query), followers_of (a handle), tweet_engagers (a tweet URL), own_followers.
GET https://opentweet.io/api/v1/dm-campaigns
Returns { "campaigns": [...] }, each with a stats funnel (found, qualified, approved, sent, replied).
GET https://opentweet.io/api/v1/dm-campaigns/{id}
POST https://opentweet.io/api/v1/dm-campaigns/{id}/discover
Sets the campaign to discovering and returns immediately. Discovery and AI qualification run in the background, then the campaign moves to reviewing with leads ready. Poll the campaign until its status is reviewing.
GET https://opentweet.io/api/v1/dm-campaigns/{id}/leads?state=qualified&page=1
State filter includes qualified (awaiting approval), approved, sent, replied, skipped. Each lead has a score, a reason, an angle_hook, and a drafted final_message.
PATCH https://opentweet.io/api/v1/dm-campaigns/{id}/leads/{leadId}
Body: { "action": "approve", "final_message": "optional edited message" }
action is approve (queues the drafted or provided message for drip sending), skip, or edit (change the draft without approving). Approving does NOT send immediately; the sender paces it within the campaign's daily cap and active hours.
GET https://opentweet.io/api/v1/dm-campaigns/{id}/inbox
Returns { "conversations": [{ "lead": {...}, "reply_tag": "interested|question|not_interested|opt_out", "events": [...] }] }. Opt-outs are auto-added to the do-not-contact list.
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 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.
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.
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.
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 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.
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.
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.
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).
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.
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).
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".
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. Returns heatmap, 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.
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).
First: verify your connection works:
GET /api/v1/me — check authenticated is true, subscription.has_access is truePost a tweet right now (one step):
GET /api/v1/me — check limits.can_post is truePOST /api/v1/posts with { "text": "...", "publish_now": true }Post a tweet with an image:
GET /api/v1/me — check limitsPOST /api/v1/upload with the image file — get back a URLPOST /api/v1/posts with { "text": "...", "media_urls": ["<url>"], "publish_now": true }Schedule a tweet:
GET /api/v1/me — check limits.remaining_posts_today > 0POST /api/v1/posts with { "text": "...", "scheduled_date": "2026-05-01T10:00:00Z" } — you MUST make this HTTP callposts[0].status === "scheduled" and show the user the id and scheduled_date from the responseSchedule a tweet with auto-retweet boost:
GET /api/v1/me — check limits.remaining_posts_today > 0POST /api/v1/posts with text, scheduled_date, auto_retweet_enabled: true, auto_retweet_offset_minutes: 240id and scheduled_date from the responseSchedule a tweet with auto-plug (monetise viral reach):
GET /api/v1/me — check limits.remaining_posts_today > 0POST /api/v1/posts with text, scheduled_date, auto_plug_enabled: true, auto_plug_threshold: 50, auto_plug_text: "..."id and confirm auto-plug is armedSchedule a week of content:
GET /api/v1/me — check remaining limitPOST /api/v1/posts with "posts": [...] array, each with a scheduled_datePost a tweet with an AI-generated image:
GET /api/v1/me — check limitsPOST /api/v1/generate/image with { "prompt": "...", "aspect_ratio": "16:9" } — get back a URL immediatelyPOST /api/v1/posts with { "text": "...", "media_urls": ["<url from step 2>"], "publish_now": true }Post a tweet with an AI-generated video:
GET /api/v1/me — check limitsPOST /api/v1/generate/video with { "prompt": "...", "aspect_ratio": "16:9", "duration": 5 } — get back a job_idGET /api/v1/generate/video/{job_id} every 10 seconds until status is "completed" — get the urlPOST /api/v1/posts with { "text": "...", "media_urls": ["<url from step 3>"], "publish_now": true }Write and schedule a long-form X Article:
GET /api/v1/me — check subscription.has_access is true (articles need a paid plan)POST /api/v1/upload with an image — get back a URLPOST /api/v1/articles with { "title": "...", "content_markdown": "# ...", "cover_image_url": "<url>", "scheduled_date": "2026-07-15T14:00:00Z" }status: "scheduled" and show the user the article idGET /api/v1/articles/{id} — article_url is the live X linkRun a DM outreach campaign (human-approved):
GET /api/v1/me — confirm the plan is Advanced or Agency (DM Campaigns return 403 otherwise)POST /api/v1/dm-campaigns with the ICP, sources, message brief, limits, and accept_policy: truePOST /api/v1/dm-campaigns/{id}/discover, then poll GET /api/v1/dm-campaigns/{id} until status is reviewingGET /api/v1/dm-campaigns/{id}/leads?state=qualified — show the user the qualified leads and their drafted messagesPATCH .../leads/{leadId} with action: approve. Never auto-approve every lead.GET /api/v1/dm-campaigns/{id}/inbox later to read repliesFind inspiration and repurpose it:
GET /api/v1/inspiration/search?q=...&min_likes=500 — pick a tweetPOST /api/v1/inspiration/repurpose with tweet_text, tweet_author, save_as_draft: trueid can then be scheduled with POST /api/v1/posts/{id}/scheduleSet up an evergreen queue from existing drafts:
PUT /api/v1/evergreen/settings with { "enabled": true, "posts_per_day": 2, "posting_times": ["09:00","17:00"] }POST /api/v1/evergreen/posts with { "post_id": "...", "cooldown_days": 14 }GET /api/v1/evergreen/history later to see what got publishedTune posting times based on engagement:
POST /api/v1/analytics/best-times/analyze — wait for profile.status: "ready" (poll if analyzing)GET /api/v1/analytics/best-times — read top_windows and best_hour / best_dayGET /api/v1/me before scheduling or publishing to check limits and connected accounts.GET /api/v1/accounts and pass x_account_id to target a specific account.id field from the API response. If you cannot show a real 24-character MongoDB ObjectId from the response, the call was not made.status field: "draft", "scheduled", "posted", or "failed".url field with the real X post URL. Always use this URL — never construct your own.status is "posted" AND url is present.status is "scheduled" AND scheduled_date is present in the response.scheduled_date or publish_now in POST /api/v1/posts requires a subscription.media_urls or thread_media.urlLimit payload means the post was saved as a draft instead of published.code: "publish_failed" carries X's exact rejection reason.auto_retweet_offset_minutes must be 1–10080 (up to 7 days) when auto_retweet_enabled is true.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).
/publish or using publish_now: true, always tell the user which post(s) you are about to publish and ask for confirmation.POST /api/v1/articles/{id}/publish or publish_now: true on an article, show the user the title and ask for confirmation. A published article cannot be edited.scheduled_date in the future, it is meant to be published at that time by the scheduler — not right now./publish on a post that has a future scheduled_date unless the user explicitly asks you to publish it immediately.isEvergreen: true is a recurring template. The scheduler publishes clones, not the source itself./publish directly on an evergreen source post (the API will reject it). Add it to the queue with POST /api/v1/evergreen/posts and let the scheduler run./publish on each one without explicit user approval for the full list.save_as_draft: true unless the user has reviewed.For complete documentation: https://opentweet.io/api/v1/docs