Self-Service Access

There are two ways to reach the CryptoStruct realtime market data feed. Managed access is what Accessing Data describes: you run a Backend installation, look up hosts through the master data API, and your server's IP is whitelisted for the markets you use.

Self-service access needs none of that. You buy access in the shop, get an API key, and connect to a fixed endpoint. This page covers what is different — the wire protocol itself is the same one documented in Protocol.


What differs from managed access

Topic Managed access Self-service access
Instrument master data /api/exchanges and /api/instruments on <api-host> /api/realtime/instruments?market=<code> on cryptostruct.com
Host discovery /api/v2/marketdata?instruments=… returns hosts[] None. One fixed endpoint per market
Authentication None; your server's IP is whitelisted An API key, passed as the apiKey query parameter
Entitlement Agreed per contract Bought per UTC calendar day, per market or per instrument
Wire protocol Protocol Identical — same messages, same encodings

Getting access

Realtime access is sold in the shop per UTC calendar day, either as a whole market (every instrument of that market) or as single instruments.

  1. Buy days. Pick a market or a set of instruments and a duration at cryptostruct.com/shop. Paid days always start on the next UTC day, and the remainder of the purchase day is included free — your key validates the moment the order is paid. Rates are on the pricing page.

  2. Get your API key. It is created with your first realtime purchase and shown at cryptostruct.com/account/realtime, together with your current and upcoming days. There is one key per account, and you can rotate it there at any time — rotation does not affect the days you already bought.

  3. Look up instrument IDs. Subscriptions take numeric instrument IDs. Fetch them from the master data export described below.

A day ends at 00:00 UTC. Extend before that if you want to stay connected across midnight.


Endpoints

Each market listens on its own port. There is no market switch inside the protocol and no discovery endpoint — pick the URL of the market you bought and append your key as ?apiKey=….

Market Code Port JSON endpoint SBE endpoint
Binance USDT-M binance_swap 14004 ws://lon1.cryptostruct.com:14004/api/v6 ws://lon1.cryptostruct.com:14004/api/v6/sbe
Binance Spot binance_spot 14003 ws://lon1.cryptostruct.com:14003/api/v6 ws://lon1.cryptostruct.com:14003/api/v6/sbe

The encoding is selected by path: /api/v6 is JSON over text frames and the place to start, /api/v6/sbe is SBE over binary frames for low-latency clients. A complete URL looks like this:

ws://lon1.cryptostruct.com:14004/api/v6?apiKey=<your key>

lon1 is the delivery endpoint, not the source: the data is captured at the venue in Tokyo and delivered at our London endpoint. Further locations are available through managed access.

Checking an endpoint

The /api/info endpoint of a port is plain HTTP and needs no key:

http://lon1.cryptostruct.com:14004/api/info?all=true

It answers with the proxy's process ID, the protocol version, both endpoints and the capabilities per topic — the same object the login response carries. Capabilities differ per market: spot markets have no mark price, index price, funding rate or liquidations, so those topics are null there and never emit. USDT-M perpetuals have all seven topics.


Instrument master data

Self-service access has no Backend installation and therefore no /api/exchanges or /api/instruments. The equivalent is a per-market export of every sellable instrument, served by the shop:

GET https://cryptostruct.com/api/realtime/instruments?market=<code>
GET https://cryptostruct.com/api/realtime/instruments?market=<code>&format=csv

<code> is the market code from the endpoint table. Authenticate with your realtime API key as an Authorization: Bearer header — the same key as the feed, but sent as a header here, never as a URL parameter.

curl -sS -H "Authorization: Bearer $CRYPTOSTRUCT_REALTIME_API_KEY" \
  "https://cryptostruct.com/api/realtime/instruments?market=binance_swap&format=csv" \
  -o realtime_instruments_binance_swap.csv

The JSON form is an envelope {market: {exchange_id, code, name}, generated_at, count, instruments: [...]} with the fields below per row. The CSV form carries one header line with the same columns in the same order.

Column Type Meaning
instrument_id integer Numeric ID — the value you subscribe with ([11, id])
code string Exchange symbol, e.g. BTCUSDT
type string Instrument class as recorded (perpetual, spot, …)
base_underlying string or null Base asset, e.g. BTC
counter_underlying string or null Quote asset, e.g. USDT
state string open or review — only sellable instruments are listed
ticksize decimal string or null Minimum price increment
lot_size decimal string or null Minimum quantity increment
contract_value decimal string or null Contract value (derivatives)
multiplier decimal string or null Contract multiplier
min_order decimal string or null Minimum order size
max_order decimal string or null Maximum order size
exchange_id integer Numeric ID of the market
exchange_code string Market code — the value of ?market=

The decimal columns are strings. Keep them as strings or parse them with a decimal type, never a float. Absent values are null in JSON and empty in CSV.

Only open and review instruments are listed — the sellable set. The list is refreshed server-side about hourly; fetch it at start-up and re-fetch daily, not per message.

Status Meaning
200 The instrument list
404 The market code is unknown, or the key was not accepted — the two are deliberately indistinguishable, check both
503 The access check is temporarily unavailable; the response carries a Retry-After header

Connecting

The server sends nothing until you log in and subscribe. Every session is the same sequence:

  1. Connect to the endpoint of your market, with your key in ?apiKey=.

  2. Send the login [13, organization, application_name, application_version, process_id] and read the [14, …] response with the capabilities. The four strings are free-form and used for logging only — authentication already happened through the key in the URL.

  3. Send [11, instrumentId] per instrument. The server answers with a [5, id, ts, "READY", ""] state event and the current state, then streams live updates.

  4. Keep reading. The server pings every 5 s; standard WebSocket libraries answer with a pong by themselves.

The optional third element of the subscribe message selects topics; the defaults are listed under Subscription. Liquidations are off by default and have to be requested explicitly.

A smoke test without writing code, using websocat — connect, then paste the two lines:

websocat -t 'ws://lon1.cryptostruct.com:14004/api/v6?apiKey=<your key>'
[13,"my-org","websocat","1.0.0","smoke"]
[11,67824]

The same in Python:

# pip install websockets
import asyncio, json, websockets

URL = "ws://lon1.cryptostruct.com:14004/api/v6?apiKey=<your key>"
INSTRUMENT = 67824  # BTCUSDT


async def recv(ws):
    raw = await ws.recv()
    if not isinstance(raw, str) or not raw.startswith("["):
        # plain-text server notice sent right before a close
        raise RuntimeError(f"server: {raw!r}")
    return json.loads(raw)


async def main():
    try:
        async with websockets.connect(URL, max_size=None) as ws:
            await ws.send(json.dumps([13, "my-org", "my-client", "1.0.0", "client-01"]))
            login = await recv(ws)
            print("logged in:", login[1], "capabilities:", sorted(login[4]))

            await ws.send(json.dumps([11, INSTRUMENT]))
            while True:
                msg = await recv(ws)
                print(msg[0], msg[1])
    except websockets.exceptions.InvalidHandshake as e:
        # HTTP 401 — unknown key, or no paid day for this market today
        print("handshake rejected:", e)


asyncio.run(main())

Keep-alive, errors and reconnecting

Heartbeat

The server sends a WebSocket ping every 5 s and closes the connection if no pong arrives within 30 s (pong check failed). Standard libraries answer pings automatically — you only have to keep reading from the socket.

Rejected at the handshake

An unknown key, or a key without a paid day for the market behind that port, never gets a WebSocket. The upgrade request is answered with HTTP 401 and a text/plain body:

api key rejected: <reason>
Reason Meaning
unknown api key The key does not exist — check for truncation or rotation
no active realtime subscription for this market today (UTC) No paid day for the market behind this port today
market not configured The endpoint could not be resolved to a market
invalid request The access check was called with an unusable request

Client libraries surface this as a failed connection rather than a WebSocket close: Python websockets raises InvalidHandshake, Node ws emits unexpected-response with the status and the body. The second reason is the common case — verify the port you connected to and your days at /account/realtime. Do not retry a rejected handshake in a tight loop.

Notices and instrument errors

Connection-level notices arrive as a plain-text frame — not a JSON array — immediately before the close, on a running session only: api key expired or revoked, not logged in, already logged in, protocol violation: <detail>, pong check failed, service shutdown. Log them verbatim; they say why the connection ended.

Instrument-level problems come as state events [5, id, ts, "ERROR", "…"] and never drop the connection: instrument not available on this endpoint, instrument not permitted for this api key, instrument permission expired or revoked and instrument unsubscribed. A whole-market day unlocks every instrument of that market; an instrument day unlocks exactly the IDs you bought.

Connection limit

Each key may hold a limited number of parallel connections per market — five by default. The current value is shown on your access card. Further handshakes are refused.

Reconnecting

Connect, log in, subscribe again. Every subscribe starts with a fresh snapshot, so no state is lost — do not try to resume by event ID. The same applies mid-session: on a prevEventId gap, or a snapshot with forceReset, unsubscribe and resubscribe the instrument and rebuild the book from the new snapshot.

When access changes take effect

Days and instruments you buy apply at your next login, so reconnect after a purchase. A rotated key or a refund is enforced at the next login and by the proxy's periodic re-check, which runs about hourly. Expect the notice api key expired or revoked followed by a close at 00:00 UTC when the next day is not bought — this is the expected end of an unextended day.


Security notes

  • On the WebSocket the key travels in the URL query string. Treat the full URL like a password: keep it out of shared configs, tickets, logs and screenshots.

  • On the master data route the key is an Authorization: Bearer header and must never be put into that URL, so it stays out of access logs.

  • Rotate the key at /account/realtime if it ever leaks. The old key stops validating at the next login and re-check; your purchased days are unaffected.

  • One key per account is the model. Do not share a key between systems you may want to cut off separately — use the connection limit for parallelism instead.


See Also

  • Accessing Data. Managed access: master data API, host discovery and IP whitelisting.

  • Protocol. Market data protocol specification.

  • SBE Encoding. Simple Binary Encoding for market data.