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.
No key yet? Binance, Upbit and Bithumb listings, delistings and risk events, and Robinhood events, are also published as a free JSON file and Atom feeds, readable without an API key — see Public JSON & Atom Feeds.
For LLMs, full documentation in a single txt file here.
Supported exchanges
| Exchange | Status |
|---|---|
| Binance | Live |
| Upbit | Live |
| Bithumb | Live |
| Robinhood | Live |
Announcement types
| Type | Description | Exchanges | Impact |
|---|---|---|---|
spot_listing | New spot market listing | Binance, Upbit, Bithumb | + |
spot_delisting | Spot market delisting | Binance, Upbit, Bithumb | − |
futures_listing | New futures / perpetual listing | Binance | + |
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 | + |
wallet_listing | Robinhood Listing: added to Robinhood, not tradable yet on Robinhood (not a promise that trading will open on Robinhood) | Robinhood | + |
cex_listing | Robinhood Live: tradable on Robinhood | Robinhood | + |
not_listing | Other announcement (maintenance, token swap, etc.) | Binance | n/a |
See Message Reference for full payload schemas.
Features
- Microsecond timestamps at every stage (detect, dispatch).
- Pre-parsed
tickerandlistingTypeon every message; originaltitlekept 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
| Page | Purpose |
|---|---|
| Quick Start | Connect in under 5 minutes |
| Authentication | API key format and usage |
| WebSocket API | Endpoints, lifecycle, query parameters |
| Message Reference | JSON schemas for every message type |
| Exchange Filtering | ?cex= and event-type filtering |
| Rate Limits | Connection, message, and per-key caps |
| Error Handling | Close codes and reconnection strategy |
| Code Examples | Python, Node.js, Go, Rust |
| Public JSON & Atom Feeds | Free feeds, no API key: JSON file and Atom |
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
| Endpoint | Region | Coverage |
|---|---|---|
wss://jp.cryptolisting.ws | AWS Tokyo | Binance + Upbit + Bithumb |
wss://kr.cryptolisting.ws | AWS Seoul | Upbit + Bithumb |
wss://us.cryptolisting.ws | AWS N. Virginia | Robinhood |
Pick one endpoint per bot — the closest to your trading region. The same key works on all three.
Pass the key as the X-API-Key header:
pip install websocket-client
import json, websocket
URL = "wss://jp.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://jp.cryptolisting.ws?cex=binance
wss://jp.cryptolisting.ws?cex=binance,upbit
wss://us.cryptolisting.ws # Robinhood — served by the US endpoint only
Omit it to receive every supported exchange.
Next
- Message Reference — every field, every event type
- Error Handling — close codes and reconnection
- Code Examples — production-ready clients
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:
| Property | Description |
|---|---|
| Tier | SpeedTrial, FreeDelayed, basic, or premium — see Tier behavior |
| Allowed CEX | Exchanges this key may subscribe to (* = all) |
| Max distinct IPs | Concurrent IPs allowed for this key |
| Expiration | Optional — key is rejected after this date |
Per-IP and absolute connection caps are fixed (3 / 20). See Rate Limits.
Lifecycle
| State | Trigger | Effect |
|---|---|---|
| Active | Created | Connections accepted |
| Expired | Expiration date reached | Handshake refused; active sessions closed with 1000 key_expired |
| Revoked | Administrator revokes | Handshake 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
Three 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 all three endpoints, the X-API-Key scheme, the
?cex= parameter, and all six message types.
Endpoints
| URL | Region | Coverage | Use it for |
|---|---|---|---|
wss://jp.cryptolisting.ws | AWS Tokyo (ap-northeast-1a, apne1-az4) | Binance + Upbit + Bithumb | Bots in Tokyo / global |
wss://kr.cryptolisting.ws | AWS Seoul (ap-northeast-2c, apne2-az3) | Upbit + Bithumb | Korea-based bots trading Upbit or Bithumb |
wss://us.cryptolisting.ws | AWS N. Virginia (us-east-1a, use1-az1) | Robinhood | Bots trading Robinhood (US) |
Pick one per bot — the closest to your trading region. The Seoul endpoint removes the Seoul → Tokyo network hop on Upbit and Bithumb detection. Binance is dispatched only from the Tokyo endpoint. The same key authenticates on all three, but rate limits and connection caps are tracked independently.
Frames are binary; payloads are UTF-8 JSON.
Query parameters
| Parameter | Required | Description | Example |
|---|---|---|---|
cex | No | Comma-separated list of exchanges | binance,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:
| Code | Cause |
|---|---|
426 | Malformed upgrade request (missing/invalid Sec-WebSocket-Key or Upgrade) |
401 | Missing X-API-Key header |
403 | Invalid, revoked, or expired key |
429 | Rate, 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.
Two different things, don’t mix them up:
- Don’t reconnect on “no announcement for N seconds”. Listings are sparse; you’d loop during quiet periods.
- Do treat total silence — no PING and no
heartbeatfor 60 s — as a dead connection, close it and reconnect. Your library’s close/error callbacks only fire when the peer says goodbye. A server that vanishes without a FIN or RST (host stopped, IP moved, path cut) sends nothing at all, and a client that only reads stays “connected” forever. If your library exposes TCP keepalive or a client-side ping with a pong timeout (Pythonwebsocketsdoes both by default), turn it on. See Detecting dead connections.
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 four 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:
| Limit | Value | Effect on excess |
|---|---|---|
| Message rate | 3 / minute | Connection closed (rate_limit_exceeded) |
| Frame size | 1 KB | Connection closed (frame_too_large) |
| Test rate | 1 / minute | test_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.
| 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 (Robinhood publishes no notice: an English line written by CryptoListing.ws). 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, robinhood) |
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 |
wallet_listing | Robinhood Listing: asset added to Robinhood, not tradable yet on Robinhood. Trading may open minutes, days or weeks later — or never: a wallet_listing is not a promise that trading will open on Robinhood. If the asset becomes tradable on Robinhood, it is reported separately as cex_listing (Robinhood Live). | Robinhood |
cex_listing | Robinhood Live: tradable on Robinhood. Robinhood publishes no notice: the event is the moment the asset becomes tradable on Robinhood. | Robinhood |
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 more than one endpoint, 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’d loop. Do reconnect on no frame at all (no PING, no heartbeat) for 60 s: a server that disappears without closing (host stopped, IP moved) never triggers your library’s close callback, and a socket that only reads stays “connected” forever. See Detecting dead connections.
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:
| URL | Receives |
|---|---|
wss://jp.cryptolisting.ws | Binance + Upbit + Bithumb (everything the Tokyo endpoint carries) |
wss://jp.cryptolisting.ws?cex=binance | Binance only |
wss://jp.cryptolisting.ws?cex=binance,upbit | Binance + Upbit |
wss://jp.cryptolisting.ws?cex=upbit | Upbit only |
wss://kr.cryptolisting.ws | Upbit + Bithumb, from the Seoul endpoint |
wss://kr.cryptolisting.ws?cex=upbit | Upbit only, from the Seoul endpoint |
wss://us.cryptolisting.ws | Robinhood, from the US endpoint (the only exchange it carries) |
wss://us.cryptolisting.ws?cex=robinhood | Same — the filter is accepted but redundant there |
robinhood is served by the US endpoint only: ?cex=robinhood on Tokyo or Seoul yields an empty
intersection (the connection stays alive, nothing is delivered). Its two event types are
wallet_listing (Robinhood Listing: asset added to Robinhood, not tradable yet on Robinhood) and
cex_listing (Robinhood Live: tradable on Robinhood) — see Listing types.
Upbit only
Two ways to receive nothing but Upbit:
wss://jp.cryptolisting.ws?cex=upbit # Tokyo endpoint, Upbit-filtered
wss://kr.cryptolisting.ws?cex=upbit # Seoul endpoint, Upbit-filtered
Since 2026-09-04 the Seoul endpoint carries Bithumb as well, so ?cex=upbit there is a real filter — it used to be a no-op. Prefer Seoul if your bot runs in Korea, Tokyo if it also needs Binance — 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 allows | You request | You receive |
|---|---|---|
* | binance | Binance |
* | none | All |
binance,upbit | binance | Binance |
binance,upbit | upbit | Upbit |
binance,upbit | none | Binance + Upbit |
binance | upbit | nothing (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://jp.cryptolisting.ws, 3 on wss://kr.cryptolisting.ws and 3 on wss://us.cryptolisting.ws simultaneously (9 total). Cooldowns also apply per endpoint.
Connection limits
Per IP (any key)
| Limit | Value |
|---|---|
| Concurrent connections | 20 |
| New connections | 10 / minute |
Per API key
| Limit | Value |
|---|---|
| Connections per IP | 3 |
| Distinct IPs | 1 (default) |
| Absolute connections (all IPs) | 20 |
| Connection cooldown | 5 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:
| Error | Cause | Action |
|---|---|---|
connection_rate_limit_exceeded | Your IP opened > 10 connections in 60 s (any key) | Back off; retry after the window |
per_ip_concurrent_limit_reached | Your IP holds 20 concurrent connections (any key) | Close unused connections |
per_ip_connection_limit_reached | This IP holds 3 connections for this specific key | Close one before opening another |
max_distinct_ips_reached | Key already in use from its max distinct IPs | Request a higher cap |
absolute_connection_cap_reached | Key hit the 20-total ceiling | Cap is fixed — use a second key |
connection_cooldown | Same key + IP reconnecting within 5 s | Wait retry_after_s (5) |
Client message limits
| Limit | Value | On excess |
|---|---|---|
| Message rate | 3 / minute | Connection closed (rate_limit_exceeded) |
| Frame size | 1 KB | Connection closed (frame_too_large) |
| Test rate | 1 / minute | Error 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 code | Cause | Action |
|---|---|---|
426 | Malformed upgrade (missing/invalid Sec-WebSocket-Key) | Fix the WebSocket client |
401 | Missing X-API-Key header | Add the header |
403 | Invalid, revoked, or expired key | Request a new key |
429 | Rate or connection limit hit | Back off, see Rate Limits |
Close codes
| Code | Reason | Meaning | Reconnect? |
|---|---|---|---|
1000 | key_expired | Key expiration date passed | No — get a new key |
1000 | key_invalidated | Administrator revoked the key | No — get a new key |
1008 | too_slow | Client lagged > 10 messages behind | Yes — process faster |
1008 | rate_limit_exceeded | Sent > 3 messages / minute to the server | Yes — stop sending |
1009 | frame_too_large | Sent a frame larger than 1 KB | Yes — 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:
| Trigger | Action |
|---|---|
Close key_expired / key_invalidated | Stop |
Close rate_limit_exceeded | Wait 60 s, stop sending client messages, reconnect |
Close too_slow | Reconnect immediately, process faster |
HTTP 429 | Back off (you hit a connection limit) |
| Any other disconnect | Reconnect 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 — when the server is there to close it.
The case that bites: the server disappears without saying goodbye (host stopped or migrated, public IP moved, path cut). No FIN, no RST, no PING — nothing arrives, and nothing tells your socket it is dead. A client that only reads stays “connected” indefinitely and misses every announcement from then on. The rule is therefore: no frame of any kind (PING or heartbeat) for 60 s = dead connection → close and reconnect. A healthy connection carries a frame at least every 15 s, so 60 s is four missed server pings, far from a false positive.
| Library | What to do |
|---|---|
Python websockets | Nothing: the default ping_interval=20, ping_timeout=20 sends client pings and raises ConnectionClosed when pongs stop. Keep those defaults. |
Node.js ws | ws does not ping by itself. Reset a 60 s timer on every 'ping' and 'message'; when it fires, ws.terminate() and let your reconnect logic run. |
Go gorilla/websocket | SetReadDeadline(now + 60 s) and push it forward in the ping handler and on every message. |
Rust tokio-tungstenite | A tokio::time::interval in your select! that checks the last-received instant and breaks out of the read loop past 60 s. |
Enable TCP keepalive on the socket too if your stack lets you (SO_KEEPALIVE, e.g. 30 s idle / 10 s probes): the kernel then detects a vanished peer even if the application does nothing.
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 confuse the two silences. “No announcement for N seconds” is normal — listings are sparse, never reconnect on that. “No frame at all (PING, heartbeat) for 60 s” is a dead connection — always reconnect on that, whether or not your library reported a close.
Code Examples
Production-ready clients with reconnection, latency tracking, and a test-announcement smoke check on connect.
Step-by-step tutorials: Binance listing sniper bot in Python · Upbit & Bithumb listing bot in Python.
Keep-alive: the server sends a PING every 15 s and a JSON heartbeat every 30 s. PONG is handled by each library, but a server that vanishes without closing sends nothing at all — your client must treat 60 s without any frame as a dead connection and reconnect (see Detecting dead connections). The examples below reconnect on close; add that silence watchdog on top.
pip install "websockets>=14"
import asyncio
import json
import time
import websockets # websockets>=14
API_KEY = "dsk_your_key_here"
WS_URL = "wss://jp.cryptolisting.ws"
# Filter exchanges (optional):
# WS_URL = "wss://jp.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())
Public JSON & Atom Feeds
The announcements shown on Latest Listings are also published as a free JSON file and as Atom feeds. No API key, no account: a plain HTTP GET.
These feeds are for dashboards, research, backfills and independent verification. They are read by polling and carry a summary of each event. To act on an announcement, use the WebSocket API: every message is pushed to you as it is dispatched, and free SpeedTrial and FreeDelayed keys are available — see Quick Start.
At a glance
| Feed | URL |
|---|---|
| Announcements, JSON | https://cryptolisting.ws/data/recent-announcements.json |
| Announcements, Atom 1.0 | https://cryptolisting.ws/feed-listings.xml |
| Blog, Atom 1.0 | https://cryptolisting.ws/feed.xml |
Both announcement feeds are built from the same data — listings, delistings, airdrops, Monitoring Tag changes, caution releases and Robinhood events — newest first. The JSON file keeps every recorded event; the Atom feed carries the most recent ones. The blog feed carries blog posts and product announcements.
JSON feed
Request
GET https://cryptolisting.ws/data/recent-announcements.json
- No authentication, no query parameters.
- CORS:
Access-Control-Allow-Origin: *— readable from browser code on any origin. - Every response carries
ETagandLast-Modified. Send them back asIf-None-Match/If-Modified-Sinceand you get304 Not Modified, with no body, while nothing new has been recorded. - Compressed (gzip or zstd) when your client accepts it.
Response
{
"updated_at_us": 1790573225386588,
"events": [
{"cex":"upbit","ticker":"CASHCAT","type":"spot_listing","detected_at_us":1790573225386588},
{"cex":"bithumb","ticker":"PYUSD","type":"spot_listing","detected_at_us":1790571091315312},
{"cex":"bithumb","ticker":"Q","type":"spot_listing","detected_at_us":1790571091315312}
]
}
The two Bithumb entries come from one announcement that listed two tokens: they share the same detected_at_us.
Top level
| Field | Type | Description |
|---|---|---|
updated_at_us | integer | When the content of the file last changed, in microseconds since the Unix epoch (UTC). It only moves when an event is recorded, so it is not a liveness signal. |
events | array | Recorded events, newest first. |
Each event
| Field | Type | Description |
|---|---|---|
cex | string | Exchange the event comes from, lowercase: binance, upbit, bithumb or robinhood. |
ticker | string | One token symbol, as written in the announcement. Can contain non-Latin characters. |
type | string | Event type. Same values as listingType on the WebSocket — see Listing types. |
detected_at_us | integer | Detection time, in microseconds since the Unix epoch (UTC). Divide by 1,000,000 for Unix seconds. |
type takes one of these values in this feed:
type | Event |
|---|---|
spot_listing | New spot market listing |
futures_listing | New futures / perpetual listing (Binance) |
spot_delisting | Spot market delisting |
futures_delisting | Futures / perpetual delisting (Binance) |
hodler_airdrop | Binance HODLer Airdrop |
monitoring_tag_extend | Token added to Binance’s Monitoring Tag |
monitoring_tag_remove | Token removed from Binance’s Monitoring Tag |
caution_released | Caution designation lifted (Upbit, Bithumb) |
wallet_listing | Robinhood Listing: token added to Robinhood, not tradable yet on Robinhood (Robinhood) |
cex_listing | Robinhood Live: the token is tradable on Robinhood (Robinhood) |
Rules worth knowing
- One entry per token. An announcement that covers several tokens becomes one entry per token, all with the same
detected_at_us. The WebSocket delivers the same announcement as a single message with a comma-separatedticker. - Newest first. Sort on
detected_at_usif the order matters to you. - No repeat within 72 hours. The same
cex+ticker+typeseen again within 72 hours is recorded once, with its first detection time. A relisting days later is a new entry. - Identify an entry by
cex+ticker+type+detected_at_us. Do not rely on its position or on the length of the list. - Not in this feed:
not_listingannouncements (maintenance, token swaps…), announcement titles, Upbit’smarketsandabnormalDetectionLatency. - Ignore what you don’t know. New fields or new
typevalues can appear; skip them rather than fail.
Update behaviour
The file is rewritten when a new event is recorded, after it has been pushed to WebSocket subscribers (for Robinhood: after it has been sent to the US endpoint). Otherwise it does not change: announcements can be hours apart, and every read in between returns the same content.
Fair use
- Read it at most once every 30 seconds per client. Latest Listings refreshes itself at that pace.
- Use conditional requests (
If-None-MatchorIf-Modified-Since): an unchanged file then costs a304and no body. - Send a
User-Agentthat names your project and a way to reach you. - Share one reader across your own processes instead of having each of them poll.
Clients that poll much faster may be throttled or blocked. Terms of use: Legal.
Examples
Python, standard library only — prints each new event, with conditional requests:
import json, time, urllib.request, urllib.error
URL = "https://cryptolisting.ws/data/recent-announcements.json"
UA = "my-listing-dashboard/1.0 (+https://example.com/contact)" # say who you are
etag, seen = None, set()
while True:
req = urllib.request.Request(URL, headers={"User-Agent": UA})
if etag:
req.add_header("If-None-Match", etag)
events = []
try:
with urllib.request.urlopen(req, timeout=10) as resp:
etag = resp.headers.get("ETag")
events = json.load(resp)["events"]
except urllib.error.HTTPError as err:
if err.code != 304: # 304 = nothing new since the last read
print("HTTP", err.code)
for ev in reversed(events): # oldest first; the first pass prints the whole file
key = (ev["cex"], ev["ticker"], ev["type"], ev["detected_at_us"])
if key not in seen:
seen.add(key)
print(ev["cex"], ev["type"], ev["ticker"], ev["detected_at_us"])
time.sleep(30) # fair use: one read every 30 s at most
JavaScript (browser, or Node.js 18+) — the ten most recent events:
const res = await fetch("https://cryptolisting.ws/data/recent-announcements.json");
const { events } = await res.json();
for (const ev of events.slice(0, 10)) {
const when = new Date(ev.detected_at_us / 1000).toISOString(); // µs → ms
console.log(when, ev.cex, ev.type, ev.ticker);
}
Atom feeds
Announcements feed
https://cryptolisting.ws/feed-listings.xml
- Atom 1.0, generated on each request from the same data as the JSON file, so it is never behind it.
- The latest announcements, newest first. Each
<entry>names the exchange, the token and the event type, links to the announcement on cryptolisting.ws, and carries the detection time (UTC) in<published>. - Stable entry
<id>s: a feed reader shows each event once. - Sent with
Cache-Control: no-store. The same fair use applies: one read every 30 seconds at most — most feed readers poll far less often.
Blog feed
https://cryptolisting.ws/feed.xml
Atom 1.0 feed of the blog: guides and product announcements. It changes when a post is published. For changes to the WebSocket feed itself, see the Changelog.
Public feeds vs WebSocket
| JSON and Atom feeds | WebSocket API | |
|---|---|---|
| Access | Open, no key | API key (free SpeedTrial and FreeDelayed tiers) |
| Delivery | You poll | Pushed to you |
| Timing | Written after WebSocket dispatch; your polling interval adds to it | At dispatch, plus your tier’s documented delay |
| Exchanges | Binance, Upbit, Bithumb, Robinhood | Binance, Upbit, Bithumb, Robinhood |
| Event types | The ten types above | All, including not_listing |
| Fields | cex, ticker, type, detected_at_us | Full message: title, ticker, publisher, listingType, timestamps, markets, abnormalDetectionLatency |
| Multi-token announcement | One entry per token | One message, comma-separated ticker |
| Built for | Dashboards, research, backfills, verification | Trading bots |
Next
- Quick Start — get a free key and connect to the WebSocket
- Message Reference — every field of a WebSocket message
- Latest Listings — the same data as a web page
CryptoListing.ws is a technical data feed, not financial advice. See Legal.
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-10-13
Tokyo endpoint renamed to wss://jp.cryptolisting.ws
- The Tokyo endpoint has a new hostname:
wss://jp.cryptolisting.ws. It is live now and serves the same feed — same API keys, same message schema, same limits. The three regional endpoints now share one naming scheme:jp.,kr.andus.. - Reason: an infrastructure upgrade for faster, more consistent delivery.
- Action required before 2026-10-31: replace
wss://cryptolisting.wswithwss://jp.cryptolisting.wsin your client and reconnect. Website, docs and public feed URLs (https://cryptolisting.ws/…) are unchanged. The address behind the name is the same (16.76.89.185), so an IP allowlist needs no update; if you allowlist by hostname or pin the certificate, addjp.cryptolisting.ws. - The old name
wss://cryptolisting.wsstops delivering the feed on 2026-10-31 (UTC). From that date a connection to it only gets a notice pointing to the new endpoint. - Until then both names reach the same endpoint, so the per-key connection limits and the 5-second reconnect cooldown count across the two names together — see Rate Limits.
- Seoul (
wss://kr.cryptolisting.ws) and US (wss://us.cryptolisting.ws) are unchanged.
2026-10-07
Robinhood events on the public feeds, Telegram and X
wallet_listingandcex_listingnow also appear in the public JSON feed, the Atom feed and on Latest Listings, one page per event, and are posted on the public Telegram channel and X account.- JSON consumers that whitelist
typevalues must add them — see Public JSON & Atom Feeds. - WebSocket delivery is unchanged: Robinhood is served by
wss://us.cryptolisting.wsonly.
2026-09-14
Faster fan-out, faster Upbit detection, Robinhood launched (beta)
- Fan-out latency. The Tokyo and Seoul dispatch servers were upgraded and tuned; events now reach every subscriber noticeably sooner after detection. No change to the protocol, messages or tiers.
- Upbit detection is ~1.5 ms faster. Same events, same schema — they just arrive earlier.
- Robinhood launched, in beta, on
wss://us.cryptolisting.ws(see 2026-09-11 below):wallet_listing(Robinhood Listing) when a token is added to Robinhood but is not tradable yet on Robinhood,cex_listing(Robinhood Live) when it becomes tradable on Robinhood. Awallet_listingis not a promise that trading will open on Robinhood. Same API key, same tiers, same message envelope. Beta means we are still validating it on live events — feedback welcome. - Reminder: treat 60 s without any frame (no PING, no
heartbeat) as a dead connection and reconnect — see Error Handling.
2026-09-13
Tokyo host migrated — check your dead-connection handling
- The Tokyo endpoint (
wss://cryptolisting.ws) moved to a new host. Its public IP changed to16.76.89.185(Seoul,kr.cryptolisting.ws, now resolves to15.165.135.192); if you pinned an IP in a firewall, update it — the hostnames are the contract, not the addresses. - During the move the old host was stopped, so open connections did not receive a close frame.
Clients that rely only on their library’s close/error callback stayed on a dead socket and
received nothing afterwards. Treat 60 s without any frame (no PING, no
heartbeat) as a dead connection and reconnect — the docs said the opposite (“no application-level watchdog needed”); that guidance was wrong for this case and is corrected in Error Handling. Pythonwebsocketsusers with the defaultping_interval/ping_timeoutwere already covered.
2026-09-11
Robinhood, on a new US endpoint
wss://us.cryptolisting.ws(AWS N. Virginia, us-east-1a - use1-az1) carries Robinhood — and only Robinhood. Tokyo and Seoul are unchanged;?cex=robinhoodthere yields an empty intersection. Same API key, same tiers, same message envelope. See Endpoints.- Two new
listingTypevalues, bothpublisher: "robinhood":wallet_listing(Robinhood Listing) — the token is added to Robinhood but is not tradable yet on Robinhood (trading may open minutes, days or weeks later, or never) — andcex_listing(Robinhood Live) — the token is tradable on Robinhood. A handler that switches onlistingTypewith a default branch needs no change; one that whitelists types must add them. See Listing types. - Pricing: Robinhood is priced like Upbit (Basic 400, Premium 750 USDT per month). New all-four-exchanges bundles: 1000 USDT per month on Basic, 1900 on Premium. See Pricing.
2026-09-04
Bithumb on the Seoul endpoint
wss://kr.cryptolisting.wsnow carries Upbit + Bithumb. It was Upbit-only. Binance is still dispatched from the Tokyo endpoint only. See Endpoints.- Bithumb on Seoul carries the same three types as on Tokyo —
spot_listing,spot_delisting,caution_released. Message schema, field names and timestamps are unchanged. - If you connect to Seoul without
?cex=, you now receive Bithumb too.?cex=upbitwas a no-op on that endpoint; it is now a real filter. Add it if your handler assumes every message there is Upbit — or branch onpublisher, which has always been the safer test. See Exchange Filtering. - Announcements carry no
id, and the two endpoints deduplicate independently: a bot connected to Tokyo and Seoul now receives each Bithumb announcement twice. Keep one endpoint per bot.
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
marketsfield 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_PROPERTIESis enabled out of the box — add@JsonIgnoreProperties(ignoreUnknown = true)to your message class, or disable the feature on yourObjectMapper. Also check for opt-in strict modes you may have enabled yourself: Go’sDecoder.DisallowUnknownFields(), serde’sdeny_unknown_fields, zod’s.strict(), or a validator configured withadditionalProperties: false. Our published schema has always setadditionalProperties: true— see/docs/asyncapi.yaml. - Delivered on every tier,
SpeedTrialincluded, with the same value as on the paid tiers — it names a quote market, not a token, so it is not redacted alongsidetitleandticker.
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 carriesid,title, an optionalversion, anddispatchTimestampUs. Deduplicate onid: 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 thewelcomeframe 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 carriestitleanddispatchTimestampUs, and no ticker. See Renewal notice.- Neither type carries
ticker,publisherorlistingType. Switch ontypeand 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
SpeedTrialtier are now sent ~1 ms after paid tiers, so paying subscribers are served first. Everything else is unchanged: same dispatch pipeline, samedetectedTimestampUs/dispatchTimestampUsprecision, same redaction oftickerandtitleon listing-type events. TheDelivery delaycolumn in Tier behavior now reads+1 msinstead ofNone. - Heartbeats are not affected on any tier — only announcements are delayed.
- The upgrade notice carried in
titleonSpeedTrialnow 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
SpeedTrialshould 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. premiumis unchanged and still has no added delay.
2026-07-13
Free-tier keys are 1-week and renewable
SpeedTrialandFreeDelayedkeys 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 inwelcome.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_listingrow of every event-type table. Upbit announcements are delivered asspot_listing,spot_delisting, orcaution_released— nevernot_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.wscarries 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.maxConnectionsPerIpreports the live value — read it rather than hard-coding. See Rate Limits.
2026-06-06
SpeedTrial and FreeDelayed tiers
- The
freetier is nowSpeedTrial: same dispatch path aspremiumwith zero added delay, buttickeris""andtitlecarries an upgrade notice on every event exceptnot_listing. All other fields —publisher,listingType, microsecond timestamps — stay fully accurate, so detection speed remains independently verifiable. - New
FreeDelayedtier: the full feed,titleandtickerincluded, free, with a +240 ms delivery delay on announcements. Heartbeats are unaffected. welcome.tiernow returnsSpeedTrial,FreeDelayed,basic, orpremium. Clients that matched on the oldfreestring 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_releasedandspot_delisting. Intermediate stages are kept off the bus. - Composite announcements covering several tickers across stages are split into multiple
WebSocket events with distinct
listingTypevalues. See Listing types.
2026-05-02
Risk events: caution_released and Monitoring Tag
- New
listingTypevaluecaution_released(Bithumb, Upbit) — a caution designation being lifted, historically a positive technical signal. - New
listingTypevaluesmonitoring_tag_extendandmonitoring_tag_remove(Binance) — a token entering or leaving Binance’s Monitoring Tag. - Clients validating
listingTypeagainst a closed list had to widen it. This is the change the forward-compatibility rule exists for: unknownlistingTypevalues must be inert, not fatal. See Listing types.
2026-04-30
Seoul endpoint
wss://kr.cryptolisting.wswent 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.truemeans 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. SeeabnormalDetectionLatency.
2026-04-12
Test announcements use DUMMYTOKEN
{"type":"test"}returns atest_announcementwith the tickerDUMMYTOKEN. 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.