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