API Reference

OpenTweet API Docs

Create, schedule, publish posts and access analytics for X/Twitter programmatically. Simple REST API with Bearer token auth.

Quick Start

Base URL

https://opentweet.io

Authentication

All endpoints require an API key. Get yours from . You can pass the key in either header:

bash
# Option 1: Authorization header
curl -H "Authorization: Bearer ot_your_api_key" \
  https://opentweet.io/api/v1/me

# Option 2: X-API-Key header
curl -H "X-API-Key: ot_your_api_key" \
  https://opentweet.io/api/v1/me

Rate Limits

Pro Plan

60
requests / minute
1,000
requests / day
50
posts / bulk request

Advanced Plan

300
requests / minute
10,000
requests / day
50
posts / bulk request

Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) are included in 429 rate-limit error responses.

Endpoints

GET/api/v1/accounts

List Connected X Accounts

Returns all X accounts connected to your OpenTweet account. Use the account "id" as x_account_id when creating or listing posts for a specific account.

Requires API Key

Response

json
{
  "accounts": [
    {
      "id": "65f1a2b3c4d5e6f7a8b9c0d1",
      "x_handle": "@primary_account",
      "x_name": "Primary Account",
      "is_primary": true,
      "nickname": null,
      "x_subscription_type": "Premium+",
      "connected_at": "2026-01-15T10:00:00Z",
      "profile_image_url": "https://pbs.twimg.com/..."
    },
    {
      "id": "65f1a2b3c4d5e6f7a8b9c0d2",
      "x_handle": "@secondary_account",
      "x_name": "Secondary Account",
      "is_primary": false,
      "nickname": "Brand",
      "x_subscription_type": "Pro",
      "connected_at": "2026-02-20T14:00:00Z",
      "profile_image_url": "https://pbs.twimg.com/..."
    }
  ]
}

⚠ Pro plan supports 1 account, Advanced up to 3, Agency up to 10.

⚠ Use the "id" field as x_account_id in other endpoints to target a specific account.

GET/api/v1/cross-posting

Cross-Posting Targets & Limits

Read where a post would go and what each network accepts. Call this to find out where a post that omits the platforms parameter would actually land, instead of assuming it goes to X.

Requires API Key

Response

json
{
  "auto_cross_post": true,
  "default_targets": ["x", "bluesky", "linkedin"],
  "deliverable_targets": ["x", "bluesky", "linkedin"],
  "platforms": [
    {
      "platform": "x",
      "label": "X",
      "connected": true,
      "usable": true,
      "needs_reconnect": false,
      "accounts": [
        {
          "account_id": "65f1a2b3c4d5e6f7a8b9c0d1",
          "remote_id": "1234567890",
          "handle": "@you",
          "display_name": "You",
          "avatar_url": "https://pbs.twimg.com/...",
          "is_primary": true,
          "disconnected": false
        }
      ],
      "limits": {
        "max_text_length": 280,
        "text_unit": "weighted",
        "max_images": 4,
        "max_thread_parts": 25
      }
    },
    {
      "platform": "bluesky",
      "label": "Bluesky",
      "connected": true,
      "usable": true,
      "needs_reconnect": false,
      "accounts": [
        {
          "account_id": "66a2b3c4d5e6f7a8b9c0d1e2",
          "remote_id": "did:plc:abc123",
          "handle": "you.bsky.social",
          "display_name": "You",
          "avatar_url": "https://cdn.bsky.app/...",
          "is_primary": true,
          "disconnected": false
        }
      ],
      "limits": {
        "max_text_length": 300,
        "text_unit": "grapheme",
        "max_images": 4,
        "max_image_bytes": 2097152,
        "max_videos": 0
      }
    },
    {
      "platform": "linkedin",
      "label": "LinkedIn",
      "connected": true,
      "usable": true,
      "needs_reconnect": false,
      "accounts": [
        {
          "account_id": "67b3c4d5e6f7a8b9c0d1e2f3",
          "remote_id": "urn:li:person:AbC123dEf",
          "handle": "Your Name",
          "display_name": "Your Name",
          "avatar_url": "https://media.licdn.com/...",
          "is_primary": true,
          "disconnected": false
        }
      ],
      "limits": {
        "max_text_length": 3000,
        "text_unit": "codeunit",
        "max_images": 20,
        "max_videos": 0,
        "max_thread_parts": 25
      }
    }
  ]
}

⚠ default_targets is where a post that omits the platforms parameter goes right now. It is the live output of the resolver, so read it rather than recomputing it from auto_cross_post.

⚠ deliverable_targets is the subset with a working account. A network in default_targets but not in deliverable_targets will be attempted and will fail until the user reconnects it, which needs_reconnect flags per network.

⚠ auto_cross_post decides one thing only: where a post that omits platforms goes. On means every connected platform, off means X only.

⚠ A post that does pass platforms is honoured exactly, so ["x"] posts to X only even while auto_cross_post is true.

⚠ limits.text_unit differs by network. X counts weighted characters, where a URL costs 23 regardless of length. Bluesky counts graphemes, so an emoji or an accented letter costs one. LinkedIn is counted in UTF-16 code units (codeunit), so an emoji costs two.

⚠ There is no Bluesky API key to create. Bluesky issues none, so the account is connected through atproto OAuth in the dashboard rather than through this API.

⚠ LinkedIn is connected in the dashboard with a LinkedIn sign-in, and posts go to that member's personal profile. The sign-in lasts 60 days, so a LinkedIn account turns usable: false and needs_reconnect: true on its own when it runs out.

GET/api/v1/me

Verify Connection & Check Limits

Verify your API key works and check your subscription status, daily post limits, and post counts. Call this first before scheduling or publishing.

Requires API Key

Response

json
{
  "authenticated": true,
  "subscription": {
    "has_access": true,
    "status": "active",
    "is_trialing": false,
    "trial_ends_at": null,
    "current_period_ends_at": "2026-04-01T00:00:00Z"
  },
  "limits": {
    "can_post": true,
    "posts_published_today": 3,
    "remaining_posts_today": 17,
    "daily_limit": 20
  },
  "stats": {
    "total_posts": 142,
    "scheduled_posts": 8,
    "posted_posts": 120,
    "draft_posts": 14
  }
}

⚠ If subscription.has_access is false, you cannot schedule or publish posts.

⚠ limits is null when the user has no active subscription.

GET/api/v1/posts

List Posts

Retrieve a paginated list of your posts. Filter by status to see only scheduled, posted, draft, or failed posts.

Requires API Key

Query Parameters

NameTypeDescription
pageintegerPage number (default: 1)
limitintegerPosts per page (default: 20, max: 100)
statusstringFilter: "scheduled", "posted", "draft", or "failed"
x_account_idstringFilter by X account ID (for multi-account users)

Response

json
{
  "posts": [
    {
      "id": "65f1a2b3c4d5e6f7a8b9c0d1",
      "x_account_id": "65f1a2b3c4d5e6f7a8b9c0d1",
      "category": "General",
      "text": "Just shipped a new feature!",
      "scheduled_date": "2026-03-01T10:00:00Z",
      "posted": false,
      "posted_date": null,
      "failed": false,
      "failed_reason": null,
      "is_thread": false,
      "thread_tweets": null,
      "media_urls": null,
      "thread_media": null,
      "community_id": null,
      "share_with_followers": false,
      "x_post_id": null,
      "review_status": "approved",
      "created_at": "2026-02-16T08:30:00Z",
      "updated_at": "2026-02-16T08:30:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 142,
    "pages": 8
  }
}
POST/api/v1/posts

Create Post(s)

Create a single post, a thread, or bulk create up to 50 posts at once. Include scheduled_date to schedule, or publish_now to post to X immediately.

Requires API Key

Request Body

Including scheduled_date requires an active subscription and counts toward daily limits. Omit it to save as a draft. Use publish_now: true to post to X immediately (single post only, cannot combine with scheduled_date or bulk posts). All dates must be in the future (ISO 8601). Use x_account_id to target a specific X account (multi-account users), bluesky_account_id for the Bluesky one, and linkedin_account_id for the LinkedIn one. platforms accepts "x", "bluesky" and "linkedin" and can be set per post or once at the top level of a bulk create.

json
// Single post (draft)
{
  "text": "Just shipped a new feature!",
  "category": "Product"
}

// Single post with schedule
{
  "text": "Just shipped a new feature!",
  "category": "Product",
  "scheduled_date": "2026-03-01T10:00:00Z"
}

// Publish immediately to X
{
  "text": "Hello from the API!",
  "publish_now": true
}

// Post with media
{
  "text": "Check out this screenshot!",
  "media_urls": ["https://your-uploaded-media-url.jpg"],
  "scheduled_date": "2026-03-01T10:00:00Z"
}

// Post to X Community
{
  "text": "Shared with the community!",
  "community_id": "1234567890",
  "share_with_followers": true
}

// Post to a specific X account (multi-account)
{
  "text": "Hello from my second account!",
  "x_account_id": "65f1a2b3c4d5e6f7a8b9c0d2",
  "publish_now": true
}

// Cross-post to X, Bluesky and LinkedIn in one call
{
  "text": "Shipped cross-posting today.",
  "platforms": ["x", "bluesky", "linkedin"],
  "publish_now": true
}

// Pin a post to X only. Always honoured, whatever
// the account's auto cross-post setting says.
{
  "text": "A long post that would not fit in 300 characters ...",
  "platforms": ["x"],
  "scheduled_date": "2026-10-01T10:00:00Z"
}

// No platforms field. This does NOT mean X only.
// It follows the account's auto cross-post setting,
// which sends it to EVERY connected platform when on.
{
  "text": "Where this lands depends on the account setting.",
  "publish_now": true
}

// Thread with per-tweet media
{
  "text": "Here's what I learned building a SaaS in 2026:",
  "is_thread": true,
  "thread_tweets": [
    "1/ Start with a problem you have yourself",
    "2/ Ship fast, iterate faster",
    "3/ Your first 10 users matter more than the next 1000"
  ],
  "thread_media": [["https://img1.jpg"], [], ["https://img3.jpg"]],
  "scheduled_date": "2026-03-01T10:00:00Z"
}

// Bulk create (up to 50)
{
  "posts": [
    { "text": "Monday motivation", "scheduled_date": "2026-03-02T09:00:00Z" },
    { "text": "Tuesday tip", "scheduled_date": "2026-03-03T14:00:00Z" }
  ],
  "x_account_id": "optional — applies to all posts if not set per-post"
}

Response

json
{
  "success": true,
  "count": 1,
  "posts": [
    {
      "id": "65f1a2b3c4d5e6f7a8b9c0d1",
      "text": "Just shipped a new feature!",
      "category": "Product",
      "platforms": ["x", "bluesky"],
      "scheduled_date": "2026-03-01T10:00:00Z",
      "is_thread": false,
      "thread_tweets": null,
      "media_urls": null,
      "thread_media": null,
      "community_id": null,
      "share_with_followers": false,
      "created_at": "2026-02-16T08:30:00Z"
    }
  ]
}

// After publishing, a post carries one result per network.
// Delivery is independent, so a partial outcome is normal.
{
  "results": [
    {
      "platform": "x",
      "status": "published",
      "post_id": "1834000000000000001",
      "url": "https://x.com/you/status/1834000000000000001",
      "attempted_at": "2026-09-05T09:00:03Z"
    },
    {
      "platform": "bluesky",
      "status": "skipped",
      "skip_reason": "This post is 412 characters, over the 300 Bluesky allows."
    }
  ]
}

⚠ Returns 201 on success.

⚠ publish_now requires an active subscription and counts toward daily limits.

⚠ When publish_now succeeds, response includes x_post_id, posted: true, and posted_date.

⚠ When publish_now fails to post to X, returns 502 with the post ID (post is saved but not published).

⚠ Omitting platforms is NOT the same as pinning a post to X. A post with no platforms field follows the account auto cross-post setting, and with that setting on it goes to every connected platform. Send "platforms": ["x"] when you mean X and only X. An explicit list is always honoured exactly.

⚠ results carries one entry per targeted network, each with platform, status (published, failed, or skipped), post_id, url, account_id, part_ids for threads, uri and cid on Bluesky, error on failure, and skip_reason on a skip. On LinkedIn, post_id is the post URN, such as urn:li:share:7300000000000000000.

⚠ A post over a network limit is skipped on that network with a reason and still publishes everywhere it fits. Bluesky allows 300 characters, 4 images at 2MB each, and no video. LinkedIn allows 3,000 characters and no video, and a thread goes to LinkedIn as one combined post. X allows 280, or 25,000 on Premium.

⚠ The top-level posted boolean is a roll-up: true when any target published. Read results per entry when you need to know what each network did.

⚠ Call GET /api/v1/cross-posting to read where an unspecified post would land, which networks are connected, and each network current limits.

GET/api/v1/posts/:id

Get Single Post

Retrieve details of a specific post by its ID.

Requires API Key

Response

json
{
  "id": "65f1a2b3c4d5e6f7a8b9c0d1",
  "category": "General",
  "text": "Just shipped a new feature!",
  "scheduled_date": "2026-03-01T10:00:00Z",
  "posted": false,
  "posted_date": null,
  "failed": false,
  "failed_reason": null,
  "is_thread": false,
  "thread_tweets": null,
  "media_urls": null,
  "thread_media": null,
  "community_id": null,
  "share_with_followers": false,
  "x_post_id": null,
  "review_status": "approved",
  "created_at": "2026-02-16T08:30:00Z",
  "updated_at": "2026-02-16T08:30:00Z"
}

⚠ Same response shape as individual post in List Posts.

PUT/api/v1/posts/:id

Update Post

Update a draft or scheduled post. You cannot update posts that have already been published.

Requires API Key

Request Body

All fields are optional. Only include the fields you want to update. Setting scheduled_date requires an active subscription. Set scheduled_date to null to unschedule (convert back to draft).

json
{
  "text": "Updated tweet text",
  "category": "Marketing",
  "is_thread": false,
  "thread_tweets": [],
  "media_urls": [],
  "thread_media": [],
  "community_id": "1234567890",
  "share_with_followers": true,
  "scheduled_date": "2026-03-01T10:00:00Z"
}

Response

json
{
  "id": "65f1a2b3c4d5e6f7a8b9c0d1",
  "text": "Updated tweet text",
  "category": "Marketing",
  "scheduled_date": "2026-03-01T10:00:00Z",
  "is_thread": false,
  "thread_tweets": null,
  "media_urls": [],
  "thread_media": null,
  "community_id": "1234567890",
  "share_with_followers": true,
  "updated_at": "2026-02-16T09:00:00Z"
}

⚠ Returns 400 if the post has already been published.

⚠ Setting scheduled_date counts toward daily limits.

DELETE/api/v1/posts/:id

Delete Post

Permanently delete a post.

Requires API Key

Response

json
{
  "success": true,
  "message": "Post deleted",
  "x_deleted": true
}

⚠ If the post was published to X, it will also be deleted from X/Twitter by default.

⚠ Pass ?delete_from_x=false to only remove the post from OpenTweet without deleting from X.

⚠ The response includes x_deleted (boolean) indicating whether the X deletion succeeded.

POST/api/v1/posts/:id/schedule

Schedule Post

Set or update the scheduled publish time for a post. Requires an active subscription.

Requires API Key

Request Body

Date must be in the future. Use ISO 8601 format.

json
{
  "scheduled_date": "2026-03-01T10:00:00Z"
}

Response

json
{
  "success": true,
  "id": "65f1a2b3c4d5e6f7a8b9c0d1",
  "text": "Just shipped a new feature!",
  "scheduled_date": "2026-03-01T10:00:00Z",
  "message": "Post scheduled successfully"
}

⚠ Requires active subscription.

⚠ Counts toward daily post limits.

⚠ Returns 400 if the post has already been published.

POST/api/v1/posts/:id/publish

Publish Immediately

Publish a post to X/Twitter right now. No request body needed. Requires an active subscription.

Requires API Key

Response

json
{
  "success": true,
  "id": "65f1a2b3c4d5e6f7a8b9c0d1",
  "text": "Just shipped a new feature!",
  "x_post_id": "1234567890123456789",
  "posted_date": "2026-02-16T10:00:00Z",
  "message": "Post published successfully"
}

⚠ Requires active subscription.

⚠ Counts toward daily post limits.

⚠ Returns 400 if the post has already been published.

⚠ Returns 502 if the X/Twitter API fails.

POST/api/v1/posts/batch-schedule

Batch Schedule Posts

Schedule multiple existing posts at once (up to 50). Requires an active subscription.

Requires API Key

Request Body

community_id, share_with_followers, and x_account_id are optional and apply to all posts in the batch. All scheduled_date values must be in the future (ISO 8601).

json
{
  "schedules": [
    { "post_id": "65f1a2b3c4d5e6f7a8b9c0d1", "scheduled_date": "2026-03-02T09:00:00Z" },
    { "post_id": "65f1a2b3c4d5e6f7a8b9c0d2", "scheduled_date": "2026-03-03T14:00:00Z" }
  ],
  "community_id": "1234567890",
  "share_with_followers": true,
  "x_account_id": "optional — target X account"
}

Response

json
{
  "success": true,
  "message": "2 post(s) scheduled",
  "scheduled": [
    {
      "id": "65f1a2b3c4d5e6f7a8b9c0d1",
      "text": "Monday motivation",
      "category": "General",
      "scheduled_date": "2026-03-02T09:00:00Z",
      "is_thread": false
    }
  ],
  "errors": [
    { "post_id": "65f1a2b3c4d5e6f7a8b9c0d2", "error": "Post not found" }
  ]
}

⚠ Requires active subscription.

⚠ Counts toward daily post limits.

⚠ Maximum 50 posts per batch.

⚠ errors array is only present if some posts failed to schedule.

GET/api/v1/articles

List Articles

List your long-form X Articles with pagination and status filtering.

Requires API Key

Query Parameters

NameTypeDescription
statusstringFilter: draft, scheduled, publishing, published, failed
x_account_idstringFilter by X account (multi-account users)
pageintegerPage number (default 1)
limitintegerResults per page (default 20, max 100)

Response

json
{
  "articles": [
    {
      "id": "65f1a2b3c4d5e6f7a8b9c0d1",
      "title": "How we grew to $10k MRR",
      "content_markdown": "# Intro\n\nIt started with...",
      "cover_image_url": null,
      "status": "published",
      "scheduled_date": null,
      "published_date": "2026-07-01T14:00:00Z",
      "x_post_id": "1234567890123456789",
      "article_url": "https://x.com/i/status/1234567890123456789",
      "failed_reason": null,
      "x_account_id": null,
      "created_at": "2026-06-30T09:00:00Z",
      "updated_at": "2026-07-01T14:00:00Z"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total_count": 1, "total_pages": 1, "has_more": false }
}

⚠ Requires a paid plan (Pro, Advanced, or Agency). Not available during trial.

POST/api/v1/articles

Create Article

Create a long-form X Article from markdown. Save as draft, schedule it, or publish immediately.

Requires API Key

Request Body

Markdown supports # / ## / ### headings, bold, italic, strikethrough, links, lists, blockquotes, images (uploaded to X automatically), and embedded posts (an x.com status URL on its own line). publish_now and scheduled_date are mutually exclusive; omit both to save a draft.

json
{
  "title": "How we grew to $10k MRR",
  "content_markdown": "# Intro\n\nIt started with **one tweet**...\n\n## The turning point\n\n- Lesson one\n- Lesson two",
  "cover_image_url": "https://example.com/cover.png",
  "scheduled_date": "2026-07-10T14:00:00Z",
  "publish_now": false,
  "x_account_id": "optional, target X account"
}

Response

json
{
  "article": {
    "id": "65f1a2b3c4d5e6f7a8b9c0d1",
    "title": "How we grew to $10k MRR",
    "status": "scheduled",
    "scheduled_date": "2026-07-10T14:00:00Z",
    "article_url": null
  }
}

⚠ Requires a paid plan (Pro, Advanced, or Agency). Not available during trial.

⚠ Publishing requires the connected X account to be eligible for Articles on X (any X Premium tier).

⚠ Fenced code blocks are preserved as plain text. X articles have no code formatting yet.

PUT/api/v1/articles/:id

Update Article

Update a draft, scheduled, or failed article. Published articles cannot be edited.

Requires API Key

Request Body

All fields optional. Set scheduled_date to null to unschedule back to draft. Updating a failed article resets it to draft/scheduled.

json
{
  "title": "New title",
  "content_markdown": "Updated body...",
  "scheduled_date": null
}

Response

json
{
  "article": { "id": "65f1a2b3c4d5e6f7a8b9c0d1", "status": "draft" }
}

⚠ GET and DELETE on /api/v1/articles/:id fetch and delete a single article.

POST/api/v1/articles/:id/publish

Publish Article

Publish a draft, scheduled, or failed article to X right now. No request body needed.

Requires API Key

Response

json
{
  "article": {
    "id": "65f1a2b3c4d5e6f7a8b9c0d1",
    "status": "published",
    "x_post_id": "1234567890123456789",
    "article_url": "https://x.com/i/status/1234567890123456789"
  }
}

⚠ Requires a paid plan (Pro, Advanced, or Agency).

⚠ Returns 502 with code publish_failed if X rejects the article (e.g. account not eligible for Articles).

POST/api/v1/upload

Upload Media

Upload an image or video to use in posts. Returns a URL to include in media_urls or thread_media when creating or updating posts.

Requires API Key

Request Body

Use Content-Type: multipart/form-data. Supported formats: JPG, PNG, GIF, WebP, MP4, MOV. Max size: 5MB for images, 20MB for videos.

json
// multipart/form-data
// field: file (the image or video file)

curl -X POST https://opentweet.io/api/v1/upload \
  -H "Authorization: Bearer ot_your_key" \
  -F "file=@screenshot.png"

Response

json
{
  "url": "https://your-cdn-url.com/uploaded-file.png"
}

⚠ Upload media first, then use the returned URL in media_urls or thread_media when creating/updating posts.

GET/api/v1/analytics/overview

Account Overview

Returns comprehensive account-level analytics including posting stats, streaks, trends, and category breakdown.

Requires API Key

Response

json
{
  "stats": {
    "total_posts_created": 142,
    "total_posts_published": 120,
    "total_posts_scheduled": 8,
    "publishing_rate": 85,
    "total_active_days": 45,
    "avg_posts_per_week": 7.2,
    "most_active_day": "monday",
    "most_active_hour": 9,
    "total_characters_written": 28400,
    "total_threads_created": 12,
    "total_media_posts": 34
  },
  "streaks": {
    "current": 5,
    "longest": 14,
    "last_posted_date": "2026-02-26T10:00:00Z",
    "streak_start_date": "2026-02-22T09:00:00Z"
  },
  "trends": {
    "this_week": 7,
    "last_week": 5,
    "this_month": 24,
    "last_month": 18,
    "best_month": { "month": "2026-01", "count": 32 }
  },
  "categories": [
    { "name": "Product", "count": 45, "percentage": 32 }
  ],
  "recent_activity": {
    "last_7_days": [3, 2, 4, 1, 3, 2, 5],
    "last_30_days": [2, 3, 1, ...]
  }
}
GET/api/v1/analytics/tweets

Tweet Engagement Metrics

Returns engagement metrics for published tweets including likes, retweets, impressions, and engagement rates. Requires Advanced plan.

Requires API Key

Query Parameters

NameTypeDescription
periodinteger or "all"Number of days (7-365) or "all" for all time (default: 30)
x_account_idstringFilter by X account ID (for multi-account users)

Response

json
{
  "has_x_account": true,
  "has_data": true,
  "sync_status": {
    "last_synced_at": "2026-02-26T08:00:00Z",
    "total_tweets_synced": 120
  },
  "summary": {
    "total_tracked_tweets": 85,
    "avg_engagement_rate": 3.2,
    "avg_impressions": 1250,
    "avg_likes": 15.4,
    "avg_retweets": 3.1,
    "avg_replies": 2.8,
    "total_impressions": 106250,
    "total_engagements": 3400
  },
  "top_performers": [
    {
      "x_post_id": "1234567890",
      "text_preview": "Just shipped...",
      "content_type": "text",
      "posted_at": "2026-02-20T10:00:00Z",
      "likes": 45, "retweets": 12, "replies": 8,
      "quotes": 3, "impressions": 5200,
      "bookmarks": 6, "engagement_rate": 5.1
    }
  ],
  "worst_performers": [],
  "content_type_stats": [],
  "engagement_timeline": [],
  "best_engagement_hours": [],
  "best_engagement_days": []
}

⚠ Requires Advanced plan. Returns 403 on Pro plan.

⚠ Returns has_x_account: false if no X account is connected.

⚠ Returns has_data: false if no engagement data has been synced yet.

GET/api/v1/analytics/best-times

Best Posting Times

Analyzes your recent publishing patterns to find optimal posting times based on your last 15 published posts.

Requires API Key

Response

json
{
  "has_enough_data": true,
  "total_posts_analyzed": 15,
  "hour_distribution": [
    { "hour": 9, "label": "9AM", "count": 5 }
  ],
  "day_distribution": [
    { "day": "Monday", "short_day": "Mon", "count": 4 }
  ],
  "best_hours": [
    { "hour": 9, "label": "9AM", "count": 5 }
  ],
  "best_days": [
    { "day": "Monday", "count": 4 }
  ],
  "summary": "Most active days: Monday, Wednesday. Best posting hours: 9AM, 2PM"
}

⚠ Requires at least 3 published posts. Returns has_enough_data: false otherwise.

Status Codes

CodeMeaning
200Success
201Created (new post)
400Bad request (validation error)
401Unauthorized (invalid API key)
403Forbidden (subscription required)
404Post not found
429Rate limit or daily post limit exceeded
500Server error
502X/Twitter API error

Error Response Shape

json
{
  "error": "Human-readable error message",
  "details": ["Array of specific validation errors (optional)"],
  "remaining": 0  // Only on 429 responses: remaining posts/requests allowed
}

Common Workflows

Verify your connection

bash
curl -H "Authorization: Bearer ot_your_key" \
  https://opentweet.io/api/v1/me

Check authenticated is true and subscription.has_access is true.

Create and schedule a tweet (one step)

bash
curl -X POST https://opentweet.io/api/v1/posts \
  -H "Authorization: Bearer ot_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Just shipped a new feature!",
    "scheduled_date": "2026-03-01T10:00:00Z"
  }'

Post immediately (one step)

bash
curl -X POST https://opentweet.io/api/v1/posts \
  -H "Authorization: Bearer ot_your_key" \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello from the API!", "publish_now": true}'

Or use the two-step approach:

bash
# Step 1: Create a draft
curl -X POST https://opentweet.io/api/v1/posts \
  -H "Authorization: Bearer ot_your_key" \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello from the API!"}'

# Step 2: Publish it (replace POST_ID with the id from step 1)
curl -X POST https://opentweet.io/api/v1/posts/POST_ID/publish \
  -H "Authorization: Bearer ot_your_key"

Schedule a week of content

bash
curl -X POST https://opentweet.io/api/v1/posts \
  -H "Authorization: Bearer ot_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "posts": [
      {"text": "Monday motivation", "scheduled_date": "2026-03-02T09:00:00Z"},
      {"text": "Tuesday tip", "scheduled_date": "2026-03-03T14:00:00Z"},
      {"text": "Wednesday wins", "scheduled_date": "2026-03-04T11:00:00Z"},
      {"text": "Thursday thoughts", "scheduled_date": "2026-03-05T16:00:00Z"},
      {"text": "Friday feels", "scheduled_date": "2026-03-06T10:00:00Z"}
    ]
  }'

Upload media and post with image

bash
# Step 1: Upload the image
curl -X POST https://opentweet.io/api/v1/upload \
  -H "Authorization: Bearer ot_your_key" \
  -F "file=@screenshot.png"
# Returns: { "url": "https://..." }

# Step 2: Create post with the media URL
curl -X POST https://opentweet.io/api/v1/posts \
  -H "Authorization: Bearer ot_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Check out this screenshot!",
    "media_urls": ["https://returned-url-from-upload"],
    "scheduled_date": "2026-03-01T10:00:00Z"
  }'

Post to a specific X account (multi-account)

bash
# Step 1: List your connected accounts
curl -H "Authorization: Bearer ot_your_key" \
  https://opentweet.io/api/v1/accounts
# Returns accounts with their IDs

# Step 2: Create a post targeting a specific account
curl -X POST https://opentweet.io/api/v1/posts \
  -H "Authorization: Bearer ot_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Hello from my second account!",
    "x_account_id": "ACCOUNT_ID_FROM_STEP_1",
    "publish_now": true
  }'

If you omit x_account_id, posts go through your primary account.

Check your analytics

bash
# Account overview (stats, streaks, trends)
curl -H "Authorization: Bearer ot_your_key" \
  https://opentweet.io/api/v1/analytics/overview

# Best posting times
curl -H "Authorization: Bearer ot_your_key" \
  https://opentweet.io/api/v1/analytics/best-times

# Tweet engagement (Advanced plan only)
curl -H "Authorization: Bearer ot_your_key" \
  "https://opentweet.io/api/v1/analytics/tweets?period=30"
🤖

Using OpenClaw?

Install the official skill and your agent handles everything above automatically.

bash
clawhub install opentweet-x-poster
export OPENTWEET_API_KEY="ot_your_key"
openclaw "schedule 5 tweets about our launch for this week"

Your agent can also read the plain-text API docs directly at /api/v1/docs.

Ready to build?

Get your API key and start posting in under 3 minutes.

API included in every plan. No extra charge.