B2B REST API: Twitter profiles, tweets and follows
All B2B API requests require a Bearer token with dt_ prefix, generated via /apikey in the bot.
Authorization: Bearer dt_your_api_key_hereThe twitterapi.io-style header X-API-Key: dt_your_api_key_here works too. Keys in the URL (?api_key=) are not accepted on REST endpoints: on /v1/dt/* they return 401 with code: "key_in_query" (the key_* codes below are /v1/dt/* only); /twitter/* returns a generic 401.
REST Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /v1/dt/targets | List monitored handles |
| POST | /v1/dt/targets | Add handle. Body: {"handle": "username"} |
| DELETE | /v1/dt/targets/{handle} | Remove handle |
| GET | /v1/dt/profile/{handle} | Latest profile snapshot |
| GET | /v1/dt/profile/{handle}/history | Profile change history |
| GET | /v1/dt/tweets/{handle} | Recent tweets |
| GET | /v1/dt/following/{handle} | Accounts the handle follows |
| GET | /v1/dt/followers/{handle} | The handle's followers |
| GET | /v1/dt/communities/{handle} | Communities the handle belongs to |
| GET | /v1/dt/social/diff/{handle} | Follow/unfollow diff over a trailing window |
| GET | /v1/dt/webhook | Get saved webhook URL |
| PUT | /v1/dt/webhook | Set webhook URL (delivery coming soon; use the WebSocket) |
| GET | /v1/dt/events/{event_id} | Trace a delivered event back to its detection record |
| GET | /v1/dt/status | Subscription status |
| GET | /twitter/user/{username} | User detail (lookup) |
| GET | /twitter/tweet/{tweetId} | Single tweet + engagement counts |
| GET | /twitter/blacklist | List your blacklist |
| GET | /twitter/communities | Browse all communities |
| POST/DEL | /twitter/blacklist/{username} | Add / remove blacklist handle |
/v1/dt/* responses use the standard envelope: {"ok":true,"data":…} on success, {"ok":false,"error":"…","code":"…"} on failure. /twitter/* routes use the twitterapi.io shape: bare objects on success, {"status":"error","msg":"…"} on failure (no code). A missing or invalid JSON body gets a plain-text 400 / 415 / 422; a body over 64 KB gets 413.
Not currently populated: /v1/dt/profile/{handle}/history, /v1/dt/following/{handle}, /v1/dt/followers/{handle}, /v1/dt/communities/{handle} and /v1/dt/social/diff/{handle} return empty lists today. For follow, unfollow, new-follower and profile data, use the realtime WebSocket events and GET /v1/dt/events/{event_id}.
POST /v1/dt/targets — add a handle
Resolves the handle, stores its numeric ID, and starts monitoring. Re-adding an active handle returns 409; re-adding one listed with is_active: false reactivates it (201). If the handle can't be resolved right away it is still added with twitter_user_id: null, and per-handle endpoints return 503 until it fills in.
Request: {"handle": "elonmusk"}
201 Created:
{"ok":true,"data":{"handle":"elonmusk","twitter_user_id":"44196397"}}Errors: 400 "handle is required" (empty handle; a body with no handle field gets a plain-text 422) · 403 handle_limit_reached "Handle limit reached (N/M)" · 404 not found or suspended · 409 "@elonmusk is already tracked" · 503 temporarily unavailable, retry with backoff.
GET /v1/dt/targets — list handles
Returns your monitored handles and your plan's handle ceiling. The list includes handles paused by the system (is_active: false) and count includes them; only active handles count toward max_handles.
{
"ok": true,
"data": {
"targets": [
{"id": 42, "screen_name": "elonmusk", "display_name": "Elon Musk", "twitter_user_id": "44196397", "is_active": true, "created_at": "2026-07-01T12:00:00Z"}
],
"count": 1,
"max_handles": 500
}
}DELETE /v1/dt/targets/{handle} — remove a handle
{"ok":true,"data":{"removed":"elonmusk"}}Returns 404 "@elonmusk not found" if the handle is not in your set.
GET /v1/dt/status — subscription status
Current handle usage, active state, days remaining, and enabled modules. days_left is 0 for plans with no end date.
{
"ok": true,
"data": {
"targets": 137,
"max_handles": 500,
"is_active": true,
"days_left": 24,
"modules": ["realtime", "follows", "profile_watch"]
}
}GET /v1/dt/profile/{handle} — latest profile snapshot
Most recent stored profile for a monitored handle. Returns 404 "No profile data for @handle yet" until profile data is available. Populated fields: twitter_id, screen_name, display_name, bio, followers, following, is_blue_verified, avatar_url and scraped_at (time of the profile snapshot). The other fields are always null or 0.
{
"ok": true,
"data": {
"twitter_id": "44196397",
"screen_name": "elonmusk",
"display_name": "Elon Musk",
"bio": "…",
"location": null,
"website": null,
"followers": 190000000,
"following": 800,
"tweet_count": 0,
"likes_count": 0,
"listed_count": 0,
"media_count": 0,
"is_blue_verified": true,
"verified_type": null,
"avatar_url": "https://pbs.twimg.com/profile_images/…",
"banner_url": null,
"account_created_at": null,
"scraped_at": "2026-07-04T09:15:00Z"
}
}GET /v1/dt/profile/{handle}/history — profile change history
Time series of follower / following / tweet / like / listed counts, newest first. Query params: limit (default 50, max 200), offset (default 0). Requires the profile_watch module. Not currently populated (returns an empty list).
{
"ok": true,
"data": {
"history": [
{"followers": 190000000, "following": 800, "tweet_count": 42000, "likes_count": 30000, "listed_count": 150000, "scraped_at": "2026-07-04T09:15:00Z"}
],
"count": 1
}
}GET /v1/dt/events/{event_id} — trace a delivered event
Takes any event_id you received over the WebSocket and returns the detection record behind it. Use it to verify what was sent, and when, if a user ever queries an alert.
Every id we emit is self-describing. Tweet events carry the tweet id (evt_<tweet_id>); the others carry the handle and the emit time in epoch milliseconds (evt_f_ follow, evt_fr_ unfollow, evt_p_ profile update, evt_fl_ new follower, evt_del_ deleted tweet).
GET /v1/dt/events/evt_2097307937588789251
Authorization: Bearer dt_…
{
"ok": true,
"data": {
"event_id": "evt_2097307937588789251",
"event": "twitter.tweet.new",
"handle": "g_kouner",
"detected_at": "2026-09-08T12:55:53.267818Z",
"match_count": 1,
"matches": [
{
"tweet_id": "2097307937588789251",
"text": "…",
"is_reply": false,
"is_quote": false,
"tweet_created_at": null,
"detected_at": "2026-09-08T12:55:53.267818Z"
}
]
}
}The trace endpoint labels tweet records twitter.tweet.new; these are the same events the WebSocket delivers as twitter.post.new.
Non-tweet ids are matched on the handle within a 10-second window, and every match in that window is returned: match_count tells you how many, and match_window_secs reports the tolerance used.
You can only resolve events for handles on your own target list, and the module gating matches the WebSocket: follow events need follows, profile events need profile_watch, follower events need followers.
Errors: 400 unrecognised event_id format · 401 missing or invalid API key · 403 the handle is not on your list, or your plan lacks that module · 404 no detection record for that id. Deleted-tweet events (evt_del_) are delivered live and not retained, so they return 404 with that explanation — the id itself still carries the handle and detection time.
GET /twitter/user/{username} — user detail (lookup)
Profile for any handle we already hold data on, in the /twitter/* lookup namespace (auth: Bearer dt_… or X-API-Key). Unlike /v1/dt/* this is not gated to your target list. A handle we hold no data on returns 404.
GET /twitter/user/elonmusk
{
"userName": "elonmusk",
"name": "Elon Musk",
"id": "44196397",
"profilePicture": "https://pbs.twimg.com/…",
"followers": 241613594,
"following": 1405,
"isVerified": true,
"description": "…"
}GET /twitter/tweet/{tweetId} — single tweet detail
Full tweet by id with current engagement counts. Numeric id required. Returns author block, media, type (post, reply or quote), and like/retweet/reply/quote/view/bookmark counts; any count can be null.
GET /twitter/tweet/1234567890123456789
{
"id": "1234567890123456789",
"text": "…",
"media": ["https://pbs.twimg.com/media/…"],
"createdAt": "Wed Jun 03 12:30:53 +0000 2026",
"type": "post",
"author": { "userName": "elonmusk", "name": "Elon Musk",
"profilePicture": "…", "followers": 241613594, "isVerified": true },
"likeCount": 12000, "retweetCount": 800, "replyCount": 430,
"quoteCount": 120, "viewCount": 2100000, "bookmarkCount": 340
}Errors: 400 non-numeric id · 401 bad key · 404 not found, unavailable, or not readable right now (retry once before treating it as deleted) · 503 service starting, retry with backoff.
POST / DELETE /twitter/blacklist/{username} — manage blacklist
Stores a per-account handle exclusion list. GET /twitter/blacklist returns your current list. Feed filtering by this list is not active yet. POST is idempotent; DELETE of a handle not on the list returns 404. It is not an action on your real Twitter account.
POST /twitter/blacklist/spamhandle -> {"status":"success","msg":"@spamhandle blacklisted"}
DELETE /twitter/blacklist/spamhandle -> {"status":"success","msg":"@spamhandle removed from blacklist"}
GET /twitter/blacklist -> {"status":"success","blacklist":["spamhandle"],"count":1}GET /twitter/communities — browse communities
Catalog of communities we have observed, ranked by member count. Query params: search (name contains), limit (default 50, max 200), offset. image is currently always null (community banners are not stored yet).
GET /twitter/communities?search=design&limit=2
{
"status": "success",
"count": 2,
"communities": [
{ "id": "1453877367030484992", "name": "The Design Sphere",
"description": "Welcome to The Design Sphere…",
"memberCount": 667456, "image": null }
]
}Realtime event types — retweets & renames
Retweets arrive on the realtime WS as a tweet event with data.type = "repost" (alongside post, reply, quote). Handle renames arrive as twitter.profile.update with field:"handle" (prev = old handle, updated = new). Both require the relevant module; no new event type was introduced, so existing parsers are unaffected.
Realtime WS — opt-in event filters
The realtime WS login is normally {"op":2,"d":"dt_yourkey"}. To filter your stream, send d as an object instead. Sending it as a string (or an object with no filters) keeps the full firehose — filters only ever remove events.
{"op":2,"d":{
"api_key": "dt_yourkey",
"onlyCA": true, // only tweets carrying a contract address
"onlyTicker": false, // only tweets carrying a $cashtag
"types": ["tweet","retweet","quote","reply"], // omit or ["All"] = everything
"categories": ["founders","exchanges"] // omit or ["All"] = every category
}}types[]— tweet types (tweet,retweet,quote,reply) and non-tweet events (follow,unfollow,follower,deleted, and profile changes such asupdate_handle,update_bio,update_photo,update_name,update_banner_url,update_location,update_website,update_pinned_post,update_verified). Note: banner changes currently matchupdate_banner_url, notupdate_banner. Case-insensitive; title-case labels like"Update Handle"are also accepted. Empty list or"All"disables type filtering.categories[]— deliver only events whose account belongs to at least one of the named categories (for examplefounders,exchanges,coin). Case-insensitive and space-insensitive ("Perp Coins"matchesperp_coins). An account with no known category is excluded while this filter is set; accounts may carry several categories. Empty list or"All"disables category filtering. Applies to both tweet and non-tweet events.onlyCA/onlyTicker— tweet-content filters; they gate tweet events only and do not affect follow/profile events.
GET /v1/dt/tweets/{handle} — recent tweets
Capped at the last 20 tweets captured for the handle (no pagination, no since). Note: engagement metrics (likes, retweets, replies, quotes, views, bookmarks) are returned as 0 and full media metadata (media_types, video_urls) is not populated on this endpoint; is_retweet is always false, reply_to_username always null, and hashtags / urls / cashtags always empty. For complete tweet objects with media and enrichment, use the realtime WebSocket.
{
"ok": true,
"data": {
"tweets": [
{
"tweet_id": "1234567890123456789",
"full_text": "Hello world",
"created_at": "2026-07-04T09:14:00Z",
"likes": 0, "retweets": 0, "replies": 0, "quotes": 0, "views": 0, "bookmarks": 0,
"is_retweet": false, "is_reply": false, "is_quote": false,
"reply_to_username": null,
"media_urls": ["https://pbs.twimg.com/media/…"],
"mentions": ["vitalikbuterin"],
"hashtags": [],
"urls": [],
"cashtags": []
}
],
"count": 1
}
}GET /v1/dt/following/{handle} & /v1/dt/followers/{handle}
The accounts a handle follows, or its followers. Query params: limit (default 100, max 500), offset (default 0). The response key is following for /following and follower (singular) for /followers; total is the number of stored current edges. Requires the follows module (following) or followers module (followers). Not currently populated (returns empty lists).
GET /v1/dt/following/elonmusk
{
"ok": true,
"data": {
"following": [
{"target_id": "44196397", "target_screen_name": "vitalikbuterin", "is_current": true, "first_seen_at": "2026-06-01T00:00:00Z", "last_seen_at": "2026-07-04T00:00:00Z"}
],
"count": 1,
"total": 800
}
}GET /v1/dt/communities/{handle} — communities
{
"ok": true,
"data": {
"communities": [
{"community_id": "1493446837214187523", "community_name": "Build in Public", "member_count": 120000, "creator_screen_name": "someuser", "is_current": true, "first_seen_at": "2026-06-01T00:00:00Z", "last_seen_at": "2026-07-04T00:00:00Z"}
],
"count": 1
}
}GET /v1/dt/social/diff/{handle} — follow / unfollow diff
Follow history. Unfollows are delivered live on the WebSocket as twitter.following.removed (follows module). This endpoint returns following and follower edges added and removed over a trailing 7-day window (up to 100 of each, newest first); relation tells them apart. Requires the follows module. Not currently populated (returns empty lists).
{
"ok": true,
"data": {
"added": [
{"target_id": "44196397", "target_screen_name": "vitalikbuterin", "relation": "following", "change": "added", "changed_at": "2026-07-03T10:00:00Z"}
],
"removed": [
{"target_id": "888", "target_screen_name": "someone", "relation": "following", "change": "removed", "changed_at": "2026-07-02T18:00:00Z"}
],
"added_count": 1,
"removed_count": 1
}
}GET /v1/dt/webhook & PUT /v1/dt/webhook
GET returns your configured callback URL and whether a signing secret is set (the secret itself is never returned by GET). PUT saves the URL (http or https; HTTPS recommended), rotates the signing secret, and returns it once.
GET /v1/dt/webhook
{"ok":true,"data":{"url":"https://your-server.com/webhook","has_secret":true}}
PUT /v1/dt/webhook Request: {"url":"https://your-server.com/webhook"}
{"ok":true,"data":{"url":"https://your-server.com/webhook","secret":"3f9a1c…","note":"Store this secret securely."}}Delivery channel: B2B events are delivered over the Realtime WebSocket. Webhook delivery for B2B is coming soon: these endpoints only save a URL today, and nothing is posted to it yet. Build on the WebSocket.
REST Error Codes
Failures return {"ok":false,"error":"…","code":"…"} with the HTTP status below; some add hint and retry_after. Match on code. The full code list is under API conventions.
| Status | Meaning | code · example error |
|---|---|---|
400 | Bad request — missing/invalid field | bad_request · "handle is required" |
401 | Auth failed: no key, wrong format, unknown, expired or deactivated key | key_missing / key_in_query / key_malformed / key_unknown / key_expired / key_deactivated · "Your subscription expired on 01 Oct 2026 10:12 UTC. Renew in @B2B_Xanguard_bot; your key stays the same." |
403 | Handle ceiling reached, handle not in your target list, or your plan lacks the required module | handle_limit_reached · "Handle limit reached (500/500)" (with a hint: remove a handle or upgrade) · forbidden · "@handle is not in your target list" / "Your plan does not include the 'follows' module" |
404 | Handle not found or suspended, or no data yet | not_found · "@handle not found on Twitter" |
409 | Handle already in your monitored set | conflict · "@handle is already tracked" |
503 | Temporary problem on our side, not with your request: retry in ~30s (retry_after: 30) | service_unavailable · "Service temporarily unavailable" |
Limits: REST requests are not per-second rate-limited, but the enforced limits are your plan's handle ceiling (max_handles → 403), a 64 KB request-body cap, and 5 concurrent WebSocket connections per key. High-volume consumers should use the WebSocket rather than polling REST.
Realtime WebSocket
wss://api.xanguard.tech/v1/dt/realtime/wsOpcode-based protocol (HELLO → LOGIN → READY → EVENT stream, TweetCatcher-style). Connect, send the LOGIN opcode with your dt_ API key, and receive real-time events for all monitored handles. See B2B Realtime WebSocket page for the full opcode table, handshake flow, and every event payload.