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.
New message types may also be added without notice. Switch on type and ignore any value you don’t know — never assume an unrecognised message is an announcement, and never let it reach your trading logic.
| 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 |
changelog | On publication, then on connection while valid | Server → client |
renewal_notice | When <24 h remain — on connection, or in-session | Server → client |
error | On test rate-limit | Server → client |
test | Request a test announcement | Client → server |
Welcome
{
"type": "welcome",
"tier": "premium",
"maxDistinctIps": 2,
"maxConnectionsPerIp": 3,
"absoluteMaxConnections": 20,
"allowedCex": "*",
"expiresInSecs": 2592000
}
| Field | Type | Description |
|---|---|---|
type | string | Always "welcome" |
tier | string | SpeedTrial, FreeDelayed, basic, or premium |
maxDistinctIps | integer | Max distinct IPs that can hold connections with this key |
maxConnectionsPerIp | integer | Per-IP cap — fixed at 3 |
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 a new event is detected.
{
"type": "announcement",
"title": "Binance Will List TOKEN (TOKEN)",
"ticker": "TOKEN",
"publisher": "binance",
"listingType": "spot_listing",
"detectedTimestampUs": 1710345000005000,
"dispatchTimestampUs": 1710345000006000,
"abnormalDetectionLatency": false
}
An Upbit spot listing, which additionally carries markets:
{
"type": "announcement",
"title": "[거래] 토큰(TOKEN) KRW, BTC 마켓 디지털 자산 추가",
"ticker": "TOKEN",
"publisher": "upbit",
"listingType": "spot_listing",
"detectedTimestampUs": 1710345000005000,
"dispatchTimestampUs": 1710345000006000,
"abnormalDetectionLatency": false,
"markets": "KRW,BTC"
}
| Field | Type | Description |
|---|---|---|
type | string | Always "announcement" |
title | string | Original exchange title. Replaced by an upgrade notice on SpeedTrial for every type except not_listing. |
ticker | string | Asset symbol(s). Comma-separated on multi-ticker events. "" on SpeedTrial 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 = detected under unusual conditions — take precautions before acting. See below. |
markets | string | Optional. Upbit spot listings only: the quote markets the listing opens, comma-separated. See below. |
markets
Present only on announcement messages where publisher is upbit and listingType
is spot_listing. Omitted entirely — not null, not "" — everywhere else.
Upbit lists an asset on one or more of its three quote markets: KRW (the Korean won),
BTC and USDT. A listing that opens the KRW market is a different event from one that
merely adds a BTC or USDT pair, and the exchange’s headline says which. This field carries
that distinction, as a comma-separated list in the fixed order KRW, BTC, USDT:
| Value | Meaning |
|---|---|
KRW | KRW market only |
BTC | BTC market only |
USDT | USDT market only |
KRW,BTC | KRW and BTC |
KRW,USDT | KRW and USDT |
BTC,USDT | BTC and USDT — no won market |
KRW,BTC,USDT | all three |
A missing markets means “we don’t know”, never “no market”. The field is derived from
the exchange headline. It is omitted when the announcement is not an Upbit spot listing, and
also when we read the headline and recognised no market in it — an unfamiliar wording, for
instance. We deliberately never send an empty value, which would read as a factual claim we
cannot make.
Never infer “this listing has no KRW market” from a missing field. publisher,
listingType and ticker remain the authoritative fields; treat markets as advisory and
do not gate your handling of an announcement on its presence.
ticker may contain several symbols. Always split on ,:
tickers = data["ticker"].split(",") # ["ABC", "DEF", "GHI"]
Upbit groups tokens. A notice that opens several assets arrives as one event carrying every
symbol — never one event per token — and the markets in the title apply to the whole group:
BTC, USDT 마켓 신규 거래지원 안내 (CYS, ICNT, XAN, EDEN, AIOZ, ALLO) yields
ticker: "CYS,ICNT,XAN,EDEN,AIOZ,ALLO" with a single markets value of "BTC,USDT".
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.
abnormalDetectionLatency
false on the overwhelming majority of events. true means the event was detected under conditions that differ from normal pipeline behavior — possibly slower, possibly not. The payload is always valid and always delivered; the usual timing assumptions just don’t hold for that message.
When true, take precautions: handle the event conservatively in automated pipelines, treat detectedTimestampUs as lower-confidence, and exclude the event from your latency statistics.
markets
Upbit lists an asset on one or more of its three quote markets — KRW, BTC, USDT. A listing that opens the KRW market is a different event from one that opens only BTC or USDT, and markets carries that distinction.
{
"type": "announcement",
"title": "플루언트(BLEND) 신규 거래지원 안내 (KRW, BTC, USDT 마켓)",
"ticker": "BLEND",
"publisher": "upbit",
"listingType": "spot_listing",
"detectedTimestampUs": 1710345000005000,
"dispatchTimestampUs": 1710345000006000,
"abnormalDetectionLatency": false,
"markets": "KRW,BTC,USDT"
}
The value is a comma-separated list in the fixed order KRW, BTC, USDT — seven possible values:
"KRW" · "BTC" · "USDT" · "KRW,BTC" · "KRW,USDT" · "BTC,USDT" · "KRW,BTC,USDT"
That order is a contract. It follows neither the order the markets appear in the announcement title nor alphabetical sorting, so you can compare the string directly.
Where it appears. Only on publisher: "upbit" with listingType: "spot_listing". On every other event the key is absent from the JSON entirely — not null, not "".
It is delivered on every tier, SpeedTrial included: it does not identify the token, so it is not redacted alongside title and ticker.
A missing markets means “we don’t know”, never “no market.” It is derived from the announcement title, so an unusual wording yields no field rather than a wrong one. Never infer “this listing has no KRW market” from its absence — publisher, listingType and ticker remain the authoritative fields, and markets is advisory.
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 |
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 |
|---|---|---|---|
SpeedTrial | ticker = "", title = upgrade notice. All other fields unchanged, markets included. | Full content | +1 ms |
FreeDelayed | Full content | Full content | +240 ms |
basic | Full content | Full content | +20 ms |
premium | Full content | Full content | None |
The SpeedTrial tier is default-deny: any new event type is automatically redacted on SpeedTrial without a schema migration. That rule covers event types. A new field is judged on its own merits: markets is delivered on every tier, because it names a quote market and not a token. SpeedTrial clients must accept ticker = "" and the upgrade-notice title gracefully. SpeedTrial also carries a +1 ms courtesy delay on announcements so paying subscribers are served first; heartbeats are never delayed. FreeDelayed is also free but delivers the full title and ticker with a +240 ms delay.
Test announcements always carry the full payload regardless of tier — with two exceptions, both of which matter if you use the test message as a fixture. See Test Announcement.
Measuring latency
All timestamps are microseconds since Unix epoch. Filter out events where abnormalDetectionLatency is true before computing averages — they are outliers by definition.
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, close 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
SpeedTrial. tickeris always the single symbolDUMMYTOKEN— never a comma-separated list.marketsis never present, even when the generated title is an Upbit spot listing naming its markets. The generator emits the eight fields above and nothing else.- Rate limit: 1 per minute per API key, shared across connections.
Do not validate your parser on the test message alone. It exercises neither the comma-split
of ticker nor the reading of markets — the two paths most likely to break on a
real Upbit event. Validate those against the JSON examples in
Announcement instead.
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 |
Changelog
A service announcement — a new field, a schema change, a maintenance window. Delivered to every subscriber on every tier, SpeedTrial included: no redaction, and none of the per-tier delivery delay described in Tier behavior.
It is not a market event and carries no ticker, no publisher and no listingType — nothing here should ever reach your trading logic.
An entry stays valid for a published window, counted in days. You receive it once, whichever comes first:
- immediately, if you are connected when it is published;
- otherwise on your next connection, right after the
welcomeframe, for as long as the entry is inside its window.
A client that was offline still gets it; a client that reconnects repeatedly does not get it again.
The written history of these changes lives on the Changelog page; a changelog message is the push notification for it, not a replacement.
{
"type": "changelog",
"id": 12,
"title": "New optional field on announcement payloads",
"version": "2.4.0",
"dispatchTimestampUs": 1712000000123456
}
| Field | Type | Description |
|---|---|---|
type | string | Always "changelog" |
id | integer | Stable identifier of the entry — deduplicate on this |
title | string | The notice text |
version | string | Optional — absent when the notice names no version |
dispatchTimestampUs | integer | Server emission time, µs since Unix epoch |
Always deduplicate on id. It is stable for the life of the entry and does not change when the text is corrected. Two situations legitimately deliver the same entry twice: being connected to both endpoints, or a service restart between two of your connections. Treating id as seen-once makes both harmless.
If an entry is edited, later connections receive the corrected text under the same id, and nobody is notified a second time.
Renewal notice
Sent when you connect with less than 24 h left on your API key, right after the welcome frame. Free tiers included.
If your session is already open when the 24 h threshold is crossed, the notice is written to it at that moment — you do not need to reconnect to be told.
It is delivered on every connection made inside the window, so a client that reconnects often receives it more than once. Treat it as a reminder, not as a one-shot event.
It ignores your ?cex= filter: it carries no publisher, so a connection subscribed to a single exchange still receives it.
{
"type": "renewal_notice",
"title": "Renewal notice: Your SpeedTrial tier API key will expire in 24 hours. Please contact us on telegram for renewal.",
"dispatchTimestampUs": 1712000000123456
}
| Field | Type | Description |
|---|---|---|
type | string | Always "renewal_notice" |
title | string | The notice text, naming your tier |
dispatchTimestampUs | integer | Server emission time, µs since Unix epoch |
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.