# Error Reference This page lists the errors each endpoint can return, what they mean, and how to handle them. Use it to build robust error handling and know when a **retry** will help. Error response format [#error-response-format] Errors return a JSON body with an `error` message. Some responses include extra fields: ```json { "error": "Human-readable message", "twitter_error_code": 187, "challenge": "rate_limited", "retry_after": 120 } ``` * **`error`**: always present. For failures that come **from X**, this is X's own message, passed through verbatim. For issues on **our** side (temporary capacity, an internal error), it's a safe generic message. * **`twitter_error_code`**: the numeric X error code (e.g. `187`, `344`, `502`). Present **only** when the failure came from X directly; omitted for validation, capacity, and internal errors. So if it's present, you can branch on it reliably. * **`challenge`**: login only. A stable, machine-readable key (`rate_limited`, `locked`, `verification_required`, `totp_required`). * **`retry_after`**: present on some rate-limit responses; seconds to wait. Common errors (all endpoints) [#common-errors-all-endpoints] These can appear on any request. | Status | Message | What it means | What to do | | ------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | **401** | `Invalid or missing API key` | Your API key is missing or wrong | Check your `Authorization: Bearer ` header | | **400** | `Missing required … param: ` | A required field/parameter is missing or invalid | Fix the request | | **429** | `Please try again shortly.` (our capacity) · `Rate limit exceeded…` · or an X-side limit message | Too many requests, our capacity, or an **X account limit** (e.g. daily DM/tweet limit, these carry a `twitter_error_code`) | Wait `retry_after` / short backoff, then retry | | **500** | `Internal server error` | A temporary error occurred | Retry shortly | **Two kinds of 401.** A `401` at the API level means your **API key** is wrong. On write endpoints (posting, liking, profile updates), a `401` can also mean the **`auth_token` you supplied** is invalid or expired, the message will say `Invalid auth_token` or `Could not authenticate you`. Re-mint the token in that case. HTTP status codes [#http-status-codes] | Status | Meaning | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **200** | Success | | **202** | Accepted, the action is queued/pending (e.g. a community post awaiting moderation) | | **400** | Bad request, missing/invalid parameters, invalid proxy URL, or media too large | | **401** | Unauthorized, bad API key or invalid/expired `auth_token` | | **403** | Forbidden — a **protected/private or suspended target account** (its posts can't be read), a suspended/locked account you're using, a permission restriction, or a Premium-only action | | **404** | Not found, the user, tweet, or resource doesn't exist | | **409** | Conflict, duplicate tweet | | **410** | Gone, the account is suspended | | **422** | The login couldn't be completed (retry usually helps) | | **423** | Locked, the account is locked or needs a verification step | | **429** | Too many requests, a rate limit was hit | | **500 / 502 / 503** | Temporary server or upstream error, retry | Twitter error codes [#twitter-error-codes] When a failure comes from X, we pass through X's own code as `twitter_error_code`. | Code | Meaning | What to do | | ------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | | **32** | Could not authenticate you | `auth_token` is invalid/expired, re-mint it | | **63** | The target account is suspended | Nothing, the account is gone | | **64** | Your account is suspended | Use a different account | | **131** | Temporary X internal error | Retry | | **139** | Already liked | Already done, safe to ignore | | **144** | No tweet found with that ID | The tweet was deleted or the ID is wrong | | **187** | Duplicate tweet | Change the text or wait \~15 minutes | | **226** | Request looked automated | Retry | | **326** | Account temporarily locked | Unlock at `x.com/account/access`, then retry | | **327** | Already retweeted | Already done, safe to ignore | | **344** | Posting temporarily limited (network/IP throttle, not the account) | Retry shortly, or post through a clean dedicated proxy — one sticky IP per account. Not billed | | **349** | Cannot message this user | The recipient doesn't accept your DMs | | **399** | Wrong password / couldn't log in | Check credentials, or retry (temporary login limit) | | **433** | Reply restricted / Premium required | The tweet is reply-gated, or the action needs X Premium | | **465** | Cannot retweet an outdated tweet | The tweet is too old to retweet | | **476** | Not allowed to send message requests | The account can't send DM requests | | **502** | Daily DM (message request) limit reached | Wait 24 hours, or use an X Premium account for higher limits | Reading tweets & search [#reading-tweets--search] Applies to `/twitter/user/tweets`, `/twitter/user/tweets_and_replies`, and `/twitter/tweet/advanced_search`. | Status | Message | What to do | | ------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **403** | `This account's posts are not available. The account is protected (private), suspended, or no longer active.` | **Don't retry** — this is permanent, not a temporary error. The target's posts aren't readable by anyone who isn't an approved follower. Point the request at a public account. | | **403** | `This account is suspended, so its posts are not available.` | The target account is suspended. Nothing to do — the account is gone. | | **502** | `Upstream returned an unexpected response — please retry.` | A genuine transient upstream hiccup for that request. **Retry** — the API already rotates accounts internally, so a retry almost always succeeds. **Not billed.** | A **protected/suspended target** returns a fast, terminal `403` (retrying never helps). A `502` here is the opposite — a transient blip that a retry clears. They are different conditions despite both meaning "no posts returned." Posting & engagement [#posting--engagement] Create Tweet [#create-tweet] **POST** `/twitter/tweet/create` | Status | Code | Message | What to do | | ------------- | ---- | ----------------------------------------------------- | ------------------------------------------------------------------------------------- | | **409** | 187 | `Status is a duplicate.` | Change the text or wait \~15 min | | **403** | 433 | `The original Tweet author restricted who can reply…` | The tweet doesn't allow your reply | | **403** | 433 | Long-form / long-video needs Premium | Use a Premium account or shorten the content | | **403** | n/a | Not a member of the community | Join the community first | | **429** | 344 | Posting temporarily limited (network/IP throttle) | Retry shortly, or use a clean dedicated proxy (one sticky IP/account). **Not billed** | | **502** | n/a | `X returned an empty result` (outcome unconfirmed) | Retry — the API already re-tries once, duplicate-safe. **Not billed** | | **400** | n/a | `File size exceeds…` | Reduce the media file size | | **202** | n/a | `pending_moderation: true` | Not an error, the community post is awaiting review | | **503** | 226 | `This request looks automated` | Retry | | **502 / 503** | n/a | Temporary connection error | Retry (if you supplied a `proxy`, confirm it's working) | Edit Tweet [#edit-tweet] **POST** `/twitter/tweet/edit` | Status | Code | Message | What to do | | ------- | ---- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | **403** | n/a | `This post can't be edited…` | The account isn't X Premium, the \~1h edit window passed, the edit limit was reached, or the post is a reply/thread (not an editable original). Don't retry | | **400** | n/a | `Missing required field: tweet_id` | Provide `tweet_id` and `text` | | **404** | n/a | Post not found | Wrong id, or you passed a superseded id (edit the latest version's `data.id`) | | **401** | 32 | `Could not authenticate you` | `auth_token` invalid/expired, re-mint | Favorite / Retweet / Bookmark [#favorite--retweet--bookmark] **POST** `/twitter/tweet/favorite` · `/twitter/tweet/retweet` · `/twitter/tweet/bookmark` · `/twitter/tweet/unbookmark` | Status | Code | Message | What to do | | ------------- | --------- | ---------------------------------------------- | ------------------------------------------------- | | **403** | n/a | `User is suspended, deactivated or offboarded` | The account you're using is dead, switch accounts | | **401** | 32 | `Could not authenticate you` | `auth_token` invalid/expired, re-mint | | **403** | 465 | `not permitted to retweet an outdated Tweet` | The tweet is too old to retweet | | **200** | 139 / 327 | already liked / already retweeted | Already done, safe to ignore | | **429** | n/a | Rate limit | Slow down, then retry | | **502 / 503** | n/a | Temporary connection error | Retry | Delete Tweet [#delete-tweet] **POST** `/twitter/tweet/delete` | Status | Message | What to do | | ------- | ------------------------------ | ----------------------------------- | | **404** | `No status found with that ID` | Already deleted or wrong ID | | **403** | Not the author | You can only delete your own tweets | | **401** | Invalid `auth_token` | Re-mint the token | Follow / Unfollow [#follow--unfollow] **POST** `/twitter/user/follow` · `/twitter/user/unfollow` | Status | Message | What to do | | ------- | -------------------- | -------------------------------------- | | **404** | `User not found` | Check the handle/ID | | **403** | Restricted | The account or target doesn't allow it | | **401** | Invalid `auth_token` | Re-mint the token | | **429** | Follow limit | Wait, then retry | Update Profile / Avatar / Banner [#update-profile--avatar--banner] **POST** `/twitter/user/update_profile` · `/twitter/user/update_avatar` · `/twitter/user/update_banner` | Status | Message | What to do | | ------- | -------------------------------------------------- | ----------------- | | **401** | `Invalid auth_token - could not fetch credentials` | Re-mint the token | | **400** | Image too large / bad format | Fix the image | Join Community [#join-community] **POST** `/twitter/community/join` | Status | Message | What to do | | ------- | ---------------------------------------------- | ---------------------- | | **429** | `You have already joined several Communities…` | Try again later | | **400** | Invalid community | Check the community ID | | **403** | Restricted | The account can't join | Login [#login] **POST** `/twitter/user_login` The `challenge` field is a stable key you can branch on programmatically. | Status | `challenge` | Message | What to do | | ------- | ----------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------- | | **429** | `rate_limited` | `Login temporarily limited by Twitter (rate limit). Try again later.` | Wait \~30 min, or retry with a higher-quality proxy | | **401** | n/a | `Wrong password` | Check the credentials | | **401** | `totp_required` | `Two-factor authentication is enabled, provide totp_secret` | Include the account's `totp_secret` | | **404** | n/a | `Username not found` | Check the handle | | **410** | n/a | `Account suspended` | The account is gone | | **423** | `locked` | `Account locked, unlock at x.com/account/access, then retry.` | Unlock the account, then retry | | **423** | `verification_required` | `Verification required: Twitter is asking to verify this login.` | Log into the account manually once to clear it | | **423** | `email_code` | `Verification required: Twitter wants a code sent to the account's email.` | Complete the email verification manually | | **502** | `proxy_unreachable` | `The login proxy did not respond…` | Try a different proxy, or omit `proxy` | | **400** | n/a | `Invalid proxy URL.` | Fix the `proxy` value | | **422** | n/a | `Login did not complete. Try again.` | **Retry**: this usually succeeds on a second attempt | | **503** | n/a | `Login service is temporarily unavailable. Try again shortly.` | Retry shortly | **Most common:** `429 rate_limited` (the login IP was throttled, retry with a cleaner proxy or after a short wait) and `422` (retry usually clears it). If you supply your own `proxy`, a high-quality residential or mobile IP gives the best login success rate. Media Upload [#media-upload] **POST** `/twitter/media/upload` | Status | Message | What to do | | ------- | ---------------------- | ---------------------- | | **400** | `File size exceeds…` | Reduce the file size | | **400** | Unsupported type | Use a supported format | | **401** | Invalid `auth_token` | Re-mint the token | | **502** | Temporary upload error | Retry | **Limits:** images (PNG / JPG / GIF / WEBP) up to 5 MB · animated GIF up to 15 MB · video (MP4) up to 512 MB. Direct Messages [#direct-messages] **POST** `/twitter/dm/send` · `/twitter/dm/list` · `/twitter/dm/conversation` | Status | Code | Message | What to do | | ------- | ---- | -------------------------------------------------------------------------------------- | ------------------------------------- | | **429** | 502 | `You've hit your daily message request limit. Subscribe to Premium for higher limits.` | Wait 24h, or use a Premium account | | **403** | 476 | `Sender is not verified to send message requests` | The account can't send DM requests | | **403** | 349 | `Cannot send messages to this user` | The recipient doesn't accept your DMs | | **401** | 32 | `Could not authenticate you` | Re-mint the token | | **404** | n/a | Conversation / user not found | Check the recipient | Reading data [#reading-data] **GET** `/twitter/tweet/detail` · `/twitter/user/tweets` · `/twitter/user/info` · `/twitter/user/followers` · `/twitter/user/following` · `/twitter/tweet/thread` · `/twitter/tweet/replies` · `/twitter/user/search` · and all other read endpoints | Status | Message | What to do | | ------- | ---------------------------------------- | ------------------------------------------------------- | | **404** | `Tweet not found: ` | The tweet was deleted, is protected, or the ID is wrong | | **404** | `Could not resolve userId for @` | The handle doesn't exist, was renamed, or is suspended | | **404** | `Could not find user with ID: ` | Check the user ID | | **400** | `Missing required query param: ` | Provide the required parameter | | **429** | Rate limit | Wait `retry_after`, then retry | **Region-restricted content.** If a tweet shows "Due to local regulations, this content is restricted on X" in your browser, that's a **regional block on your location**, not an API error, the API can still return it. If you route the request through your own `proxy` that exits in the restricted country, the same regional block applies. Use no proxy, or a proxy in an unrestricted region. Spaces [#spaces] **GET** `/twitter/spaces/info` · **POST** `/twitter/spaces/download` | Status | Message | What to do | | ------- | ----------------------- | ------------------------------------------------------------------- | | **404** | `Space not found` | Use the **Space** URL (not a tweet URL); the Space may have expired | | **400** | Missing `space_url` | Provide the Space URL | | **200** | `"downloadable": false` | Not an error, this Space has no downloadable audio | Articles [#articles] **GET** `/twitter/article/get` · **POST** `/twitter/article/create|update|publish|unpublish|delete|list` | Status | Message | What to do | | ------- | -------------------- | ------------------------------------- | | **403** | Premium required | Articles require an X Premium account | | **404** | Article not found | Check the article ID | | **401** | Invalid `auth_token` | Re-mint the token | | **400** | Invalid content | Fix the article body | Retry guide [#retry-guide] | You see… | Retry? | Notes | | -------------------------- | ---------------------------------- | ---------------------------------------------------- | | **429** | ✅ after `retry_after` / short wait | You hit a rate limit | | **422** (login) | ✅ | Usually succeeds on the next attempt | | **500 / 502 / 503** | ✅ | Temporary, retry shortly | | **226** | ✅ | Retry | | **401** | ❌ | Fix your API key or re-mint the `auth_token` | | **403** (suspended/locked) | ❌ | Use a different account, or unlock it first | | **404** | ❌ | Check the ID/handle | | **400 / 409** | ❌ | Fix the request (params, media size, duplicate text) | **Tip:** Build retry-with-backoff for `429`, `422` (login), and `5xx`. For `401`/`403`/`404`/`400`, don't retry, fix the input or the account first. # GetXAPI Documentation GetXAPI is a REST API for Twitter/X data with **73 endpoints**: 62 usage-priced read and write endpoints from **$0.001 per call**, 2 free account endpoints, and 9 real-time monitoring endpoints included with a Monitoring plan. Authentication is a single Bearer key, with no OAuth flow and no X developer account required. **New here?** Jump straight to the **[Quickstart](#quick-start)** for the base URL, authentication, and a working request in curl, JavaScript, or Python. There are no endpoint-specific request quotas on paid data endpoints; general service throttling still applies, and monitoring management and free polling endpoints use abuse-prevention limits. [Get your API key →](https://www.getxapi.com/twitter-api-key) Try it without code [#try-it-without-code] Three ways to explore every endpoint interactively, pick whichever you already use: * **Hosted Postman docs** → [documenter.getpostman.com/view/54999474/2sBXqRkd7E](https://documenter.getpostman.com/view/54999474/2sBXqRkd7E), searchable, with example responses for every endpoint * **Fern docs** → [getxapi.docs.buildwithfern.com](https://getxapi.docs.buildwithfern.com/get-xapi-twitter-x-api-0-001-call), same collection rendered in Fern's clean reference layout Use it in Claude, Cursor & any MCP client [#use-it-in-claude-cursor--any-mcp-client] GetXAPI ships an official **[Model Context Protocol](https://modelcontextprotocol.io) server**, point your AI assistant at it and the full Twitter/X API becomes native tools (search tweets, look up users, read followers, post tweets, send DMs, publish articles). Add it to your MCP client config (Claude Desktop, Cursor, Windsurf, etc.): ```json { "mcpServers": { "getxapi": { "command": "npx", "args": ["-y", "@getxapi/mcp@latest"], "env": { "GETXAPI_KEY": "YOUR_API_KEY" } } } } ``` Read tools need only your `GETXAPI_KEY`; write tools (post, like, DM, etc.) additionally use your X account auth. * **npm** → [npmjs.com/package/@getxapi/mcp](https://www.npmjs.com/package/@getxapi/mcp) * **GitHub** → [github.com/getxapi/getxapi-mcp](https://github.com/getxapi/getxapi-mcp) Quick Start [#quick-start] Base URL [#base-url] ``` https://api.getxapi.com ``` Authentication [#authentication] All requests require an API key passed as a Bearer token in the `Authorization` header. ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.getxapi.com/twitter/user/info?userName=elonmusk" ``` Example: Get a User Profile [#example-get-a-user-profile] ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.getxapi.com/twitter/user/info?userName=elonmusk" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/user/info?userName=elonmusk", { headers: { Authorization: "Bearer YOUR_API_KEY" }, } ); const data = await response.json(); console.log(data); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/user/info", params={"userName": "elonmusk"}, headers={"Authorization": "Bearer YOUR_API_KEY"}) print(response.json()) ``` Example: Search Tweets [#example-search-tweets] ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.getxapi.com/twitter/tweet/advanced_search?q=from:elonmusk&product=Latest" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/tweet/advanced_search?q=from:elonmusk&product=Latest", { headers: { Authorization: "Bearer YOUR_API_KEY" }, } ); const data = await response.json(); console.log(data.tweets); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/tweet/advanced_search", params={"q": "from:elonmusk", "product": "Latest"}, headers={"Authorization": "Bearer YOUR_API_KEY"}) print(response.json()["tweets"]) ``` Endpoint Index [#endpoint-index] Tweets [#tweets] | Endpoint | Method | Path | Cost | | ----------------------------------------------- | ------ | -------------------------------- | ------ | | [Advanced Search](/docs/tweets/advanced-search) | GET | `/twitter/tweet/advanced_search` | $0.001 | | [Tweet Detail](/docs/tweets/tweet-detail) | GET | `/twitter/tweet/detail` | $0.001 | | [Tweet Thread](/docs/tweets/tweet-thread) | GET | `/twitter/tweet/thread` | $0.005 | | [Tweet Replies](/docs/tweets/tweet-replies) | GET | `/twitter/tweet/replies` | $0.001 | | [Tweet Retweeters](/docs/tweets/retweeters) | GET | `/twitter/tweet/retweeters` | $0.001 | | [Create Tweet](/docs/tweets/create-tweet) | POST | `/twitter/tweet/create` | $0.002 | | [Edit Tweet](/docs/tweets/edit-tweet) | POST | `/twitter/tweet/edit` | $0.002 | | [Like Tweet](/docs/tweets/favorite-tweet) | POST | `/twitter/tweet/favorite` | $0.001 | | [Retweet](/docs/tweets/retweet-tweet) | POST | `/twitter/tweet/retweet` | $0.001 | Articles [#articles] | Endpoint | Method | Path | Cost | | ----------------------------------------------------- | ------ | ---------------------------- | ------ | | [Get Article](/docs/articles/get-article) | GET | `/twitter/article/get` | $0.001 | | [Create Article](/docs/articles/create-article) | POST | `/twitter/article/create` | $0.01 | | [Update Article](/docs/articles/update-article) | POST | `/twitter/article/update` | $0.005 | | [List Articles](/docs/articles/list-articles) | POST | `/twitter/article/list` | $0.005 | | [Publish Article](/docs/articles/publish-article) | POST | `/twitter/article/publish` | $0.005 | | [Unpublish Article](/docs/articles/unpublish-article) | POST | `/twitter/article/unpublish` | $0.005 | | [Delete Article](/docs/articles/delete-article) | POST | `/twitter/article/delete` | $0.005 | Users [#users] | Endpoint | Method | Path | Cost | | ------------------------------------------------------------ | ------ | ----------------------------------------- | ------ | | [Search Users](/docs/users/user-search) | GET | `/twitter/user/search` | $0.001 | | [User Status](/docs/users/user-status) | GET | `/twitter/user/status` | $0.001 | | [User Info](/docs/users/user-info) | GET | `/twitter/user/info` | $0.001 | | [User Info by ID](/docs/users/user-info-by-id) | GET | `/twitter/user/info_by_id` | $0.001 | | [User About](/docs/users/user-about) | GET | `/twitter/user/user_about` | $0.001 | | [User Tweets](/docs/users/user-tweets) | GET | `/twitter/user/tweets` | $0.001 | | [User Tweets & Replies](/docs/users/user-tweets-and-replies) | GET | `/twitter/user/tweets_and_replies` | $0.001 | | [User Tweets Complete](/docs/users/user-tweets-complete) | GET | `/twitter/user/tweets/complete` | $0.003 | | [User Media](/docs/users/user-media) | GET | `/twitter/user/media` | $0.001 | | [User Likes](/docs/users/user-likes) | POST | `/twitter/user/likes` | $0.001 | | [Followers](/docs/users/followers) | GET | `/twitter/user/followers` | $0.001 | | [Followers v2](/docs/users/followers-v2) | GET | `/twitter/user/followers_v2` | $0.001 | | [Following](/docs/users/following) | GET | `/twitter/user/following` | $0.001 | | [Following v2](/docs/users/following-v2) | GET | `/twitter/user/following_v2` | $0.001 | | [Verified Followers](/docs/users/verified-followers) | GET | `/twitter/user/verified_followers` | $0.001 | | [Followers You Know](/docs/users/followers-you-know) | POST | `/twitter/user/followers_you_know` | $0.001 | | [Check Relationship](/docs/users/check-follow-relationship) | GET | `/twitter/user/check_follow_relationship` | $0.001 | | [Follow User](/docs/users/follow-user) | POST | `/twitter/user/follow` | $0.001 | | [Unfollow User](/docs/users/unfollow-user) | POST | `/twitter/user/unfollow` | $0.001 | | [Home Timeline](/docs/users/home-timeline) | POST | `/twitter/user/home_timeline` | $0.001 | | [Bookmark Search](/docs/users/bookmark-search) | POST | `/twitter/user/bookmark_search` | $0.001 | | [User Affiliates](/docs/users/user-affiliates) | GET | `/twitter/user/affiliates` | $0.001 | | [User Login](/docs/users/user-login) | POST | `/twitter/user_login` | $0.01 | | [User Login V2 (Recovery)](/docs/users/user-login-v2) | POST | `/twitter/user_login_v2` | $0.005 | | [Reset Password, Send Code](/docs/users/reset-password) | POST | `/twitter/reset-password/send-code` | $0.005 | | [Reset Password, Confirm](/docs/users/reset-password) | POST | `/twitter/reset-password/confirm` | $0.005 | Lists [#lists] | Endpoint | Method | Path | Cost | | ---------------------------------------- | ------ | ----------------------- | ------ | | [List Members](/docs/lists/list-members) | GET | `/twitter/list/members` | $0.001 | Direct Messages [#direct-messages] | Endpoint | Method | Path | Cost | | ---------------------------- | ------ | ------------------ | ------ | | [Send DM](/docs/dm/dm-send) | POST | `/twitter/dm/send` | $0.002 | | [List DMs](/docs/dm/dm-list) | POST | `/twitter/dm/list` | $0.002 | Media [#media] | Endpoint | Method | Path | Cost | | ---------------------------------------- | ------ | ----------------------- | ------ | | [Upload Media](/docs/media/upload-media) | POST | `/twitter/media/upload` | $0.001 | Community [#community] | Endpoint | Method | Path | Cost | | ------------------------------------------------ | ------ | ------------------------- | ------ | | [Community Info](/docs/community/community-info) | GET | `/twitter/community/info` | $0.001 | | [Join Community](/docs/community/join-community) | POST | `/twitter/community/join` | $0.002 | Spaces [#spaces] | Endpoint | Method | Path | Cost | | ---------------------------------------------- | ------ | -------------------------- | ------------------ | | [Space Info](/docs/spaces/spaces-info) | GET | `/twitter/spaces/info` | $0.001 | | [Download Space](/docs/spaces/spaces-download) | POST | `/twitter/spaces/download` | $0.05 + $0.015/min | Trends [#trends] | Endpoint | Method | Path | Cost | | ----------------------------------------------- | ------ | --------------------------- | ------ | | [Get Trends](/docs/trends/get-trends) | GET | `/twitter/trends` | $0.001 | | [Trend Locations](/docs/trends/trend-locations) | GET | `/twitter/trends/locations` | $0.001 | Notifications [#notifications] | Endpoint | Method | Path | Cost | | -------------------------------------------------- | ------ | ------------------------ | ------ | | [Notifications](/docs/notifications/notifications) | POST | `/twitter/notifications` | $0.002 | Account [#account] | Endpoint | Method | Path | Cost | | ------------------------------------------------ | ------ | ------------------- | ---- | | [Account Info](/docs/account/account-info) | GET | `/account/me` | Free | | [Payment History](/docs/account/payment-history) | GET | `/account/payments` | Free | Pagination [#pagination] All list endpoints support cursor-based pagination: * Omit `cursor` for the first page * Pass `next_cursor` from the response for subsequent pages * Each page typically returns \~20 items * Some endpoints (followers/following v1) return up to 200 per page * The v2 variants (followers\_v2/following\_v2) return \~70 per page * Keep paginating until `has_more` is `false` Error Codes [#error-codes] | Status | Meaning | Common Cause | | ------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Bad Request | Missing required parameter | | `401` | Unauthorized | Invalid or missing API key / auth token | | `404` | Not Found | User or tweet does not exist | | `429` | Rate Limited | Too many requests. Rare on paid data endpoints, which have no endpoint-specific quota, though general service throttling applies under sustained concurrency; the [monitoring](/docs/monitoring/overview#rate-limits) management endpoints do have per-minute limits | | `502` | Bad Gateway | Twitter rejected the mutation (write endpoints) | All errors return a JSON body with an `error` field: ```json { "error": "Missing required query param: userName" } ``` Pricing [#pricing] Most read endpoints cost **$0.001** per call. User Tweets Complete costs **$0.003** per call, Tweet Thread costs **$0.005** per call, and DM endpoints cost **$0.002** per call. No subscription is required for pay-per-call access; optional plans are available. There are no endpoint-specific request quotas on these paid data endpoints, so you pay only for what you use. General service throttling still applies. (The [monitoring](/docs/monitoring/overview#rate-limits) management endpoints are not billed per call and carry abuse-prevention limits instead.) [View full pricing →](https://www.getxapi.com) # Account Info This endpoint is **free** (no credits deducted). Rate limited to **30 requests per minute** per API key. Notes [#notes] * Returns your GetXAPI account details, credit balances, and usage summary. * Uses the same `Authorization: Bearer ` header as the Twitter endpoints. * Free to call, no credits consumed. * **Two balances.** `credits_remaining` is your permanent top-up wallet. `plan_credits_remaining` is the expiring allowance from a subscription. Plan credits are spent **first**, then the wallet. Use `balance_total` for what you can actually spend right now. Response (200) [#response-200] ```json { "email": "user@example.com", "name": "John Doe", "credits_remaining": 2.26, "credits_used": 13.832, "total_requests": 12251, "created_at": "2026-02-06T10:46:59.399986+00:00", "plan_name": "pro_sub", "plan_credits_total": 40, "plan_credits_remaining": 27.57, "plan_credits_expires_at": "2026-08-26", "plan_credits_expired": false, "balance_total": 29.83 } ``` | Field | Type | Description | | ------------------------- | -------------- | ------------------------------------------------------------------------------------ | | `email` | string | Account email | | `name` | string | Account name | | `credits_remaining` | number | **Top-up wallet** balance ($). Permanent, never expires | | `credits_used` | number | Total credits spent ($) | | `total_requests` | number | Total API calls made | | `created_at` | string | Account registration date (ISO 8601) | | `plan_name` | string \| null | Active subscription plan id, or `null` when there is no plan | | `plan_credits_total` | number | **Opening** plan-credit balance for the current cycle. See the note below | | `plan_credits_remaining` | number | Plan credits left this cycle ($) | | `plan_credits_expires_at` | string \| null | Date the plan credits lapse (`YYYY-MM-DD`) | | `plan_credits_expired` | boolean | `true` when the plan credits are past their expiry and no longer spendable | | `balance_total` | number | `credits_remaining` + **unexpired** `plan_credits_remaining`. What you can spend now | **`plan_credits_total` is the opening balance, not the plan's advertised allowance.** A renewal stacks your unused credits underneath the new grant, so a Pro plan advertising $30 can open a cycle at $42 if you carried $12 over. This makes `plan_credits_total - plan_credits_remaining` your spend for the current cycle, which is usually what you want. To check the advertised allowance instead, read the [pricing page](https://www.getxapi.com/pricing). **Plan credits expire, wallet credits do not.** Once `plan_credits_expires_at` passes, the remaining plan credits stop working. `plan_credits_remaining` may still show a non-zero figure briefly, so check `plan_credits_expired` or just use `balance_total`, which already excludes them. Error Responses [#error-responses] 429 - Rate limited [#429---rate-limited] ```json { "error": "Too many requests. Limit: 30 per minute." } ``` Examples [#examples] ```bash curl -X GET "https://api.getxapi.com/account/me" \ -H "Authorization: Bearer API_KEY" ``` ```javascript const response = await fetch("https://api.getxapi.com/account/me", { headers: { Authorization: "Bearer API_KEY" }, }); const data = await response.json(); console.log(data); ``` ```python import requests response = requests.get( "https://api.getxapi.com/account/me", headers={"Authorization": "Bearer API_KEY"}) print(response.json()) ``` # Feedback Hit a broken endpoint, a wrong field, or a gap we should fill? Send it to us from wherever you are: a curl, your own code, or the AI agent you are building with. Every report is tied to your account, so we can follow up, and you can check what happened to it at any time. | Endpoint | What it does | | -------------------- | ---------------------------------------------- | | `POST /feedback` | Open a report | | `GET /feedback` | See your reports and what state each one is in | | `GET /feedback/{id}` | Look up one report | All three are **free** and never touch your credits. They take the same `Authorization: Bearer ` header as everything else (`x-api-key` works too). To keep the inbox usable there is a soft ceiling of **10 calls a minute** across the three, and **50 new reports a day** per account. If you use the [GetXAPI MCP server](https://www.getxapi.com/mcp), your agent already has these as `send_feedback`, `list_feedback`, and `get_feedback_status`. Claude Code, Cursor, and similar tools will offer to file a report when a GetXAPI call fails on them, and ask you first. Open a report [#open-a-report] Body [#body] | Field | Required | Notes | | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | yes | `bug` when something we already offer misbehaves. `missing_capability` when you need data or an action we do not expose. `idea` for anything else | | `title` | yes | A single line, up to 120 characters. Say what is wrong, not "help" | | `details` | yes | Up to 8000 characters. The more of this you include, the faster we can act: the exact request, the response you got, the response you expected, and how often it happens | | `area` | no | Which endpoint or feature, e.g. `user/followers` or `monitoring`. Up to 80 characters | | `evidence` | no | A small JSON object of identifiers we can look up: tweet ids, user ids, status codes, timestamps, your client version. Up to 4 KB | | `client` | no | The tool sending the report, e.g. `getxapi-mcp/0.1.1` or `acme-crawler/3.2`. Up to 120 characters | | `images` | no | Up to 3 screenshots, inline: `[{ "data": "" }]`. PNG, JPEG, WebP, or GIF, 2 MB each. See below | Keep secrets out of it. Auth tokens, cookies, proxy strings, and passwords do not belong in `details` or `evidence`; we do not need them to reproduce a problem. Screenshots [#screenshots] A picture of the broken response often says more than the text. Send up to three with the report, base64-encoded: ```json { "type": "bug", "title": "user/info returns the wrong avatar for @nasa", "details": "Avatar url points at a different account's image. Screenshot attached.", "area": "user/info", "images": [ { "data": "iVBORw0KGgoAAAANSUhEUgAA..." } ] } ``` A `data:image/png;base64,...` URL works as well. We read the file type from the bytes, not from what you say it is, and accept PNG, JPEG, WebP, and GIF up to **2 MB each**, **30 MB per account per day**. Images are stored privately, only we see them; the report itself is not rejected if an upload fails. Response [#response] `201 Created` ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "status": "new", "created_at": "2026-09-05T10:15:00.000Z", "attachments": [] } ``` `attachments` lists what was stored (type and size); it is empty when you sent no images. Hang on to `id` if you want to look the report up later, though `GET /feedback` will always find it for you. Errors [#errors] | Status | When | | ------ | ----------------------------------------------------------------------------------------------------------------------------------- | | `400` | A required field is missing, a value is too long, `type` is not one of the three, or an image is not a PNG/JPEG/WebP/GIF under 2 MB | | `401` | No API key, or an invalid one | | `429` | Over 10 calls in the last minute, 50 reports today, or 30 MB of images today. The `Retry-After` header says how long to wait | Try it [#try-it] ```bash curl -X POST "https://api.getxapi.com/feedback" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "bug", "title": "user/followers stops after ~800 results for large accounts", "details": "GET /twitter/user/followers?userName=nasa pages fine until roughly the 40th cursor, then returns has_more: false with an empty list. The account has 90M followers. Reproduced 3 times between 09:00 and 09:30 UTC.", "area": "user/followers", "evidence": { "userName": "nasa", "last_cursor": "DAABCgABG...", "pages_received": 40 }, "client": "acme-crawler/3.2" }' ``` ```javascript const res = await fetch("https://api.getxapi.com/feedback", { method: "POST", headers: { Authorization: "Bearer API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ type: "missing_capability", title: "Need the pinned tweet id on user/info", details: "I show a profile card with the pinned tweet. Today that costs two calls: user/info, then user/tweets and a scan for isPinned. A pinned_tweet_id field on user/info would make it one.", area: "user/info", client: "profile-cards/1.4", }), }); const { id } = await res.json(); ``` ```python import requests r = requests.post( "https://api.getxapi.com/feedback", headers={"Authorization": "Bearer API_KEY"}, json={ "type": "idea", "title": "Let trends accept a city name, not only a country", "details": "trends?country=India works. trends?location=Mumbai returns 400 even though Mumbai is in trends/locations. Matching city names would save a lookup.", "area": "trends", }, ) print(r.status_code, r.json()) ``` See your reports [#see-your-reports] Newest first. Use it to find an id you did not save, or to check whether something was already reported before opening a duplicate. Query [#query] | Parameter | Notes | | --------- | -------------------------------------------------------------------------- | | `status` | Only show reports in one state: `new`, `triaged`, `shipped`, or `declined` | | `limit` | How many to return, 1 to 50. Default 20 | Response [#response-1] ```json { "count": 2, "feedback": [ { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "type": "bug", "title": "user/followers stops after ~800 results for large accounts", "area": "user/followers", "status": "shipped", "response": "Pagination past page 40 is fixed as of today. Thanks for the cursor.", "attachments": [{ "content_type": "image/png", "bytes": 184322 }], "created_at": "2026-09-05T10:15:00.000Z", "updated_at": "2026-09-06T08:02:11.000Z" }, { "id": "1b4e28ba-2fa1-4d3b-9c6e-1f0a2b3c4d5e", "type": "missing_capability", "title": "Need the pinned tweet id on user/info", "area": "user/info", "status": "new", "response": null, "attachments": [], "created_at": "2026-09-04T18:40:12.000Z", "updated_at": "2026-09-04T18:40:12.000Z" } ] } ``` You only ever see your own account's reports. ```bash curl "https://api.getxapi.com/feedback?status=shipped" \ -H "Authorization: Bearer API_KEY" ``` ```javascript const res = await fetch("https://api.getxapi.com/feedback?limit=5", { headers: { Authorization: "Bearer API_KEY" }, }); const { feedback } = await res.json(); ``` ```python import requests r = requests.get( "https://api.getxapi.com/feedback", params={"status": "new"}, headers={"Authorization": "Bearer API_KEY"}, ) print(r.json()["count"]) ``` Look up one report [#look-up-one-report] Same fields as the list, for a single `id`. An id that belongs to another account, or does not exist, comes back as `404`. What the states mean [#what-the-states-mean] | `status` | Meaning | | ---------- | ---------------------------------------------------- | | `new` | We have it, nobody has read it yet | | `triaged` | Read, understood, and on the list | | `shipped` | Done. The fix or the feature is live | | `declined` | We are not going to do this one. `response` explains | `response` is a short note from us, when there is something worth saying. `updated_at` moves whenever the state or the note changes. `attachments` lists the screenshots stored with the report (type and size). ```bash curl "https://api.getxapi.com/feedback/7c9e6679-7425-40de-944b-e07fc1f90ae7" \ -H "Authorization: Bearer API_KEY" ``` ```javascript const res = await fetch( "https://api.getxapi.com/feedback/7c9e6679-7425-40de-944b-e07fc1f90ae7", { headers: { Authorization: "Bearer API_KEY" } }, ); const { status, response } = await res.json(); ``` ```python import requests r = requests.get( "https://api.getxapi.com/feedback/7c9e6679-7425-40de-944b-e07fc1f90ae7", headers={"Authorization": "Bearer API_KEY"}, ) print(r.json()["status"]) ``` # Payment History This endpoint is **free** (no credits deducted). Rate limited to **30 requests per minute** per API key. Notes [#notes] * Returns your payment history, sorted by most recent first. * Uses the same `Authorization: Bearer ` header as the Twitter endpoints. * Free to call, no credits consumed. * **Two kinds of credit.** A top-up adds permanent wallet credits (`credits_added`). A plan grants expiring plan credits (`plan_credits_added`). A plan payment therefore shows `credits_added: 0` — that is correct, not a bug. Response (200) [#response-200] ```json { "payments": [ { "amount": 15, "status": "completed", "created_at": "2026-08-01T00:17:02.346Z", "type": "plan", "plan": "pro_sub", "credits_added": 0, "plan_credits_added": 40, "plan_credits_added_estimated": false }, { "amount": 10, "status": "completed", "created_at": "2026-03-18T00:17:02.346Z", "type": "topup", "plan": null, "credits_added": 10, "plan_credits_added": null, "plan_credits_added_estimated": false } ] } ``` | Field | Type | Description | | ------------------------------ | -------------- | -------------------------------------------------------------------------------------------------------- | | `amount` | number | Payment amount ($) | | `status` | string | `pending`, `partial`, `completed`, `expired`, `failed`, or `refunded`. Only `completed` granted anything | | `created_at` | string | Payment date (ISO 8601) | | `type` | string | `topup`, `plan`, or `monitoring` | | `plan` | string \| null | Plan id for `plan` and `monitoring` payments, else `null` | | `credits_added` | number | Permanent **wallet** credits added. `0` on plan payments | | `plan_credits_added` | number \| null | Expiring **plan** credits granted. `null` when nothing was granted | | `plan_credits_added_estimated` | boolean | `true` if the figure was derived rather than recorded. See below | **`plan_credits_added` is `null` unless the payment completed.** Crypto checkouts create a row before payment arrives, and `expired` or `failed` payments never granted anything, so those rows report `null` rather than the amount they would have granted. Top-ups are also `null` — they add wallet credits, reported in `credits_added`. **`plan_credits_added_estimated`.** Payments made before we began recording the grant have it derived from the plan tier instead. Derived figures reflect *current* plan pricing, so they can drift from what was actually granted at the time. Recorded figures (`estimated: false`) are exact. Error Responses [#error-responses] 429 - Rate limited [#429---rate-limited] ```json { "error": "Too many requests. Limit: 30 per minute." } ``` Examples [#examples] ```bash curl -X GET "https://api.getxapi.com/account/payments" \ -H "Authorization: Bearer API_KEY" ``` ```javascript const response = await fetch("https://api.getxapi.com/account/payments", { headers: { Authorization: "Bearer API_KEY" }, }); const data = await response.json(); console.log(data.payments); ``` ```python import requests response = requests.get( "https://api.getxapi.com/account/payments", headers={"Authorization": "Bearer API_KEY"}) print(response.json()["payments"]) ``` # Create Article This endpoint costs `$0.01` per API call. **Premium-only** when `publish: true`, the `auth_token` account must have an active X Premium subscription. Single endpoint that creates the draft, sets title + content + cover, and (optionally) publishes, all in one call. **For 100% success rate, pass your own proxy via the `proxy` field.** Using a custom proxy posts the article from your own clean IP and gives the most reliable results. Request Body [#request-body] | Field | Type | Required | Description | | -------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token (Premium account when publishing) | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `title` | string | Yes | Article title | | `content` | string | Yes | Article body in **Markdown**. Supports H1, H2, paragraphs, bold/italic/strikethrough, ordered + unordered lists, blockquotes | | `publish` | bool | No | If `true`, publishes immediately after creation. Default `false` (saved as draft) | | `cover_media_id` | string | No | Pre-uploaded media id (from `/media/upload`) | | `cover_image_url` | string | No | URL of an image to fetch and upload as the cover | | `cover_image_base64` | string | No | Base64-encoded image data for the cover | | `proxy` | string | No | Your proxy URL (e.g. `http://user:pass@host:port`) | Provide at most one of `cover_media_id` / `cover_image_url` / `cover_image_base64`. Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * When `publish: false` (default), the response returns a `draft_url` you can hand back to the user for in-app editing. * When `publish: true`, the article goes live as a wrapper tweet, `tweet_id` and `public_url` are returned. * Markdown limitations (X's article schema, not our limits): * Only H1 and H2 render distinctly. `###` (H3) and deeper coerce to H2. * Code blocks (fenced ` ``` `) degrade to plain paragraphs, X doesn't support them. * Inline code (` `x` `) renders as plain text. * Inline links degrade to ` ()` plain text. * Inline images inside the markdown body are not yet supported, use the `cover_*` fields for the header image. Response (200), Draft [#response-200-draft] ```json { "status": "success", "data": { "article_id": "2055606612363202560", "status": "draft", "tweet_id": null, "draft_url": "https://x.com/compose/articles/edit/2055606612363202560", "public_url": null, "cover_media_id": "2055606606466023424" } } ``` Response (200), Published [#response-200-published] ```json { "status": "success", "data": { "article_id": "2055606612363202560", "status": "published", "tweet_id": "2055606621209002254", "draft_url": "https://x.com/compose/articles/edit/2055606612363202560", "public_url": "https://x.com/i/status/2055606621209002254", "cover_media_id": "2055606606466023424" } } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required field: title" } ``` 403 - Not Premium (when publish: true) [#403---not-premium-when-publish-true] ```json { "error": "Publishing articles requires an active X Premium subscription on this account" } ``` 502 - Mutation rejected by X [#502---mutation-rejected-by-x] ```json { "error": "Twitter did not return an article_id from DraftCreate" } ``` Examples [#examples] ```bash curl -X POST "https://api.getxapi.com/twitter/article/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "title": "Three Books Worth Re-reading This Year", "content": "Some books reveal more on the second pass...\n\n## The Beginning of Infinity, David Deutsch\n\nDeutsch makes the case that **knowledge is the engine of change**...", "cover_image_url": "https://picsum.photos/1200/675", "publish": true }' ``` ```javascript const response = await fetch("https://api.getxapi.com/twitter/article/create", { method: "POST", headers: { Authorization: "Bearer API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ auth_token: "your_auth_token", title: "Three Books Worth Re-reading This Year", content: "Some books reveal more on the second pass...", cover_image_url: "https://picsum.photos/1200/675", publish: true, }), }); const data = await response.json(); console.log(data); ``` ```python import requests response = requests.post( "https://api.getxapi.com/twitter/article/create", headers={"Authorization": "Bearer API_KEY"}, json={ "auth_token": "your_auth_token", "title": "Three Books Worth Re-reading This Year", "content": "Some books reveal more on the second pass...", "cover_image_url": "https://picsum.photos/1200/675", "publish": True, }) print(response.json()) ``` Save as draft (no Premium required) [#save-as-draft-no-premium-required] ```bash curl -X POST "https://api.getxapi.com/twitter/article/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "title": "Draft for later", "content": "Work in progress..." }' ``` # Delete Article This endpoint costs `$0.005` per API call. Deletes an article entity (draft or published). Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `article_id` | string | Yes | Article id | | `proxy` | string | No | Your proxy URL | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * If you've already published the article, this deletes the article entity but the wrapper tweet may remain on the timeline. Call `/article/unpublish` first to clean both up. Response (200) [#response-200] ```json { "status": "success", "msg": "Article deleted", "data": { "article_id": "2055606612363202560" } } ``` Examples [#examples] ```bash curl -X POST "https://api.getxapi.com/twitter/article/delete" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "article_id": "2055606612363202560" }' ``` # Get Article This endpoint costs `$0.001` per API call. Query Parameters [#query-parameters] Provide **one** of the two lookups below: By wrapper tweet ID (public read) [#by-wrapper-tweet-id-public-read] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------------------------------------- | | `id` | string | Yes | Tweet ID that wraps the article (e.g. from `x.com//status/`) | By article entity ID (owner-only read) [#by-article-entity-id-owner-only-read] | Parameter | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------- | | `article_id` | string | Yes | The article's entity `rest_id` (as returned by `/article/list` or `/article/create`) | | `auth_token` | string | Yes | Required when using `article_id`. The article entity lookup is owner-scoped, returns no data for non-owners | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `proxy` | string | No | Caller-supplied proxy URL (`http`, `https`, `socks5`, `socks4`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Both paths return the same response shape, your client doesn't need to branch. * For `id` (wrapper tweet ID), the tweet must be an article tweet (e.g. `x.com/i/article/...`). Regular tweets return 404. * For `article_id` lookups, the auth\_token must own the article entity. Returns 404 with `Article not found or not owned by caller` otherwise. * Owner-only lookups (`article_id` path) include two extra fields in the response: `tweet_id` (null for drafts) and `lifecycle` (`"Draft"` or `"Published"`). * `contents` is an array of rich text blocks with types: `unstyled`, `header-two`, `header-three`, `image` (with `url`, `width`, `height`), `divider`, `blockquote`, `ordered-list-item`, `unordered-list-item`. * Text blocks may include `inlineStyleRanges` for formatting (`Bold`, `Italic`). Response (200) [#response-200] ```json { "status": "success", "msg": "success", "article": { "id": "QXJ0aWNsZUVudGl0eToxOTA1NTQxMTM5MTQ0Mjk4NDk4", "author": { "type": "user", "userName": "Decentralisedco", "url": "https://x.com/Decentralisedco", "twitterUrl": "https://twitter.com/Decentralisedco", "id": "1764631141926002689", "name": "DCo", "isVerified": false, "isBlueVerified": true, "verifiedType": null, "profilePicture": "https://pbs.twimg.com/profile_images/..._normal.jpg", "coverPicture": "https://pbs.twimg.com/profile_banners/...", "description": "Investments and Research.", "location": "Subscribe →", "followers": 13657, "following": 325, "canDm": true, "createdAt": "Mon Mar 04 12:37:18 +0000 2024", "favouritesCount": 1620, "statusesCount": 1053, "mediaCount": 487, "pinnedTweetIds": ["2029569587029934133"] }, "replyCount": 16, "likeCount": 160, "quoteCount": 8, "viewCount": 44300, "createdAt": "Fri Mar 28 09:01:12 +0000 2025", "title": "When Tokens Burn", "preview_text": "Breaking down how crypto protocols approach revenue and cash flow...", "cover_media_img_url": "https://pbs.twimg.com/media/GnHXrznXUAEs-v5.jpg", "contents": [ { "type": "unstyled", "text": "Breaking down how crypto protocols approach revenue and cash flow", "inlineStyleRanges": [{ "length": 65, "offset": 0, "style": "Italic" }] }, { "type": "header-two", "text": "The Zero-Sum Attention Game" }, { "type": "unstyled", "text": "In 2021, each crypto asset had, on average, ~$1.8M in stablecoin liquidity..." }, { "type": "image", "url": "https://pbs.twimg.com/media/GnHZB47WwAAb6hU.png", "width": 2040, "height": 1372 }, { "type": "divider" }, { "type": "blockquote", "text": "One key insight here is..." }, { "type": "ordered-list-item", "text": "First point", "inlineStyleRanges": [{ "length": 5, "offset": 0, "style": "Bold" }] } ] } } ``` Error Responses [#error-responses] 400 - Missing or invalid parameters [#400---missing-or-invalid-parameters] ```json { "error": "Missing required query param. Provide either `id` (wrapper tweet ID, public) or `article_id` (article entity rest_id, owner-only, also requires `auth_token`)." } ``` ```json { "error": "`article_id` requires `auth_token` (owner-only read). Use `id` (wrapper tweet ID) for public lookups." } ``` 404 - Not an article tweet / not owned [#404---not-an-article-tweet--not-owned] ```json { "error": "No article found for tweet: 2019264360682778716" } ``` ```json { "error": "Article not found or not owned by caller: 2055606612363202560" } ``` Examples [#examples] Public read, by wrapper tweet ID [#public-read-by-wrapper-tweet-id] ```bash curl -X GET "https://api.getxapi.com/twitter/article/get?id=1905545699552375179" \ -H "Authorization: Bearer API_KEY" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/article/get?id=1905545699552375179", { headers: { Authorization: "Bearer API_KEY", }, } ); const data = await response.json(); console.log(data.article); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/article/get", headers={"Authorization": "Bearer API_KEY"}, params={"id": "1905545699552375179"}) print(response.json()["article"]) ``` Owner-only read, by article entity ID [#owner-only-read-by-article-entity-id] ```bash curl -X GET "https://api.getxapi.com/twitter/article/get?article_id=2055606612363202560&auth_token=YOUR_AUTH_TOKEN" \ -H "Authorization: Bearer API_KEY" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/article/get?article_id=2055606612363202560&auth_token=YOUR_AUTH_TOKEN", { headers: { Authorization: "Bearer API_KEY", }, } ); const data = await response.json(); console.log(data.article.lifecycle, data.article.tweet_id); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/article/get", headers={"Authorization": "Bearer API_KEY"}, params={ "article_id": "2055606612363202560", "auth_token": "YOUR_AUTH_TOKEN", }) article = response.json()["article"] print(article["lifecycle"], article["tweet_id"]) ``` # List Articles This endpoint costs `$0.005` per API call. Paginated list of the `auth_token` user's own articles. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `lifecycle` | string | No | `"Draft"` (default) or `"Published"` | | `cursor` | string | No | Pagination cursor from a previous response | | `count` | number | No | Page size, default `20` | | `proxy` | string | No | Your proxy URL | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. Response (200) [#response-200] ```json { "userId": "1454465540949307394", "lifecycle": "Draft", "article_count": 5, "has_more": true, "next_cursor": "...", "articles": [ { "article_id": "2055861112222986240", "title": "Renamed (still safe to delete)", "preview_text": "", "lifecycle": "Draft", "cover_media": null, "tweet_id": null, "created_at_secs": 1778990421, "modified_at_secs": 1778990510 } ] } ``` Examples [#examples] ```bash curl -X POST "https://api.getxapi.com/twitter/article/list" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "lifecycle": "Published" }' ``` Paginate [#paginate] ```bash curl -X POST "https://api.getxapi.com/twitter/article/list" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "lifecycle": "Draft", "cursor": "NEXT_CURSOR_FROM_PREV_RESPONSE", "count": 50 }' ``` # Publish Article This endpoint costs `$0.005` per API call. **Premium-only**: the `auth_token` account must have an active X Premium subscription. Non-Premium accounts will receive `403`. Publishes an existing draft. Use this if you saved the article first via `/article/create` with `publish: false`. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token (Premium account) | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `article_id` | string | Yes | Draft article id | | `proxy` | string | No | Your proxy URL | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. Response (200) [#response-200] ```json { "status": "success", "msg": "Article published successfully", "data": { "article_id": "2055606612363202560", "tweet_id": "2055606621209002254", "public_url": "https://x.com/i/status/2055606621209002254" } } ``` Error Responses [#error-responses] 403 - Not Premium [#403---not-premium] ```json { "error": "Publishing articles requires an active X Premium subscription on this account" } ``` Examples [#examples] ```bash curl -X POST "https://api.getxapi.com/twitter/article/publish" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "article_id": "2055606612363202560" }' ``` # Unpublish Article This endpoint costs `$0.005` per API call. Moves a published article back to draft and removes the wrapper tweet from the timeline. The article entity is preserved as a draft. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `article_id` | string | Yes | Published article id | | `proxy` | string | No | Your proxy URL | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. Response (200) [#response-200] ```json { "status": "success", "msg": "Article unpublished", "data": { "article_id": "2055606612363202560" } } ``` Examples [#examples] ```bash curl -X POST "https://api.getxapi.com/twitter/article/unpublish" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "article_id": "2055606612363202560" }' ``` # Update Article This endpoint costs `$0.005` per API call. Works on both drafts and published articles. Partial update, pass only the fields you want to change; the others stay untouched. Request Body [#request-body] | Field | Type | Required | Description | | -------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `article_id` | string | Yes | The article id returned from `/article/create` or `/article/list` | | `title` | string | No | New title | | `content` | string | No | New markdown body (replaces the body wholesale) | | `cover_media_id` | string | No | Pre-uploaded media id to set as new cover | | `cover_image_url` | string | No | URL to upload as the new cover | | `cover_image_base64` | string | No | Base64-encoded image data for the new cover | | `proxy` | string | No | Your proxy URL | At least one of `title`, `content`, or one of the three cover fields must be provided. Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. Response (200) [#response-200] ```json { "status": "success", "msg": "Article updated", "data": { "article_id": "2055606612363202560", "updated": { "title": true, "content": false, "cover_media_id": "2055606606466023424" } } } ``` Error Responses [#error-responses] 400 - Missing identifier or update fields [#400---missing-identifier-or-update-fields] ```json { "error": "Missing required field: article_id" } ``` Examples [#examples] ```bash curl -X POST "https://api.getxapi.com/twitter/article/update" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "article_id": "2055606612363202560", "title": "Renamed Article Title" }' ``` Replace the body and cover [#replace-the-body-and-cover] ```bash curl -X POST "https://api.getxapi.com/twitter/article/update" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "article_id": "2055606612363202560", "content": "# Rewritten\n\nFresh take.", "cover_image_url": "https://picsum.photos/1200/675" }' ``` # Community Info This endpoint costs `$0.001` per API call. Returns public details for an X community — name, description, banner image, member/moderator counts, rules, admin/creator, topic, and join policy. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------------------------ | | `communityId` | string | Yes | Numeric community ID (from the community URL `x.com/i/communities/`) | Response [#response] ```json { "status": "success", "msg": "success", "data": { "id": "1493446837214187523", "name": "Build in Public", "description": "Share what you're working on. Get feedback. Help each other move forward.", "createdAt": "2022-02-15T04:47:27.551Z", "memberCount": 264947, "moderatorCount": 6, "bannerUrl": "https://pbs.twimg.com/community_banner_img/.../orig", "joinPolicy": "Open", "invitesPolicy": "MemberInvitesAllowed", "isNsfw": false, "role": "NonMember", "primaryTopic": "Entrepreneurship", "rules": [ { "name": "Share what you're working on", "description": "Don't be scared to share unfinished work" }, { "name": "No self-promotion", "description": "Don't ask for upvotes or advertise your products/services." } ], "searchTags": ["buildinpublic", "buildinginpublic"], "question": "What are you working on?", "admin": { "id": "641633", "name": "Marc Köhlbrugge", "userName": "marckohlbrugge" }, "creator": { "id": "641633", "name": "Marc Köhlbrugge", "userName": "marckohlbrugge" } } } ``` Response Fields [#response-fields] | Field | Type | Description | | ---------------- | -------------- | ----------------------------------------------------------------- | | `id` | string | Community ID | | `name` | string | Community name | | `description` | string | Community description | | `createdAt` | string | Creation date (ISO 8601) | | `memberCount` | number | Total members | | `moderatorCount` | number | Number of moderators | | `bannerUrl` | string \| null | Banner image URL (custom, or default if none) | | `joinPolicy` | string | `Open`, or a restricted policy requiring approval | | `invitesPolicy` | string | Who can invite (e.g. `MemberInvitesAllowed`) | | `isNsfw` | boolean | Marked NSFW | | `role` | string | The requesting account's role (`NonMember`, `Member`, `Admin`, …) | | `primaryTopic` | string \| null | Community topic/category | | `rules` | array | Community rules — each `{ name, description }` | | `searchTags` | array | Associated hashtags/tags | | `question` | string \| null | Join question shown to prospective members | | `admin` | object \| null | Admin user `{ id, name, userName }` | | `creator` | object \| null | Creator user `{ id, name, userName }` | Error Responses [#error-responses] 404 - Community not found [#404---community-not-found] ```json { "error": "Could not find community 1111111111111111" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/community/info?communityId=1493446837214187523" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/community/info?communityId=1493446837214187523", { headers: { Authorization: "Bearer API_KEY" } } ); const { data } = await response.json(); console.log(data.name, data.memberCount); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/community/info", params={"communityId": "1493446837214187523"}, headers={"Authorization": "Bearer API_KEY"}, ) print(response.json()["data"]["name"]) ``` # Join Community This endpoint costs `$0.002` per API call. Joins an X community on behalf of the `auth_token`'s account. Request Body [#request-body] | Field | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `community_id` | string | Yes | Numeric community ID (e.g. from the community URL `x.com/i/communities/`) | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * The server derives `ct0` and `twid` automatically from `auth_token`. * Some communities require approval; in that case the account is left in a pending/requested state rather than immediately joined. Response (200) [#response-200] ```json { "status": "success", "msg": "Joined community", "data": { "community_id": "1493446837214187523", "name": "Build in Public", "role": "Member", "joined": true } } ``` Error Responses [#error-responses] 400 - Missing community_id [#400---missing-community_id] ```json { "error": "Missing required field: community_id" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/community/join" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "community_id": "1493446837214187523" }' ``` # DM Conversation This endpoint costs `$0.001` per API call and returns \~50 messages per page. Fetches the full message history of one conversation, newest first, with cursor pagination. Get the `conversation_id` from [DM List](/docs/dm/dm-list). Request Body [#request-body] | Field | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `conversation_id` | string | Yes | Conversation ID from `/twitter/dm/list` (e.g. `3012852462-1345154135381794816`) | | `cursor` | string | No | `next_cursor` from a previous response, fetches older messages | | `count` | number | No | Page size (default 50) | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Messages carry the same rich fields as [DM List](/docs/dm/dm-list): `media` (photo/video/gif), `shared_tweet`, `card`, `links`, `reactions`, `reply_to_message_id`, `edit_count`. * Page through `next_cursor` until `has_more` is `false` to read the whole thread. Response (200) [#response-200] ```json { "userId": "1345154135381794816", "conversation_id": "3012852462-1345154135381794816", "message_count": 14, "has_more": false, "next_cursor": null, "messages": [ { "id": "2059239475419779072", "conversationId": "3012852462-1345154135381794816", "createdAt": "2026-05-26T10:11:00.000Z", "senderId": "3012852462", "recipientId": "1345154135381794816", "text": "see you then", "media": null, "shared_tweet": null, "card": null, "links": [], "reactions": [], "reply_to_message_id": null, "edit_count": 0 } ], "events": [], "users": { "3012852462": { "id": "3012852462", "screen_name": "someuser", "name": "Some User", "profile_image_url": "https://pbs.twimg.com/...", "verified": false, "is_blue_verified": true, "protected": false } } } ``` Error Responses [#error-responses] 400 - Missing conversation_id [#400---missing-conversation_id] ```json { "error": "Missing required field: conversation_id (get it from /dm/list)" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/dm/conversation" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "conversation_id": "3012852462-1345154135381794816" }' # Older messages (pagination) curl -X POST "https://api.getxapi.com/twitter/dm/conversation" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "conversation_id": "3012852462-1345154135381794816", "cursor": "1956659797668864455", "count": 50 }' ``` # DM List This endpoint costs `$0.002` per API call and returns \~50 conversations/messages per page. Lists the auth\_token holder's DM inbox, conversations plus their recent messages, across the three inbox tabs. Page through `next_cursor` to retrieve the entire inbox. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `tab` | string | No | Inbox to read: `all` (default, your main inbox), `requests` (message requests), `hidden` (low-quality / spam) | | `cursor` | string | No | Pagination cursor (`next_cursor`) from a previous response. Pages within the same `tab` | | `count` | number | No | Page size (default 50) | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * The first page (no `cursor`) returns the most recent conversations for the tab. Keep calling with `next_cursor` until `has_more` is `false` to retrieve the whole inbox. * `conversations` groups messages by thread (full participant profiles + last-message preview + read/unread state); `messages` is the flat list of message events in the page; `events` are non-message conversation events (request accepted, encryption enabled, etc.); `users` is a lookup of every referenced user. * To read one conversation's full history, use [DM Conversation](/docs/dm/dm-conversation). Field reference [#field-reference] **Conversation:** `conversation_id`, `type` (`ONE_TO_ONE` | `GROUP_DM`), `name` + `avatar_url` (group DMs), `trusted`, `low_quality`, `muted`, `nsfw`, `read_only`, `notifications_disabled`, `unread`, `last_read_event_id`, `sort_timestamp`, `participants[]`, `last_message`. **Participant / user:** `id`, `screen_name`, `name`, `description`, `profile_image_url`, `followers_count`, `friends_count`, `verified`, `is_blue_verified`, `protected`. **Message:** `id`, `conversationId`, `createdAt`, `senderId`, `recipientId`, `text`, `media` (photo/video/animated\_gif, `url`, `width`, `height`, plus `video_url` + `duration_ms` for video), `shared_tweet` (a quoted tweet's id/url/text/author), `card` (link preview), `links[]`, `reactions[]`, `reply_to_message_id`, `edit_count`. Response (200) [#response-200] ```json { "userId": "1345154135381794816", "tab": "all", "conversation_count": 14, "message_count": 99, "has_more": true, "next_cursor": "1928684596763250690", "conversations": [ { "conversation_id": "3012852462-1345154135381794816", "type": "ONE_TO_ONE", "name": null, "avatar_url": null, "trusted": true, "low_quality": false, "muted": false, "nsfw": false, "read_only": false, "notifications_disabled": false, "unread": false, "last_read_event_id": "2059239475419779072", "sort_timestamp": "2026-05-26T10:11:00.000Z", "participants": [ { "id": "3012852462", "screen_name": "someuser", "name": "Some User", "profile_image_url": "https://pbs.twimg.com/...", "followers_count": 1200, "verified": false, "is_blue_verified": true, "protected": false } ], "last_message": { "id": "2059239475419779072", "text": "see you then", "senderId": "3012852462", "createdAt": "2026-05-26T10:11:00.000Z" } } ], "messages": [ { "id": "2059239475419779072", "conversationId": "3012852462-1345154135381794816", "createdAt": "2026-05-26T10:11:00.000Z", "senderId": "3012852462", "recipientId": "1345154135381794816", "text": "check this out", "media": { "type": "photo", "url": "https://pbs.twimg.com/dm/.../photo.jpg", "width": 1200, "height": 800 }, "shared_tweet": null, "card": null, "links": [], "reactions": [], "reply_to_message_id": null, "edit_count": 0 } ], "events": [ { "type": "trust_conversation", "conversationId": "1345154135381794816-1516927548352917504", "createdAt": "2026-03-01T09:00:00.000Z", "reason": "accept" } ], "users": { "3012852462": { "id": "3012852462", "screen_name": "someuser", "name": "Some User", "profile_image_url": "https://pbs.twimg.com/...", "followers_count": 1200, "verified": false, "is_blue_verified": true, "protected": false } } } ``` Error Responses [#error-responses] 400 - Invalid tab [#400---invalid-tab] ```json { "error": "Invalid tab \"foo\". Allowed: all (inbox), requests (message requests), hidden (low-quality/spam)." } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Examples [#examples] ```bash # Main inbox curl -X POST "https://api.getxapi.com/twitter/dm/list" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token" }' # Message requests, next page curl -X POST "https://api.getxapi.com/twitter/dm/list" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "tab": "requests", "cursor": "1928684596763250690", "count": 50 }' # Hidden / spam requests curl -X POST "https://api.getxapi.com/twitter/dm/list" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "tab": "hidden" }' ``` # DM Send This endpoint costs `$0.002` per API call. Request Body [#request-body] | Field | Type | Required | Description | | -------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `recipient_id` | string | No | Recipient user id | | `recipient_username` | string | No | Recipient username (without @) | | `text` | string | Yes | Message text | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Provide either `recipient_id` or `recipient_username`. * If `recipient_username` is used, it is resolved to a user id via the pool. Response (200) [#response-200] ```json { "status": "success", "msg": "DM sent successfully", "data": { "id": "2019384131067818211", "createdAt": "2026-02-05T12:14:29.846Z", "senderId": "1858475867762270208", "recipientId": "1234567890123456789", "text": "Hello!", "recipient_username": "target_user" } } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required field: recipient_id or recipient_username" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/dm/send" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "recipient_username": "target_user", "text": "Hello!" }' # With recipient_id curl -X POST "https://api.getxapi.com/twitter/dm/send" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "recipient_id": "1234567890123456789", "text": "Hello!" }' ``` ```javascript const response = await fetch("https://api.getxapi.com/twitter/dm/send", { method: "POST", headers: { Authorization: "Bearer API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ auth_token: "your_auth_token", recipient_username: "target_user", text: "Hello!", }), }); const data = await response.json(); console.log(data); ``` ```python import requests response = requests.post( "https://api.getxapi.com/twitter/dm/send", headers={"Authorization": "Bearer API_KEY"}, json={ "auth_token": "your_auth_token", "recipient_username": "target_user", "text": "Hello!", }, ) print(response.json()) ``` # List Members This endpoint costs `$0.001` per API call and returns \~20 users per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------- | | `listId` | string | Yes | List id | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Returns list members using the account pool. * Pagination uses `next_cursor` until `has_more` is false. Response (200) [#response-200] ```json { "listId": "1826032140213172224", "user_count": 20, "has_more": true, "next_cursor": "DAABCgABG...", "members": [ { "type": "user", "id": "1335924847940079620", "userName": "0xumarkhatab", "name": "Umar" } ] } ``` Error Responses [#error-responses] 400 - Missing listId [#400---missing-listid] ```json { "error": "Missing required query param: listId" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/list/members?listId=1826032140213172224" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/list/members?listId=1826032140213172224&cursor=DAABCgABG..." ``` # Upload Media This endpoint costs `$0.001` per API call. Upload an image, GIF, or video to Twitter and receive a `media_id` you can attach to a tweet via [Create Tweet](/docs/tweets/create-tweet). Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `media_url` | string | Conditional | Public URL of the media to fetch and upload. Provide exactly one of `media_url` or `media_data`. | | `media_data` | string | Conditional | Base64-encoded media bytes. Provide exactly one of `media_url` or `media_data`. | | `media_type` | string | Conditional | MIME type (e.g. `image/png`, `image/gif`, `video/mp4`). Required when using `media_data`; auto-detected from `media_url` when possible. | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Returns a `media_id` (string) you pass in the `media_ids` array of [Create Tweet](/docs/tweets/create-tweet). * Media IDs are short-lived, upload shortly before creating the tweet. * Supported: images (PNG, JPG, GIF, WEBP up to 5 MB), GIFs (up to 15 MB), videos (MP4 up to 512 MB). * For videos, the server runs the chunked INIT → APPEND → FINALIZE flow and waits for processing to complete. * `media_type` is required when sending `media_data`; for `media_url` we infer it from the response headers / file extension. Response (200) [#response-200] ```json { "status": "success", "msg": "Media uploaded successfully", "data": { "media_id": "2058029045372379136", "media_type": "image/png", "size": 24531 } } ``` Error Responses [#error-responses] 400 - No media provided [#400---no-media-provided] ```json { "error": "Provide either media_url or media_data" } ``` 502 - Upload failed [#502---upload-failed] ```json { "error": "Media upload failed, Twitter did not return a media_id" } ``` Examples [#examples] Upload by URL [#upload-by-url] ```bash curl -X POST "https://api.getxapi.com/twitter/media/upload" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "media_url": "https://example.com/photo.jpg" }' ``` ```javascript const response = await fetch("https://api.getxapi.com/twitter/media/upload", { method: "POST", headers: { Authorization: "Bearer API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ auth_token: "your_auth_token", media_url: "https://example.com/photo.jpg", }), }); const data = await response.json(); console.log(data.data.media_id); ``` ```python import requests response = requests.post( "https://api.getxapi.com/twitter/media/upload", headers={"Authorization": "Bearer API_KEY"}, json={ "auth_token": "your_auth_token", "media_url": "https://example.com/photo.jpg", }) print(response.json()["data"]["media_id"]) ``` Upload base64 bytes [#upload-base64-bytes] ```bash curl -X POST "https://api.getxapi.com/twitter/media/upload" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "media_data": "iVBORw0KGgo...", "media_type": "image/png" }' ``` Then attach to a tweet [#then-attach-to-a-tweet] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "text": "Posted with a pre-uploaded image", "media_ids": ["2058029045372379136"] }' ``` # Notifications This endpoint costs `$0.002` per API call. Returns the `auth_token` account's own notifications timeline across the **All**, **Mentions**, and **Verified** tabs. Notifications are private to the account, so this runs entirely as that account. Page through `next_cursor` to retrieve older notifications. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | The account's auth token cookie | | `ct0` | string | No | Optional CSRF token cookie. When provided with `twid`, skips server-side credential resolution | | `twid` | string | No | Optional user ID cookie, for example `u=1234567890`. Must be provided with `ct0` | | `tab` | string | No | Which tab to read: `all` (default), `mentions`, `verified` | | `cursor` | string | No | Pagination cursor (`next_cursor`) from a previous response | | `count` | number | No | Page size, 1-40 (default 20) | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either is omitted, the server resolves them from `auth_token`. * Keep calling with `next_cursor` until `has_more` is `false` to walk the full timeline. * Each item has a `type` (`reply`, `mention`, `quote`, `like`, `retweet`, `follow`, or X's raw event name for anything else), the `from_users` who triggered it, and the `target_tweet` involved (the reply/mention itself, or the tweet that was liked/retweeted). `target_tweet` is `null` for tweetless notifications such as follows. * `from_users` and `target_tweet` use the same user and tweet shapes as the rest of the API, so no extra lookup is needed for profile pictures or tweet text. Response (200) [#response-200] ```json { "status": "success", "msg": "success", "data": { "notifications": [ { "id": "HA4f2tBaMAAAAAABSQ3bERhMX3OVru__O_I", "type": "reply", "text": "@you That's really cool.", "timestamp": "2026-08-07T10:07:00.000Z", "from_users": [ { "type": "user", "userName": "someuser", "name": "Some User", "id": "195999802", "isBlueVerified": false, "profilePicture": "https://pbs.twimg.com/profile_images/.../normal.jpg" } ], "target_tweet": { "type": "tweet", "id": "2085669028551651826", "url": "https://x.com/someuser/status/2085669028551651826", "text": "@you That's really cool.", "createdAt": "Fri Aug 07 10:07:00 +0000 2026", "author": { "userName": "someuser", "name": "Some User" } } } ], "next_cursor": null, "has_more": false } } ``` Response Fields [#response-fields] | Field | Type | Description | | ------------------------------ | -------------- | ------------------------------------------------------------------------------- | | `notifications` | array | The notifications in this page, newest first | | `notifications[].id` | string | Notification ID | | `notifications[].type` | string | `reply`, `mention`, `quote`, `like`, `retweet`, `follow`, or X's raw event name | | `notifications[].text` | string \| null | The notification text (the reply/mention text, or the aggregated message) | | `notifications[].timestamp` | string \| null | ISO 8601 timestamp | | `notifications[].from_users` | array | The user(s) who triggered the notification | | `notifications[].target_tweet` | object \| null | The tweet involved, or `null` for tweetless notifications (e.g. follows) | | `next_cursor` | string \| null | Cursor for the next page, or `null` when there are no more | | `has_more` | boolean | Whether another page is available | Error Responses [#error-responses] 400 - Invalid tab [#400---invalid-tab] ```json { "error": "Invalid tab \"foo\". Allowed: all, mentions, verified." } ``` 400 - Missing auth_token [#400---missing-auth_token] ```json { "error": "Missing required field: auth_token" } ``` Examples [#examples] ```bash # All notifications curl -X POST "https://api.getxapi.com/twitter/notifications" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token" }' # Mentions, next page curl -X POST "https://api.getxapi.com/twitter/notifications" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "tab": "mentions", "cursor": "PASTE_NEXT_CURSOR" }' ``` ```javascript const response = await fetch("https://api.getxapi.com/twitter/notifications", { method: "POST", headers: { Authorization: "Bearer API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ auth_token: "your_auth_token", tab: "all" }), }); const { data } = await response.json(); for (const n of data.notifications) { console.log(n.type, n.from_users[0]?.userName, n.text); } ``` ```python import requests response = requests.post( "https://api.getxapi.com/twitter/notifications", headers={"Authorization": "Bearer API_KEY"}, json={"auth_token": "your_auth_token", "tab": "all"}, ) data = response.json()["data"] for n in data["notifications"]: print(n["type"], n["from_users"][0]["userName"] if n["from_users"] else None, n["text"]) ``` # Add Monitor Start watching an X account. From this moment on, every new tweet is delivered to your webhook. Each monitor counts as one slot against your plan limit, regardless of tier. For plans, detection latency and the polling comparison, see the [Twitter webhook API](https://www.getxapi.com/twitter-webhook-api) overview on the main site. Headers [#headers] | Header | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------- | | `Authorization` | string | Yes | `Bearer YOUR_API_KEY`, the same key used for every other GetXAPI endpoint | | `Content-Type` | string | Yes | `application/json` | Request Body [#request-body] | Field | Type | Required | Description | | ----------------- | ------- | ----------- | ---------------------------------------------------------------------------------------------------- | | `userName` | string | Yes | X handle without `@`, e.g. `elonmusk` | | `webhook_id` | string | Conditional | An existing webhook's UUID. Provide **this or** `webhook_url`, not both. | | `webhook_url` | string | Conditional | An HTTPS URL. Creates a webhook atomically and returns its secret. Provide **this or** `webhook_id`. | | `tier` | string | No | `standard` (default) or `fast`. See below. | | `include_replies` | boolean | No | Also deliver the account's replies. Default `false`. | You must supply **exactly one** of `webhook_id` or `webhook_url`. Sending both, or neither, returns `400`. Detection tiers [#detection-tiers] | Tier | Detection | Use when | | ---------- | ------------ | --------------------------------------------------------------------- | | `standard` | ≈ 15 seconds | Most cases: content pipelines, alerting, archiving | | `fast` | ≈ 2 seconds | Latency genuinely changes the outcome: trading signals, breaking news | Both consume one plan slot. `fast` availability depends on your plan. Notes [#notes] * **Monitoring starts now.** We record the account's newest tweet as a baseline and deliver only what comes after. No history is backfilled. * `userName` is resolved to X's permanent numeric account ID. If the account later changes its handle, monitoring continues uninterrupted. * `include_replies: false` means only the account's own posts and threads, with no replies to other people. Turn it on for full conversational coverage. * You can only have **one monitor per account**. Adding one you already watch, active or paused, returns `409 ALREADY_MONITORED`. Change it with [Update monitor](/docs/monitoring/update-monitor) instead. * Adding an account you previously **removed** reuses the original monitor rather than creating a duplicate. The response has `reactivated: true`, and it re-baselines, so tweets posted since you removed it are not replayed. * Using `webhook_url` creates the webhook and the monitor in a single atomic operation. If either fails, neither is created. What real-time monitoring is used for [#what-real-time-monitoring-is-used-for] The pattern is the same across most use cases: pick the accounts whose posts should trigger something, and let the tweet itself be the trigger. * **Market and trading signals**, where a handful of accounts move prices and `fast` detection earns its keep. * **Brand and reputation monitoring**, catching complaints and viral threads while they are still live. * **Competitor watch**, for launches, pricing changes, and announcements. * **Lead routing**, pushing tweets that show buying intent into a CRM or Slack channel. * **News and content pipelines**, assembling a feed from curated reporters without polling any of them. Detection frequency is the one decision worth thinking about. `standard` at around 15 seconds is right for nearly everything, because a content pipeline or an alert does not benefit from the extra 13 seconds. Reach for `fast` only where latency changes the outcome, since both tiers consume the same single plan slot either way. Response (200) [#response-200] ```json { "monitor_id": "b7e2c910-4f38-4a5d-9c81-2e6a7f0d3b45", "target_user_id": "44196397", "userName": "elonmusk", "tier": "fast", "include_replies": false, "webhook_id": "8f14e45f-ceea-467a-9f2b-1c3d4e5f6a7b", "status": "active", "reactivated": false, "message": "Monitoring @elonmusk. New tweets from now on will be delivered to your webhook." } ``` When `webhook_url` was used, the response also carries the new webhook's secret: ```json { "signing_secret": "whsec_9f2b1c3d4e5f6a7b8c9d0e1f2a3b4c5d", "note": "Store this secret now. It is shown only once." } ``` Response Fields [#response-fields] | Field | Type | Description | | ----------------- | ------- | --------------------------------------------------------------- | | `monitor_id` | string | UUID for updating or removing this monitor | | `target_user_id` | string | X's permanent numeric ID for the account | | `userName` | string | Handle as X currently reports it | | `tier` | string | The tier you requested | | `include_replies` | boolean | Whether replies are delivered | | `webhook_id` | string | Webhook receiving these deliveries | | `status` | string | `active` | | `reactivated` | boolean | `true` when an existing monitor was resumed rather than created | | `signing_secret` | string | Present only when `webhook_url` was used | Rate limit [#rate-limit] **20 requests per minute.** If you pass `webhook_url` to create a webhook inline, the request consumes **two** allowances: monitor-add *and* [webhook-create](/docs/monitoring/webhook-create) (10/min). Whichever runs out first returns the `429`. Passing an existing `webhook_id` instead consumes only the monitor-add allowance. Limits use a fixed 60-second window and are **shared across every API key on your account**, so creating extra keys does not raise the ceiling. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`; a `429` adds `Retry-After`. See [rate limits](/docs/monitoring/overview#rate-limits) for the full table. Errors [#errors] | Status | Code | Meaning | | ------ | ------------------- | --------------------------------------------------------------------------------------------------- | | `400` | n/a | Missing `userName`, both or neither webhook field, invalid `tier`, or a webhook URL we won't accept | | `403` | n/a | Monitoring is not enabled for this account | | `403` | `QUOTA_EXCEEDED` | Plan monitor limit reached. Remove one or upgrade | | `404` | n/a | No X account with that handle | | `409` | `ALREADY_MONITORED` | You already monitor this account. Update the existing monitor instead | | `429` | n/a | Rate limit exceeded. Wait `retry_after` seconds, then retry | | `502` | `BASELINE_FAILED` | We couldn't read the account's timeline to set a start point. **Retry** | `BASELINE_FAILED` is a safety behaviour, not a bug. If we can't reliably read where the timeline currently ends, starting anyway could deliver hundreds of old tweets as though they were new. We stop instead and ask you to retry. Example [#example] Using an existing webhook: ```bash curl -X POST https://api.getxapi.com/twitter/monitor/add \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userName": "elonmusk", "webhook_id": "8f14e45f-ceea-467a-9f2b-1c3d4e5f6a7b", "tier": "fast" }' ``` Creating the webhook inline: ```bash curl -X POST https://api.getxapi.com/twitter/monitor/add \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userName": "elonmusk", "webhook_url": "https://your-server.com/hooks/getxapi", "include_replies": true }' ``` # List Monitors Return every monitor on your account, active and paused. Headers [#headers] | Header | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------- | | `Authorization` | string | Yes | `Bearer YOUR_API_KEY`, the same key used for every other GetXAPI endpoint | Query Parameters [#query-parameters] None. The response is scoped to your account by your API key. Notes [#notes] * **Both `active` and `paused` monitors count against your plan limit.** Pausing stops delivery but keeps the slot so your configuration survives. Only [removing](/docs/monitoring/remove-monitor) a monitor frees one. * `current_username` reflects the account's handle **now**. If someone changed their handle after you added the monitor, this shows the new one. Monitoring follows the permanent account ID, not the handle. * `match_count_24h` counts tweets matched in the last 24 hours, which is the quickest way to spot a monitor that has silently stopped producing. * **`disabled` monitors were suspended because your subscription lapsed.** Nothing is deleted: the configuration is kept, the slot is released, and after you renew, [resuming](/docs/monitoring/update-monitor) revives the monitor (re-baselined, quota re-checked). Response (200) [#response-200] ```json { "monitors": [ { "monitor_id": "b7e2c910-4f38-4a5d-9c81-2e6a7f0d3b45", "target_user_id": "44196397", "current_username": "elonmusk", "requested_tier": "standard", "effective_tier": "fast", "include_replies": false, "status": "active", "webhook_id": "8f14e45f-ceea-467a-9f2b-1c3d4e5f6a7b", "last_tweet_id": "1815432109876543210", "match_count_24h": 37, "created_at": "2026-07-26T09:14:22.000Z" } ] } ``` Response Fields [#response-fields] | Field | Type | Description | | ------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `monitor_id` | string | UUID for updating or removing | | `target_user_id` | string | X's permanent numeric account ID | | `current_username` | string | The account's handle as of now | | `requested_tier` | string | The tier you asked for | | `effective_tier` | string | The tier this account is actually polled at (see below) | | `include_replies` | boolean | Whether replies are delivered | | `status` | string | `active`, `paused`, or `disabled` (suspended by a plan lapse, configuration kept; [resume](/docs/monitoring/update-monitor) after renewing) | | `webhook_id` | string | Where deliveries go | | `last_tweet_id` | string \| null | Most recent tweet delivered. `null` until the first one. | | `match_count_24h` | number | Tweets matched in the last 24 hours | | `created_at` | string | ISO 8601 creation time | requested_tier vs effective_tier [#requested_tier-vs-effective_tier] Accounts are polled once and the result shared by everyone watching them. So if another customer watching the same account requested `fast`, that account is polled every 2 seconds, and you receive tweets at that speed even though you asked for `standard`. That's why `effective_tier` can read higher than `requested_tier`. It is never *lower*. Treat it as a bonus rather than something to depend on: if the other customer removes their monitor, the account drops back to your requested tier. If you need `fast` reliably, request it. Rate limit [#rate-limit] **60 requests per minute.** Limits use a fixed 60-second window and are **shared across every API key on your account**, so creating extra keys does not raise the ceiling. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`; a `429` adds `Retry-After`. See [rate limits](/docs/monitoring/overview#rate-limits) for the full table. Errors [#errors] | Status | Meaning | | ------ | ----------------------------------------------------------- | | `403` | Monitoring is not enabled for this account | | `429` | Rate limit exceeded. Wait `retry_after` seconds, then retry | Example [#example] ```bash curl https://api.getxapi.com/twitter/monitor/list \ -H "Authorization: Bearer YOUR_API_KEY" ``` # Monitor Health A quick status read on your monitoring: how many monitors are running, how many deliveries are queued, and how many succeeded or failed in the last 24 hours. Use it as a liveness check from your own monitoring, or to debug why tweets stopped arriving. Headers [#headers] | Header | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------- | | `Authorization` | string | Yes | `Bearer YOUR_API_KEY`, the same key used for every other GetXAPI endpoint | Query Parameters [#query-parameters] None. Every count is scoped to your account by your API key. Response (200) [#response-200] ```json { "status": "operational", "monitors": { "active": 18, "paused": 2 }, "deliveries": { "pending": 3, "delivered_24h": 412, "failed_24h": 0 } } ``` Response Fields [#response-fields] | Field | Type | Description | | -------------------------- | ------ | ---------------------------------------------------------------- | | `status` | string | `operational` or `degraded`, the state of the monitoring service | | `monitors.active` | number | Your monitors currently delivering | | `monitors.paused` | number | Your paused monitors. These still hold a plan slot. | | `deliveries.pending` | number | Deliveries queued or waiting on a retry | | `deliveries.delivered_24h` | number | Successfully delivered in the last 24 hours | | `deliveries.failed_24h` | number | Permanently failed in the last 24 hours | `monitors.active + monitors.paused` is your plan-slot usage. Both count toward your limit. See [Remove monitor](/docs/monitoring/remove-monitor) to free one. Reading the numbers [#reading-the-numbers] | What you see | What it means | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `pending` steady near 0 | Healthy. Deliveries are being accepted as fast as they're produced. | | `pending` climbing | Your endpoint is slow, erroring, or unreachable. Fix it before the retry window runs out. | | `failed_24h > 0` | Deliveries were permanently dropped after \~21 minutes of retries. Those tweets are lost. | | `delivered_24h` at 0 with active monitors | Either the accounts genuinely haven't posted, or nothing is reaching you. Send a [test delivery](/docs/monitoring/webhook-test) to tell the two apart. | | `status: "degraded"` | A problem on our side. Deliveries may be delayed. | `failed_24h` is the number worth alerting on. It only rises when a tweet was permanently lost, which almost always means your endpoint was down or rejecting valid signatures for more than 20 minutes. If it's non-zero, backfill the gap with [User Tweets](/docs/users/user-tweets). Rate limit [#rate-limit] **60 requests per minute**, per customer. Limits use a fixed 60-second window and are **shared across every API key on your account**, so creating extra keys does not raise the ceiling. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`; a `429` adds `Retry-After`. See [rate limits](/docs/monitoring/overview#rate-limits) for the full table. Errors [#errors] | Status | Meaning | | ------ | ----------------------------------------------------------- | | `403` | Monitoring is not enabled for this account | | `429` | Rate limit exceeded. Wait `retry_after` seconds, then retry | | `503` | Monitoring is temporarily unavailable | Example [#example] ```bash curl https://api.getxapi.com/twitter/monitor/health \ -H "Authorization: Bearer YOUR_API_KEY" ``` Frequently asked questions [#frequently-asked-questions] How do I tell a quiet account from a broken pipeline? [#how-do-i-tell-a-quiet-account-from-a-broken-pipeline] Send a [test delivery](/docs/monitoring/webhook-test). If the test succeeds while `delivered_24h` sits at 0, your endpoint is fine and the accounts genuinely have not posted. If the test fails too, the problem is on the delivery path. What should I alert on? [#what-should-i-alert-on] `failed_24h` rising above 0, because that only happens once a tweet has been permanently lost. A climbing `pending` count is the earlier warning: it means deliveries are queuing up and you still have time to fix the endpoint before the retry window expires. Does a paused monitor still count against my plan? [#does-a-paused-monitor-still-count-against-my-plan] Yes. `monitors.active + monitors.paused` is your slot usage. Pausing stops delivery but keeps the configuration and the slot. What does status: "degraded" mean? [#what-does-status-degraded-mean] A problem on our side rather than yours. Deliveries may be delayed while it clears. Your own endpoint health is better read from `pending`, `delivered_24h` and `failed_24h`. # Overview Real-time tweet monitoring pushes new tweets to **your** server. Register the X accounts you care about, point them at a webhook URL, and every new tweet is delivered as a signed HTTPS `POST` within seconds of being published. No polling. No cron jobs. No missed tweets while your job was asleep. This section is the endpoint reference. For plans, detection latency and how push compares with polling, start with the [Twitter webhook API](https://www.getxapi.com/twitter-webhook-api) page on the main site. If you were looking for X's own webhook product and its CRC check, that is a different system, covered in [X webhooks, the CRC check and subscription limits](https://www.getxapi.com/blogs/twitter-webhooks-crc-guide). Monitoring endpoints are **not billed per API call**. Access comes with a Monitoring plan, and your plan sets how many accounts you can watch at once. Webhook deliveries and retries are never charged individually. How it works [#how-it-works] 1. **Create a webhook**: a destination URL plus a signing secret. [Create webhook](/docs/monitoring/webhook-create) 2. **Add a monitor**: tell us which X account to watch and which webhook to deliver to. [Add monitor](/docs/monitoring/add-monitor) 3. **Receive tweets**: every new tweet arrives at your URL as a signed `POST`. [Delivery contract](/docs/monitoring/webhook-delivery) That's the whole loop. Everything else is management: listing, pausing, repointing, removing. Authentication [#authentication] Every request uses the same bearer key as the rest of the API: ```bash curl https://api.getxapi.com/twitter/monitor/list \ -H "Authorization: Bearer YOUR_API_KEY" ``` If your account has never had a Monitoring plan, monitoring endpoints return `403`. If a previous subscription has **lapsed**, the read endpoints ([`list`](/docs/monitoring/list-monitors), [`health`](/docs/monitoring/monitor-health), [`webhook/list`](/docs/monitoring/webhook-list)) keep working so you can see your saved setup; everything else returns `403` with code `SUBSCRIPTION_LAPSED` until you renew. See the [error reference](/docs/monitoring/overview#errors) below. Concepts [#concepts] | Term | Meaning | | ------------ | ------------------------------------------------------------------------------------------------------------- | | **Webhook** | A destination URL and its signing secret. Reusable, so many monitors can deliver to one webhook. | | **Monitor** | One subscription: *you* watch *this X account*, delivering to *this webhook*. Counts against your plan limit. | | **Target** | The X account being watched. Shared across all customers watching it. | | **Delivery** | A single webhook `POST` for one tweet on one monitor. Identified by a stable `delivery_id`. | Two different "tiers" [#two-different-tiers] These are unrelated, and the names are easy to confuse: | | What it controls | Where it's set | | ------------- | ----------------------------------------------------------- | ---------------------------- | | **Poll tier** | How fast a tweet is detected: `standard` ≈ 15s, `fast` ≈ 2s | Per monitor, via `tier` | | **Plan tier** | How **many** accounts you can watch at once | Your Monitoring subscription | A `fast` monitor and a `standard` monitor both count as exactly one monitor against your plan limit. `fast` changes latency, not quota. Monitoring starts from now [#monitoring-starts-from-now] When you add a monitor we record a **baseline**, the newest tweet on that account at that moment. Only tweets posted **after** the baseline are delivered. There is no backfill. Adding a monitor for an account with 10,000 existing tweets will not send you 10,000 webhooks. The same applies when you resume a paused monitor: resuming re-baselines, so tweets posted during the pause are **not** replayed. If you need history, use [User Tweets](/docs/users/user-tweets) or [Advanced Search](/docs/tweets/advanced-search) alongside monitoring. Shared targets and effective_tier [#shared-targets-and-effective_tier] Targets are shared. If several customers watch the same account, it is polled once and the result fanned out to everyone watching it. A consequence you will see in [List monitors](/docs/monitoring/list-monitors): the response has both `requested_tier` and `effective_tier`. If someone else watching the same account requested `fast`, that account is polled at `fast` speed and **you get the benefit**. Your `effective_tier` reads `fast` even though you requested `standard`. `effective_tier` is never *slower* than what you requested. It is only ever equal or better, and it is not something you can rely on or pay less for. Treat it as a bonus, not a guarantee. Why monitor X accounts in real time [#why-monitor-x-accounts-in-real-time] Real-time tweet monitoring is the building block behind most X automation: * **Trading and market signals.** Headline accounts move markets, and a 15-second lag is the difference between acting on news and reading about it. * **Brand monitoring and social listening.** Catch mentions, complaints, and viral threads while the conversation is still live. * **Competitor tracking.** Know the moment a rival announces a launch, a price change, or a funding round. * **Lead generation.** Watch accounts that post buying intent and route new tweets straight into your CRM. * **News aggregation.** Build a feed from a curated set of reporters and wire services without polling any of them. * **Community and moderation tooling.** React to posts from members of your own community as they land. Because delivery is push-based, none of these need a scheduler, a queue of cron jobs, or a last-seen-ID table on your side. Webhooks vs polling vs the X streaming API [#webhooks-vs-polling-vs-the-x-streaming-api] Three ways to get new tweets, and what each actually costs you: | Approach | Latency | What you maintain | Availability | | ----------------------------------- | ------------------------- | ---------------------------------------------------- | ---------------------------- | | **GetXAPI webhooks** (this section) | 2s to 15s | A single HTTPS endpoint | Any Monitoring plan | | **Polling a read endpoint** | Your interval, plus drift | Cron, last-seen-ID state, backoff, dedupe | Any account, billed per call | | **Official X filtered stream** | Near real time | A held-open connection, reconnect and backfill logic | Gated to the enterprise tier | Polling works and is still the right answer when you need history rather than new posts. What it cannot do is scale to many accounts cheaply: every account you watch multiplies your call volume, whether or not anything was posted. Webhooks invert that, so you are only doing work when a tweet actually exists. For pulling the past rather than the present, use [User Tweets](/docs/users/user-tweets) or [Advanced Search](/docs/tweets/advanced-search). Delivery guarantees [#delivery-guarantees] * **At-least-once.** A tweet may be delivered more than once. Dedupe on `delivery_id`. * **Signed.** Every request carries an HMAC-SHA256 signature. [Verify it](/docs/monitoring/webhook-delivery#verifying-the-signature). It is the only thing proving the request came from us. * **Retried.** Failed deliveries retry with exponential backoff for roughly 21 minutes before being dropped. * **Ordered per target, not globally.** Tweets from one account arrive in order. Tweets from different accounts may interleave. Endpoints [#endpoints] **Webhooks** | Endpoint | Purpose | | --------------------------------------------------------------------------- | --------------------------------------------- | | [`POST /twitter/monitor/webhook/create`](/docs/monitoring/webhook-create) | Register a delivery URL, get a signing secret | | [`GET /twitter/monitor/webhook/list`](/docs/monitoring/webhook-list) | List your webhooks | | [`POST /twitter/monitor/webhook/test`](/docs/monitoring/webhook-test) | Send a signed test payload | | [`DELETE /twitter/monitor/webhook/delete`](/docs/monitoring/webhook-delete) | Remove a webhook | **Monitors** | Endpoint | Purpose | | ----------------------------------------------------------------- | ------------------------------------- | | [`POST /twitter/monitor/add`](/docs/monitoring/add-monitor) | Start watching an account | | [`GET /twitter/monitor/list`](/docs/monitoring/list-monitors) | List your monitors | | [`POST /twitter/monitor/update`](/docs/monitoring/update-monitor) | Pause, resume, change tier or webhook | | [`POST /twitter/monitor/remove`](/docs/monitoring/remove-monitor) | Stop watching, free a plan slot | | [`GET /twitter/monitor/health`](/docs/monitoring/monitor-health) | Your queue and delivery status | Rate limits [#rate-limits] The endpoints above are the **control plane**, how you manage and test your setup. They are rate limited to prevent abuse. This is not a cap on how much data you receive: **tweet deliveries and their retries are never rate limited**, no matter how often a monitored account posts. | Endpoint / action | Limit | | ------------------------------------------------------------------------- | --------------------------------- | | [`POST /webhook/create`](/docs/monitoring/webhook-create) | 10/min | | [`POST` or `DELETE /webhook/delete`](/docs/monitoring/webhook-delete) | 10/min combined across both verbs | | [`POST /webhook/test`](/docs/monitoring/webhook-test) | 5/min | | [`GET /webhook/list`](/docs/monitoring/webhook-list) | 60/min | | [`POST /add`](/docs/monitoring/add-monitor) | 20/min | | [`GET /list`](/docs/monitoring/list-monitors) | 60/min | | [`POST /update`](/docs/monitoring/update-monitor) | 30/min | | [`POST /update`](/docs/monitoring/update-monitor) with `action: "resume"` | 20/min | | [`POST /remove`](/docs/monitoring/remove-monitor) | 30/min | | [`GET /health`](/docs/monitoring/monitor-health) | 60/min | **Fixed 60-second windows.** A window opens on your first request and resets 60 seconds later, when the full allowance returns. There is no gradual refill. **Shared across all your API keys.** The counter is keyed to your account, not to the key you send. Creating more keys does not raise your ceiling. **Some requests consume two allowances.** [`POST /add`](/docs/monitoring/add-monitor) with an inline `webhook_url` draws on both the monitor-add *and* the webhook-create budget, since it creates a webhook as a side effect. Both are checked before either is charged, so a `429` never spends the allowance that still had room. **Throttled requests cost nothing.** No monitoring endpoint is billed per call, so a `429` never touches your credit balance. Rate limit headers [#rate-limit-headers] Every response tells you where you stand on whichever limit that request came closest to reaching: | Header | Meaning | | --------------------- | -------------------------------------------------- | | `RateLimit-Limit` | Requests allowed in the current window | | `RateLimit-Remaining` | Requests left in the current window | | `RateLimit-Reset` | Seconds until the window resets | | `Retry-After` | **On `429` only.** Seconds to wait before retrying | 429 response [#429-response] ```json { "error": "Rate limit exceeded. Please try again later.", "retry_after": 42 } ``` The webhook endpoints return the same shape with a more specific message: ```json { "error": "Webhook rate limit exceeded. Please try again later.", "retry_after": 42 } ``` `retry_after` is in seconds and always matches the `Retry-After` header. Wait it out rather than retrying immediately: a retry inside the same window returns `429` again. Errors [#errors] | Status | Code | Meaning | | ------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `400` | n/a | Missing or invalid parameters, or a webhook URL we won't accept | | `403` | n/a | Monitoring is not enabled for this account | | `403` | `SUBSCRIPTION_LAPSED` | Your subscription has ended or has a payment issue. Reads still work; renew to resume. Your monitors are kept, suspended | | `403` | `QUOTA_EXCEEDED` | You've hit your plan's monitor limit. Remove one or upgrade | | `404` | n/a | Monitor, webhook, or X account not found | | `409` | `ALREADY_MONITORED` | You already have a monitor on that account | | `409` | `WEBHOOK_IN_USE` | The webhook still has active monitors attached | | `429` | n/a | Rate limit exceeded. Wait `retry_after` seconds, then retry. See [rate limits](#rate-limits) | | `502` | `BASELINE_FAILED` | We couldn't read the account's timeline to set a start point. Retry | | `503` | n/a | Monitoring is temporarily unavailable | `BASELINE_FAILED` is deliberate. Rather than risk starting a monitor from the wrong point and flooding you with old tweets, we fail and ask you to retry. Frequently asked questions [#frequently-asked-questions] How fast will I receive a new tweet? [#how-fast-will-i-receive-a-new-tweet] Around 15 seconds on the `standard` tier and around 2 seconds on `fast`, measured from the moment the tweet is published to the moment we send the `POST`. Your own handler time is on top of that. Do I get charged per tweet delivered? [#do-i-get-charged-per-tweet-delivered] No. Monitoring is not billed per API call. Your plan sets how many accounts you can watch at once, and deliveries plus retries are never charged individually. Can I monitor replies and quote tweets? [#can-i-monitor-replies-and-quote-tweets] Replies, yes: set `include_replies: true` on the monitor. Quote tweets are the account's own posts, so they are always delivered, and each payload carries an `isQuote` flag. Will I get tweets from before I added the monitor? [#will-i-get-tweets-from-before-i-added-the-monitor] No. Monitoring is forward-only from the baseline we record when the monitor is created. There is no backfill, and resuming a paused monitor re-baselines rather than replaying the gap. How many accounts can I monitor? [#how-many-accounts-can-i-monitor] As many as your plan's monitor limit allows. Both active and paused monitors hold a slot, so only [removing](/docs/monitoring/remove-monitor) a monitor frees one. What happens if my server goes down? [#what-happens-if-my-server-goes-down] Failed deliveries retry with exponential backoff for roughly 21 minutes. Past that they are dead-lettered and counted in `failed_24h` on [Health](/docs/monitoring/monitor-health). Tweets dropped that way are not recoverable through monitoring, so backfill them with [User Tweets](/docs/users/user-tweets). # Remove Monitor Stop watching an account and free the plan slot it occupied. Headers [#headers] | Header | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------- | | `Authorization` | string | Yes | `Bearer YOUR_API_KEY`, the same key used for every other GetXAPI endpoint | | `Content-Type` | string | Yes | `application/json` | Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------- | | `monitor_id` | string | Yes | UUID of the monitor to remove | Notes [#notes] * This is the **only** way to free a plan slot. [Pausing](/docs/monitoring/update-monitor) keeps the slot. * Removing does not delete the webhook. It stays registered and can serve other monitors. * You can re-add the same account later with [Add monitor](/docs/monitoring/add-monitor). The response will show `reactivated: true`, and it re-baselines from the account's newest tweet at that moment, so nothing from the gap is replayed. * Deliveries already queued or mid-retry when you remove a monitor may still arrive. Your handler should tolerate a delivery for a monitor you just removed. Removing every monitor attached to a webhook also unblocks [deleting that webhook](/docs/monitoring/webhook-delete), which otherwise returns `409 WEBHOOK_IN_USE`. Response (200) [#response-200] ```json { "status": "removed", "monitor_id": "b7e2c910-4f38-4a5d-9c81-2e6a7f0d3b45" } ``` Rate limit [#rate-limit] **30 requests per minute.** Limits use a fixed 60-second window and are **shared across every API key on your account**, so creating extra keys does not raise the ceiling. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`; a `429` adds `Retry-After`. See [rate limits](/docs/monitoring/overview#rate-limits) for the full table. Errors [#errors] | Status | Meaning | | ------ | ----------------------------------------------------------- | | `400` | `monitor_id` missing or not a valid UUID | | `403` | Monitoring is not enabled for this account | | `404` | No monitor with that ID on your account | | `429` | Rate limit exceeded. Wait `retry_after` seconds, then retry | Example [#example] ```bash curl -X POST https://api.getxapi.com/twitter/monitor/remove \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "monitor_id": "b7e2c910-4f38-4a5d-9c81-2e6a7f0d3b45" }' ``` # Update Monitor Change an existing monitor in place: pause it, resume it, switch tier, toggle replies, or send it to a different webhook. Headers [#headers] | Header | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------- | | `Authorization` | string | Yes | `Bearer YOUR_API_KEY`, the same key used for every other GetXAPI endpoint | | `Content-Type` | string | Yes | `application/json` | Request Body [#request-body] | Field | Type | Required | Description | | ----------------- | ------- | -------- | ----------------------------------------- | | `monitor_id` | string | Yes | UUID of the monitor to change | | `action` | string | No | `pause` or `resume` | | `tier` | string | No | `standard` or `fast` | | `webhook_id` | string | No | Repoint deliveries to a different webhook | | `include_replies` | boolean | No | Turn reply delivery on or off | At least one of `action`, `tier`, `webhook_id`, or `include_replies` is required. You can combine them in a single call. Notes [#notes] * **Pausing does not free a plan slot.** A paused monitor stops delivering but still holds its slot, because your configuration is kept intact for when you resume. To free a slot, use [Remove monitor](/docs/monitoring/remove-monitor). * **`resume` re-baselines.** Tweets posted while the monitor was paused are **not** replayed. Resuming starts fresh from the account's current newest tweet, exactly like adding a monitor for the first time. * **`resume` also revives a monitor suspended by a plan lapse** (status `disabled`). The revival re-checks your current plan's quota. On a smaller plan than before, reviving past the limit returns `403 QUOTA_EXCEEDED`. If the monitor was `fast` but your current plan doesn't include fast detection, it resumes at `standard`. * Changing `tier` takes effect on the next poll cycle. * Repointing `webhook_id` affects future deliveries only. Deliveries already queued or retrying still go to the old webhook. Pausing is not a way to buffer tweets. Nothing is stored while paused. If you need continuity, leave the monitor running and filter on your side. Response (200) [#response-200] ```json { "monitor_id": "b7e2c910-4f38-4a5d-9c81-2e6a7f0d3b45", "status": "active", "requested_tier": "fast", "include_replies": false, "webhook_id": "8f14e45f-ceea-467a-9f2b-1c3d4e5f6a7b" } ``` Response Fields [#response-fields] | Field | Type | Description | | ----------------- | ------- | ------------------------------------- | | `monitor_id` | string | The monitor that was changed | | `status` | string | `active` or `paused` after the change | | `requested_tier` | string | Tier after the change | | `include_replies` | boolean | Reply setting after the change | | `webhook_id` | string | Webhook after the change | Rate limit [#rate-limit] **30 requests per minute** for most updates, but **20 requests per minute** for `action: "resume"`. Resume is stricter because it re-reads the account from X to set a fresh start point, while pause, tier changes, and webhook changes only touch our database. The two draw on separate allowances, so heavy pausing does not eat into your resume budget. Limits use a fixed 60-second window and are **shared across every API key on your account**, so creating extra keys does not raise the ceiling. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`; a `429` adds `Retry-After`. See [rate limits](/docs/monitoring/overview#rate-limits) for the full table. Errors [#errors] | Status | Code | Meaning | | ------ | ----------------- | ------------------------------------------------------------------------------------- | | `400` | n/a | `monitor_id` missing or invalid, no fields to change, or an invalid `tier` / `action` | | `403` | n/a | Monitoring is not enabled for this account | | `403` | `QUOTA_EXCEEDED` | Reviving a suspended monitor would exceed your current plan's limit | | `404` | n/a | No monitor with that ID on your account, or the target `webhook_id` doesn't exist | | `429` | n/a | Rate limit exceeded. Wait `retry_after` seconds, then retry | | `502` | `BASELINE_FAILED` | Couldn't read the timeline to re-baseline on `resume`. Retry | Examples [#examples] Pause: ```bash curl -X POST https://api.getxapi.com/twitter/monitor/update \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "monitor_id": "b7e2c910-4f38-4a5d-9c81-2e6a7f0d3b45", "action": "pause" }' ``` Upgrade to `fast` and start delivering replies, in one call: ```bash curl -X POST https://api.getxapi.com/twitter/monitor/update \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "monitor_id": "b7e2c910-4f38-4a5d-9c81-2e6a7f0d3b45", "tier": "fast", "include_replies": true }' ``` # Create Webhook Register a destination URL for tweet deliveries. Returns a signing secret you'll use to verify every request we send. One webhook can serve many monitors, so create one per destination, not one per account you watch. For plans, delivery latency and the polling comparison, see the [Twitter webhook API](https://www.getxapi.com/twitter-webhook-api) overview on the main site. Headers [#headers] | Header | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------- | | `Authorization` | string | Yes | `Bearer YOUR_API_KEY`, the same key used for every other GetXAPI endpoint | | `Content-Type` | string | Yes | `application/json` | Request Body [#request-body] | Field | Type | Required | Description | | ----- | ------ | -------- | --------------------------------------------------------------------------------- | | `url` | string | Yes | Where deliveries are sent. Must be **HTTPS** and resolve to a **public** address. | Notes [#notes] * The URL must be HTTPS. Plain HTTP is rejected. * It must resolve to a public address. Loopback, private, and link-local ranges are refused. `localhost`, `127.0.0.1`, and `10.x` / `192.168.x` addresses will not be accepted. Use a tunnel (ngrok, Cloudflare Tunnel) while developing. * The returned `signing_secret` is shown **once** and cannot be retrieved afterwards. Store it before you make another request. * Creating a webhook doesn't start any monitoring on its own. Attach it to a monitor with [Add monitor](/docs/monitoring/add-monitor). Response (200) [#response-200] ```json { "webhook_id": "8f14e45f-ceea-467a-9f2b-1c3d4e5f6a7b", "url": "https://your-server.com/hooks/getxapi", "status": "active", "signing_secret": "whsec_9f2b1c3d4e5f6a7b8c9d0e1f2a3b4c5d", "note": "Store this secret now. It is shown only once." } ``` **Save `signing_secret` immediately.** It is encrypted at rest and never returned again, not by [List webhooks](/docs/monitoring/webhook-list) and not by any other endpoint. If you lose it, delete the webhook and create a new one, then repoint your monitors. Storing the signing secret securely [#storing-the-signing-secret-securely] The secret is the only thing standing between your endpoint and a forged request, so treat it like a database password rather than a config value: * Keep it in an environment variable or a secret manager, never in source control and never in client-side code. * Give each environment its own webhook, so a leaked staging secret cannot be used to forge production traffic. * To rotate it, create a second webhook, repoint your monitors with [Update monitor](/docs/monitoring/update-monitor), then delete the old one. There is no in-place rotation, because the secret is never retrievable after creation. * If you lose it, you cannot recover it. Delete the webhook and create a new one. Response Fields [#response-fields] | Field | Type | Description | | ---------------- | ------ | ----------------------------------------------------------------------------------------------------------- | | `webhook_id` | string | UUID used to attach monitors to this webhook | | `url` | string | The registered destination | | `status` | string | `active` | | `signing_secret` | string | HMAC key for [verifying deliveries](/docs/monitoring/webhook-delivery#verifying-the-signature). Shown once. | | `note` | string | Reminder that the secret is not retrievable | Rate limit [#rate-limit] **10 requests per minute.** Limits use a fixed 60-second window and are **shared across every API key on your account**, so creating extra keys does not raise the ceiling. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`; a `429` adds `Retry-After`. See [rate limits](/docs/monitoring/overview#rate-limits) for the full table. Errors [#errors] | Status | Meaning | | ------ | ------------------------------------------------------------------------- | | `400` | `url` missing, not HTTPS, malformed, or resolving to a non-public address | | `403` | Monitoring is not enabled for this account | | `429` | Rate limit exceeded. Wait `retry_after` seconds, then retry | Example [#example] ```bash curl -X POST https://api.getxapi.com/twitter/monitor/webhook/create \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/hooks/getxapi" }' ``` Next: send yourself a [test delivery](/docs/monitoring/webhook-test) to confirm signature verification works before attaching a real monitor. # Delete Webhook Remove a webhook. Its signing secret is destroyed and it can no longer receive deliveries. Headers [#headers] | Header | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------- | | `Authorization` | string | Yes | `Bearer YOUR_API_KEY`, the same key used for every other GetXAPI endpoint | | `Content-Type` | string | Yes | `application/json` | Parameters [#parameters] | Field | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------------------------------------------- | | `webhook_id` | string | Yes | UUID of the webhook to delete. Accepted in the request body **or** as a query parameter. | Notes [#notes] * A webhook with **active monitors attached** cannot be deleted. You'll get `409 WEBHOOK_IN_USE`. Repoint those monitors with [Update monitor](/docs/monitoring/update-monitor) or remove them first. This is deliberate: silently orphaning monitors would stop deliveries with no visible cause. * Deletion is permanent. The signing secret is gone; recreating the same URL issues a **new** secret. * Deleting a webhook does not free plan slots. Slots are held by monitors. See [Remove monitor](/docs/monitoring/remove-monitor). **If your HTTP client can't send a body with `DELETE`**, pass it as a query parameter instead, `?webhook_id=…`, or use `POST /twitter/monitor/webhook/delete`, which is accepted on the same route and behaves identically. Response (200) [#response-200] ```json { "status": "deleted", "webhook_id": "8f14e45f-ceea-467a-9f2b-1c3d4e5f6a7b" } ``` Rate limit [#rate-limit] **10 requests per minute**, shared across both verbs. `POST` and `DELETE` to this path draw on the same allowance, so alternating between them does not double it. Limits use a fixed 60-second window and are **shared across every API key on your account**, so creating extra keys does not raise the ceiling. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`; a `429` adds `Retry-After`. See [rate limits](/docs/monitoring/overview#rate-limits) for the full table. Errors [#errors] | Status | Code | Meaning | | ------ | ---------------- | ---------------------------------------------------------------- | | `400` | n/a | `webhook_id` missing or not a valid UUID | | `403` | n/a | Monitoring is not enabled for this account | | `404` | n/a | No webhook with that ID on your account | | `409` | `WEBHOOK_IN_USE` | Active monitors still deliver here. Repoint or remove them first | | `429` | n/a | Rate limit exceeded. Wait `retry_after` seconds, then retry | Example [#example] ```bash curl -X DELETE https://api.getxapi.com/twitter/monitor/webhook/delete \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_id": "8f14e45f-ceea-467a-9f2b-1c3d4e5f6a7b" }' ``` Or with the ID in the query string: ```bash curl -X DELETE "https://api.getxapi.com/twitter/monitor/webhook/delete?webhook_id=8f14e45f-ceea-467a-9f2b-1c3d4e5f6a7b" \ -H "Authorization: Bearer YOUR_API_KEY" ``` # Webhook Delivery Everything your endpoint receives, and what it has to do in return. Payload [#payload] Every delivery is a `POST` with a JSON body: ```json { "type": "tweet", "delivery_id": "3f1c8a2e-4b7d-4c19-9e5a-8d2f6b1c0a34", "tweet": { "id": "1815432109876543210", "text": "Shipping something today.", "createdAt": "2026-07-26T09:14:22.000Z", "isReply": false, "isQuote": false, "author": { "id": "44196397", "userName": "elonmusk", "name": "Elon Musk", "isVerified": true } } } ``` `tweet` is the same normalized tweet object returned by the read endpoints, so whatever parses [Tweet Detail](/docs/tweets/tweet-detail) already parses this. `type` is `"tweet"` for real deliveries and `"test"` for payloads sent by [Test webhook](/docs/monitoring/webhook-test). Headers [#headers] ``` X-GetXAPI-Signature: t=1785057262,v1=4a7f2e... X-GetXAPI-Delivery-Id: 3f1c8a2e-4b7d-4c19-9e5a-8d2f6b1c0a34 Content-Type: application/json User-Agent: GetXAPI-Webhook/1.0 ``` | Header | Meaning | | ----------------------- | --------------------------------------------------------------- | | `X-GetXAPI-Signature` | `t` = Unix timestamp when signed, `v1` = HMAC-SHA256 hex digest | | `X-GetXAPI-Delivery-Id` | Stable ID for this delivery. Also present inside the body. | Verifying the signature [#verifying-the-signature] **This is not optional.** Your webhook URL is reachable by anyone who guesses it. The signature is the only thing that proves a request came from GetXAPI and wasn't modified in transit. Reject anything that fails verification. Why a signature and not a shared secret [#why-a-signature-and-not-a-shared-secret] A common shortcut is to put a static token in a header or query string and compare it. That proves the caller knows a secret, but nothing else: it does not prove the body is unmodified, and the token is replayable forever by anyone who captures one request. An HMAC signature covers the payload itself, so tampering invalidates it, and it is bound to a timestamp, so a captured request expires. That is why the check below verifies three things rather than one: the digest, the age, and the comparison method. The signature is an HMAC-SHA256 over the string `` `${t}.${raw_body}` `` keyed with your webhook's `signing_secret`. Three rules that catch most integration bugs: 1. **Use the raw request body**, exactly as received, before any JSON parsing. Parsing and re-serializing changes whitespace and key order, which changes the bytes, which breaks the signature. 2. **Check the timestamp.** Reject anything older than about 5 minutes so a captured request can't be replayed later. 3. **Compare in constant time.** Use your language's timing-safe comparison, not `==`. `delivery_id` is inside the signed body, so an attacker can't swap it to bypass your deduplication. Node.js (Express) [#nodejs-express] ```js const crypto = require("crypto"); const express = require("express"); const app = express(); // raw body is required, do NOT use express.json() on this route app.post("/hooks/getxapi", express.raw({ type: "application/json" }), (req, res) => { const rawBody = req.body.toString("utf8"); if (!verify(rawBody, req.get("X-GetXAPI-Signature"), process.env.GETXAPI_WEBHOOK_SECRET)) { return res.status(401).send("bad signature"); } const { tweet, delivery_id } = JSON.parse(rawBody); res.sendStatus(200); // ack FIRST enqueue(delivery_id, tweet); // then do the work }); function verify(rawBody, header, secret) { const m = /t=(\d+),v1=([a-f0-9]+)/.exec(header || ""); if (!m) return false; const [, t, sig] = m; if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // 5-minute window const expected = crypto .createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex"); const a = Buffer.from(sig, "utf8"); const b = Buffer.from(expected, "utf8"); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` Python (Flask) [#python-flask] ```python import hmac, hashlib, os, re, time from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ["GETXAPI_WEBHOOK_SECRET"] @app.post("/hooks/getxapi") def handle(): raw = request.get_data() # bytes, unparsed if not verify(raw, request.headers.get("X-GetXAPI-Signature", "")): abort(401) payload = request.get_json() enqueue(payload["delivery_id"], payload["tweet"]) return "", 200 def verify(raw: bytes, header: str) -> bool: m = re.match(r"t=(\d+),v1=([a-f0-9]+)", header) if not m: return False t, sig = m.group(1), m.group(2) if abs(time.time() - int(t)) > 300: return False expected = hmac.new( SECRET.encode(), f"{t}.".encode() + raw, hashlib.sha256 ).hexdigest() return hmac.compare_digest(sig, expected) ``` Go [#go] ```go func verify(rawBody []byte, header, secret string) bool { m := regexp.MustCompile(`t=(\d+),v1=([a-f0-9]+)`).FindStringSubmatch(header) if m == nil { return false } ts, err := strconv.ParseInt(m[1], 10, 64) if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(m[1] + ".")) mac.Write(rawBody) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(m[2]), []byte(expected)) } ``` PHP [#php] ```php function verify(string $rawBody, string $header, string $secret): bool { if (!preg_match('/t=(\d+),v1=([a-f0-9]+)/', $header, $m)) return false; [, $t, $sig] = $m; if (abs(time() - (int)$t) > 300) return false; $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); return hash_equals($expected, $sig); } // $rawBody = file_get_contents('php://input'); ``` Requirements for your endpoint [#requirements-for-your-endpoint] | Requirement | Why | | ----------------------------- | ------------------------------------------------------------------------------------------------- | | **HTTPS on a public address** | Plain HTTP and private/internal addresses are rejected when the webhook is created | | **Respond within 10 seconds** | Slower than that counts as a failure and triggers a retry | | **Return `2xx` immediately** | Acknowledge first, process asynchronously. Doing work inline is the most common cause of timeouts | | **Be idempotent** | Delivery is at-least-once. Dedupe on `delivery_id` | The single biggest mistake is doing real work (a database write, an LLM call, a downstream API request) before responding. Push it onto a queue, return `200`, and process it out of band. A busy account can deliver faster than your handler runs. Retries [#retries] We retry only when a failure looks temporary: | Response | Retried? | | ----------------------------------------------- | ----------------------- | | `2xx` | No, this is success | | `408`, `429`, any `5xx` | **Yes** | | Network error, DNS failure, timeout | **Yes** | | Any other `4xx` (`400`, `401`, `404`, `410`, …) | No, dropped immediately | A `404` or `410` means the endpoint is wrong, and repeating the request won't fix it. Those are dead-lettered right away so a stale URL doesn't generate traffic forever. Backoff is deliberate on both sides. Retrying immediately and at a fixed interval is what turns one brief outage into a **thundering herd**, where every pending delivery for every customer hits your endpoint at the same instant the moment it recovers. Exponential spacing plus jitter spreads that reconnect out so a recovering server is not knocked over by its own backlog. Retries use exponential backoff starting at 10 seconds, doubling each time, with ±20% jitter, up to **8 attempts**: ``` 10s → 20s → 40s → 80s → 2.7m → 5.3m → 10.7m ``` That's roughly **21 minutes** of retrying. After the last attempt the delivery is dead-lettered and appears in `failed_deliveries` on [Health](/docs/monitoring/monitor-health). Testing your integration [#testing-your-integration] Before pointing a monitor at it, use [Test webhook](/docs/monitoring/webhook-test). It sends a real, correctly signed request through the same delivery path as a live tweet and reports back the HTTP status your server returned, so you can confirm signature verification works before real traffic depends on it. ```json { "type": "test", "message": "GetXAPI webhook test", "timestamp": "2026-07-26T09:14:22.000Z", "delivery_id": "…" } ``` Note that a test payload has no `tweet` field. Branch on `type` if your handler assumes one is always present. Frequently asked questions [#frequently-asked-questions] How many times is a webhook retried? [#how-many-times-is-a-webhook-retried] Up to 8 attempts, spaced by exponential backoff with jitter, which is roughly 21 minutes end to end. After the final attempt the delivery is dead-lettered. Which HTTP status codes trigger a retry? [#which-http-status-codes-trigger-a-retry] `408`, `429`, and any `5xx`, plus network-level failures such as a timeout or DNS error. Every other `4xx` is treated as permanent and dropped immediately, because a `400`, `401`, `404` or `410` will not fix itself on the next attempt. Why do retries need jitter? [#why-do-retries-need-jitter] Without it every queued delivery retries on the same schedule and arrives simultaneously, so a recovering endpoint is hit by its entire backlog at once. The ±20% jitter spreads those attempts apart. Why did I receive the same tweet twice? [#why-did-i-receive-the-same-tweet-twice] Delivery is at-least-once. If your endpoint accepts a delivery but the acknowledgement is lost in transit, we retry it, because from our side an unacknowledged delivery is indistinguishable from a failed one. Dedupe on `delivery_id`, which is inside the signed body and therefore cannot be tampered with. My signature check keeps failing. What is wrong? [#my-signature-check-keeps-failing-what-is-wrong] Almost always the raw body. If any middleware parses the JSON before you verify, then re-serializes it, the bytes change and the digest will never match. Capture the raw bytes on that route specifically, as the examples above do. How long should I allow before rejecting an old timestamp? [#how-long-should-i-allow-before-rejecting-an-old-timestamp] About 5 minutes. Long enough to tolerate clock skew between your server and ours, short enough that a captured request cannot be replayed later. # List Webhooks Return every webhook registered on your account. For plans, delivery latency and the polling comparison, see the [Twitter webhook API](https://www.getxapi.com/twitter-webhook-api) overview on the main site. Headers [#headers] | Header | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------- | | `Authorization` | string | Yes | `Bearer YOUR_API_KEY`, the same key used for every other GetXAPI endpoint | Query Parameters [#query-parameters] None. The response is scoped to your account by your API key. Notes [#notes] * Signing secrets are **never** included. They are only shown once, at [creation](/docs/monitoring/webhook-create). * Use `webhook_id` from here when adding or repointing monitors. Response (200) [#response-200] ```json { "webhooks": [ { "webhook_id": "8f14e45f-ceea-467a-9f2b-1c3d4e5f6a7b", "url": "https://your-server.com/hooks/getxapi", "status": "active", "created_at": "2026-07-26T09:14:22.000Z" }, { "webhook_id": "2c9a7b31-5d84-4e60-b1f7-9a3c5e2d8f10", "url": "https://staging.your-server.com/hooks/getxapi", "status": "active", "created_at": "2026-07-20T16:02:41.000Z" } ] } ``` Response Fields [#response-fields] | Field | Type | Description | | ----------------------- | ------ | --------------------------- | | `webhooks[].webhook_id` | string | UUID for attaching monitors | | `webhooks[].url` | string | Destination URL | | `webhooks[].status` | string | `active` or `disabled` | | `webhooks[].created_at` | string | ISO 8601 creation time | Rate limit [#rate-limit] **60 requests per minute.** Limits use a fixed 60-second window and are **shared across every API key on your account**, so creating extra keys does not raise the ceiling. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`; a `429` adds `Retry-After`. See [rate limits](/docs/monitoring/overview#rate-limits) for the full table. Errors [#errors] | Status | Meaning | | ------ | ----------------------------------------------------------- | | `403` | Monitoring is not enabled for this account | | `429` | Rate limit exceeded. Wait `retry_after` seconds, then retry | Example [#example] ```bash curl https://api.getxapi.com/twitter/monitor/webhook/list \ -H "Authorization: Bearer YOUR_API_KEY" ``` # Test Webhook Send a real, correctly signed request to one of your webhooks and report back exactly what your server did with it. This goes through the same delivery path as a live tweet: same signing, same headers, same timeout. If a test succeeds, real deliveries will too. For plans, delivery latency and the polling comparison, see the [Twitter webhook API](https://www.getxapi.com/twitter-webhook-api) overview on the main site. Headers [#headers] | Header | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------- | | `Authorization` | string | Yes | `Bearer YOUR_API_KEY`, the same key used for every other GetXAPI endpoint | | `Content-Type` | string | Yes | `application/json` | Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | --------------------------- | | `webhook_id` | string | Yes | UUID of the webhook to test | Notes [#notes] * Run this **before** attaching a monitor. It's the cheapest way to catch a broken signature check. * The test payload has `type: "test"` and **no `tweet` field**. If your handler assumes `tweet` is always present, it will throw here, which is exactly the bug worth finding now. * A `delivered: false` response is a successful API call. The endpoint tells you what happened; it doesn't fail because your server did. * Test deliveries are not retried. What your server receives [#what-your-server-receives] ```json { "type": "test", "message": "GetXAPI webhook test", "timestamp": "2026-07-26T09:14:22.000Z", "delivery_id": "3f1c8a2e-4b7d-4c19-9e5a-8d2f6b1c0a34" } ``` With the same headers as a real delivery, including a valid `X-GetXAPI-Signature`. See the [delivery contract](/docs/monitoring/webhook-delivery) for how to verify it. Response (200) [#response-200] Your endpoint accepted it: ```json { "delivered": true, "http_status": 200, "error": null } ``` Your endpoint responded, but rejected it: ```json { "delivered": false, "http_status": 401, "error": null } ``` Your endpoint was never reached: ```json { "delivered": false, "http_status": 0, "error": "timeout" } ``` Response Fields [#response-fields] | Field | Type | Description | | ------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `delivered` | boolean | `true` when your endpoint returned `2xx` | | `http_status` | number | Status your server returned, or `0` if it was never reached | | `error` | string \| null | Only set when the request failed at the **network** level: `timeout`, a DNS failure, a refused connection. `null` whenever your server answered at all, including on a `4xx` or `5xx`. | Read the two fields together. `http_status: 500, error: null` means your server was reached and threw. `http_status: 0` with an `error` string means we never got a response at all: wrong host, firewall, or a handler slower than 10 seconds. Troubleshooting [#troubleshooting] | What you see | Usual cause | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `401` / `403` | Signature check failing. Almost always the body was parsed before verifying. You need the **raw** bytes. | | `404` | Route not registered, or a typo in the URL | | `500` | Your handler threw. Common when it assumes `tweet` exists on a `test` payload. | | `http_status: 0` with `error: "timeout"` | Server unreachable, or it took longer than 10 seconds to respond | Rate limit [#rate-limit] **5 requests per minute**, the tightest limit in the monitoring API, because each call makes us send a real HTTP request to your server. A throttled call **never contacts your webhook URL**. The `429` is returned before we attempt delivery, so a burst of test calls cannot be turned into a burst of traffic against someone else's server. Limits use a fixed 60-second window and are **shared across every API key on your account**, so creating extra keys does not raise the ceiling. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`; a `429` adds `Retry-After`. See [rate limits](/docs/monitoring/overview#rate-limits) for the full table. Errors [#errors] | Status | Meaning | | ------ | ----------------------------------------------------------- | | `400` | `webhook_id` missing or not a valid UUID | | `403` | Monitoring is not enabled for this account | | `404` | No webhook with that ID on your account | | `429` | Rate limit exceeded. Wait `retry_after` seconds, then retry | Example [#example] ```bash curl -X POST https://api.getxapi.com/twitter/monitor/webhook/test \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_id": "8f14e45f-ceea-467a-9f2b-1c3d4e5f6a7b" }' ``` # Download Space Download a Space as an MP3, with an optional timestamped transcript. Downloads run as an **async job**: you queue it, get a `job_id`, then poll for the result. Files are hosted for **7 days**, then auto-deleted. **Spaces downloads are pay-per-use.** Each download is billed at its real cost (a base fee plus a per-minute transcript rate) from your credit balance, spending your plan credits first and then your top-up wallet. Pricing [#pricing] Unlike standard endpoints, a Space download is priced by usage: | Component | Cost | | --------------------------- | -------------------------------------------------------------- | | Base (queue + MP3 download) | **$0.05** flat per job | | Transcript | **+$0.015 per minute** of audio (only when `transcribe: true`) | Polling the job status (below) is **free**. **Worked examples:** | Space length | `transcribe` | Cost | | ------------ | ------------ | ------------------------------- | | 30 min | `false` | $0.05 | | 30 min | `true` | $0.05 + 30 × $0.015 = **$0.50** | | 60 min | `false` | $0.05 | | 60 min | `true` | $0.05 + 60 × $0.015 = **$0.95** | | 90 min | `true` | $0.05 + 90 × $0.015 = **$1.40** | Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------- | -------- | -------------------------------------------------------------------------- | | `space_url` | string | Yes | Space URL or ID | | `transcribe` | boolean | No | Also produce a timestamped transcript (`.txt` + `.json`). Default `false`. | Notes [#notes] * Returns immediately with a `job_id` and `status: "queued"`. Poll [Get Download](#get-download-status) to retrieve the files. * A Space is only downloadable if it's currently `Running`, or `Ended` with replay enabled by the host. Otherwise the job fails (or returns `422` at queue time). Response (200) [#response-200] ```json { "status": "success", "msg": "Download queued", "data": { "job_id": "spc_1f1e0d24cdd9480880b9ad3f7b44640d", "status": "queued", "space_url": "https://x.com/i/spaces/1mrGmBvwaBqJy", "title": "CommonPrefix <> XRP Ledger", "host_username": "Vet_X0", "state": "Ended", "transcribe": false, "created_at": "2026-06-30T06:39:34.309Z", "expires_at": "2026-07-07T06:39:34.119Z" } } ``` Error Responses [#error-responses] 422 - Not downloadable [#422---not-downloadable] The Space has ended and replay was not enabled by the host. Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/spaces/download" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "space_url": "https://x.com/i/spaces/1mrGmBvwaBqJy", "transcribe": true }' ``` *** Get Download Status [#get-download-status] Poll a download job. While running, returns `status` + `progress`. When `done`, returns signed download URLs. **Free**: this poll is part of the download flow, not a separate billable call. Path Parameters [#path-parameters] | Field | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------- | | `job_id` | string | Yes | The ID returned by **Download Space** | Notes [#notes-1] * `status` progresses: `queued` → `downloading` → `uploading` → `transcribing` (if requested) → `done`. It can also be `failed` or `expired`. * Poll every few seconds. A \~1-hour Space typically finishes in 2-3 minutes. * This poll is **free**, but rate limited to **30 requests per minute** (one poll every \~2s). Polling every 2-5s is well within the limit; polling more aggressively (e.g. every second) may return `429`. A rate-limited poll is **not billed**. * Signed download URLs are valid for \~6 hours per request, re-poll to refresh them. All files are removed 7 days after creation. Response (200), in progress [#response-200-in-progress] ```json { "status": "success", "msg": "success", "data": { "job_id": "spc_1f1e0d24cdd9480880b9ad3f7b44640d", "status": "downloading", "progress": "62%", "title": "CommonPrefix <> XRP Ledger", "transcribe": true } } ``` Response (200), done [#response-200-done] ```json { "status": "success", "msg": "success", "data": { "job_id": "spc_1f1e0d24cdd9480880b9ad3f7b44640d", "status": "done", "title": "CommonPrefix <> XRP Ledger", "duration_sec": 4732, "transcribe": true, "mp3_url": "https://.../signed/....mp3?token=...", "transcript_url": "https://.../signed/....txt?token=...", "transcript_json_url": "https://.../signed/....json?token=...", "finished_at": "2026-06-30T06:42:01.000Z", "expires_at": "2026-07-07T06:39:34.119Z" } } ``` Response Fields (done) [#response-fields-done] | Field | Type | Description | | --------------------- | ------ | ----------------------------------------------------------------------------- | | `status` | string | `done` when ready | | `duration_sec` | number | Audio length in seconds | | `mp3_url` | string | Signed URL to the MP3 | | `transcript_url` | string | Signed URL to the `.txt` transcript (timestamped), only if `transcribe: true` | | `transcript_json_url` | string | Signed URL to the raw JSON transcript (segments), only if `transcribe: true` | | `expires_at` | string | When the files are deleted (7 days after creation) | Response (200), failed [#response-200-failed] ```json { "status": "success", "msg": "success", "data": { "job_id": "spc_...", "status": "failed", "error": "This Space has ended and replay was not enabled by the host." } } ``` Response (429), polling too fast [#response-429-polling-too-fast] Polling faster than the 30/min limit. Back off and retry; this response is not billed. ```json { "error": "Too many requests. Limit: 30 per minute." } ``` Example [#example-1] ```bash curl -X GET "https://api.getxapi.com/twitter/spaces/download/spc_1f1e0d24cdd9480880b9ad3f7b44640d" \ -H "Authorization: Bearer API_KEY" ``` The transcript `.txt` is formatted with per-segment timestamps: ``` [00:00:34 → 00:00:46] GM, GM, everyone. [00:00:46 → 00:00:58] Today we're diving into the XRP Ledger... ``` # Space Info This endpoint costs `$0.001` per API call. Fetch metadata for an X (Twitter) Space, title, host, state, listener counts, and whether it can be downloaded. Synchronous (\~1-2s). **Billed from your credits.** Spaces info is charged at its standard per-call cost from your credit balance, spending your plan credits first and then your top-up wallet. Query Parameters [#query-parameters] | Field | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------------------- | | `space_url` | string | Yes | A Space URL or ID, e.g. `https://x.com/i/spaces/1mrGmBvwaBqJy` | Notes [#notes] * Returns the Space's metadata. * `downloadable` tells you whether the audio can be downloaded right now, `true` when the Space is `Running`, or `Ended` with replay enabled by the host. Pass the Space to [Download Space](/docs/spaces/spaces-download) to get the audio. Response (200) [#response-200] ```json { "status": "success", "msg": "success", "data": { "space_id": "1mrGmBvwaBqJy", "title": "CommonPrefix <> XRP Ledger", "state": "Ended", "host_username": "Vet_X0", "host_name": "Vet", "started_at": "2025-12-23T18:30:26.256Z", "ended_at": "2025-12-23T19:49:17.692Z", "is_live": false, "is_replay_available": true, "total_live_listeners": 599, "total_replay_watched": 2935, "downloadable": true } } ``` Response Fields [#response-fields] | Field | Type | Description | | ------------------------- | ------- | ----------------------------------------------------- | | `space_id` | string | The Space ID | | `title` | string | Space title | | `state` | string | `Running` \| `Ended` \| `NotStarted` | | `host_username` | string | Host's @handle | | `host_name` | string | Host's display name | | `started_at` / `ended_at` | string | ISO 8601 timestamps (nullable) | | `is_live` | boolean | Space is currently live | | `is_replay_available` | boolean | Replay is available to download | | `total_live_listeners` | number | Peak live listeners | | `total_replay_watched` | number | Replay views | | `downloadable` | boolean | Whether the Space's audio can be downloaded right now | Error Responses [#error-responses] 400 - Missing space_url [#400---missing-space_url] ```json { "error": "Missing required field: space_url" } ``` Example [#example] ```bash curl -X GET "https://api.getxapi.com/twitter/spaces/info?space_url=https://x.com/i/spaces/1mrGmBvwaBqJy" \ -H "Authorization: Bearer API_KEY" ``` # Get Trends This endpoint costs `$0.001` per API call. Fetch the current top X (Twitter) trends for a location, up to 50, in X's own ranking order. Query Parameters [#query-parameters] | Field | Type | Required | Description | | --------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `country` | string | No | A location name, **country or city**: or an ISO country code, e.g. `US`, `india`, `japan`, `tokyo`, `london`, `dubai`, `são paulo` (accents optional). \~530 locations resolvable, no need to know WOEIDs. Defaults to worldwide. | | `woeid` | string | No | Numeric Yahoo WOEID, e.g. `1` = worldwide, `23424977` = USA. Overrides `country` when both are given. | | `count` | integer | No | Cap the number of trends returned (X returns up to 50). | Notes [#notes] * Returns the current top trends for a location (up to 50), in X's own ranking order. * Pass `country` for convenience, or a raw `woeid` for any location; with neither you get worldwide trends. * Use each trend's `search_url` (or `query`) with [Advanced Search](/docs/tweets/advanced-search) to pull tweets for a trend. * `tweet_volume` is supplied by X only for some trends and is often `null`. * Need a WOEID for a city the `country` shortcut doesn't cover? Look it up with [Trend Locations](/docs/trends/trend-locations). Response (200) [#response-200] ```json { "status": "success", "woeid": 23424977, "location": "United States", "as_of": "2026-07-09T04:51:10Z", "trends_count": 50, "trends": [ { "rank": 1, "name": "#AEWDynamite", "is_hashtag": true, "tweet_volume": null, "query": "#AEWDynamite", "search_url": "https://x.com/search?q=%23AEWDynamite&src=trend_click", "promoted": false } ] } ``` Response Fields [#response-fields] | Field | Type | Description | | ----------------------- | ------- | ---------------------------------------------- | | `woeid` | number | The resolved location WOEID | | `location` | string | The resolved location name | | `as_of` | string | ISO 8601 timestamp of when X computed the list | | `trends_count` | number | Number of trends returned | | `trends[].rank` | number | Position in X's ranking | | `trends[].name` | string | The trend text | | `trends[].is_hashtag` | boolean | Whether the trend is a hashtag | | `trends[].tweet_volume` | number | Approximate tweet volume, or `null` | | `trends[].query` | string | Search query for the trend | | `trends[].search_url` | string | X search URL for the trend | | `trends[].promoted` | boolean | Whether the trend is a promoted (ad) trend | Error Responses [#error-responses] 400 - Unknown location [#400---unknown-location] ```json { "error": "Unknown location \"narnia\". Use a country name/ISO code, a numeric woeid, or GET /twitter/trends/locations to look one up." } ``` Example [#example] ```bash # Worldwide curl -X GET "https://api.getxapi.com/twitter/trends" \ -H "Authorization: Bearer API_KEY" # By country or city curl -X GET "https://api.getxapi.com/twitter/trends?country=india" \ -H "Authorization: Bearer API_KEY" # By WOEID, capped to 20 curl -X GET "https://api.getxapi.com/twitter/trends?woeid=23424977&count=20" \ -H "Authorization: Bearer API_KEY" ``` # Trend Locations This endpoint costs `$0.001` per API call. List every place X (Twitter) offers trends for, so you can look up a `woeid` for a specific city or country not covered by the [Get Trends](/docs/trends/get-trends) `country` shortcut. Query Parameters [#query-parameters] None. Response (200) [#response-200] ```json { "status": "success", "count": 467, "locations": [ { "woeid": 1, "name": "Worldwide", "country": "", "country_code": null, "type": "Supername" }, { "woeid": 23424977, "name": "United States", "country": "United States", "country_code": "US", "type": "Country" } ] } ``` Response Fields [#response-fields] | Field | Type | Description | | -------------------------- | ------ | --------------------------------------------------------------------- | | `count` | number | Number of locations returned | | `locations[].woeid` | number | Yahoo WOEID, pass to [Get Trends](/docs/trends/get-trends) as `woeid` | | `locations[].name` | string | Location name | | `locations[].country` | string | Country name (empty for Worldwide) | | `locations[].country_code` | string | ISO country code, or `null` | | `locations[].type` | string | Location type, e.g. `Supername`, `Country`, `Town` | Example [#example] ```bash curl -X GET "https://api.getxapi.com/twitter/trends/locations" \ -H "Authorization: Bearer API_KEY" ``` # Advanced Search This endpoint costs `$0.001` per API call and returns \~20 tweets per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `q` | string | Yes | Search query (supports advanced operators) | | `product` | enum | No | `Latest` (default) or `Top` | | `cursor` | string | No | Pagination cursor | Limits and Behaviour [#limits-and-behaviour] * Returns up to 20 tweets per page; use `cursor` to page further. * Paginate with `next_cursor` from the previous response and stop when `has_more` is `false`. * Each page is a separate billable call at `$0.001`. * For large historical pulls, split the query into `since:`/`until:` date-range chunks rather than paging one long cursor chain. * `q` must be URL-encoded: `min_faves:1000` becomes `min_faves%3A1000` and spaces become `+`. * Supports the full web advanced search operator set. Syntax walkthrough with tested examples: [Twitter advanced search operators guide](https://www.getxapi.com/blogs/twitter-advanced-search-operators). Community operator reference: [https://github.com/igorbrigadir/twitter-advanced-search](https://github.com/igorbrigadir/twitter-advanced-search) Query Examples [#query-examples] * `from:elonmusk`, tweets from elonmusk * `"AI" OR "crypto" from:elonmusk`, tweets containing AI or crypto * `from:elonmusk since:2024-01-01`, tweets since Jan 1, 2024 * `to:elonmusk`, tweets replying to elonmusk * `#crypto min_faves:100`, tweets with at least 100 likes * `solana memecoin`, tweets mentioning solana and memecoin Response (200) [#response-200] ```json { "query": "from:0xsweep", "tweet_count": 20, "has_more": true, "next_cursor": "DAABCgABG...", "tweets": [ { "type": "tweet", "id": "2015410770834591992", "url": "https://x.com/0xsweep/status/2015410770834591992", "twitterUrl": "https://twitter.com/0xsweep/status/2015410770834591992", "text": "Thread on the latest memecoin trends...", "source": "Twitter Web App", "retweetCount": 12, "replyCount": 5, "likeCount": 89, "quoteCount": 3, "viewCount": 4500, "createdAt": "Sun Jan 25 13:05:46 +0000 2026", "lang": "en", "bookmarkCount": 10, "isReply": false, "inReplyToId": null, "conversationId": "2015410770834591992", "media": [], "inReplyToUserId": null, "author": { "type": "user", "userName": "0xsweep", "url": "https://x.com/0xsweep", "id": "1234567890", "name": "Sweep", "isVerified": false, "isBlueVerified": true, "profilePicture": "https://pbs.twimg.com/profile_images/...", "coverPicture": "https://pbs.twimg.com/profile_banners/...", "description": "Crypto researcher", "location": "", "followers": 15000, "following": 500, "createdAt": "Mon Jan 01 00:00:00 +0000 2022" }, "quoted_tweet": null } ] } ``` Errors [#errors] | Status | Meaning | | ------ | -------------------------------------------------------------------- | | `400` | Missing or empty `q`, or an invalid `product` value | | `401` | Missing, malformed, or revoked API key in the `Authorization` header | | `402` | Credit balance exhausted. Top up, then retry | 400 - Missing q [#400---missing-q] ```json { "error": "Missing required query param: q" } ``` An empty `tweets` array with `has_more: false` is a successful response, not an error: it means the query is valid but matched nothing. Example [#example] ```bash # Latest tweets curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/tweet/advanced_search?q=from:0xsweep&product=Latest" # Top tweets curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/tweet/advanced_search?q=from:0xsweep&product=Top" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/tweet/advanced_search?q=from:0xsweep&product=Latest&cursor=DAABCgABG..." ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/tweet/advanced_search?q=from:0xsweep&product=Latest", { headers: { Authorization: "Bearer API_KEY" }, } ); const data = await response.json(); console.log(data.tweets); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/tweet/advanced_search", params={"q": "from:0xsweep", "product": "Latest"}, headers={"Authorization": "Bearer API_KEY"}) print(response.json()["tweets"]) ``` # Bookmark This endpoint costs `$0.001` per API call. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `tweet_id` | string | Yes | Tweet id to bookmark | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Adds a tweet to the auth\_token owner's bookmarks. * Idempotent on already-bookmarked tweets, Twitter returns the same `Done` confirmation. Response (200) [#response-200] ```json { "status": "success", "msg": "Tweet bookmarked successfully", "data": { "tweetId": "2019264360682778716", "bookmarked": true } } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required field: tweet_id" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/bookmark" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "tweet_id": "2019264360682778716" }' ``` # Create Tweet This endpoint costs `$0.002` per API call. **For 100% success rate, pass your own proxy via the `proxy` field.** Using a custom proxy posts the tweet from your own clean IP and gives the most reliable results. Request Body [#request-body] | Field | Type | Required | Description | | ---------------------- | ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `text` | string | Yes | Tweet text | | `reply_to_tweet_id` | string | No | Tweet id to reply to | | `quote_tweet_url` | string | No | Full URL of the tweet to quote (e.g. `https://x.com/username/status/123456`) | | `quote_tweet_id` | string | No | Tweet ID to quote, resolved to URL automatically. Use `quote_tweet_url` for faster requests | | `proxy` | string | No | Your proxy URL (e.g. `http://user:pass@host:port`) | | `media_ids` | array of strings | No | Pre-uploaded Twitter media ids | | `media_urls` | array of strings | No | URLs to images/videos, fetched and uploaded automatically | | `media` | array of objects | No | Base64-encoded media, uploaded automatically | | `media[].data` | string | Yes\* | Base64-encoded file content | | `media[].type` | string | Yes\* | MIME type (e.g. `image/png`, `video/mp4`) | | `verify_premium` | boolean | No | Default `false`. When `true` and a long video (>2:20) is detected in the upload, we verify the `auth_token`'s account is on X Premium and return `403` if it isn't. See [Long video](#long-video-22014-hours). | | `community_id` | string | No | Numeric community ID. Posts the tweet to a community that the `auth_token`'s user is already a member of. See [Community posting](#community-posting). | | `disable_link_preview` | boolean | No | Default `false`. When `true`, posts a link **without** X's auto-generated preview card. See [Link previews](#link-previews). | \* Required when using the `media` field. Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Creates a tweet for the auth token owner. * **Proxy**: Pass your own proxy URL to post the tweet using your IP instead of the server's. * **Quote tweets**: Use `quote_tweet_url` to quote by URL, or `quote_tweet_id` to quote by ID (resolved to URL automatically). * Supports 3 ways to attach media (can be mixed in a single request): * `media_ids`, use if you already have Twitter media ids * `media_urls`, pass image/video URLs, the server fetches and uploads them to Twitter * `media`, pass base64-encoded files inline * For videos, the server handles chunked upload (INIT → APPEND → FINALIZE) and waits for processing to complete. * If X rejects the mutation, this endpoint returns `502`. Long video (2:20-4 hours) [#long-video-220-4-hours] Long videos are handled automatically. We parse the duration from the MP4 header and route uploads longer than 2 minutes 20 seconds through X's long-video pipeline internally. No extra fields needed, just send your video the same way you'd send a short one. For non-MP4 files or unusual encodings where duration can't be parsed, the standard short-video upload path is used as a fallback (and would fail at the tweet attachment step if the actual video is longer than 2:20). **`verify_premium`, opt-in Premium check** Pass `verify_premium: true` to confirm the `auth_token`'s account is on X Premium before performing a long-video upload: * We detect the video duration. If it's >2:20, we verify the account's Premium status before uploading. * If the account is **not** on Premium, we return `403` and **don't** start the upload, your credits are not deducted. * For videos ≤2:20, the check is skipped (no Premium requirement). * The check runs against the caller's own `auth_token` (counts toward the auth\_token's rate limit, not yours). * Results are cached for 1 hour per `auth_token`, so repeated calls aren't penalized. Community posting [#community-posting] Pass `community_id` to post into one of X's communities. The numeric ID can be lifted from the community URL on x.com, e.g. `https://x.com/i/communities/1641395384835780609` → `community_id: "1641395384835780609"`. Requirements & behavior: * The `auth_token`'s user **must already be a member of the community**. Non-members get a `403` from X, we do not auto-join. * Both short (≤280) and long-form (>280, requires Premium) posts to a community are supported. We auto-pick the right underlying mutation based on text length. * **Moderation queue:** some communities require moderator review before posts go live. When that happens, X returns a successful response with no tweet ID. We surface this as **HTTP 202** with `status: "pending_moderation"` and the `community_id` echoed back. You won't get a `tweet_id` until/unless the moderator approves the post. Media Limits [#media-limits] Images (all tiers) [#images-all-tiers] | | Limit | | ---------------- | ---------------------------- | | Max per tweet | 4 images | | File size | 5 MB (JPEG/PNG), 15 MB (GIF) | | Formats | JPEG, PNG, GIF, WEBP | | Recommended size | 1600 × 900 px (16:9) | GIFs (all tiers) [#gifs-all-tiers] | | Limit | | ------------- | -------------------------- | | Max per tweet | 1 GIF | | File size | 15 MB (mobile), 5 MB (web) | Videos [#videos] | | Free | Premium | Premium+ | | ------------- | ------------ | ------------------------ | ----------------- | | Max duration | 2 min 20 sec | \~3 hours | 4 hours (web/iOS) | | File size | 512 MB | 8 GB | 16 GB | | Android max | 2:20 | 10 min | 10 min | | Resolution | 1080p | 1080p (≤2h), 720p (2-4h) | Same as Premium | | Formats | MP4, MOV | MP4, MOV | MP4, MOV | | Max per tweet | 1 video | 1 video | 1 video | Limits depend on the X account tier of the `auth_token` owner. Most accounts are free tier (2 min 20 sec video, 512 MB max). **Link previews** When your text contains a URL, X automatically attaches a preview card built from that page's title, description and image. Pass `disable_link_preview: true` to post the link as plain text with no card: ```json { "auth_token": "YOUR_AUTH_TOKEN", "text": "Worth reading: https://example.com/post", "disable_link_preview": true } ``` This is the same result as clicking the small **x** on the card in X's web composer before posting. * **Only affects links.** The flag is harmless on text with no URL, so you can set it unconditionally. * **The URL still appears in the text.** Suppressing the card does not shorten or remove the link, it only removes the visual preview beneath the tweet. * **Applies to long posts too.** Works the same on Premium long-form posts over 280 characters. Response (200) [#response-200] ```json { "status": "success", "msg": "Tweet created successfully", "data": { "id": "2019384131067818211", "text": "Hello world!", "createdAt": "Thu Feb 05 12:14:29 +0000 2026", "pending_moderation": false, "community_id": null } } ``` When `community_id` is provided and the post lands directly in the community, `community_id` echoes the ID in the response. Response (202) - Community moderation queue [#response-202---community-moderation-queue] Returned only when `community_id` is provided and the target community has moderator approval enabled. The post was accepted by X but isn't visible yet, no `id` is issued until a moderator approves. ```json { "status": "pending_moderation", "msg": "Post submitted to community moderator queue. It will appear once approved.", "data": { "id": null, "text": "your text", "createdAt": null, "pending_moderation": true, "community_id": "1641395384835780609" } } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required field: text" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` 409 - Duplicate tweet [#409---duplicate-tweet] X rejected the post because the exact same text was already tweeted recently from that account. X blocks identical back-to-back tweets as spam prevention. Change the text slightly (add a timestamp, vary the wording) or wait \~15 minutes for X's duplicate window to expire. ```json { "error": "Status is a duplicate.", "twitter_error_code": 187 } ``` 403 - Not a community member [#403---not-a-community-member] You passed `community_id` for a community the `auth_token`'s user isn't a member of (or has been restricted from). X's code `440` is mapped to HTTP `403` here. ```json { "error": "Authorization: User is not authorized to post to this community (440)", "twitter_error_code": 440 } ``` 403 - Reply restricted [#403---reply-restricted] The original tweet author restricted who can reply to their tweet. ```json { "error": "Authorization: The original Tweet author restricted who can reply to this Tweet. (433)", "twitter_error_code": 433 } ``` 429 - Posting temporarily limited (error 344) [#429---posting-temporarily-limited-error-344] X's error `344` is a **posting throttle tied to the network identity (exit IP / subnet reputation), not your account's daily cap.** A fresh account posting through a flagged or high-volume IP can hit it, and the same account can still post manually from a clean network. It clears by posting through a **clean, dedicated proxy — one sticky IP per account** — and keeping per-IP volume modest. This error is **not billed**. ```json { "error": "Posting temporarily limited (network/IP throttle, not your account). Retry shortly, or post through a clean dedicated proxy — one sticky IP per account.", "twitter_error_code": 344 } ``` 503 - Try again later [#503---try-again-later] ```json { "error": "Action could not be completed at this time. Please try again later." } ``` 502 - Mutation rejected by X [#502---mutation-rejected-by-x] ```json { "error": "Tweet creation failed - Twitter did not return a tweet ID" } ``` 403 - verify_premium failed [#403---verify_premium-failed] `verify_premium: true` was set, and the `auth_token`'s account is not on X Premium. Returned before the upload starts, credits are not deducted. ```json { "error": "Account is not X Premium. Long-video upload (>2:20) is intended for Premium accounts to avoid risking the account. Either remove `verify_premium`, use a video ≤2:20, or use a Premium account." } ``` 400 - Long video with unparseable duration [#400---long-video-with-unparseable-duration] If you upload a video whose duration we can't parse from the MP4 header (unusual encoding, fragmented MP4, corrupted file), we fall back to the standard short-video path. If the video is actually longer than 2:20, Twitter accepts the upload but rejects the tweet attachment with error 324: ```json { "error": "BadRequest: Duration too long, maximum:2.minutes+20.seconds, actual:3.minutes (MediaId: snf:2058029045372379136) (324)", "twitter_error_code": 324 } ``` If you key error handling off the Twitter code, use the `twitter_error_code` field, the human message format may change without notice. This case is rare; standard MP4 files always parse cleanly. Examples [#examples] Text only [#text-only] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "text": "Hello world!" }' ``` ```javascript const response = await fetch("https://api.getxapi.com/twitter/tweet/create", { method: "POST", headers: { Authorization: "Bearer API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ auth_token: "your_auth_token", text: "Hello world!", }), }); const data = await response.json(); console.log(data); ``` ```python import requests response = requests.post( "https://api.getxapi.com/twitter/tweet/create", headers={"Authorization": "Bearer API_KEY"}, json={ "auth_token": "your_auth_token", "text": "Hello world!", }) print(response.json()) ``` With media URL [#with-media-url] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "text": "Check this out", "media_urls": ["https://example.com/photo.jpg"] }' ``` With base64 media [#with-base64-media] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "text": "Photo upload", "media": [{"data": "iVBORw0KGgo...", "type": "image/png"}] }' ``` Quote tweet [#quote-tweet] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "text": "Interesting take!", "quote_tweet_url": "https://x.com/elonmusk/status/2019264360682778716" }' ``` With your own proxy [#with-your-own-proxy] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "proxy": "http://user:pass@host:port", "text": "Posted via my own proxy!" }' ``` Reply with media [#reply-with-media] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "text": "Great thread!", "reply_to_tweet_id": "2019264360682778716", "media_urls": ["https://example.com/reaction.mp4"] }' ``` Long video (3 minutes) [#long-video-3-minutes] Posting a 3-minute video. We auto-detect the duration from the MP4 header and route it through X's long-video pipeline internally. ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "text": "3-minute deep dive", "media_urls": ["https://example.com/long-video.mp4"] }' ``` Long video with Premium check [#long-video-with-premium-check] Same as above, but ask us to verify Premium before uploading. Returns `403` if the account is not on Premium, credits are not deducted. ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "text": "3-minute deep dive", "media_urls": ["https://example.com/long-video.mp4"], "verify_premium": true }' ``` Post to a community [#post-to-a-community] The `auth_token`'s user must already be a member of the community. ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/create" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "text": "Hello from the API", "community_id": "1641395384835780609" }' ``` # Delete Tweet This endpoint costs `$0.001` per API call. Permanently deletes a tweet owned by the `auth_token`'s account. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `tweet_id` | string | Yes | Numeric id of the tweet to delete. Must be owned by the `auth_token` holder | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * **Permanent**: a deleted tweet cannot be recovered. * You can only delete your **own** tweets. Attempting to delete another account's tweet returns `403`. * **Idempotent**: deleting an already-deleted tweet still returns `200` (with `"already": true` when X reports it as already gone). Response (200) [#response-200] ```json { "status": "success", "msg": "Tweet deleted successfully", "data": { "tweetId": "2019264360682778716", "deleted": true } } ``` Error Responses [#error-responses] 400 - Missing or invalid tweet_id [#400---missing-or-invalid-tweet_id] ```json { "error": "Invalid tweet_id: \"abc\", must be a numeric tweet ID" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` 403 - Not your tweet [#403---not-your-tweet] Returned when the tweet isn't owned by the `auth_token`'s account. Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/delete" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "tweet_id": "2019264360682778716" }' ``` # Edit Tweet This endpoint costs `$0.002` per API call. Edit an existing post's text and/or media. Under the hood this is the same mutation as [Create Tweet](/docs/tweets/create-tweet) with an added edit marker, so it behaves the same way (direct HTTP fast path, browser fallback, optional `proxy`). **Editing is gated by X, not us.** The `auth_token` account must be on **X Premium**, edits are allowed only within **\~1 hour** of the original post, there's a **limited number of edits** per post, and **only original posts** can be edited (not replies or thread tweets). Each edit creates a **new tweet id** (X keeps the edit history and shows an "Edited" label). The response `data.id` is the new version; `data.previous_tweet_id` echoes the post you edited. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | --------- | -------- | ---------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `tweet_id` | string | Yes | Numeric id of the post to edit | | `text` | string | Yes | New full text for the post (replaces the existing text) | | `ct0` | string | No | CSRF token; with `twid`, skips server-side credential resolution | | `twid` | string | No | User id cookie (e.g. `u%3D123456`); must be provided with `ct0` | | `media_ids` | string\[] | No | Pre-uploaded media ids to attach | | `media_urls` | string\[] | No | Image/video URLs, fetched and uploaded automatically | | `media` | object\[] | No | Base64-encoded media (`data` + `type`), uploaded automatically | | `proxy` | string | No | Your proxy URL; runs via a temporary headless browser on your IP | Notes [#notes] * Long edits (over 280 characters) are routed through the Premium long-form path automatically. * **Editing a post you already edited:** every edit creates a new tweet id, and X only lets you edit the **current** version. To edit again, pass the id from the previous edit's `data.id`. Editing a superseded id fails. * Confirm the edit by checking `data.id` (non-null = the new edited version). Response (200) [#response-200] ```json { "status": "success", "msg": "Tweet edited successfully", "data": { "id": "2093260000000000000", "text": "how are you doing today friend?", "createdAt": "Thu Aug 28 09:15:00 +0000 2026", "previous_tweet_id": "2093255791965016333" } } ``` Error Responses [#error-responses] 403 - Not editable [#403---not-editable] The account isn't Premium, the \~1h window passed, the edit limit was reached, or the post isn't an editable original (reply/thread). ```json { "error": "This post can't be edited. Editing requires an X Premium account and only works within ~1 hour of the original post (limited edits, original posts only - not replies or thread tweets).", "twitter_error_code": null } ``` 400 - Missing required field [#400---missing-required-field] ```json { "error": "Missing required field: tweet_id" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/edit" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "tweet_id": "2093255791965016333", "text": "how are you doing today friend?" }' ``` # Like Tweet This endpoint costs `$0.001` per API call. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `tweet_id` | string | Yes | Tweet id to like | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Likes a tweet for the auth token owner. * If X does not confirm the like action, this endpoint returns `502`. Response (200) [#response-200] ```json { "status": "success", "msg": "Tweet liked successfully", "data": { "tweetId": "2019264360682778716", "liked": true } } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required field: tweet_id" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` 502 - Like not confirmed by X [#502---like-not-confirmed-by-x] ```json { "error": "Like failed - Twitter did not confirm the action" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/favorite" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "tweet_id": "2019264360682778716" }' ``` # Retweet Tweet This endpoint costs `$0.001` per API call. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `tweet_id` | string | Yes | Tweet id to retweet | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Retweets a tweet for the auth token owner. * If X does not confirm the retweet action, this endpoint returns `502`. Response (200) [#response-200] ```json { "status": "success", "msg": "Retweeted successfully", "data": { "tweetId": "2019264360682778716", "retweetId": "2019384131067818211", "retweeted": true } } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required field: tweet_id" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` 502 - Retweet not confirmed by X [#502---retweet-not-confirmed-by-x] ```json { "error": "Retweet failed - Twitter did not confirm the retweet" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/retweet" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "tweet_id": "2019264360682778716" }' ``` # Tweet Retweeters This endpoint costs `$0.001` per API call and returns \~20 users per page. Returns the list of users who reposted (retweeted) the given tweet. Pool-based read, no customer `auth_token` needed. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------- | | `id` | string | Yes | Tweet ID whose retweeters to fetch (numeric) | | `cursor` | string | No | Pagination cursor from a previous response | Notes [#notes] * Returns \~20 users per page. * Paginate by passing `next_cursor` from the previous response until `has_more` is `false`. Response (200) [#response-200] ```json { "tweetId": "2059393717111308634", "user_count": 95, "has_more": true, "next_cursor": "eyJpZCI6...", "users": [ { "type": "user", "id": "50115087", "userName": "NASAJohnson", "name": "NASA's Johnson Space Center", "url": "https://x.com/NASAJohnson", "isVerified": false, "isBlueVerified": true, "profilePicture": "https://pbs.twimg.com/profile_images/..._400x400.jpg", "coverPicture": "https://pbs.twimg.com/profile_banners/...", "description": "NASA's center for human spaceflight.", "location": "Houston, TX", "followers": 2254727, "following": 173, "tweets": 20792, "listed": 10927, "createdAt": "Tue Jun 23 14:31:13 +0000 2009", "canDm": false } ] } ``` Error Responses [#error-responses] 400 - Missing or invalid id [#400---missing-or-invalid-id] ```json { "error": "Missing required query param: id" } ``` ```json { "error": "id must be a numeric tweet id" } ``` Examples [#examples] ```bash curl -X GET "https://api.getxapi.com/twitter/tweet/retweeters?id=2059393717111308634" \ -H "Authorization: Bearer API_KEY" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/tweet/retweeters?id=2059393717111308634", { headers: { Authorization: "Bearer API_KEY" } } ); const data = await response.json(); console.log(data.user_count, data.users[0]?.userName); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/tweet/retweeters", headers={"Authorization": "Bearer API_KEY"}, params={"id": "2059393717111308634"}) data = response.json() print(data["user_count"], data["users"][0]["userName"]) ``` Paginate [#paginate] ```bash curl -X GET "https://api.getxapi.com/twitter/tweet/retweeters?id=2059393717111308634&cursor=eyJpZCI6..." \ -H "Authorization: Bearer API_KEY" ``` # Tweet Detail This endpoint costs `$0.001` per API call. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `id` | string | Yes | Tweet id | Notes [#notes] * Returns a single tweet with full author and media details. Response (200) [#response-200] ```json { "status": "success", "msg": "success", "data": { "type": "tweet", "id": "2019264360682778716", "url": "https://x.com/elonmusk/status/2019264360682778716", "twitterUrl": "https://twitter.com/elonmusk/status/2019264360682778716", "text": "@Gaurab Yes, it is extremely difficult", "source": "Twitter for iPhone", "retweetCount": 136, "replyCount": 396, "likeCount": 3676, "quoteCount": 11, "viewCount": 298555, "createdAt": "Thu Feb 05 04:18:34 +0000 2026", "lang": "en", "bookmarkCount": 121, "isReply": true, "inReplyToId": "2019199325365227607", "conversationId": "2019199325365227607", "media": [], "inReplyToUserId": "18433952", "author": { "type": "user", "userName": "elonmusk", "url": "https://x.com/elonmusk", "id": "44196397", "name": "Elon Musk", "isVerified": false, "isBlueVerified": true, "profilePicture": "https://pbs.twimg.com/profile_images/.../57KcqsTA_normal.jpg", "coverPicture": "https://pbs.twimg.com/profile_banners/44196397/...", "description": "", "location": "", "followers": 233932369, "following": 1283, "createdAt": "Tue Jun 02 20:12:29 +0000 2009" }, "quoted_tweet": null } } ``` Error Responses [#error-responses] 400 - Missing id [#400---missing-id] ```json { "error": "Missing required query param: id" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/tweet/detail?id=2019264360682778716" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/tweet/detail?id=2019264360682778716", { headers: { Authorization: "Bearer API_KEY" }, } ); const data = await response.json(); console.log(data); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/tweet/detail", params={"id": "2019264360682778716"}, headers={"Authorization": "Bearer API_KEY"}, ) print(response.json()) ``` # Tweet Replies This endpoint costs `$0.001` per API call and returns \~20 replies per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------- | | `id` | string | Yes | Tweet id | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Returns replies for a tweet. * Pagination uses `next_cursor` until `has_more` is false. Response (200) [#response-200] ```json { "tweetId": "2019199325365227607", "reply_count": 38, "has_more": false, "next_cursor": null, "replies": [ { "type": "tweet", "id": "2019264360682778716", "url": "https://x.com/elonmusk/status/2019264360682778716", "text": "@Gaurab Yes, it is extremely difficult", "likeCount": 3718, "replyCount": 396, "viewCount": 300017, "author": { "userName": "elonmusk", "name": "Elon Musk" } } ] } ``` Error Responses [#error-responses] 400 - Missing id [#400---missing-id] ```json { "error": "Missing required query param: id" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/tweet/replies?id=2019199325365227607" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/tweet/replies?id=2019199325365227607&cursor=DAABCgABG..." ``` # Tweet Thread This endpoint costs `$0.005` per API call and returns the ordered self-thread for a tweet. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------------------- | | `id` | string | Yes | Numeric tweet id. Can be the root tweet or any tweet in the thread. | Notes [#notes] * Resolves the conversation root automatically, so mid-thread tweet ids return the full thread from the first tweet. * Follows `inReplyToId` links from the root author and excludes side replies from other users or replies the author made to commenters. * `complete` is `false` only when the internal pagination cap is reached on a very long thread. * Pool-based read: no customer `auth_token` or proxy parameter is needed. Response (200) [#response-200] ```json { "conversationId": "2057796375233073524", "author": { "type": "user", "id": "123456789", "userName": "thedefiedge", "name": "The Defi Edge" }, "thread_length": 2, "complete": true, "tweets": [ { "type": "tweet", "id": "2057796375233073524", "text": "Thread opener...", "conversationId": "2057796375233073524", "inReplyToId": null, "author": { "id": "123456789", "userName": "thedefiedge", "name": "The Defi Edge" } }, { "type": "tweet", "id": "2057796380000000000", "text": "Thread continuation...", "conversationId": "2057796375233073524", "inReplyToId": "2057796375233073524", "author": { "id": "123456789", "userName": "thedefiedge", "name": "The Defi Edge" } } ] } ``` Error Responses [#error-responses] 400 - Missing or invalid id [#400---missing-or-invalid-id] ```json { "error": "Missing required query param: id" } ``` ```json { "error": "id must be a numeric tweet id" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/tweet/thread?id=2057796375233073524" ``` # UnBookmark This endpoint costs `$0.001` per API call. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `tweet_id` | string | Yes | Tweet id to remove from bookmarks | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Removes the tweet from the auth\_token owner's bookmarks. Response (200) [#response-200] ```json { "status": "success", "msg": "Tweet unbookmarked successfully", "data": { "tweetId": "2019264360682778716", "unbookmarked": true } } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required field: tweet_id" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/tweet/unbookmark" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "tweet_id": "2019264360682778716" }' ``` # Bookmark Search This endpoint costs `$0.001` per API call and returns \~20 results per page. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `q` | string | Yes | Search query | | `cursor` | string | No | Pagination cursor | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Searches your bookmarks. * Pagination uses `next_cursor` until `has_more` is false. Response (200) [#response-200] ```json { "userId": "1858475867762270208", "query": "claude", "tweet_count": 20, "has_more": true, "next_cursor": "DAAHCgABHAY...", "bookmarks": [ { "type": "tweet", "id": "2010705621524292007", "text": "Hello from GetXAPI.", "author": { "userName": "example_user" } } ] } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required field: q" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/user/bookmark_search" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "q": "claude" }' # With pagination curl -X POST "https://api.getxapi.com/twitter/user/bookmark_search" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "q": "claude", "cursor": "DAAHCgABHAY..." }' ``` # All Bookmarks This endpoint costs `$0.001` per API call and returns \~20 results per page. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `cursor` | string | No | Pagination cursor from previous response | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * Lists all bookmarks for the authenticated user, newest first. * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Pagination uses `next_cursor` until `has_more` is false. Response (200) [#response-200] ```json { "userId": "2021588307394834432", "tweet_count": 20, "has_more": true, "next_cursor": "HBbysNrihaHT4jMAAA==", "bookmarks": [ { "type": "tweet", "id": "2055000191112286719", "text": "Small accounts posting into absolute silence", "author": { "userName": "Tekeee" } } ] } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required field: auth_token" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/user/bookmarks" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token" }' # With pagination curl -X POST "https://api.getxapi.com/twitter/user/bookmarks" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "cursor": "HBbysNrihaHT4jMAAA==" }' ``` # Check Relationship This endpoint costs `$0.001` per API call. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `source_user_name` | string | Yes | Source user screen name (without @) | | `target_user_name` | string | Yes | Target user screen name (without @) | | `auth_token` | string | No | `auth_token` of the **source** account. Required to read `blocking`/`blockedBy`/`muting` — without it they return `null` (see Notes). | | `ct0` | string | No | Current CSRF token cookie for the source account. Send with `twid` to skip credential bootstrap. | | `twid` | string | No | Current user ID cookie for the source account, e.g. `u=1234567890`. Send with `ct0`. | | `proxy` | string | No | Proxy URL — `http://`, `https://`, `socks5://`, or `socks4://`. | Notes [#notes] * `sourceFollowsTarget` and `targetFollowsSource` are **public** and always accurate. * `blocking`, `blockedBy` and `muting` are **private to the source account**. X only returns them when the request is authenticated *as the source account*. Without `auth_token` the lookup runs through a shared account and these come back **`null`** (unknown) — do **not** read `null` as "not blocking". * To read block/mute status reliably, pass `auth_token` (and ideally `ct0` + `twid`) of the account named in `source_user_name`. Response [#response] Without `auth_token` — private fields are `null` and a `note` explains why: ```json { "status": "success", "msg": "success", "data": { "sourceUserName": "example_user", "targetUserName": "target_user", "sourceFollowsTarget": true, "targetFollowsSource": false, "canDm": false, "blocking": null, "blockedBy": null, "muting": null, "note": "blocking, blocked_by and muting are private to the source account and unavailable via a shared lookup. To read them, pass auth_token of the source_user_name account." } } ``` With `auth_token` of the source account — private fields are accurate: ```json { "status": "success", "msg": "success", "data": { "sourceUserName": "example_user", "targetUserName": "target_user", "sourceFollowsTarget": false, "targetFollowsSource": false, "canDm": false, "blocking": true, "blockedBy": false, "muting": false } } ``` Response Fields [#response-fields] | Field | Type | Description | | --------------------- | --------------- | ----------------------------------------------------------------------------- | | `sourceUserName` | string | @handle of source user | | `targetUserName` | string | @handle of target user | | `sourceFollowsTarget` | boolean | True if source follows target (public) | | `targetFollowsSource` | boolean | True if target follows source (public) | | `canDm` | boolean | True if source can DM target | | `blocking` | boolean \| null | True if source is blocking target; `null` if unknown (no source `auth_token`) | | `blockedBy` | boolean \| null | True if source is blocked by target; `null` if unknown | | `muting` | boolean \| null | True if source is muting target; `null` if unknown | | `note` | string | Present only when the private fields are `null`; explains how to get them | Example [#example] Public relationship only: ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/check_follow_relationship?source_user_name=example_user&target_user_name=target_user" ``` Accurate block/mute (authenticated as the source account): ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/check_follow_relationship?source_user_name=example_user&target_user_name=target_user&auth_token=SOURCE_AUTH_TOKEN" ``` # Follow User This endpoint costs `$0.001` per API call. Follows a user on behalf of the `auth_token` owner. Identify the target by **either** `user_id` or `username`. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | ----------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `user_id` | string | Conditional | Numeric id of the account to follow. Provide either `user_id` or `username` | | `username` | string | Conditional | Handle (without `@`) of the account to follow. Provide either `user_id` or `username` | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Provide **one of** `user_id` or `username`. Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * **Idempotent**: following an already-followed account succeeds without error. * For **protected accounts**, the follow is queued: the response returns `followRequestSent: true` and `following: false` until the owner approves. Response (200) [#response-200] ```json { "status": "success", "msg": "User followed successfully", "data": { "userId": "11348282", "userName": "NASA", "name": "NASA", "following": true, "followRequestSent": false, "protected": false } } ``` Error Responses [#error-responses] 400 - Missing target [#400---missing-target] ```json { "error": "Provide user_id or username of the account to follow" } ``` 404 - User not found [#404---user-not-found] ```json { "error": "User not found" } ``` Examples [#examples] ```bash curl -X POST "https://api.getxapi.com/twitter/user/follow" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "username": "nasa" }' ``` ```javascript const response = await fetch("https://api.getxapi.com/twitter/user/follow", { method: "POST", headers: { Authorization: "Bearer API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ auth_token: "your_auth_token", username: "nasa", }), }); const data = await response.json(); console.log(data.data); ``` ```python import requests response = requests.post( "https://api.getxapi.com/twitter/user/follow", headers={"Authorization": "Bearer API_KEY"}, json={ "auth_token": "your_auth_token", "username": "nasa", }) print(response.json()["data"]) ``` Follow by user_id [#follow-by-user_id] ```bash curl -X POST "https://api.getxapi.com/twitter/user/follow" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "user_id": "11348282" }' ``` # User Followers v2 This endpoint costs `$0.001` per API call and returns \~70 users per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Uses the followers endpoint (higher fidelity than REST v1). * Returns \~70 followers per page (ordered by follow time desc). * Includes the `canDm` field for each follower. * **Capped at \~800 followers total**: matches what x.com's web UI shows. Pagination stops after \~800 results regardless of the user's actual follower count. Use [User Followers (v1)](./followers) if you need the full list. Response [#response] ```json { "userName": "elonmusk", "user_count": 70, "has_more": true, "next_cursor": "DAABCgABG...", "followers": [ { "type": "user", "id": "1335924847940079620", "userName": "0xumarkhatab", "name": "Umar", "url": "https://x.com/0xumarkhatab", "isVerified": false, "isBlueVerified": true, "profilePicture": "https://pbs.twimg.com/profile_images/...", "coverPicture": "https://pbs.twimg.com/profile_banners/...", "description": "Founding EVM Engineer @ViFi_Labs", "location": "Metaverse", "followers": 599, "following": 307, "tweets": 2002, "listed": 12, "createdAt": "Mon Dec 07 12:32:21 +0000 2020", "canDm": false } ] } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/followers_v2?userName=elonmusk" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/followers_v2?userName=elonmusk&cursor=DAABCgABG..." ``` # Followers You Know This endpoint costs `$0.001` per API call and returns \~20 users per page. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `user_id` | string | No | Target user id | | `user_name` | string | No | Target username (without @) | | `cursor` | string | No | Pagination cursor | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Provide either `user_id` or `user_name`. * If `user_name` is provided, the API resolves it to `user_id`. * Pagination uses `next_cursor` until `has_more` is false. Response (200) [#response-200] ```json { "userId": "1858475867762270208", "targetUserId": "44196397", "user_count": 20, "has_more": true, "next_cursor": "DAABCgABG...", "followers_you_know": [ { "type": "user", "id": "1335924847940079620", "userName": "0xumarkhatab", "name": "Umar" } ] } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required field: user_id or user_name" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/user/followers_you_know" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "user_name": "elonmusk" }' # With pagination curl -X POST "https://api.getxapi.com/twitter/user/followers_you_know" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "user_name": "elonmusk", "cursor": "DAABCgABG..." }' ``` # User Followers This endpoint costs `$0.001` per API call and returns \~200 users per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Uses the REST followers endpoint. * Returns up to 200 followers per page (ordered by follow time desc). * **No total-user cap**: you can paginate all the way through to the full follower list. Use this endpoint when you need the complete list. Response [#response] ```json { "userName": "elonmusk", "user_count": 200, "has_more": true, "next_cursor": "DAABCgABG...", "followers": [ { "type": "user", "id": "1335924847940079620", "userName": "0xumarkhatab", "name": "Umar", "url": "https://x.com/0xumarkhatab", "isVerified": false, "isBlueVerified": true, "profilePicture": "https://pbs.twimg.com/profile_images/...", "coverPicture": "https://pbs.twimg.com/profile_banners/...", "description": "Founding EVM Engineer @ViFi_Labs", "location": "Metaverse", "followers": 599, "following": 307, "tweets": 2002, "listed": 12, "createdAt": "Mon Dec 07 12:32:21 +0000 2020", "canDm": false } ] } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/followers?userName=elonmusk" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/followers?userName=elonmusk&cursor=DAABCgABG..." ``` # User Following v2 This endpoint costs `$0.001` per API call and returns \~70 users per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Uses the following endpoint (higher fidelity than REST v1). * Returns \~70 following per page (ordered by follow time desc). * Includes the `canDm` field for each user. * **Capped at \~800 users total**: matches what x.com's web UI shows. Pagination stops after \~800 results regardless of the user's actual following count. Use [User Following (v1)](./following) if you need the full list. Response [#response] ```json { "userName": "elonmusk", "user_count": 70, "has_more": true, "next_cursor": "DAABCgABG...", "following": [ { "type": "user", "id": "1995710751097659392", "userName": "clawdbot", "name": "Clawd", "url": "https://x.com/clawdbot", "isVerified": false, "isBlueVerified": false, "profilePicture": "https://pbs.twimg.com/profile_images/...", "coverPicture": "https://pbs.twimg.com/profile_banners/...", "description": "I help @steipete manage his digital life", "location": "Peter's Castle (any OS)", "followers": 50708, "following": 7, "tweets": 389, "listed": 45, "createdAt": "Tue Dec 02 04:25:09 +0000 2025", "canDm": false } ] } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/following_v2?userName=elonmusk" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/following_v2?userName=elonmusk&cursor=DAABCgABG..." ``` # User Following This endpoint costs `$0.001` per API call and returns \~200 users per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Uses the REST following endpoint. * Returns up to 200 following per page (ordered by follow time desc). * **No total-user cap**: you can paginate all the way through to the full following list. Use this endpoint when you need the complete list. Response [#response] ```json { "userName": "elonmusk", "user_count": 200, "has_more": true, "next_cursor": "DAABCgABG...", "following": [ { "type": "user", "id": "1995710751097659392", "userName": "clawdbot", "name": "Clawd", "url": "https://x.com/clawdbot", "isVerified": false, "isBlueVerified": false, "profilePicture": "https://pbs.twimg.com/profile_images/...", "coverPicture": "https://pbs.twimg.com/profile_banners/...", "description": "I help @steipete manage his digital life", "location": "Peter's Castle (any OS)", "followers": 50708, "following": 7, "tweets": 389, "listed": 45, "createdAt": "Tue Dec 02 04:25:09 +0000 2025", "canDm": false } ] } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/following?userName=elonmusk" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/following?userName=elonmusk&cursor=DAABCgABG..." ``` # Home Timeline This endpoint costs `$0.001` per API call and returns \~20 tweets per page. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `following` | boolean | No | Which home feed to return. `false` (default) → **For You** (algorithmic; mixes in recommended posts from accounts the user doesn't follow). `true` → **Following** (reverse-chronological; only accounts the user follows). | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `cursor` | string | No | Pagination cursor | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Returns your home timeline feed. The default (`following` omitted or `false`) returns the **For You** feed, which mixes in recommended posts from accounts you don't follow. Set `following: true` to get the **Following** feed, only posts from accounts you follow, in reverse-chronological order. * Pagination uses `next_cursor` until `has_more` is false. Response (200) [#response-200] ```json { "userId": "1858475867762270208", "tweet_count": 20, "has_more": true, "next_cursor": "DAABCgABHAY...", "tweets": [ { "type": "tweet", "id": "2010705621524292007", "text": "Hello from GetXAPI.", "author": { "userName": "example_user" } } ] } ``` Error Responses [#error-responses] 400 - Missing auth_token [#400---missing-auth_token] ```json { "error": "Missing required field: auth_token" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/user/home_timeline" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token" }' # With pagination curl -X POST "https://api.getxapi.com/twitter/user/home_timeline" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "cursor": "DAABCgABHAY..." }' # Following feed only (posts from accounts you follow, reverse-chronological) curl -X POST "https://api.getxapi.com/twitter/user/home_timeline" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "following": true }' ``` # Reset Password (two-step) A two-step recovery flow for when you **don't** have (or can't read) the mailbox: **you** trigger the reset, the **account owner** reads the emailed code from their own inbox and hands it back, and we complete the reset and return fresh tokens. Because the owner reads the code, this works for **any** email provider, including **Hotmail/Outlook, Gmail and Yahoo** (which the auto-read [User Login V2](/docs/users/user-login-v2) can't handle). It's effectively a "refresh the tokens" tool: it sets a new password we control and returns a fresh `auth_token` / `ct0` / `twid`. Each step costs `$0.005` per call. Step 1, Send the code [#step-1-send-the-code] Triggers X's password-reset email for the account and returns a `session_id`. Request Body [#request-body] | Field | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------- | | `username` | string | Yes | Twitter username (without @) | | `proxy` | string | No | Proxy URL for the reset trigger | Response (200) [#response-200] ```json { "status": "success", "session_id": "1d171e717491ab...", "expires_in": 900, "message": "A password-reset code was emailed to the account. Submit it to /twitter/reset-password/confirm with this session_id." } ``` The `session_id` is valid for \~15 minutes (the code's lifetime). Step 2, Confirm with the code [#step-2-confirm-with-the-code] Completes the reset with the code the owner received, and returns fresh tokens plus the new password. **Save the returned `new_password`.** It's the account's password now. Request Body [#request-body-1] | Field | Type | Required | Description | | ------------- | ------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `session_id` | string | Yes | From step 1 | | `code` | string | Conditional | The reset code the owner read from their email. Required on the **first** confirm. On a **2FA resume** (after a `TOTP_REQUIRED` response) the code is already consumed, send `session_id` + `totp_secret` and omit `code`. | | `totp_secret` | string | Conditional | Base32 TOTP secret, required **if the account has 2FA** | Response (200) [#response-200-1] Same shape as [User Login V2](/docs/users/user-login-v2): ```json { "username": "myaccount", "new_password": "Gx7hcuyxk5xr", "auth_token": "59bace0de501...", "ct0": "9959...", "twid": "u=155977652", "profile": { "...": "..." } } ``` Error Responses [#error-responses] Errors include a `code` field: | HTTP | `code` | Meaning | | ---- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | 404 | `ACCOUNT_NOT_FOUND` | Account not found (suspended, deleted, or renamed) | | 410 | `SESSION_EXPIRED` | Unknown/expired `session_id`, start again at step 1 | | 409 | `SESSION_BUSY` | A confirm is already in progress for this session, retry shortly | | 400 | `CODE_REJECTED` | Wrong code, the session stays valid, retry with the correct code | | 400 | `TOTP_REQUIRED` | Account has 2FA, **password was already reset** (`new_password` is returned); call confirm again with the **same** `session_id` and `totp_secret` | | 502 | `TWO_FA_FAILED` | 2FA failed, check `totp_secret`, retry with the same `session_id` (password already reset) | Once the password is set, the flow is **resumable**: a missing/wrong `totp_secret` never loses `new_password`, retry 2FA with the same `session_id` rather than restarting. When to use which [#when-to-use-which] * [User Login V2](/docs/users/user-login-v2), one call if you have the mailbox password and it's a supported provider (GMX/mail.com, Hotmail/Outlook). * **This two-step flow**: when you don't have the mailbox password, the provider isn't auto-readable (Gmail/Yahoo), or the account owner is in the loop to read the code. Example [#example] ```bash # 1) send the code curl -X POST "https://api.getxapi.com/twitter/reset-password/send-code" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "username": "myaccount" }' # 2) confirm with the code the owner received curl -X POST "https://api.getxapi.com/twitter/reset-password/confirm" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "session_id": "", "code": "avfw47d9", "totp_secret": "SXGS5MN2IHUMV34H" }' ``` # Unfollow User This endpoint costs `$0.001` per API call. Unfollows a user on behalf of the `auth_token` owner. Identify the target by **either** `user_id` or `username`. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | ----------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `user_id` | string | Conditional | Numeric id of the account to unfollow. Provide either `user_id` or `username` | | `username` | string | Conditional | Handle (without `@`) of the account to unfollow. Provide either `user_id` or `username` | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Provide **one of** `user_id` or `username`. Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * **Idempotent**: unfollowing an account you don't follow succeeds without error. Response (200) [#response-200] ```json { "status": "success", "msg": "User unfollowed successfully", "data": { "userId": "11348282", "userName": "NASA", "name": "NASA", "following": false } } ``` Error Responses [#error-responses] 400 - Missing target [#400---missing-target] ```json { "error": "Provide user_id or username of the account to unfollow" } ``` 404 - User not found [#404---user-not-found] ```json { "error": "User not found" } ``` Examples [#examples] ```bash curl -X POST "https://api.getxapi.com/twitter/user/unfollow" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "username": "nasa" }' ``` ```javascript const response = await fetch("https://api.getxapi.com/twitter/user/unfollow", { method: "POST", headers: { Authorization: "Bearer API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ auth_token: "your_auth_token", username: "nasa", }), }); const data = await response.json(); console.log(data.data); ``` ```python import requests response = requests.post( "https://api.getxapi.com/twitter/user/unfollow", headers={"Authorization": "Bearer API_KEY"}, json={ "auth_token": "your_auth_token", "username": "nasa", }) print(response.json()["data"]) ``` Unfollow by user_id [#unfollow-by-user_id] ```bash curl -X POST "https://api.getxapi.com/twitter/user/unfollow" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "user_id": "11348282" }' ``` # Update Avatar This endpoint costs `$0.001` per API call. Updates the profile picture for the user owning `auth_token`. Provide the image as a public URL or as base64, exactly one of the two. Body Parameters [#body-parameters] | Parameter | Type | Required | Description | | -------------- | ------ | ----------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's `auth_token` cookie | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `image_url` | string | Conditional | Public URL to fetch the image from. Required if `image_base64` is not provided. | | `image_base64` | string | Conditional | Raw base64 (with or without `data:image/...;base64,` prefix). Required if `image_url` is not provided. | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Recommended size: 400×400. Supported formats: JPG, PNG. * Provide exactly one of `image_url` or `image_base64`. * The mutation is performed using the user's own session. Response (200) [#response-200] ```json { "status": "success", "msg": "Avatar updated successfully", "data": { "id": "1740174562631856128", "screen_name": "example_user", "profile_image_url": "https://pbs.twimg.com/profile_images/.../abc_normal.jpg" } } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Provide either image_url or image_base64" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash # Via image URL curl --request POST \ --header "Authorization: Bearer API_KEY" \ --header "Content-Type: application/json" \ --url "https://api.getxapi.com/twitter/user/update_avatar" \ --data '{ "auth_token": "", "image_url": "https://example.com/avatar.png" }' # Via base64 curl --request POST \ --header "Authorization: Bearer API_KEY" \ --header "Content-Type: application/json" \ --url "https://api.getxapi.com/twitter/user/update_avatar" \ --data '{ "auth_token": "", "image_base64": "" }' ``` # Update Banner This endpoint costs `$0.001` per API call. Updates the profile banner for the user owning `auth_token`. Provide the image as a public URL or as base64, exactly one of the two. Body Parameters [#body-parameters] | Parameter | Type | Required | Description | | -------------- | ------ | ----------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's `auth_token` cookie | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `image_url` | string | Conditional | Public URL to fetch the image from. Required if `image_base64` is not provided. | | `image_base64` | string | Conditional | Raw base64 (with or without `data:image/...;base64,` prefix). Required if `image_url` is not provided. | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Recommended size: 1500×500. Supported formats: JPG, PNG. * Provide exactly one of `image_url` or `image_base64`. Response (200) [#response-200] ```json { "status": "success", "msg": "Banner updated successfully", "data": { "success": true } } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Provide either image_url or image_base64" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl --request POST \ --header "Authorization: Bearer API_KEY" \ --header "Content-Type: application/json" \ --url "https://api.getxapi.com/twitter/user/update_banner" \ --data '{ "auth_token": "", "image_url": "https://example.com/banner.png" }' ``` # Update Profile This endpoint costs `$0.001` per API call. Updates one or more profile fields for the user owning `auth_token`. Send only the fields you want to change. Body Parameters [#body-parameters] | Parameter | Type | Required | Description | | ---------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's `auth_token` cookie | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `name` | string | No | Display name (1-50 chars). Cannot be empty, a blank `name` is ignored rather than applied. | | `description` | string | No | Bio text (max 160 chars). Send `""` to clear it. | | `location` | string | No | Location text | | `url` | string | No | Website URL shown on profile | | `profile_link_color` | string | No | Hex color (e.g. `1DA1F2`) | | `birthdate_year` | number | No | Birthdate year | | `birthdate_month` | number | No | Birthdate month (1-12) | | `birthdate_day` | number | No | Birthdate day | | `birthdate_visibility` | string | No | `self`, `mutualfollow`, `followers`, `following`, or `public` | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * At least one field other than `auth_token` must be provided. * Only included fields are updated; omitted fields are left unchanged. * `name` cannot be set to an empty string. If you send a blank `name`, it is skipped and the other fields still apply; if `name` was the only field, the request returns `400`. Response (200) [#response-200] ```json { "status": "success", "msg": "Profile updated successfully", "data": { "id": "1740174562631856128", "screen_name": "example_user", "name": "New Display Name", "description": "Updated bio", "location": "San Francisco", "url": "https://example.com", "profile_link_color": "1DA1F2" } } ``` Error Responses [#error-responses] 400 - No fields to update [#400---no-fields-to-update] ```json { "error": "At least one profile field is required." } ``` 400 - Empty name [#400---empty-name] ```json { "error": "Profile 'name' cannot be empty." } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl --request POST \ --header "Authorization: Bearer API_KEY" \ --header "Content-Type: application/json" \ --url "https://api.getxapi.com/twitter/user/update_profile" \ --data '{ "auth_token": "", "name": "New Display Name", "description": "Updated bio", "location": "San Francisco", "url": "https://example.com" }' ``` # User About This endpoint costs `$0.001` per API call. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | Notes [#notes] * Returns account metadata like creation location, signup source, and username change history. * Useful for verifying account authenticity. * No pagination needed - returns complete data in one call. Response [#response] ```json { "status": "success", "msg": "success", "data": { "id": "1234567890123456789", "userName": "example_user", "name": "Example User", "profilePicture": "https://pbs.twimg.com/profile_images/.../example_400x400.jpg", "createdAt": "2024-01-01T00:00:00.000000Z", "isVerified": false, "isBlueVerified": true, "isIdentityVerified": false, "verifiedSince": "2026-01-01T00:00:00.000Z", "accountBasedIn": "United States", "createdVia": "Web", "usernameChanges": { "count": 1, "lastChangedAt": "2025-01-01T00:00:00.000Z" } } } ``` Response Fields [#response-fields] | Field | Type | Description | | ------------------------------- | ------- | ------------------------------------------------------- | | `id` | string | Unique user ID | | `userName` | string | @handle (screen name) | | `name` | string | Display name | | `profilePicture` | string | Avatar URL (400x400) | | `createdAt` | string | Account creation date (ISO 8601) | | `isVerified` | boolean | Legacy verification (pre-Elon blue check) | | `isBlueVerified` | boolean | Twitter Blue subscriber (paid) | | `isIdentityVerified` | boolean | Verified with government ID | | `verifiedSince` | string | Blue verification date (ISO 8601), null if not verified | | `accountBasedIn` | string | Country where account was created | | `createdVia` | string | Signup source (Web, iOS, Android) | | `usernameChanges.count` | number | Number of username changes | | `usernameChanges.lastChangedAt` | string | Last username change date (ISO 8601) | Error Responses [#error-responses] 404 - User not found [#404---user-not-found] ```json { "error": "Could not find about info for @nonexistent_user" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/user_about?userName=example_user" ``` # User Affiliates This endpoint costs `$0.001` per API call and returns \~20 users per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------------- | | `userName` | string | Yes | Username of the organization | | `cursor` | string | No | Pagination cursor from previous response | Notes [#notes] * Returns affiliated accounts of a **verified organization** (companies, brands, nonprofits). * Only organizations with the "Affiliates" tab on their profile have affiliates. Regular user accounts return 0 results. * Supports cursor pagination (\~20 users per page). Response (200) [#response-200] ```json { "userName": "eigencloud", "user_count": 20, "has_more": true, "next_cursor": "DAAC...", "affiliates": [ { "type": "user", "id": "44838", "userName": "sreeramkannan", "name": "Sreeram Kannan", "url": "https://x.com/sreeramkannan", "isVerified": false, "isBlueVerified": true, "profilePicture": "https://pbs.twimg.com/profile_images/..._400x400.jpg", "coverPicture": "https://pbs.twimg.com/profile_banners/...", "description": "Founder @eigenlayer", "location": "Seattle", "followers": 44838, "following": 1200, "tweets": 5000, "createdAt": "Mon Mar 04 12:37:18 +0000 2024" } ] } ``` Error Responses [#error-responses] 400 - Missing userName [#400---missing-username] ```json { "error": "Missing required query param: userName" } ``` Examples [#examples] ```bash curl -X GET "https://api.getxapi.com/twitter/user/affiliates?userName=eigencloud" \ -H "Authorization: Bearer API_KEY" # With pagination curl -X GET "https://api.getxapi.com/twitter/user/affiliates?userName=eigencloud&cursor=DAAC..." \ -H "Authorization: Bearer API_KEY" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/user/affiliates?userName=eigencloud", { headers: { Authorization: "Bearer API_KEY", }, } ); const data = await response.json(); console.log(data.affiliates); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/user/affiliates", headers={"Authorization": "Bearer API_KEY"}, params={"userName": "eigencloud"}, ) print(response.json()["affiliates"]) ``` # User Info by ID This endpoint costs `$0.001` per API call. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `userId` | string | Yes | User id | Notes [#notes] * Returns full profile information for a Twitter user by numeric id. Response (200) [#response-200] ```json { "status": "success", "msg": "success", "data": { "id": "44196397", "name": "Elon Musk", "userName": "elonmusk", "location": null, "description": null, "protected": false, "isVerified": false, "isBlueVerified": true, "followers": 233934766, "following": 1283, "favouritesCount": 206311, "statusesCount": 96229, "mediaCount": 4346, "createdAt": "2009-06-02T20:12:29.000000Z", "coverPicture": "https://pbs.twimg.com/...", "profilePicture": "https://pbs.twimg.com/...", "canDm": false, "pinnedTweetIds": ["2018784828129243614"] } } ``` Error Responses [#error-responses] 400 - Missing userId [#400---missing-userid] ```json { "error": "Missing required query param: userId" } ``` 404 - User not found [#404---user-not-found] ```json { "error": "Could not find user with ID: 999999999999" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/info_by_id?userId=44196397" ``` # User Info This endpoint costs `$0.001` per API call. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | Notes [#notes] * Returns full profile information for a Twitter user. * No pagination needed - returns complete profile in one call. Response [#response] ```json { "status": "success", "msg": "success", "data": { "id": "1234567890123456789", "name": "Example User", "userName": "example_user", "location": "Worldwide", "url": "https://example.com/", "description": "Building on GetXAPI.", "protected": false, "isVerified": false, "isBlueVerified": true, "verifiedType": null, "followers": 229, "following": 233, "favouritesCount": 4079, "statusesCount": 986, "mediaCount": 113, "createdAt": "2021-01-01T23:45:21.000000Z", "coverPicture": null, "profilePicture": "https://pbs.twimg.com/profile_images/.../pj0DTqs2_400x400.jpg", "canDm": false, "pinnedTweetIds": [] } } ``` Response Fields [#response-fields] | Field | Type | Description | | ----------------- | ------- | -------------------------------- | | `id` | string | Unique user ID | | `name` | string | Display name | | `userName` | string | @handle (screen name) | | `location` | string | User's location | | `url` | string | Website URL (expanded) | | `description` | string | Bio | | `protected` | boolean | Is account private? | | `isVerified` | boolean | Legacy verification | | `isBlueVerified` | boolean | Twitter Blue subscriber | | `verifiedType` | string | Verification type (usually null) | | `followers` | number | Follower count | | `following` | number | Following count | | `favouritesCount` | number | Likes count | | `statusesCount` | number | Tweet count | | `mediaCount` | number | Media posts count | | `createdAt` | string | Account creation date (ISO 8601) | | `coverPicture` | string | Banner image URL | | `profilePicture` | string | Avatar URL (400x400) | | `canDm` | boolean | Can receive DMs | | `pinnedTweetIds` | array | IDs of pinned tweets | Error Responses [#error-responses] 404 - User not found [#404---user-not-found] ```json { "error": "Could not find user @nonexistent_user" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/info?userName=example_user" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/user/info?userName=example_user", { headers: { Authorization: "Bearer API_KEY" }, } ); const data = await response.json(); console.log(data); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/user/info", params={"userName": "example_user"}, headers={"Authorization": "Bearer API_KEY"}, ) print(response.json()) ``` # User Likes This endpoint costs `$0.001` per API call and returns \~20 tweets per page. Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | `auth_token` | string | Yes | User's auth token | | `ct0` | string | No | Optional current CSRF token cookie. When provided with `twid`, skips server-side credential resolution. | | `twid` | string | No | Optional current user ID cookie, for example `u=1234567890`. Must be provided with `ct0`. | | `cursor` | string | No | Pagination cursor | | `proxy` | string | No | Proxy URL (`http://`, `https://`, `socks5://`, or `socks4://`) | Notes [#notes] * If you already have current `ct0` and `twid`, send both to skip the extra server-side credential bootstrap from `auth_token`. If either one is omitted, the server resolves them from `auth_token` as before. Stale or invalid supplied values are not validated upfront and may fail on the actual Twitter action instead of returning an early `401`. * Fetches the user's own liked tweets. * Pagination uses `next_cursor` until `has_more` is false. Response (200) [#response-200] ```json { "userId": "1858475867762270208", "tweet_count": 20, "has_more": true, "next_cursor": "DAAHCgABHAY...", "likes": [ { "type": "tweet", "id": "2010705621524292007", "text": "Hello from GetXAPI.", "author": { "userName": "example_user" } } ] } ``` Error Responses [#error-responses] 400 - Missing auth_token [#400---missing-auth_token] ```json { "error": "Missing required field: auth_token" } ``` 401 - Invalid auth_token [#401---invalid-auth_token] ```json { "error": "Invalid auth_token - could not extract userId" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/user/likes" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token" }' # With pagination curl -X POST "https://api.getxapi.com/twitter/user/likes" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "auth_token": "your_auth_token", "cursor": "DAAHCgABHAY..." }' ``` # User Login V2 (Recovery) This endpoint costs `$0.005` per API call. Recovers an account when the normal [User Login](/docs/users/user-login) can't authenticate, i.e. the `auth_token` is dead **and** the password was changed after the account was captured (common with resold accounts). It regains access using the account's mailbox and returns fresh tokens **plus a new password**. High success rate on dead / changed-password accounts. **Save the returned `new_password`.** This recovery flow sets a new password on the account, the old password no longer works. Request Body [#request-body] | Field | Type | Required | Description | | ------------- | ------ | ----------- | ------------------------------------------------------------------------------------- | | `username` | string | Yes | Twitter username (without @) | | `email` | string | Yes | The email address on the account | | `mail_pw` | string | Yes | The **mailbox password** for that email | | `totp_secret` | string | Conditional | Base32 TOTP secret, required if the account has 2FA | | `proxy` | string | No | Proxy URL. Recommended so the recovery/login IP matches the IP used for later writes. | Email Provider Support [#email-provider-support] Supported mailbox providers include **GMX / mail.com** and **Hotmail / Outlook / Live**. **Gmail and Yahoo mailboxes are not supported** and return `EMAIL_AUTH_FAILED`, use the two-step [Reset Password](/docs/users/reset-password) flow for those instead. Notes [#notes] * Try `/user_login` first (fast, no password change). If it fails with dead-token / wrong-password errors and you have the mailbox password, fall back to `/user_login_v2`. * For Gmail/Yahoo, or when you don't have the mailbox password, use the two-step [Reset Password](/docs/users/reset-password) flow (the account owner reads the code). Response (200) [#response-200] Same shape as `/user_login`, plus `new_password`: ```json { "username": "myaccount", "new_password": "Gx7c266vra3z", "auth_token": "a3b02c3782dc4f...", "ct0": "e63a5e4d0a...", "twid": "u=143711528", "profile": { "...": "..." } } ``` Error Responses [#error-responses] Errors include a `code` field identifying the reason: | HTTP | `code` | Meaning | | ---- | ------------------- | --------------------------------------------------------------------------------- | | 400 | n/a | Missing `username` / `email` / `mail_pw` | | 404 | `ACCOUNT_NOT_FOUND` | Account not found (suspended, deleted, or renamed) | | 502 | `EMAIL_AUTH_FAILED` | Mailbox not accessible, check `email` / `mail_pw` (Gmail and Yahoo not supported) | | 502 | `NO_CODE` | Could not verify the account in time, retry | | 400 | `TOTP_REQUIRED` | Account has 2FA, supply `totp_secret` | | 502 | `TWO_FA_FAILED` | 2FA step failed (check `totp_secret`) | | 422 | `NO_EMAIL_METHOD` | Account can't be recovered automatically | | 503 | `NO_BEGIN_TOKEN` | Transient, retry | ```json { "error": "Could not access the mailbox, check the email and mail_pw. Gmail and Yahoo mailboxes are not supported.", "code": "EMAIL_AUTH_FAILED" } ``` Example [#example] ```bash curl -X POST "https://api.getxapi.com/twitter/user_login_v2" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "username": "myaccount", "email": "myaccount@gmx.com", "mail_pw": "mailboxpassword", "totp_secret": "SXGS5MN2IHUMV34H", "proxy": "http://user:pass@host:port" }' ``` # User Login This endpoint costs `$0.01` per API call. **Use your own `proxy` for reliable logins.** Twitter rate-limits logins **per IP** (roughly 1-2 logins per IP, then a cooldown), so without a proxy you may hit `rate_limited` frequently. Supply a **residential or ISP (static-residential) proxy with a sticky session**, the entire login flow, including 2FA, must come from a single IP. **Datacenter and rotating proxies will fail or get rate-limited.** Format: `http://user:pass@host:port`. A fresh IP per login (or one IP per account) is best. Request Body [#request-body] | Field | Type | Required | Description | | ------------- | ------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `username` | string | Yes | Twitter username (without @) | | `password` | string | Yes | Account password | | `email` | string | Yes | Email address for verification. Required in practice, Twitter's login flow conditionally triggers an alternate-identifier subtask asking for the account's email, and login fails with `366 "Missing data"` if not supplied. Always pass email if you have it. | | `totp_secret` | string | Conditional | TOTP secret for 2FA-enabled accounts (base32 string). Required whenever 2FA is enabled on the account. | | `proxy` | string | No (recommended) | Your own **residential or ISP static** proxy with a **sticky session** (`http://user:pass@host:port`). Strongly recommended, logins are rate-limited per IP, so your own proxy avoids the shared-pool `rate_limited`. **Datacenter and rotating proxies will fail or get rate-limited.** | Notes [#notes] * Returns fresh auth tokens (auth\_token, ct0, twid) for the account. * **Why email matters:** Twitter's login is a multi-step subtask flow. After password, Twitter's risk engine decides whether to trigger an alternate-identifier subtask ("verify it's you, enter your email"). Whether it fires depends on account age, IP reputation, prior login patterns, and random risk scoring, you cannot predict it per request, so the safe default is to always send email. * For 2FA accounts, provide the `totp_secret` to auto-generate the 6-digit code. * **Proxy (recommended):** Pass your own proxy to log in from your IP instead of the shared server pool. Twitter throttles logins **per IP**, so a dedicated IP is the most reliable way to avoid `rate_limited`. Use a **residential or ISP static-residential** proxy with a **sticky session**: the whole flow (password → 2FA) must stay on one IP. **Datacenter or rotating proxies will fail or get rate-limited.** Best practice: a fresh IP per login, or one IP per account. Also keeps your login IP consistent with subsequent calls. * Profile data is fetched automatically after successful login. Response (200) [#response-200] ```json { "username": "blueriver_42", "auth_token": "7b3f9c1e2d8a4f6b9c0e1a2d3f4b5c6e7a8b9c0d", "ct0": "9a2c4e6f8b1d3a5c7e9f0b2d4a6c8e0f", "twid": "u=1623458791034556928", "profile": { "type": "user", "id": "1623458791034556928", "userName": "blueriver_42", "name": "Blue River", "url": "https://x.com/blueriver_42", "isVerified": false, "isBlueVerified": false, "profilePicture": "https://pbs.twimg.com/profile_images/...", "description": "", "location": "", "followers": 18, "following": 33, "createdAt": "Wed Mar 15 09:42:11 +0000 2023" } } ``` Error Responses [#error-responses] 400 - Missing fields [#400---missing-fields] ```json { "error": "Missing required fields: username, password" } ``` 500 - Login failed [#500---login-failed] ```json { "error": "Login failed: invalid credentials or account locked" } ``` Example [#example] ```bash # Basic login (no 2FA) curl -X POST "https://api.getxapi.com/twitter/user_login" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "username": "myaccount", "password": "mypassword", "email": "myemail@example.com" }' # Login with 2FA (TOTP) curl -X POST "https://api.getxapi.com/twitter/user_login" \ -H "Authorization: Bearer API_KEY" \ -H "Content-Type: application/json" \ -d '{ "username": "blueriver_42", "password": "kp7m9q2nx4tj", "email": "blueriver42@mailprovider.com", "totp_secret": "JBSWY3DPEHPK3PXP" }' ``` # User Media This endpoint costs `$0.001` per API call and returns \~20 results per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Returns tweets that contain media (photos, videos, GIFs). * Pagination uses `next_cursor` until `has_more` is false. Response (200) [#response-200] ```json { "userName": "example_user", "userId": "1234567890123456789", "tweet_count": 13, "has_more": true, "next_cursor": "DAABCgABHAYpKoW___0...", "media": [ { "type": "tweet", "id": "2010705621524292007", "text": "Hello from GetXAPI.", "media": [ { "type": "photo", "url": "https://pbs.twimg.com/media/example.jpg" } ], "author": { "userName": "example_user" } } ] } ``` Error Responses [#error-responses] 400 - Missing userName [#400---missing-username] ```json { "error": "Missing required query param: userName" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/media?userName=example_user" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/media?userName=example_user&cursor=DAABCgABHAYpKoW___0..." ``` # User Mentions This endpoint costs `$0.001` per API call and returns \~20 tweets per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | -------------------------------------------------------------- | | `userName` | string | Yes | Screen name (without @), fetches tweets that mention this user | | `cursor` | string | No | Pagination cursor from previous response | Notes [#notes] * Returns tweets that mention `@userName` (authored by other users), most recent first. * Tweet objects have the same shape as `/twitter/user/tweets` (includes `entities`, `media`, `author`, etc.). * Pagination via `next_cursor` until `has_more` is `false`. Response (200) [#response-200] ```json { "userName": "elonmusk", "tweet_count": 20, "has_more": true, "next_cursor": "DAADDAABCgABH...", "tweets": [ { "type": "tweet", "id": "2047800000000000000", "url": "https://x.com/someuser/status/2047800000000000000", "twitterUrl": "https://twitter.com/someuser/status/2047800000000000000", "text": "@elonmusk thanks for the update", "source": "Twitter Web App", "retweetCount": 0, "replyCount": 1, "likeCount": 4, "quoteCount": 0, "viewCount": 120, "createdAt": "Fri Apr 25 12:34:56 +0000 2026", "lang": "en", "bookmarkCount": 0, "isReply": false, "inReplyToId": null, "conversationId": "2047800000000000000", "media": [], "entities": { "hashtags": [], "symbols": [], "timestamps": [], "urls": [], "user_mentions": [ { "id_str": "44196397", "indices": [0, 9], "name": "Elon Musk", "screen_name": "elonmusk" } ] }, "author": { "type": "user", "userName": "someuser", "url": "https://x.com/someuser", "id": "1234567890", "name": "Some User", "isVerified": false, "isBlueVerified": false, "profilePicture": "https://pbs.twimg.com/profile_images/.../abc_normal.jpg", "followers": 412, "following": 188, "createdAt": "Tue Jan 04 11:12:13 +0000 2022" }, "quoted_tweet": null } ] } ``` Error Responses [#error-responses] 400 - Missing param [#400---missing-param] ```json { "error": "Missing required query param: userName" } ``` Example [#example] ```bash # Fresh curl -H "Authorization: Bearer API_KEY" \ "https://api.getxapi.com/twitter/user/mentions?userName=elonmusk" # With pagination curl -H "Authorization: Bearer API_KEY" \ "https://api.getxapi.com/twitter/user/mentions?userName=elonmusk&cursor=DAADDAABCgABH..." ``` # Search Users This endpoint costs `$0.001` per API call and returns \~20 users per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `q` | string | Yes | Search query (username, keyword, or topic) | | `cursor` | string | No | Pagination cursor from previous response | Notes [#notes] * Searches for Twitter users matching the query (equivalent to the "People" tab in Twitter search). * Returns up to 20 users per page. * Pagination uses `next_cursor` until `has_more` is false. Response (200) [#response-200] ```json { "query": "@eigencloud", "user_count": 20, "has_more": true, "next_cursor": "DAAFCgABHDho...", "users": [ { "type": "user", "id": "3185716686", "userName": "AshCrypto", "name": "Ash Crypto", "url": "https://x.com/AshCrypto", "isVerified": false, "isBlueVerified": true, "profilePicture": "https://pbs.twimg.com/profile_images/.../h_vgXOAw_400x400.jpg", "coverPicture": "https://pbs.twimg.com/profile_banners/3185716686/...", "description": "News, Memes, Charts, Hopium, Market analysis and Latest crypto updates.", "location": "X", "followers": 2148334, "following": 342, "tweets": 28986, "listed": 7544, "createdAt": "Tue May 05 02:01:09 +0000 2015", "canDm": true } ] } ``` Error Responses [#error-responses] 400 - Missing q [#400---missing-q] ```json { "error": "Missing required query param: q" } ``` Examples [#examples] ```bash curl -X GET "https://api.getxapi.com/twitter/user/search?q=crypto" \ -H "Authorization: Bearer API_KEY" # Search by username curl -X GET "https://api.getxapi.com/twitter/user/search?q=@eigencloud" \ -H "Authorization: Bearer API_KEY" # With pagination curl -X GET "https://api.getxapi.com/twitter/user/search?q=crypto&cursor=DAAFCgABHDho..." \ -H "Authorization: Bearer API_KEY" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/user/search?q=crypto", { headers: { Authorization: "Bearer API_KEY", }, } ); const data = await response.json(); console.log(data); ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/user/search", headers={"Authorization": "Bearer API_KEY"}, params={"q": "crypto"}, ) print(response.json()) ``` # User Status This endpoint costs `$0.001` per API call. Reliably reports whether an account is **alive**, **suspended**, or **not found** (deleted / renamed / never existed). Unlike `/user/info`, suspended and not-found accounts are returned as normal data (`200`), not as errors — so you can branch on `data.status` directly. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | Notes [#notes] * The check is performed via a third-party lookup, which is what makes suspension detection reliable — a suspended account cannot see its own suspended state, so self-checks are misleading. * `alive`, `suspended`, and `not_found` all return HTTP `200` with `data.status` set. This endpoint does **not** return a `404` for a missing user; it returns `"status": "not_found"`. Response [#response] ```json { "status": "success", "msg": "success", "data": { "userName": "example_user", "status": "alive", "id": "1234567890123456789", "reason": null } } ``` Suspended account [#suspended-account] ```json { "status": "success", "msg": "success", "data": { "userName": "banned_user", "status": "suspended", "id": null, "reason": "Suspended" } } ``` Not found (deleted / renamed / never existed) [#not-found-deleted--renamed--never-existed] ```json { "status": "success", "msg": "success", "data": { "userName": "nonexistent_user", "status": "not_found", "id": null, "reason": null } } ``` Response Fields [#response-fields] | Field | Type | Description | | ---------- | -------------- | --------------------------------------------------------------------- | | `userName` | string | The screen name that was checked | | `status` | string | `alive`, `suspended`, `not_found`, or `unavailable` | | `id` | string \| null | Unique user ID (present when `alive`; may be present for `suspended`) | | `reason` | string \| null | Unavailability reason from Twitter (e.g. `"Suspended"`), else `null` | Status values [#status-values] | Value | Meaning | | ------------- | ---------------------------------------------------------------------------- | | `alive` | The account exists and is active. | | `suspended` | The account exists but has been suspended by Twitter. | | `not_found` | No such account — deleted, renamed, or never existed. | | `unavailable` | The account exists but is unavailable for another reason (e.g. deactivated). | Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/status?userName=example_user" ``` ```javascript const response = await fetch( "https://api.getxapi.com/twitter/user/status?userName=example_user", { headers: { Authorization: "Bearer API_KEY" }, } ); const { data } = await response.json(); if (data.status === "suspended") { console.log(`${data.userName} is suspended`); } ``` ```python import requests response = requests.get( "https://api.getxapi.com/twitter/user/status", params={"userName": "example_user"}, headers={"Authorization": "Bearer API_KEY"}, ) data = response.json()["data"] print(data["status"]) # alive | suspended | not_found ``` # User Tweets and Replies This endpoint costs `$0.001` per API call and returns \~20 results per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Returns tweets and replies posted by the user (their profile "Replies" tab). This is NOT their home feed, it only includes tweets and replies they authored. * Pagination uses `next_cursor` until `has_more` is false. * Each tweet includes an `entities` object, see [User Tweets](./user-tweets) for the field shape. Response (200) [#response-200] ```json { "userName": "example_user", "userId": "1234567890123456789", "tweet_count": 20, "has_more": true, "next_cursor": "DAABCgABHAY...", "tweets": [ { "type": "tweet", "id": "2010705621524292007", "url": "https://x.com/example_user/status/2010705621524292007", "twitterUrl": "https://twitter.com/example_user/status/2010705621524292007", "text": "@another_user great point, totally agree", "source": "Twitter Web App", "retweetCount": 0, "replyCount": 1, "likeCount": 4, "quoteCount": 0, "viewCount": 132, "createdAt": "Mon Jan 12 13:44:55 +0000 2026", "lang": "en", "bookmarkCount": 0, "isReply": true, "inReplyToId": "2010700000000000000", "conversationId": "2010699000000000000", "media": [], "entities": { "hashtags": [], "symbols": [], "timestamps": [], "urls": [], "user_mentions": [ { "id_str": "9876543210987654321", "indices": [0, 13], "name": "Another User", "screen_name": "another_user" } ] }, "author": { "type": "user", "userName": "example_user", "url": "https://x.com/example_user", "id": "1234567890123456789", "name": "Example User", "isVerified": false, "isBlueVerified": true, "profilePicture": "https://pbs.twimg.com/profile_images/.../abc_normal.jpg", "coverPicture": "https://pbs.twimg.com/profile_banners/1234567890123456789/...", "description": "Building things on the internet", "location": "San Francisco, CA", "followers": 1280, "following": 312, "createdAt": "Fri Jan 01 23:45:21 +0000 2021" }, "quoted_tweet": null } ] } ``` Error Responses [#error-responses] 400 - Missing userName [#400---missing-username] ```json { "error": "Missing required query param: userName" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/tweets_and_replies?userName=example_user" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/tweets_and_replies?userName=example_user&cursor=DAABCgABHAY..." ``` # User Tweets Complete This endpoint costs `$0.003` per API call. It fetches 40 raw items from the recent tweets-and-replies timeline per page, then expands detected self-threads. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Returns the user's recent tweets-and-replies timeline, not the full historical archive. X's available recent timeline window is typically around 3,200 items. * Expands detected self-threads so thread continuations inside that recent window are returned more completely. * Thread expansion is capped to 20 distinct self-thread roots per page; `complete` is `false` if that cap is exceeded or an expansion could not finish. * Expanded threads can overlap with adjacent pages. If you paginate, dedupe tweets by `id` across pages. * Pagination uses `next_cursor` until `has_more` is false. * Each tweet includes an `entities` object, see [User Tweets](./user-tweets) for the field shape. Response (200) [#response-200] ```json { "userName": "example_user", "userId": "1234567890123456789", "tweet_count": 43, "has_more": true, "next_cursor": "DAABCgABHAY...", "expanded_threads": 3, "complete": true, "tweets": [ { "type": "tweet", "id": "2010705621524292007", "text": "Hello from GetXAPI.", "entities": { "hashtags": [], "symbols": [], "timestamps": [], "urls": [], "user_mentions": [] }, "author": { "userName": "example_user" } } ] } ``` Error Responses [#error-responses] 400 - Missing userName [#400---missing-username] ```json { "error": "Missing required query param: userName" } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/tweets/complete?userName=example_user" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/tweets/complete?userName=example_user&cursor=DAABCgABHAY..." ``` # User Tweets This endpoint costs `$0.001` per API call and returns \~20 tweets per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `userName` | string | Conditional | Screen name (without @). Required if `userId` is not provided. | | `userId` | string | Conditional | Numeric Twitter user ID. Required if `userName` is not provided. Faster than `userName` (\~1s saved) since it skips the username-to-ID lookup. | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Provide either `userName` or `userId`, if both are given, `userId` takes priority. * Using `userId` is recommended for high-throughput use cases as it skips an internal API call. * Returns tweets posted by the user (their profile "Posts" tab). This is NOT their home feed, it only includes tweets they authored. * Pagination uses `next_cursor` until `has_more` is false. * Each tweet includes an `entities` object (`hashtags`, `symbols`, `timestamps`, `urls`, `user_mentions`) so you can read `expanded_url` directly without HEAD-resolving `t.co` links. Works for both standard tweets and Note Tweets. Response (200) [#response-200] ```json { "userName": "example_user", "userId": "1234567890123456789", "tweet_count": 20, "has_more": true, "next_cursor": "DAABCgABHAY...", "tweets": [ { "type": "tweet", "id": "2010705621524292007", "url": "https://x.com/example_user/status/2010705621524292007", "twitterUrl": "https://twitter.com/example_user/status/2010705621524292007", "text": "Check out our latest update https://t.co/abc123", "source": "Twitter Web App", "retweetCount": 3, "replyCount": 12, "likeCount": 87, "quoteCount": 1, "viewCount": 4521, "createdAt": "Mon Jan 12 13:44:55 +0000 2026", "lang": "en", "bookmarkCount": 6, "isReply": false, "inReplyToId": null, "conversationId": "2010705621524292007", "media": [ { "type": "photo", "url": "https://pbs.twimg.com/media/abc123.jpg", "expanded_url": "https://x.com/example_user/status/2010705621524292007/photo/1", "video_url": null } ], "entities": { "hashtags": [], "symbols": [], "timestamps": [], "urls": [ { "url": "https://t.co/abc123", "expanded_url": "https://docs.getxapi.com/changelog", "display_url": "docs.getxapi.com/changelog", "indices": [29, 52] } ], "user_mentions": [] }, "author": { "type": "user", "userName": "example_user", "url": "https://x.com/example_user", "id": "1234567890123456789", "name": "Example User", "isVerified": false, "isBlueVerified": true, "profilePicture": "https://pbs.twimg.com/profile_images/.../abc_normal.jpg", "coverPicture": "https://pbs.twimg.com/profile_banners/1234567890123456789/...", "description": "Building things on the internet", "location": "San Francisco, CA", "followers": 1280, "following": 312, "createdAt": "Fri Jan 01 23:45:21 +0000 2021" }, "quoted_tweet": null } ] } ``` Error Responses [#error-responses] 400 - Missing params [#400---missing-params] ```json { "error": "Missing required query param: userName or userId" } ``` Example [#example] ```bash # By userName curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/tweets?userName=example_user" # By userId (faster) curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/tweets?userId=44196397" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/tweets?userId=44196397&cursor=DAABCgABHAY..." ``` # Verified Followers This endpoint costs `$0.001` per API call and returns \~20 users per page. Query Parameters [#query-parameters] | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `userName` | string | Yes | Screen name (without @) | | `cursor` | string | No | Pagination cursor | Notes [#notes] * Returns \~20 verified (blue check) followers per page. Response [#response] ```json { "userName": "elonmusk", "user_count": 20, "has_more": true, "next_cursor": "DAABCgABG...", "verified_followers": [ { "type": "user", "id": "44073696", "userName": "roydanroy", "name": "Dan Roy", "url": "https://x.com/roydanroy", "isVerified": false, "isBlueVerified": true, "profilePicture": "https://pbs.twimg.com/profile_images/...", "coverPicture": "https://pbs.twimg.com/profile_banners/...", "description": "@Google DeepMind. On leave, Canada CIFAR AI Chair...", "location": "London", "followers": 61518, "following": 1858, "tweets": 5200, "listed": 300, "createdAt": "Tue Jun 02 06:32:26 +0000 2009", "canDm": true } ] } ``` Example [#example] ```bash curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/verified_followers?userName=elonmusk" # With pagination curl -H "Authorization: Bearer API_KEY" "https://api.getxapi.com/twitter/user/verified_followers?userName=elonmusk&cursor=DAABCgABG..." ```