Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

TypeSentDirection
welcomeOnce after handshakeServer → client
heartbeatEvery 30 sServer → client
announcementWhen an event is detectedServer → client
test_announcementAfter {"type":"test"}Server → client
changelogOn publication, then on connection while validServer → client
renewal_noticeWhen <24 h remain — on connection, or in-sessionServer → client
errorOn test rate-limitServer → client
testRequest a test announcementClient → server

Welcome

{
  "type": "welcome",
  "tier": "premium",
  "maxDistinctIps": 2,
  "maxConnectionsPerIp": 3,
  "absoluteMaxConnections": 20,
  "allowedCex": "*",
  "expiresInSecs": 2592000
}
FieldTypeDescription
typestringAlways "welcome"
tierstringSpeedTrial, FreeDelayed, basic, or premium
maxDistinctIpsintegerMax distinct IPs that can hold connections with this key
maxConnectionsPerIpintegerPer-IP cap — fixed at 3
absoluteMaxConnectionsintegerHard cap across all IPs — fixed at 20
allowedCexstringEffective filter after key restriction × ?cex=. "*" = all
expiresInSecsinteger or nullSeconds 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"
}
FieldTypeDescription
typestringAlways "announcement"
titlestringOriginal exchange title. Replaced by an upgrade notice on SpeedTrial for every type except not_listing.
tickerstringAsset symbol(s). Comma-separated on multi-ticker events. "" on SpeedTrial for every type except not_listing.
publisherstringLowercase exchange name (binance, upbit, bithumb)
listingTypestringOne of Listing types
detectedTimestampUsintegerDetection time, µs since Unix epoch
dispatchTimestampUsintegerDispatch time, µs since Unix epoch
abnormalDetectionLatencybooleantrue = detected under unusual conditions — take precautions before acting. See below.
marketsstringOptional. 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:

ValueMeaning
KRWKRW market only
BTCBTC market only
USDTUSDT market only
KRW,BTCKRW and BTC
KRW,USDTKRW and USDT
BTC,USDTBTC and USDT — no won market
KRW,BTC,USDTall 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

ValueExchange
binanceBinance
upbitUpbit
bithumbBithumb

Listing types

ValueMeaningExchanges
spot_listingNew spot market listingBinance, Upbit, Bithumb
futures_listingNew futures/perpetual listingBinance
spot_delistingSpot market delistingBinance, Upbit, Bithumb
futures_delistingFutures/perpetual delistingBinance
hodler_airdropBinance HODLer AirdropBinance
monitoring_tag_extendToken added to Binance’s Monitoring TagBinance
monitoring_tag_removeToken removed from Binance’s Monitoring TagBinance
caution_releasedCaution designation lifted (유의 종목 지정 해제)Bithumb, Upbit
not_listingOther 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 as not_listing.
  • Mixed extend + remove. When a single Binance announcement both adds and removes tickers, the dispatch emits two separate events with the same title and disjoint ticker lists.

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.

TierAll types except not_listingnot_listingDelivery delay
SpeedTrialticker = "", title = upgrade notice. All other fields unchanged, markets included.Full content+1 ms
FreeDelayedFull contentFull content+240 ms
basicFull contentFull content+20 ms
premiumFull contentFull contentNone

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.
  • ticker is always the single symbol DUMMYTOKEN — never a comma-separated list.
  • markets is 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}
FieldTypeDescription
typestringAlways "error"
codestringCurrently only test_rate_limited
retryAfterSecsintegerSeconds 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"
}
FieldTypeDescription
typestringAlways "heartbeat"
timestampNsintegerServer emission time, ns since Unix epoch
timeUtcstringISO 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 welcome frame, 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
}
FieldTypeDescription
typestringAlways "changelog"
idintegerStable identifier of the entry — deduplicate on this
titlestringThe notice text
versionstringOptional — absent when the notice names no version
dispatchTimestampUsintegerServer 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
}
FieldTypeDescription
typestringAlways "renewal_notice"
titlestringThe notice text, naming your tier
dispatchTimestampUsintegerServer 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.