WebSocket · Real-time · Python

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.

Last updated: April 2026·~6 min read
Endpoint
wss://ws.twitterapi.io/twitter/tweet/websocket
Authentication
x-api-key header
Connections
1 per API key — duplicates rejected

How 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, no rule_id.
See Stream plans

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.

Set up filter rules

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.

URLwss://ws.twitterapi.io/twitter/tweet/websocket
HEADERx-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-client
client.py
import 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.

fast_tweetStream priority lane · one tweet per event · authors with 5,000+ followers

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.

fast_tweet event payload
{
  "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
  }
}
tweet object fields
FieldTypeDescription
id*stringTwitter snowflake ID. Use as-is; don't parseInt.
screen_name*stringAuthor handle, no @ prefix.
text*stringTweet body. May be empty string but key always present.
type*stringpost / reply / repost / quote / thread / like / mention / follow
created_ms*intTweet created time (epoch ms, decoded from snowflake).
display_namestringAuthor display name. May be null.
user_idstringAuthor user ID.
snowflake_created_msintSame as created_ms (fallback).
received_at_msintWhen our backend received the tweet.
sent_at_msintUpstream send-out timestamp.
mediastring[]Media URLs. May be null or empty array.
mentionsstring[]@-mentioned handles. May be null or empty array.
snow_delay_msintTwitter post → our server latency (ms).
tweetStandard lane (Stream or Custom Rules) · full Twitter-shape objects

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 a tweets[] array so you can multiplex several rules over one socket. The payload below shows this shape.
  • Stream non-priority tweet — same event_type, but without rule_id. Fired from your Stream subscription for accounts that don't (yet) qualify for the fast_tweet lane. Same underlying tweet data — see the API reference for the exact shape.

Rule-match payload shape (two-tweet batch collapsed to one for brevity):

tweet event payload
{
  "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
}
top-level fields
FieldTypeDescription
event_typestringAlways "tweet".
rule_idstringYour filter rule's ID.
rule_tagstringOptional label you set on the rule.
tweetsTweet[]Batch of matched tweets. Full Twitter-compatible shape.
timestampintServer 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.).

event_type: "connected"

Sent once, right after handshake, confirming your API key is valid and the stream is open.

{ "event_type": "connected",
  "timestamp": 1642789123456 }
event_type: "ping"

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:

CodeLabelWhat it means
1000Normal ClosureYou or the server closed the connection cleanly.
1001Going AwayServer restart or client navigated away.
1002Protocol ErrorMalformed WebSocket frame.
1003Unsupported DataNon-text frame received.
1006Abnormal ClosureNetwork dropped without a close frame. Retry with backoff.
1008Policy ViolationUsually: duplicate connection for the same API key, or expired key.
1011Internal ErrorServer-side failure; safe to reconnect.
1013Try Again LaterService 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. On 1013, wait longer (60s+).
  • Dedupe by id. Network retries or dual subscriptions can occasionally deliver the same tweet twice; key by id and drop duplicates client-side.
  • Handle empty fields. text may be an empty string, media / mentions may be null or []. Don't assume presence.
  • Don't parseInt tweet 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?

Yes. Any client that speaks WebSocket and can set a custom header works — Python, Node.js, Go, Rust, browser JavaScript. Browser caveat: the 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?

There is no client-side rate limit on either stream. Your throughput is bounded by how active your subscribed accounts or filter rules are. Tweets for an inactive rule cost nothing and deliver nothing.

My second connection got closed immediately. Why?

One API key can hold exactly one active WebSocket. Opening a second returns close code 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?

Pricing follows the same per-tweet model as our REST API — full details on the pricing page. Billing starts when a rule is set active, independent of whether you've connected the WebSocket yet.

Does the server send binary frames?

No. All frames are text (UTF-8 JSON). If you receive a non-text frame, drop it and keep the connection open.

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.