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_here

The 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

MethodPathDescription
GET/v1/dt/targetsList monitored handles
POST/v1/dt/targetsAdd handle. Body: {"handle": "username"}
DELETE/v1/dt/targets/{handle}Remove handle
GET/v1/dt/profile/{handle}Latest profile snapshot
GET/v1/dt/profile/{handle}/historyProfile 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/webhookGet saved webhook URL
PUT/v1/dt/webhookSet 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/statusSubscription status
GET/twitter/user/{username}User detail (lookup)
GET/twitter/tweet/{tweetId}Single tweet + engagement counts
GET/twitter/blacklistList your blacklist
GET/twitter/communitiesBrowse 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 as update_handle, update_bio, update_photo, update_name, update_banner_url, update_location, update_website, update_pinned_post, update_verified). Note: banner changes currently match update_banner_url, not update_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 example founders, exchanges, coin). Case-insensitive and space-insensitive ("Perp Coins" matches perp_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.

StatusMeaningcode · example error
400Bad request — missing/invalid fieldbad_request · "handle is required"
401Auth failed: no key, wrong format, unknown, expired or deactivated keykey_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."
403Handle ceiling reached, handle not in your target list, or your plan lacks the required modulehandle_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"
404Handle not found or suspended, or no data yetnot_found · "@handle not found on Twitter"
409Handle already in your monitored setconflict · "@handle is already tracked"
503Temporary 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/ws

Opcode-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.