Message Reference
All messages are binary WebSocket frames containing UTF-8 JSON. Every message has a type field.
Forward compatibility. New fields may be added at any time without notice. Your parser must ignore unknown fields. Don’t use strict schema validation.
| Type | Sent | Direction |
|---|---|---|
welcome | Once after handshake | Server → client |
heartbeat | Every 30 s | Server → client |
announcement | When an event is detected | Server → client |
test_announcement | After {"type":"test"} | Server → client |
error | On test rate-limit | Server → client |
test | Request a test announcement | Client → server |
Welcome
{
"type": "welcome",
"tier": "premium",
"maxDistinctIps": 2,
"maxConnectionsPerIp": 5,
"absoluteMaxConnections": 20,
"allowedCex": "*",
"expiresInSecs": 2592000
}
| Field | Type | Description |
|---|---|---|
type | string | Always "welcome" |
tier | string | free, basic, premium, or enterprise |
maxDistinctIps | integer | Max distinct IPs that can hold connections with this key |
maxConnectionsPerIp | integer | Per-IP cap — fixed at 5 |
absoluteMaxConnections | integer | Hard cap across all IPs — fixed at 20 |
allowedCex | string | Effective filter after key restriction × ?cex=. "*" = all |
expiresInSecs | integer or null | Seconds until the key expires; null = no expiration |
Announcement
Sent when the detection engine captures a new event.
{
"type": "announcement",
"title": "Binance Will List TOKEN (TOKEN)",
"ticker": "TOKEN",
"publisher": "binance",
"listingType": "spot_listing",
"detectedTimestampUs": 1710345000005000,
"dispatchTimestampUs": 1710345000006000,
"abnormalDetectionLatency": false
}
| Field | Type | Description |
|---|---|---|
type | string | Always "announcement" |
title | string | Original exchange title. Replaced by an upgrade notice on free for every type except not_listing. |
ticker | string | Asset symbol(s). Comma-separated on multi-ticker events. "" on free for every type except not_listing. |
publisher | string | Lowercase exchange name (binance, upbit, bithumb) |
listingType | string | One of Listing types |
detectedTimestampUs | integer | Detection time, µs since Unix epoch |
dispatchTimestampUs | integer | Dispatch time, µs since Unix epoch |
abnormalDetectionLatency | boolean | true if publish→detect was abnormally high; payload still valid |
ticker may contain several symbols. Always split on ,:
tickers = data["ticker"].split(",") # ["ABC", "DEF", "GHI"]
Run your own parser as a fallback. ticker is pre-extracted from title for speed. Cross-check it against title in production code so a single parser bug doesn’t take you down. title is always the original, unmodified exchange title.
Publishers
Listing types
| Value | Meaning | Exchanges |
|---|---|---|
spot_listing | New spot market listing | Binance, Upbit, Bithumb |
futures_listing | New futures/perpetual listing | Binance |
spot_delisting | Spot market delisting | Binance, Upbit, Bithumb |
futures_delisting | Futures/perpetual delisting | Binance |
hodler_airdrop | Binance HODLer Airdrop | Binance |
monitoring_tag_extend | Token added to Binance’s Monitoring Tag | Binance |
monitoring_tag_remove | Token removed from Binance’s Monitoring Tag | Binance |
caution_released | Caution designation lifted (유의 종목 지정 해제) | Bithumb, Upbit |
not_listing | Other announcement (maintenance, token swap, etc.) | Binance, Upbit |
Monitoring Tag — edge cases
- Mixed Seed Tag bundles. A
monitoring_tag_*event lists Monitoring Tag tickers only. Tickers in the same announcement that belong to the Seed Tag program are excluded. Seed-Tag-only announcements arrive asnot_listing. - Mixed extend + remove. When a single Binance announcement both adds and removes tickers, the dispatch emits two separate events with the same
titleand disjointtickerlists.
Caution lifecycle (Bithumb / Upbit)
Korean exchanges run a multi-stage risk track on tokens already trading (pre-warning, formal designation, extension, release, delisting). We narrow the broadcast to the two stages that drive trading decisions:
caution_released— the caution designation is lifted (유의 종목 지정 해제). Often a positive technical signal.spot_delisting— final removal (거래지원 종료).
The intermediate stages (유의촉구 / 거래유의·투자유의종목 지정 / 지정 연장) and risk-context deposit halts are detected but intentionally not forwarded — they generate noise without clear actionable signal on the trading side.
Composite announcements. A single title that pairs a release with a delisting (e.g. 신세틱스(SNX) 거래유의종목 지정 해제 및 (BCD, WTC) 거래지원 종료) is split into one event per (listingType, ticker-group). Multi-ticker, single-stage titles (e.g. (BCD, WTC) 거래지원 종료) are joined comma-separated like listings.
Example: caution_released
{
"type": "announcement",
"title": "신세틱스(SNX) 거래유의종목 지정 해제",
"ticker": "SNX",
"publisher": "bithumb",
"listingType": "caution_released",
"detectedTimestampUs": 1745971200834000,
"dispatchTimestampUs": 1745971200842117,
"abnormalDetectionLatency": false
}
Example: spot_delisting
{
"type": "announcement",
"title": "고트세우스 막시무스(GOAT) 거래지원 종료",
"ticker": "GOAT",
"publisher": "bithumb",
"listingType": "spot_delisting",
"detectedTimestampUs": 1745971200834000,
"dispatchTimestampUs": 1745971200842117,
"abnormalDetectionLatency": false
}
Tier behavior
Tier is returned in welcome.tier. Pricing details on the pricing page.
| Tier | All types except not_listing | not_listing | Delivery delay |
|---|---|---|---|
free | ticker = "", title = upgrade notice. Other fields unchanged. | Full content | None |
basic | Full content | Full content | +20 ms |
premium | Full content | Full content | None |
enterprise | Full content | Full content | None |
The free tier is default-deny: any new event type is automatically redacted on free without a schema migration. Free clients must accept ticker = "" and the upgrade-notice title gracefully.
Test announcements always carry the full payload regardless of tier.
Measuring latency
All timestamps are microseconds since Unix epoch.
Dispatch delay = dispatchTimestampUs − detectedTimestampUs
Network delay = your_receive_us − dispatchTimestampUs
Total = your_receive_us − detectedTimestampUs
import time
def on_announcement(msg):
now_us = int(time.time() * 1_000_000)
dispatch_ms = (msg["dispatchTimestampUs"] - msg["detectedTimestampUs"]) / 1000
network_ms = (now_us - msg["dispatchTimestampUs"]) / 1000
total_ms = (now_us - msg["detectedTimestampUs"]) / 1000
print(f"dispatch={dispatch_ms:.2f}ms network={network_ms:.2f}ms total={total_ms:.2f}ms")
Test Announcement
Send {"type":"test"}. The server replies with a fake announcement, identical in shape to a real one but with type = "test_announcement" and ticker = "DUMMYTOKEN".
{
"type": "test_announcement",
"title": "Binance Will List DUMMYTOKEN (DUMMYTOKEN)",
"ticker": "DUMMYTOKEN",
"publisher": "binance",
"listingType": "spot_listing",
"detectedTimestampUs": 1743850001999800,
"dispatchTimestampUs": 1743850002000000,
"abnormalDetectionLatency": false
}
- Sent only to the requester, not broadcast.
- Always full payload, including on
free. - Rate limit: 1 per minute per API key, shared across connections.
On excess, the server returns an error instead:
{"type": "error", "code": "test_rate_limited", "retryAfterSecs": 42}
| Field | Type | Description |
|---|---|---|
type | string | Always "error" |
code | string | Currently only test_rate_limited |
retryAfterSecs | integer | Seconds until next request allowed |
Heartbeat
Sent every 30 s to every subscriber, every tier.
{
"type": "heartbeat",
"timestampNs": 1710345030123456789,
"timeUtc": "2026-04-17T08:30:30.123456Z"
}
| Field | Type | Description |
|---|---|---|
type | string | Always "heartbeat" |
timestampNs | integer | Server emission time, ns since Unix epoch |
timeUtc | string | ISO 8601 UTC, microsecond precision |
Keep-alive (PING/PONG)
Separate from the application-level heartbeat, the server sends a WebSocket PING control frame (opcode 0x9, empty payload) every 15 s (0–5 s jitter on the first). Your library responds with PONG automatically. If no PONG arrives within 30 s of the last ping, the server closes the TCP connection.
Don’t reconnect on “no announcement for N seconds”. Listings are sparse — you’ll loop. Watch the 30 s heartbeat instead, or rely on your WebSocket library’s close/error callbacks.