Twitter WebSocket Client Guide
Stream real-time tweets into your app via a single persistent WebSocket connection — no polling, no rate-limit juggling. This guide covers the connection handshake, a working Python client, and the JSON shape of every event you'll receive.
wss://ws.twitterapi.io/twitter/tweet/websocketx-api-key headerHow tweets reach your WebSocket
WebSocket is the transport — it's the pipe every tweet flows through. What you actually receive depends on which of our two delivery models you've subscribed to: the Stream, Custom Filter Rules, or both at once on the same connection.
Stream subscription
Monitors the accounts you've subscribed to and pushes every new tweet in real time. Within the stream, tweets travel through one of two lanes depending on the author's follower count:
fast_tweetPriority lane for authors with 5,000+ followers (dropping to 2,000+ soon). Single tweet per event, sub-second delivery target.tweetStandard lane for the rest of the accounts you're monitoring. Same underlying tweet data, norule_id.
Custom filter rules
Define rules (keywords, specific accounts, time windows). Any tweet that matches a rule is pushed to your WebSocket as event_type: "tweet", batched in a tweets[] array with rule_id and rule_tag attached so you can multiplex several rules over one socket.
Connecting
Open a standard WebSocket to the endpoint below and pass your API key in the x-api-key header. No query-string tokens, no OAuth dance.
wss://ws.twitterapi.io/twitter/tweet/websocketx-api-key: <your api key>One API key = one active connection
Opening a second WebSocket with the same API key while one is already live rejects the new connection immediately (close code 1008). If you need multiple parallel streams, use multiple API keys. After a disconnect, wait at least 90 seconds before reconnecting so the server can fully release your slot.
Python client example
A complete, ready-to-run Python reference client using the websocket-client library. Install it first:
pip install websocket-clientimport threading
import time
import traceback
import websocket
import json
# Message handling callback
def on_message(ws, message):
try:
print(f"\nReceived message: {message}")
# Convert to JSON
result_json = json.loads(message)
event_type = result_json.get("event_type")
if event_type == "connected":
print("Connection successful!")
return
if event_type == "ping":
print("ping!")
timestamp = result_json.get("timestamp")
current_time_ms = time.time() * 1000
current_time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))
# Calculate and format time difference
diff_time_ms = current_time_ms - timestamp
diff_time_seconds = diff_time_ms / 1000
diff_time_formatted = f"{int(diff_time_seconds // 60)}min{int(diff_time_seconds % 60)}sec"
# Format original timestamp
timestamp_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp/1000))
# Print information
print(f"Current time: {current_time_str}")
print(f"Message timestamp: {timestamp_str}")
print(f"Time difference: {diff_time_formatted} ({diff_time_ms:.0f} milliseconds)")
return
if event_type == "tweet":
print("tweet!")
# Extract fields
rule_id = result_json.get("rule_id")
rule_tag = result_json.get("rule_tag")
event_type = result_json.get("event_type")
tweets = result_json.get("tweets", [])
timestamp = result_json.get("timestamp")
# Print key information
print(f"rule_id: {rule_id}")
print(f"rule_tag: {rule_tag}")
print(f"event_type: {event_type}")
print(f"Number of tweets: {len(tweets)}")
print(f"timestamp: {timestamp}")
# Calculate time difference
current_time_ms = time.time() * 1000
current_time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))
diff_time_ms = current_time_ms - timestamp
diff_time_seconds = diff_time_ms / 1000
diff_time_formatted = f"{int(diff_time_seconds // 60)}min{int(diff_time_seconds % 60)}sec"
timestamp_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp/1000))
print(f"Current time: {current_time_str}")
print(f"Message timestamp: {timestamp_str}")
print(f"Time difference: {diff_time_formatted} ({diff_time_ms:.0f} milliseconds)")
except json.JSONDecodeError as e:
print(f"JSON parsing error: {e}. traceback: {traceback.format_exc()}")
except Exception as e:
print(f"Error occurred while processing message: {e}. traceback: {traceback.format_exc()}")
# Error handling callback
def on_error(ws, error):
print(f"\nError occurred: {error}, traceback: {traceback.format_exc()}")
if isinstance(error, websocket.WebSocketTimeoutException):
print("Connection timeout. Please check if server is running or network connection.")
elif isinstance(error, websocket.WebSocketBadStatusException):
print(f"Server returned error status code: {error}")
print("Please check if API key and endpoint path are correct.")
elif isinstance(error, ConnectionRefusedError):
print("Connection refused. Please confirm server address and port are correct.")
# Connection close callback
def on_close(ws, close_status_code, close_msg):
print(f"\nConnection closed: status_code={close_status_code}, message={close_msg}")
if close_status_code == 1000:
print("Normal connection closure")
elif close_status_code == 1001:
print("Server is shutting down or client navigating away")
elif close_status_code == 1002:
print("Protocol error")
elif close_status_code == 1003:
print("Received unacceptable data type")
elif close_status_code == 1006:
print("Abnormal connection closure, possibly network issues")
elif close_status_code == 1008:
print("Policy violation")
elif close_status_code == 1011:
print("Server internal error")
elif close_status_code == 1013:
print("Server overloaded")
# Connection established callback
def on_open(ws):
print("\nConnection established!")
# Main function
def main(x_api_key):
url = "wss://ws.twitterapi.io/twitter/tweet/websocket"
headers = {"x-api-key": x_api_key}
ws = websocket.WebSocketApp(
url,
header=headers,
on_message=on_message,
on_error=on_error,
on_close=on_close,
on_open=on_open
)
ws.run_forever(ping_interval=40, ping_timeout=30, reconnect=90)
if __name__ == "__main__":
x_api_key = "xxxx" # Replace with your own API key
main(x_api_key)
Replace "xxxx" with your API key from the dashboard. The client auto-pings every 40s and auto-reconnects after 90s of silence.
Event types & JSON formats
Every message on the socket is a JSON object with an event_type key. Four types exist in practice — two carry tweets, two are connection housekeeping.
Priority lane for high-follower accounts (Stream)
Fires for tweets from authors with 5,000+ followers (dropping to 2,000+ soon) on your Stream subscription. Emitted as soon as the tweet lands in our system — the snow_delay_ms field reports the latency from "tweet created on Twitter" to "tweet left our server." Typically sub-second.
Coverage note. We're actively onboarding eligible accounts into the fast-lane pool. Due to recent high demand, a given 5,000+ follower account you're monitoring may initially arrive as a regular tweet event before migrating to fast_tweet. No tweets are lost — the same data just flows through the standard path during onboarding.
{
"event_type": "fast_tweet",
"timestamp": 1776623420082,
"tweet": {
"id": "2045879341243043889",
"screen_name": "MarioNawfal",
"display_name": "Mario Nawfal",
"user_id": "1319287761048723458",
"text": "Hezbollah fighters and IDF soldiers are...",
"type": "quote",
"created_ms": 1776623419483,
"snowflake_created_ms": 1776623419483,
"received_at_ms": 1776623420054,
"sent_at_ms": 1776623419975,
"media": ["https://pbs.twimg.com/media/HGCSUtAb0AArAuc.jpg"],
"mentions": ["user1", "user2"],
"snow_delay_ms": 571
}
}| Field | Type | Description |
|---|---|---|
| id* | string | Twitter snowflake ID. Use as-is; don't parseInt. |
| screen_name* | string | Author handle, no @ prefix. |
| text* | string | Tweet body. May be empty string but key always present. |
| type* | string | post / reply / repost / quote / thread / like / mention / follow |
| created_ms* | int | Tweet created time (epoch ms, decoded from snowflake). |
| display_name | string | Author display name. May be null. |
| user_id | string | Author user ID. |
| snowflake_created_ms | int | Same as created_ms (fallback). |
| received_at_ms | int | When our backend received the tweet. |
| sent_at_ms | int | Upstream send-out timestamp. |
| media | string[] | Media URLs. May be null or empty array. |
| mentions | string[] | @-mentioned handles. May be null or empty array. |
| snow_delay_ms | int | Twitter post → our server latency (ms). |
Standard tweet — Stream (non-priority) or Rule match
The tweet event covers two scenarios — tell them apart by the presence of rule_id:
- Custom rule match — has
rule_id+rule_tag. Matched tweets are batched into atweets[]array so you can multiplex several rules over one socket. The payload below shows this shape. - Stream non-priority tweet — same
event_type, but withoutrule_id. Fired from your Stream subscription for accounts that don't (yet) qualify for thefast_tweetlane. Same underlying tweet data — see the API reference for the exact shape.
Rule-match payload shape (two-tweet batch collapsed to one for brevity):
{
"event_type": "tweet",
"rule_id": "rule_12345",
"rule_tag": "elon_musk_tweets",
"tweets": [
{
"id": "1234567890",
"text": "This is a tweet matching your filter",
"author": {
"id": "12345",
"username": "username",
"name": "Display Name"
},
"createdAt": "Sat Mar 15 05:31:28 +0000 2025",
"retweetCount": 42,
"likeCount": 420,
"replyCount": 10
}
],
"timestamp": 1642789123456
}| Field | Type | Description |
|---|---|---|
| event_type | string | Always "tweet". |
| rule_id | string | Your filter rule's ID. |
| rule_tag | string | Optional label you set on the rule. |
| tweets | Tweet[] | Batch of matched tweets. Full Twitter-compatible shape. |
| timestamp | int | Server timestamp when the batch was emitted (epoch ms). |
Each item in tweets[] is a full Twitter tweet object — see the API reference for the complete shape (author, engagement counts, entities, etc.).
Sent once, right after handshake, confirming your API key is valid and the stream is open.
{ "event_type": "connected",
"timestamp": 1642789123456 }Application-level heartbeat every ~40s. Ignore or log it — you don't need to respond.
{ "event_type": "ping",
"timestamp": 1642789123456 }Close codes
When the server closes the connection, it sends a standard WebSocket close code. Here's what each one means in our context:
| Code | Label | What it means |
|---|---|---|
| 1000 | Normal Closure | You or the server closed the connection cleanly. |
| 1001 | Going Away | Server restart or client navigated away. |
| 1002 | Protocol Error | Malformed WebSocket frame. |
| 1003 | Unsupported Data | Non-text frame received. |
| 1006 | Abnormal Closure | Network dropped without a close frame. Retry with backoff. |
| 1008 | Policy Violation | Usually: duplicate connection for the same API key, or expired key. |
| 1011 | Internal Error | Server-side failure; safe to reconnect. |
| 1013 | Try Again Later | Service at capacity. Wait 60s+ before reconnecting. |
Best practices
- Reuse one connection. Keep the WebSocket open for the lifetime of your process — don't tear it down per request.
- Back off on reconnect. On
1006/ network errors, wait 90s before reconnecting. On1013, wait longer (60s+). - Dedupe by
id. Network retries or dual subscriptions can occasionally deliver the same tweet twice; key byidand drop duplicates client-side. - Handle empty fields.
textmay be an empty string,media/mentionsmay be null or[]. Don't assume presence. - Don't
parseInttweet IDs. They're 64-bit snowflake IDs; keep them as strings to avoid precision loss. - Log
snow_delay_ms. It's your best signal for stream health. Sustained spikes >2s suggest an issue upstream — worth paging on.
FAQ
Can I use the WebSocket from the browser?
x-api-key header can't be set on a browser WebSocket directly, so production browser use typically goes through a thin backend proxy.What's the difference between fast_tweet and tweet events?
fast_tweet is the priority lane within the Stream subscription — fires for authors with 5,000+ followers (dropping to 2,000+ soon), one tweet per event, optimized for sub-second delivery. tweet is a shared event type that arrives in two situations: from your Stream subscription for accounts that don't qualify for the fast lane, and from matched custom filter rules (these carry rule_id and rule_tag). Tell the two tweet sources apart by whether rule_id is present.How many tweets per second can I receive?
My second connection got closed immediately. Why?
1008. If a previous connection crashed, wait 90 seconds for the server to release the slot before reconnecting. For parallel pipelines, provision additional API keys.Do WebSocket tweets count against my REST API quota?
Does the server send binary frames?
Ready to connect?
Grab your API key from the dashboard and paste it into the Python example above — you'll be receiving tweets in under a minute.