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

Introduction

Real-time WebSocket feed for crypto exchange listing announcements. Connect over WSS, receive structured JSON the moment a listing, delisting, or risk event is detected.

For LLMs, full documentation in a single txt file here.

Supported exchanges

ExchangeStatus
BinanceLive
UpbitLive
BithumbLive

Announcement types

TypeDescriptionExchangesImpact
spot_listingNew spot market listingBinance, Upbit, Bithumb+
spot_delistingSpot market delistingBinance, Upbit, Bithumb
futures_listingNew futures / perpetual listingBinance+
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 liftedBithumb, Upbit+
not_listingOther announcement (maintenance, token swap, etc.)Binancen/a

See Message Reference for full payload schemas.

Features

  • Microsecond timestamps at every stage (detect, dispatch).
  • Pre-parsed ticker and listingType on every message; original title kept for cross-checking.
  • Upbit spot listings name the quote markets they open (markets), and group several tokens into one event.
  • Per-exchange filtering via ?cex=.
  • WebSocket PING every 15 s; libraries handle PONG automatically.

Next steps

PagePurpose
Quick StartConnect in under 5 minutes
AuthenticationAPI key format and usage
WebSocket APIEndpoints, lifecycle, query parameters
Message ReferenceJSON schemas for every message type
Exchange Filtering?cex= and event-type filtering
Rate LimitsConnection, message, and per-key caps
Error HandlingClose codes and reconnection strategy
Code ExamplesPython, Node.js, Go, Rust

Quick Start

Connect, authenticate, and receive announcements in five minutes.

1. Get an API key

Request a key on Telegram: @CLWfeed — or see Pricing for the free SpeedTrial and FreeDelayed keys (no payment required).

Format: dsk_ + 64 hex characters.

dsk_a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890ab

The key is shown once at creation. Store it securely.

2. Connect

EndpointRegionCoverage
wss://cryptolisting.wsAWS TokyoBinance + Upbit + Bithumb
wss://kr.cryptolisting.wsAWS SeoulUpbit only

Pick one endpoint per bot — the closest to your trading region. Same key works on both.

Pass the key as the X-API-Key header:

pip install websocket-client
import json, websocket

URL    = "wss://cryptolisting.ws"
HEADER = ["X-API-Key: dsk_your_key_here"]

def on_message(ws, message):
    data = json.loads(message)
    if data["type"] == "announcement":
        tickers = data["ticker"].split(",")   # one alert can carry several tokens
        print(f"{data['listingType']} | {','.join(tickers)} | {data['publisher']}")
        print(f"  {data['title']}")

websocket.WebSocketApp(URL, header=HEADER, on_message=on_message).run_forever()

3. Receive messages

On connect, the server sends a welcome message with your tier and limits. It may then write two service messages straight away, before anything else: a renewal_notice if your key has less than 24 h left, and any valid changelog entry you have not been given yet. After that comes the stream of announcement and heartbeat (every 30 s) messages, plus a changelog whenever one is published.

Always switch on type and ignore values you don’t know, as the example above does: new types may ship without notice, and an unrecognised message must never reach your trading logic.

{
  "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 covering three tokens at once. Note the comma-separated ticker — this is one alert, not three — and the extra markets field, which Binance and Bithumb events never carry:

{
  "type": "announcement",
  "title": "[거래] 비코(BICO), 비트마트(BMT), 닐(NIL) KRW, USDT 마켓 디지털 자산 추가",
  "ticker": "BICO,BMT,NIL",
  "publisher": "upbit",
  "listingType": "spot_listing",
  "detectedTimestampUs": 1710345000005000,
  "dispatchTimestampUs": 1710345000006000,
  "abnormalDetectionLatency": false,
  "markets": "KRW,USDT"
}

markets is optional and appears on Upbit spot listings only. A missing field means we could not identify the markets — never that the listing has none. See markets.

On the SpeedTrial tier, ticker is "" and title carries an upgrade notice for every event except not_listing. All other fields are unchanged, markets included — it names a quote market, not a token, so it is never redacted. Announcements also carry a +1 ms courtesy delay on SpeedTrial so paying subscribers are served first (heartbeats are never delayed). The FreeDelayed tier is also free but delivers the full title and ticker with a +240 ms delay. See Tier behavior.

Check abnormalDetectionLatency. true means the event was detected under unusual conditions — possibly slower, possibly not. Take precautions before acting on it. See abnormalDetectionLatency.

4. Filter by exchange (optional)

Add the cex query parameter to scope the stream:

wss://cryptolisting.ws?cex=binance
wss://cryptolisting.ws?cex=binance,upbit

Omit it to receive every supported exchange.

Next


CryptoListing.ws is a technical data feed, not financial advice. See Legal.

Authentication

Every connection requires an API key in the X-API-Key header.

Getting a key

Request one on Telegram: @CLWfeed — or see Pricing for the free SpeedTrial and FreeDelayed keys (no payment required). The same channel handles rotation, expiry extensions, and tier changes.

Key format

dsk_<64 hex characters>
  • Prefix: dsk_
  • Body: 64 hex characters (32 random bytes)

Passing the key

Set X-API-Key on the WebSocket upgrade request:

X-API-Key: dsk_your_key_here

Query-parameter authentication (?api_key=…) is not supported and the handshake returns 401. This keeps keys out of URLs, browser history, and proxy logs.

Key properties

Set by the administrator at creation:

PropertyDescription
TierSpeedTrial, FreeDelayed, basic, or premium — see Tier behavior
Allowed CEXExchanges this key may subscribe to (* = all)
Max distinct IPsConcurrent IPs allowed for this key
ExpirationOptional — key is rejected after this date

Per-IP and absolute connection caps are fixed (3 / 20). See Rate Limits.

Lifecycle

StateTriggerEffect
ActiveCreatedConnections accepted
ExpiredExpiration date reachedHandshake refused; active sessions closed with 1000 key_expired
RevokedAdministrator revokesHandshake refused; active sessions closed with 1000 key_invalidated

Security

  • All connections use TLS (WSS).
  • Treat the key like a password. Lost or leaked? Ask for a rotation on @CLWfeed. Once a key is revoked it stops authenticating and its live sessions close with 1000 key_invalidated.

WebSocket API

Two endpoints share the same API-key namespace.

Machine-readable spec. Everything on this page is also published as an AsyncAPI 3.0 document: /docs/asyncapi.yaml. Feed it to a code generator, an agent, or npx @asyncapi/cli validate — it defines both endpoints, the X-API-Key scheme, the ?cex= parameter, and all six message types.

Endpoints

URLRegionCoverageUse it for
wss://cryptolisting.wsAWS Tokyo (ap-northeast-1a, apne1-az4)Binance + Upbit + BithumbBots in Tokyo / global
wss://kr.cryptolisting.wsAWS Seoul (ap-northeast-2c, apne2-az3)Upbit onlyKorea-based bots trading Upbit

Pick one per bot — the closest to your trading region. The Seoul endpoint removes the Seoul → Tokyo network hop on Upbit detection. Bithumb is currently dispatched only from the Tokyo endpoint. Same key authenticates on both, but rate limits and connection caps are tracked independently.

Frames are binary; payloads are UTF-8 JSON.

Query parameters

ParameterRequiredDescriptionExample
cexNoComma-separated list of exchangesbinance,upbit

See Exchange Filtering.

Lifecycle

Client                              Server
  |                                   |
  |--- WSS handshake + API key -----→ |
  |←---- 101 Switching Protocols -----|
  |                                   |
  |←---- welcome --------------------|  (immediate)
  |←---- renewal_notice -------------|  (if <24 h left; also mid-session
  |                                   |   when the threshold is crossed)
  |←---- changelog ------------------|  (any valid entry not yet given to you)
  |←---- PING (control frame) -------|  (every 15 s)
  |--- PONG ------------------------→ |  (handled by your WS lib)
  |←---- heartbeat (JSON) -----------|  (every 30 s)
  |←---- announcement ---------------|  (when detected)
  |--- {"type":"test"} -------------→ |  (optional)
  |←---- test_announcement ---------|  (only to you)
  |←---- close (1000, key_expired) -|  (if key expires)

Handshake

If the key fails validation, the server rejects the upgrade with an HTTP error:

CodeCause
426Malformed upgrade request (missing/invalid Sec-WebSocket-Key or Upgrade)
401Missing X-API-Key header
403Invalid, revoked, or expired key
429Rate, per-IP, distinct-IP, or absolute-cap limit hit (or cooldown active)

See Error Handling for retry strategy.

Welcome

Sent once after a successful handshake. Confirms tier and limits. See welcome.

Keep-alive

The server sends a WebSocket PING control frame every 15 s (with 0–5 s jitter on the first). Your library responds with PONG automatically — Python websockets, Node.js ws, Go gorilla/websocket, Rust tokio-tungstenite all handle it without configuration.

If the server does not receive a PONG within 30 s of the last ping, it closes the connection.

Don’t add an “no-message-for-N-seconds → reconnect” watchdog at the application layer. Listings are sparse; you’ll reconnect in a loop during quiet periods. Use your library’s close/error callbacks instead.

Heartbeat

A JSON heartbeat message every 30 s, every tier. Lets your client confirm the connection is alive without waiting for a rare announcement. See heartbeat.

Announcements

When an event is detected, every subscriber whose filter matches receives an announcement. See announcement.

The envelope is identical on all three publishers — only title carries the exchange’s own language. A real Upbit KRW listing, as delivered on wss://kr.cryptolisting.ws?cex=upbit:

{
  "type": "announcement",
  "title": "플루언트(BLEND) 신규 거래지원 안내 (KRW, BTC, USDT 마켓)",
  "ticker": "BLEND",
  "publisher": "upbit",
  "listingType": "spot_listing",
  "detectedTimestampUs": 1710345000005000,
  "dispatchTimestampUs": 1710345000006000,
  "abnormalDetectionLatency": false,
  "markets": "KRW,BTC,USDT"
}

ticker is pre-extracted from the Korean title, so a Binance-shaped handler works on Upbit unchanged. Same for the two other Upbit event classes — 거래지원 종료 안내spot_delisting, 유의 종목 지정 해제 안내caution_released. See Exchange Filtering.

Every announcement also carries abnormalDetectionLatency. When true, the event was detected under unusual conditions — take precautions before acting on it.

Disconnection

The server may close the connection with a close frame. The reason field tells you why — see Error Handling.

Client-to-server messages

The only supported client message is the test request:

{"type":"test"}

Returns a fake test_announcement. Limits:

LimitValueEffect on excess
Message rate3 / minuteConnection closed (rate_limit_exceeded)
Frame size1 KBConnection closed (frame_too_large)
Test rate1 / minutetest_rate_limited error response

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.

Exchange Filtering

Two filter layers narrow the stream you receive. The effective scope is their intersection.

1. Key restriction

Set by the administrator at key creation. Your effective allow-list appears in the welcome message:

{ "type": "welcome", "allowedCex": "binance,upbit", ... }

"*" means all exchanges.

2. ?cex= query parameter

Pass on the WebSocket URL:

URLReceives
wss://cryptolisting.wsAll exchanges
wss://cryptolisting.ws?cex=binanceBinance only
wss://cryptolisting.ws?cex=binance,upbitBinance + Upbit
wss://cryptolisting.ws?cex=upbitUpbit only
wss://kr.cryptolisting.ws?cex=upbitUpbit only, from the Seoul endpoint

Upbit only

Two ways to receive nothing but Upbit:

wss://cryptolisting.ws?cex=upbit      # Tokyo endpoint, Upbit-filtered
wss://kr.cryptolisting.ws?cex=upbit   # Seoul endpoint — Upbit is all it carries

The Seoul endpoint only ever carries Upbit, so ?cex=upbit there is a no-op you can keep for clarity. Prefer Seoul if your bot runs in Korea, Tokyo if it also needs Binance or Bithumb — see WebSocket API.

Upbit titles are Korean. The schema is identical to every other publisher — only title changes language:

{
  "type": "announcement",
  "title": "플루언트(BLEND) 신규 거래지원 안내 (KRW, BTC, USDT 마켓)",
  "ticker": "BLEND",
  "publisher": "upbit",
  "listingType": "spot_listing",
  "detectedTimestampUs": 1710345000005000,
  "dispatchTimestampUs": 1710345000006000,
  "abnormalDetectionLatency": false,
  "markets": "KRW,BTC,USDT"
}

ticker is pre-extracted, so you never have to parse Korean yourself. Branch on listingType and publisher, not on title:

if msg["publisher"] == "upbit" and msg["listingType"] == "spot_listing":
    for ticker in msg["ticker"].split(","):   # already extracted from the Korean title
        snipe(ticker)

The split(",") is not defensive padding. Upbit routinely opens several assets in a single notice, and that notice arrives as one event listing every symbol — see ticker. Passing msg["ticker"] straight to an order would send "CYS,ICNT,XAN,EDEN,AIOZ,ALLO" as a symbol.

Effective filter

Key allowsYou requestYou receive
*binanceBinance
*noneAll
binance,upbitbinanceBinance
binance,upbitupbitUpbit
binance,upbitnoneBinance + Upbit
binanceupbitnothing (no overlap)

If the intersection is empty, the connection still succeeds and stays alive (PING and heartbeat keep flowing) but you receive no announcement messages. Verify your allowedCex against your ?cex= value.

Filtering by event type

?cex= narrows by exchange. To narrow by event class, branch on listingType in your handler:

def on_message(ws, raw):
    msg = json.loads(raw)
    if msg["type"] != "announcement":
        return

    lt = msg["listingType"]

    if lt == "spot_listing":
        snipe(msg["publisher"], msg["ticker"])

    elif lt in ("spot_delisting", "futures_delisting"):
        unwind(msg["publisher"], msg["ticker"])

    elif lt in ("monitoring_tag_extend", "monitoring_tag_remove", "caution_released"):
        log_risk_signal(msg["publisher"], msg["ticker"], lt)

The schema is identical across exchanges, so a single match / switch / if-elif chain covers every type. See Listing types for the full list.

Rate Limits & Security

Limits are tracked per endpoint. A key with a 3-connection-per-IP cap can hold 3 connections on wss://cryptolisting.ws and 3 on wss://kr.cryptolisting.ws simultaneously (6 total). Cooldowns also apply per endpoint.

Connection limits

Per IP (any key)

LimitValue
Concurrent connections20
New connections10 / minute

Per API key

LimitValue
Connections per IP3
Distinct IPs1 (default)
Absolute connections (all IPs)20
Connection cooldown5 s per IP

Distinct IPs defaults to 1. The administrator can raise it at key creation.

429 error codes (handshake)

The server returns HTTP 429 with a JSON body whose error field tells you which limit you tripped:

ErrorCauseAction
connection_rate_limit_exceededYour IP opened > 10 connections in 60 s (any key)Back off; retry after the window
per_ip_concurrent_limit_reachedYour IP holds 20 concurrent connections (any key)Close unused connections
per_ip_connection_limit_reachedThis IP holds 3 connections for this specific keyClose one before opening another
max_distinct_ips_reachedKey already in use from its max distinct IPsRequest a higher cap
absolute_connection_cap_reachedKey hit the 20-total ceilingCap is fixed — use a second key
connection_cooldownSame key + IP reconnecting within 5 sWait retry_after_s (5)

Client message limits

LimitValueOn excess
Message rate3 / minuteConnection closed (rate_limit_exceeded)
Frame size1 KBConnection closed (frame_too_large)
Test rate1 / minuteError response (test_rate_limited)

The test rate is shared across all connections sharing a key. Excess returns an error message, not a disconnect — see Test Announcement.

Security

  • All connections use TLS (WSS).
  • Keys can be revoked instantly. Active sessions close with 1000 key_invalidated.
  • Keys with an expiration date are rejected after expiry. Active sessions close with 1000 key_expired.

Error Handling & Reconnection

Handshake errors

HTTP codeCauseAction
426Malformed upgrade (missing/invalid Sec-WebSocket-Key)Fix the WebSocket client
401Missing X-API-Key headerAdd the header
403Invalid, revoked, or expired keyRequest a new key
429Rate or connection limit hitBack off, see Rate Limits

Close codes

CodeReasonMeaningReconnect?
1000key_expiredKey expiration date passedNo — get a new key
1000key_invalidatedAdministrator revoked the keyNo — get a new key
1008too_slowClient lagged > 10 messages behindYes — process faster
1008rate_limit_exceededSent > 3 messages / minute to the serverYes — stop sending
1009frame_too_largeSent a frame larger than 1 KBYes — fix the client

Reconnection strategy

Exponential backoff, capped at 5 minutes:

attempt 1 → 1 s
attempt 2 → 2 s
attempt 3 → 4 s
attempt 4 → 8 s
…           cap at 300 s

Reset the counter after a successful connection.

Decision logic:

TriggerAction
Close key_expired / key_invalidatedStop
Close rate_limit_exceededWait 60 s, stop sending client messages, reconnect
Close too_slowReconnect immediately, process faster
HTTP 429Back off (you hit a connection limit)
Any other disconnectReconnect with exponential backoff

Detecting dead connections

The server sends a WebSocket PING every 15 s and closes the connection if no PONG arrives within 30 s. Your library handles PONG automatically and surfaces the disconnect as a close event.

import asyncio, json, websockets

async def stream(ws):
    try:
        async for raw in ws:
            data = json.loads(raw)
            if data["type"] == "announcement":
                handle(data)
    except websockets.ConnectionClosed as e:
        print(f"closed code={e.code} reason={e.reason!r}")

Don’t wrap recv() in a timeout shorter than ~60 s as a liveness check. Listings are sparse — you’ll time out and reconnect during quiet periods. Trust the library’s PING/PONG, or watch for the 30 s heartbeat.

Code Examples

Production-ready clients with reconnection, latency tracking, and a test-announcement smoke check on connect.

Keep-alive (PING/PONG) is handled by each WebSocket library — no application-level watchdog needed.

pip install "websockets>=14"
import asyncio
import json
import time
import websockets  # websockets>=14

API_KEY = "dsk_your_key_here"
WS_URL  = "wss://cryptolisting.ws"
# Filter exchanges (optional):
# WS_URL = "wss://cryptolisting.ws?cex=binance,upbit"

MAX_RETRIES = 20


def on_announcement(msg: dict):
    now_us = int(time.time() * 1_000_000)
    network_ms = (now_us - msg["dispatchTimestampUs"]) / 1000
    print(f"[{msg['listingType']}] {msg['ticker']} on {msg['publisher']}")
    print(f"  {msg['title']}")
    print(f"  network={network_ms:.2f}ms")
    if msg.get("abnormalDetectionLatency"):
        print("  ⚠ abnormal detection latency — proceed with caution")


async def run():
    headers = {"X-API-Key": API_KEY}

    for attempt in range(MAX_RETRIES):
        try:
            # websockets>=14 — the parameter is `additional_headers`.
            # (`extra_headers` was removed in v14; on older versions use that name.)
            async with websockets.connect(WS_URL, additional_headers=headers) as ws:
                print("connected")

                async for raw in ws:
                    msg = json.loads(raw)

                    if msg["type"] == "welcome":
                        print(f"welcome: tier={msg['tier']} cex={msg['allowedCex']}")
                        await asyncio.sleep(15)
                        await ws.send(json.dumps({"type": "test"}))

                    elif msg["type"] in ("announcement", "test_announcement"):
                        if msg["type"] == "test_announcement":
                            print("[TEST] ", end="")
                        on_announcement(msg)

        except websockets.ConnectionClosed as e:
            reason = e.rcvd.reason if e.rcvd else ""
            if reason in ("key_expired", "key_invalidated"):
                print(f"key invalid: {reason}"); return
            print(f"closed: {reason}")

        except Exception as e:
            print(f"error: {e}")

        backoff = min(2 ** attempt, 300)
        print(f"reconnecting in {backoff}s")
        await asyncio.sleep(backoff)


if __name__ == "__main__":
    asyncio.run(run())

Changelog

Notable changes to the WebSocket feed, its message schema, and its tiers. Newest first.

Dates are the day the change landed in the service. Only user-visible changes are listed — if a change would alter what your client receives or how you connect, it belongs here.

The machine-readable schema is /docs/asyncapi.yaml.


2026-08-22

Upbit multi-token alerts, new markets field, faster dispatch

  • Upbit announcements covering several tokens now arrive as a single alert, with every token listed comma-separated in ticker.
  • New optional markets field on Upbit spot listings: which markets the listing opens — KRW, BTC, USDT, or a combination, always in that order. If it is absent we could not identify them; it never means the listing has none. See Announcement.
  • Upbit announcements are ~1.3 ms faster from detection to dispatch. Message content and timestamps unchanged.
  • If your client rejects unknown JSON fields, it needs a one-line change. Most decoders ignore them by default. The notable exception is Jackson 2.x in Java, where FAIL_ON_UNKNOWN_PROPERTIES is enabled out of the box — add @JsonIgnoreProperties(ignoreUnknown = true) to your message class, or disable the feature on your ObjectMapper. Also check for opt-in strict modes you may have enabled yourself: Go’s Decoder.DisallowUnknownFields(), serde’s deny_unknown_fields, zod’s .strict(), or a validator configured with additionalProperties: false. Our published schema has always set additionalProperties: true — see /docs/asyncapi.yaml.
  • Delivered on every tier, SpeedTrial included, with the same value as on the paid tiers — it names a quote market, not a token, so it is not redacted alongside title and ticker.

2026-08-02

Two new message types: changelog and renewal_notice

  • changelog — a new message type for service announcements (schema changes, maintenance windows). Delivered to every subscriber on every tier, SpeedTrial included, with no redaction and none of the per-tier delivery delay. An entry is valid for a window counted in days: you receive it immediately if connected when it is published, otherwise on your next connection while it is still valid — once either way. It carries id, title, an optional version, and dispatchTimestampUs. Deduplicate on id: the same entry can legitimately arrive twice if you connect to both endpoints, or across a service restart. See Changelog.
  • renewal_notice — sent when less than 24 h remain on your API key: right after the welcome frame if you connect inside that window, and also written to an already-open session at the moment the threshold is crossed, so a long-lived connection is told without having to reconnect. It carries title and dispatchTimestampUs, and no ticker. See Renewal notice.
  • Neither type carries ticker, publisher or listingType. Switch on type and ignore values you don’t know — never let an unrecognised message reach your trading logic.

SpeedTrial announcements carry a +1 ms courtesy delay

  • Announcements delivered on the SpeedTrial tier are now sent ~1 ms after paid tiers, so paying subscribers are served first. Everything else is unchanged: same dispatch pipeline, same detectedTimestampUs / dispatchTimestampUs precision, same redaction of ticker and title on listing-type events. The Delivery delay column in Tier behavior now reads +1 ms instead of None.
  • Heartbeats are not affected on any tier — only announcements are delayed.
  • The upgrade notice carried in title on SpeedTrial now states the delay and the reason for it, so the behaviour is visible in the message itself and not only in this changelog.
  • No action required. Benchmarks run on SpeedTrial should account for this ~1 ms when comparing against a paid tier; the timestamps in each message remain exact, so detection-to-dispatch stays directly measurable.
  • premium is unchanged and still has no added delay.

2026-07-13

Free-tier keys are 1-week and renewable

  • SpeedTrial and FreeDelayed keys are issued for 1 week, renewable on request. They were previously advertised as free for an unlimited period. Nothing changed in what the two tiers deliver — only how long a key stays valid before renewal. Ask on @CLWfeed to renew. Time left on a key is published in welcome.expiresInSecs.
  • Documented the default distinct-IP allowance per key: 1 (was described as “configurable”). The per-IP cap (3) and absolute cap (20) are unchanged. See Rate Limits.

2026-06-18

not_listing is Binance-only

  • Upbit was removed from the not_listing row of every event-type table. Upbit announcements are delivered as spot_listing, spot_delisting, or caution_released — never not_listing. Documentation fix; no dispatch behaviour changed. See Listing types.

2026-06-17

Announcement channel is @CLWfeed

  • The Telegram channel for keys, support, and breaking-change notices is @CLWfeed.

2026-06-16

Seoul endpoint is Upbit-only

  • Documented that wss://kr.cryptolisting.ws carries Upbit only. Bithumb is dispatched from the Tokyo endpoint (wss://cryptolisting.ws), as is Binance. If your bot needs Bithumb or Binance, connect to Tokyo. See WebSocket API.

2026-06-12

Upbit stream narrowed to actionable signals

  • On both endpoints, the Upbit stream carries only the events that drive trades. Upbit notices that are not a listing, delisting, or caution release are no longer broadcast to subscribers. See Listing types and Upbit only.

2026-06-09

Per-IP connection cap lowered to 3

  • The per-key, per-IP concurrent-connection cap went from 5 to 3. The absolute cap across all IPs (20) is unchanged. welcome.maxConnectionsPerIp reports the live value — read it rather than hard-coding. See Rate Limits.

2026-06-06

SpeedTrial and FreeDelayed tiers

  • The free tier is now SpeedTrial: same dispatch path as premium with zero added delay, but ticker is "" and title carries an upgrade notice on every event except not_listing. All other fields — publisher, listingType, microsecond timestamps — stay fully accurate, so detection speed remains independently verifiable.
  • New FreeDelayed tier: the full feed, title and ticker included, free, with a +240 ms delivery delay on announcements. Heartbeats are unaffected.
  • welcome.tier now returns SpeedTrial, FreeDelayed, basic, or premium. Clients that matched on the old free string must be updated. See Tier behavior.

2026-05-03

Caution lifecycle narrowed to two stages

  • Upbit and Bithumb both run a multi-stage caution lifecycle on tokens already trading (pre-warning, designation, extension, release, delisting). The feed carries the two stages that drive trades: caution_released and spot_delisting. Intermediate stages are kept off the bus.
  • Composite announcements covering several tickers across stages are split into multiple WebSocket events with distinct listingType values. See Listing types.

2026-05-02

Risk events: caution_released and Monitoring Tag

  • New listingType value caution_released (Bithumb, Upbit) — a caution designation being lifted, historically a positive technical signal.
  • New listingType values monitoring_tag_extend and monitoring_tag_remove (Binance) — a token entering or leaving Binance’s Monitoring Tag.
  • Clients validating listingType against a closed list had to widen it. This is the change the forward-compatibility rule exists for: unknown listingType values must be inert, not fatal. See Listing types.

2026-04-30

Seoul endpoint

  • wss://kr.cryptolisting.ws went live in AWS Seoul (ap-northeast-2c, apne2-az3), alongside the existing Tokyo endpoint. It removes the Seoul → Tokyo network hop for Korea-based bots trading Upbit. The same API key authenticates on both endpoints; connection caps are tracked independently per endpoint. Pick one endpoint per bot. See Endpoints.

2026-04-13

abnormalDetectionLatency flag

  • Announcements carry a boolean abnormalDetectionLatency. true means the exchange publish → detection interval was abnormally high for that event. The payload stays valid — treat the flag as a hint that the event may be stale by the time it reaches you. See abnormalDetectionLatency.

2026-04-12

Test announcements use DUMMYTOKEN

  • {"type":"test"} returns a test_announcement with the ticker DUMMYTOKEN. It is identical in shape to a real announcement and always carries the full payload regardless of tier — use it to validate a parser on any tier, SpeedTrial included. See Test Announcement.