Introduction
Real-time WebSocket feed for crypto exchange listing announcements.
What is this?
This service delivers instant notifications when a cryptocurrency exchange publishes an announcement – listings and other exchange announcements. You connect via WebSocket and receive structured JSON messages in real time.
Currently supported exchanges:
| Exchange | Status |
|---|---|
| Binance | Live |
| Bithumb | Live |
| Upbit | Live |
| More coming soon | – |
Supported announcement types:
| Type | Description |
|---|---|
spot_listing | New spot market listing |
futures_listing | New futures/perpetual listing |
spot_delisting | Spot market delisting (coming soon) |
futures_delisting | Futures/perpetual delisting (coming soon) |
not_listing | Other exchange announcement (maintenance, airdrop, token swap, etc.) |
How it works
Exchange publishes announcement
|
v
Detection engine (< 5ms)
|
v
Dispatch server (< 1ms)
|
v
Your WebSocket client
- Our detection engine monitors exchange announcement pages continuously.
- When a new listing is detected, it is parsed, classified, and forwarded to the dispatch server.
- The dispatch server broadcasts the announcement to all connected WebSocket subscribers simultaneously.
- You receive a JSON message with the ticker, exchange, listing type, and precise timestamps.
Key features
- Ultra-low latency – sub-millisecond dispatch from detection to your connection.
- Precise timestamps – microsecond-resolution timestamps at every stage (publish, detect, dispatch) so you can measure your exact latency.
- Exchange filtering – subscribe only to the exchanges you care about.
- Always-on heartbeat – know immediately if your connection drops.
- Zero-copy broadcast – announcements are serialized once and shared across all subscribers for maximum throughput.
Quick links
| Topic | Description |
|---|---|
| Quick Start | Connect in under 5 minutes |
| Authentication | API key format and usage |
| WebSocket API | Connection details and parameters |
| Message Reference | Full JSON schema for all messages |
| Exchange Filtering | Subscribe to specific exchanges |
| Rate Limits & Security | Limits, TLS, and security details |
| Error Handling & Reconnection | Close codes and retry strategy |
| Code Examples | Python, Node.js, Go examples |
Quick Start
Get connected and receiving announcements in under 5 minutes.
1. Get your API key
You will receive an API key from the administrator. It looks like this:
dsk_a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890ab
All keys start with the dsk_ prefix followed by 64 hexadecimal characters.
Store your API key securely. It is shown only once when created and cannot be retrieved later.
2. Connect to the WebSocket
wss://cryptolisting.ws
Pass your API key via the X-API-Key HTTP header during the WebSocket handshake.
pip install websocket-client
# pip install websocket-client
import json, websocket
from datetime import datetime
URL = "wss://cryptolisting.ws"
HEADER = ["X-API-Key: dsk_your_key_here"]
def on_open(ws):
print("Connected!")
def on_message(ws, message):
data = json.loads(message)
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
if data["type"] == "announcement":
print(f"[{ts}] {data['listingType']} | {data['ticker']} on {data['publisher']}")
print(f" {data['title']}")
elif data["type"] == "welcome":
print(f"[{ts}] Welcome — tier={data['tier']}, cex={data['allowedCex']}")
def on_close(ws, code, msg):
print(f"Disconnected ({code}). Reconnecting...")
connect()
def connect():
websocket.WebSocketApp(
URL, header=HEADER,
on_open=on_open, on_message=on_message, on_close=on_close
).run_forever(ping_interval=30, ping_timeout=10)
connect()
3. Receive messages
Once connected, you will immediately receive a welcome message confirming your subscription:
{
"type": "welcome",
"tier": "premium",
"maxConnections": 5,
"allowedCex": "*",
"expiresInSecs": 86400
}
Then you will receive:
- Heartbeats every 30 seconds – confirms the connection is alive.
- Announcements whenever a listing or other exchange announcement is detected.
{
"type": "announcement",
"title": "Binance Will List TOKEN (TOKEN)",
"ticker": "TOKEN",
"publisher": "binance",
"listingType": "spot_listing",
"publishTimestampUs": 1710345000000000,
"detectedTimestampUs": 1710345000005000,
"dispatchTimestampUs": 1710345000006000
}
4. Filter by exchange (optional)
To receive announcements only from specific exchanges, add the cex query parameter:
wss://cryptolisting.ws?cex=binance
wss://cryptolisting.ws?cex=binance,upbit
Omit the parameter to receive announcements from all exchanges.
Next steps
- Message Reference – full field-by-field documentation
- Error Handling & Reconnection – build a resilient client
- Code Examples – production-ready implementations
Authentication
Every WebSocket connection must be authenticated with a valid API key.
API key format
dsk_<64 hex characters>
Example:
dsk_a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890ab
- Prefix:
dsk_(always) - Body: 64 hexadecimal characters (32 random bytes)
Passing your API key
Method 1: HTTP header (recommended)
Set the X-API-Key header during the WebSocket handshake:
X-API-Key: dsk_your_key_here
This is the recommended method. The key stays out of URLs and server logs.
Method 2: Query parameter
Append the key as a query parameter:
wss://cryptolisting.ws?api_key=dsk_your_key_here
Not recommended. Query parameters may appear in server access logs, proxy logs, and browser history. Use the header method whenever possible.
Key properties
Each API key has the following properties, configured by the administrator:
| Property | Description |
|---|---|
| Tier | Subscription tier: basic, premium, or enterprise |
| Max connections | Maximum simultaneous WebSocket connections allowed with this key |
| Allowed CEX | Exchanges this key can subscribe to ("*" = all) |
| Expiration | Optional expiration date – key becomes invalid after this time |
Key lifecycle
- Created – The administrator generates a key. The full key is shown exactly once.
- Active – The key can be used to connect. Connections are subject to the key’s limits.
- Expired – If the key has an expiration date and that date has passed, connections are refused and active sessions are disconnected with close code
1000and reasonkey_expired. - Revoked – The administrator revokes the key. All active sessions are immediately disconnected with close code
1000and reasonkey_revoked. The key can no longer be used.
Security
- Keys are hashed with SHA-256 before storage. Even if the database is compromised, your raw key cannot be recovered.
- Keys are transmitted over TLS-encrypted WebSocket connections (WSS).
- Invalid keys are cached server-side for 30 seconds to prevent brute-force attempts.
WebSocket API
Connection endpoint
wss://cryptolisting.ws
The server uses binary WebSocket frames containing UTF-8 JSON.
Query parameters
| Parameter | Required | Description | Example |
|---|---|---|---|
cex | No | Comma-separated list of exchanges to subscribe to | binance,upbit |
Connection lifecycle
Client Server
| |
|--- WSS handshake + API key -----→ |
| |-- Validate key
| |-- Check rate limits
| |-- Check connection limits
|←---- 101 Switching Protocols -----|
| |
|←---- Welcome message -------------| (immediate)
| |
|←---- Heartbeat -------------------| (every 30s)
|←---- Heartbeat -------------------|
| |
|←---- Announcement ----------------| (when detected)
| |
|--- {"type":"test"} -------------→ | (optional)
|←---- Test Announcement -----------| (only to you)
| |
|←---- PING ----------------------- | (every 15s)
|--- PONG ------------------------→ |
| |
|←---- Close (1000, key_expired) ---| (if key expires)
| |
1. Handshake
The server validates your API key during the HTTP upgrade handshake. If validation fails, you receive an HTTP error response (not a WebSocket frame):
| HTTP Code | Reason |
|---|---|
400 | Malformed WebSocket upgrade request |
401 | Missing API key |
403 | Invalid, revoked, or expired API key |
429 | Rate limit or connection limit exceeded |
2. Welcome message
Immediately after a successful handshake, the server sends a welcome message confirming your subscription parameters. See Message Reference.
3. Heartbeats
The server sends a JSON heartbeat message every 30 seconds. If you do not receive a heartbeat within ~35 seconds, your connection may be dead.
4. Ping/Pong
In addition to JSON heartbeats, the server sends WebSocket PING frames every 15 seconds. Your WebSocket library handles PONG responses automatically. If the server receives no PONG within 30 seconds, it closes your connection.
Most WebSocket libraries (Python websockets, Node.js ws, Go gorilla/websocket) handle PING/PONG automatically. You do not need to implement this yourself.
5. Announcements
When a listing or other exchange announcement is detected, the server sends an announcement message to all subscribers whose exchange filter matches. See Message Reference.
6. Disconnection
The server may close your connection with a WebSocket close frame. The close code and reason indicate why. See Error Handling.
Client-to-server messages
Client messages are subject to rate limiting (3 messages/minute) and frame size limits (1 KB max). The only supported client message is the test request:
{"type":"test"}
This returns a fake announcement to verify your integration is working. See Message Reference — Test Announcement for details.
Exceeding the client message rate limit (3/min) will result in disconnection. Test requests are additionally limited to 1 per minute per API key.
Message Reference
All messages are delivered as binary WebSocket frames containing UTF-8 JSON. Every message has a type field.
Welcome
Sent once, immediately after a successful connection.
{
"type": "welcome",
"tier": "premium",
"maxConnections": 5,
"allowedCex": "*",
"expiresInSecs": 2592000
}
| Field | Type | Description |
|---|---|---|
type | string | Always "welcome" |
tier | string | Your subscription tier: basic, premium, or enterprise |
maxConnections | integer | Maximum simultaneous connections allowed with your key |
allowedCex | string | Exchanges you will receive announcements from (effective filter after applying key restrictions and your cex preference). "*" means all exchanges |
expiresInSecs | integer|null | Seconds until your key expires, or null if no expiration |
Announcement
Sent whenever a listing or other exchange announcement is detected on a monitored exchange.
Single ticker:
{
"type": "announcement",
"title": "Binance Will List TOKEN (TOKEN)",
"ticker": "TOKEN",
"publisher": "binance",
"listingType": "spot_listing",
"publishTimestampUs": 1710345000000000,
"detectedTimestampUs": 1710345000005000,
"dispatchTimestampUs": 1710345000006000
}
Multiple tickers (one announcement listing several assets at once):
{
"type": "announcement",
"title": "Binance Will List ABC, DEF and GHI",
"ticker": "ABC,DEF,GHI",
"publisher": "binance",
"listingType": "spot_listing",
"publishTimestampUs": 1710345000000000,
"detectedTimestampUs": 1710345000005000,
"dispatchTimestampUs": 1710345000006000
}
| Field | Type | Description |
|---|---|---|
type | string | Always "announcement" |
title | string | Original announcement title from the exchange |
ticker | string | Asset symbol(s). Comma-separated when multiple tickers are listed in the same announcement (e.g. "BTC" or "ABC,DEF,GHI") |
publisher | string | Exchange name in lowercase (e.g. "binance", "upbit", "bithumb") |
listingType | string | One of the listing types below |
publishTimestampUs | integer | When the exchange published the announcement (microseconds since UNIX epoch) |
detectedTimestampUs | integer | When our detection engine captured it (microseconds since UNIX epoch) |
dispatchTimestampUs | integer | When the dispatch server broadcasted it (microseconds since UNIX epoch) |
Always handle ticker as a potentially comma-separated list. Some exchange announcements list multiple assets at once. Split on , to get individual tickers:
tickers = data["ticker"].split(",") # ["ABC", "DEF", "GHI"]
Supported exchanges (publisher)
More exchanges coming soon. This list will grow over time. Use the cex query parameter when connecting to filter which exchanges you receive.
Listing types
| Value | Meaning |
|---|---|
spot_listing | New spot market listing |
futures_listing | New futures/perpetual listing |
spot_delisting | Spot market delisting (coming soon) |
futures_delisting | Futures/perpetual delisting (coming soon) |
not_listing | Other exchange announcement (maintenance, airdrop, token swap, etc.) |
Delistings coming soon. The spot_delisting and futures_delisting types are reserved for future use. Currently, announcements that are not listings are sent with listingType: "not_listing".
Measuring your latency
All timestamps are in microseconds since UNIX epoch (not milliseconds, not nanoseconds).
Detection delay = detectedTimestampUs - publishTimestampUs
Dispatch delay = dispatchTimestampUs - detectedTimestampUs
Your network delay = your_receive_time_us - dispatchTimestampUs
Total end-to-end = your_receive_time_us - publishTimestampUs
Python example:
import time
def on_announcement(msg):
now_us = int(time.time() * 1_000_000)
detection_ms = (msg["detectedTimestampUs"] - msg["publishTimestampUs"]) / 1000
dispatch_ms = (msg["dispatchTimestampUs"] - msg["detectedTimestampUs"]) / 1000
network_ms = (now_us - msg["dispatchTimestampUs"]) / 1000
total_ms = (now_us - msg["publishTimestampUs"]) / 1000
print(f"Detection: {detection_ms:.2f}ms")
print(f"Dispatch: {dispatch_ms:.2f}ms")
print(f"Network: {network_ms:.2f}ms")
print(f"Total: {total_ms:.2f}ms")
Test Announcement
You can request a fake announcement to verify your integration. Send a JSON message to the server:
{"type":"test"}
The server replies with a test_announcement — identical to a real announcement except for the type field:
{
"type": "test_announcement",
"title": "Binance Will List MOCKX (MOCKX)",
"ticker": "MOCKX",
"publisher": "binance",
"listingType": "spot_listing",
"publishTimestampUs": 1743850000000000,
"detectedTimestampUs": 1743850001999800,
"dispatchTimestampUs": 1743850002000000
}
The fields are the same as a regular Announcement. The timestamps are simulated: publishTimestampUs is 1 second before dispatch, and detectedTimestampUs is 200µs before dispatch.
This message is only sent to you, not broadcast to other subscribers. The exchange, listing type, ticker, and title are randomized from realistic templates.
Test rate limit
Test requests are rate-limited to 1 per minute per API key (shared across all connections using the same key). If you exceed the limit, the server responds with an error instead of a test announcement:
{
"type": "error",
"code": "test_rate_limited",
"retryAfterSecs": 42
}
| Field | Type | Description |
|---|---|---|
type | string | Always "error" |
code | string | Error code: "test_rate_limited" |
retryAfterSecs | integer | Seconds to wait before retrying |
Heartbeat
Sent every 30 seconds to indicate the server is alive.
{
"type": "heartbeat",
"timestampNs": 1710345030000000000,
"timeUtc": "2024-03-13T10:30:30.000000Z"
}
| Field | Type | Description |
|---|---|---|
type | string | Always "heartbeat" |
timestampNs | integer | Current server time in nanoseconds since UNIX epoch |
timeUtc | string | Current server time in ISO 8601 UTC format |
If you do not receive a heartbeat within 35 seconds, your connection is likely dead. Initiate a reconnection.
Exchange Filtering
You can choose to receive announcements from specific exchanges only, rather than receiving everything.
How filtering works
Filtering is applied at two levels:
1. Key-level restriction (set by administrator)
When your API key is created, the administrator can restrict it to specific exchanges. For example, a key may be limited to binance,upbit only.
You can check your key’s allowed exchanges in the welcome message you receive upon connection:
{
"type": "welcome",
"allowedCex": "binance,upbit",
...
}
A value of "*" means your key has access to all exchanges.
2. Client-side preference (set by you)
When connecting, you can further narrow your subscription using the cex query parameter:
wss://cryptolisting.ws?cex=binance
Multiple exchanges:
wss://cryptolisting.ws?cex=binance,upbit
All exchanges (default):
wss://cryptolisting.ws
Effective filter
The effective filter is the intersection of your key’s allowed exchanges and your client-side preference:
| Key allows | You request | You receive |
|---|---|---|
* (all) | binance | binance |
* (all) | binance,upbit | binance,upbit |
* (all) | (nothing) | All exchanges |
binance,upbit | binance | binance |
binance,upbit | upbit | upbit |
binance,upbit | (nothing) | binance,upbit |
binance | upbit | (nothing – no match) |
If your key restricts you to binance and you request upbit, you will connect successfully but receive no announcements (heartbeats are still sent).
Heartbeats are always delivered
Heartbeat messages are always sent regardless of your exchange filter. They are not tied to any specific exchange.
Rate Limits & Security
Connection limits
| Limit | Value | Scope |
|---|---|---|
| Per-IP concurrent connections | 20 | Single IP address |
| Per-IP connection rate | 10 / minute | Sliding window |
| Per-key concurrent connections | Configurable | Per API key |
If you exceed any of these limits, the server responds with HTTP 429 Too Many Requests during the handshake:
{
"error": "max_connections_reached",
"limit": 5,
"current": 5
}
Client message limits
| Limit | Value | Scope | Consequence |
|---|---|---|---|
| Message rate | 3 / minute | Per connection | Connection closed (rate_limit_exceeded) |
| Max frame size | 1 KB | Per frame | Connection closed (frame_too_large) |
| Test requests | 1 / minute | Per API key | Error response (test_rate_limited) |
The test request rate limit is shared across all connections using the same API key. Unlike the other limits, exceeding it does not disconnect you — you receive an error message with a retryAfterSecs field instead. See Message Reference — Test Announcement.
TLS
All connections use TLS encryption by default (WSS on port 9201).
The server may use a self-signed certificate. If so, you will need to disable certificate verification in your client. See Code Examples for how to do this in each language.
API key security
- Keys are hashed before storage – raw keys cannot be recovered from the database.
- Keys can be revoked instantly by an administrator, immediately disconnecting all active sessions.
- Keys can have expiration dates – expired keys are automatically rejected.
Error Handling & Reconnection
HTTP errors (during handshake)
If authentication or rate limiting fails, the server rejects the WebSocket upgrade with an HTTP error:
| HTTP Code | Reason | Action |
|---|---|---|
400 | Malformed request | Fix your WebSocket client |
401 | Missing API key | Add X-API-Key header |
403 | Invalid, revoked, or expired key | Contact administrator for a new key |
429 | Rate limit or connection limit exceeded | Wait and retry with backoff |
WebSocket close codes
Once connected, the server may close your connection with a close frame. The reason field tells you why:
| Code | Reason | Meaning | Should reconnect? |
|---|---|---|---|
1000 | key_expired | Your API key’s expiration date has passed | No – get a new key |
1000 | key_revoked | Administrator revoked your key | No – get a new key |
1008 | too_slow | You fell 10+ messages behind (lagging consumer) | Yes |
1008 | rate_limit_exceeded | You sent too many messages to the server (>3/min) | Yes – stop sending messages |
1009 | frame_too_large | You sent a frame larger than 1 KB | Yes – stop sending large frames |
Recommended reconnection strategy
Exponential backoff
Attempt 1: wait 1 second
Attempt 2: wait 2 seconds
Attempt 3: wait 4 seconds
Attempt 4: wait 8 seconds
...
Cap: 300 seconds (5 minutes)
Decision logic
On disconnect:
|
|-- Close reason = "key_expired" or "key_revoked"?
| → Stop. Your key is no longer valid.
|
|-- Close reason = "rate_limit_exceeded"?
| → Wait 60s, then reconnect. Stop sending messages.
|
|-- Close reason = "too_slow"?
| → Reconnect immediately. Process messages faster.
|
|-- HTTP 429?
| → Back off. You're connecting too frequently.
|
|-- Unexpected disconnect / network error?
→ Reconnect with exponential backoff.
Heartbeat-based health check
Monitor heartbeats to detect silent disconnections:
import asyncio
HEARTBEAT_TIMEOUT = 35 # seconds
async def monitor_connection(ws):
while True:
try:
msg = await asyncio.wait_for(ws.recv(), timeout=HEARTBEAT_TIMEOUT)
data = json.loads(msg)
if data["type"] == "announcement":
handle_announcement(data)
except asyncio.TimeoutError:
print("No heartbeat received -- reconnecting")
break # Exit loop and reconnect
The server sends heartbeats every 30 seconds. If you receive nothing for 35 seconds, assume the connection is dead and reconnect.
Code Examples
Production-ready client implementations with reconnection, heartbeat monitoring, and latency tracking.
Requires: pip install websockets
import asyncio
import json
import ssl
import time
import websockets
API_KEY = "dsk_your_key_here"
WS_URL = "wss://cryptolisting.ws"
# Filter specific exchanges (optional):
# WS_URL = "wss://cryptolisting.ws?cex=binance,upbit"
HEARTBEAT_TIMEOUT = 35 # seconds
MAX_RETRIES = 20
def on_announcement(msg: dict):
now_us = int(time.time() * 1_000_000)
network_ms = (now_us - msg["dispatchTimestampUs"]) / 1000
total_ms = (now_us - msg["publishTimestampUs"]) / 1000
print(f"[{msg['listingType']}] {msg['ticker']} on {msg['publisher']}")
print(f" Title: {msg['title']}")
print(f" Latency: {total_ms:.2f}ms total, {network_ms:.2f}ms network")
async def connect():
ssl_ctx = ssl.create_default_context()
ssl_ctx.check_hostname = False
ssl_ctx.verify_mode = ssl.CERT_NONE
headers = {"X-API-Key": API_KEY}
for attempt in range(MAX_RETRIES):
try:
async with websockets.connect(
WS_URL, extra_headers=headers, ssl=ssl_ctx
) as ws:
print("Connected!")
while True:
try:
raw = await asyncio.wait_for(
ws.recv(), timeout=HEARTBEAT_TIMEOUT
)
except asyncio.TimeoutError:
print("No heartbeat -- reconnecting")
break
msg = json.loads(raw)
if msg["type"] == "welcome":
print(f"Welcome: tier={msg['tier']}, "
f"cex={msg['allowedCex']}")
# Request a test announcement to verify integration
await ws.send(json.dumps({"type": "test"}))
elif msg["type"] in ("announcement", "test_announcement"):
prefix = "[TEST] " if msg["type"] == "test_announcement" else ""
print(f"{prefix}", end="")
on_announcement(msg)
elif msg["type"] == "heartbeat":
pass # Connection alive
except websockets.ConnectionClosed as e:
if e.rcvd:
reason = e.rcvd.reason
if reason in ("key_expired", "key_revoked"):
print(f"Key is no longer valid: {reason}")
return
print(f"Disconnected: {reason}")
except Exception as e:
print(f"Connection error: {e}")
backoff = min(2 ** attempt, 300)
print(f"Reconnecting in {backoff}s (attempt {attempt + 1})")
await asyncio.sleep(backoff)
print("Max retries exceeded")
if __name__ == "__main__":
asyncio.run(connect())