One socket carries every topic you follow, and each message carries the same bytes as the topic's REST answer. Fetch the day once, then apply what arrives.
POST /v1/stream/negotiate with your key, before every socket. It answers the topics on offer, your limits, the socket's path in transports.ws.url and a neg token, good for 300 s.wss://api.datafirsttrading.com/v1/stream?api=1&encoding=zstd&token=<key>&neg=<neg>POST /v1/stream/ticket and hands the page the ticket and the neg. The ticket goes in token, so the key never reaches the browser.{"k": "sub", "topic": "levels:SPY:exp4:-"}, one topic a message. A topic is family:SYMBOL:scope:-. Also unsub, resync, pause and resume.snap: the topic's whole REST answer. Then its changes: a new snap when a snapshot topic changes, an app with only what's new on an append topic. A topic that didn't change sends nothing; a beat names the quiet ones.zstd, the default, and deflate send MessagePack, compressed. json sends text frames, for building and debugging.err with its code and reason. The socket stays open./v1/stream/negotiateWhat the stream offers your key, and the token its socket requires. Call it before every socket. The body is optional and an empty one negotiates the defaults.
apiintegerThe socket's api value: 1.
negotiation_idstringThis negotiation's id: the short hello's neg.
features, ignoredstring[]The words you sent, split into what is agreed and what is not offered.
transports.restobjectbase (/v1), body_encodings (json), compression (gzip).
transports.ws.urlstringThe socket path: /v1/stream.
transports.ws.enabled, .dialects, .body_encodings, .compactWhether the socket is served; zstd, deflate, json; json; [] (no compact bodies yet).
transports.ws.beat_ms, .every_msinteger, integer[]millisecondsThe beat period (5000); the every_ms rungs.
transports.ws.list_sep, .gap_noticestring, boolean+; true: a gap always precedes a body after messages you did not get.
topics[]object[]One per family: name, path (the REST route whose body it carries), params ([]), scopes, schema (the body's version), encoding (json), kind (snap or append). Build topic names from it.
catalogue.hashstringThe hash of the topic list. When it changes, a socket opened with an older token gets the full hello.
catalogue.url, catalogue.inlinenull, object or nullNo separate catalogue address; inline is the full hello's topic description when you asked catalogue.inline and hold another hash.
limits.rate_bucket, .rate_limit, .rate_window_s, .rate_leftstring, integer or nullrequests; secondsYour key's REST budget: per_key, the plan's sustained rate over a 60 s window, the tokens left in the key's bucket now (null: no rate limit).
limits.sockets_cap, .sockets_openinteger or null, integerconnectionsThe plan's connections at once (null: no limit; 1 for a quotes-only plan), and the key's open now.
limits.topics_capintegertopicsTopics one socket may hold.
limits.symbols_per_connection, .quotes_symbols_per_connectionintegersymbolsThe plan's symbols per connection (0: quotes only); the quotes allowance (50).
limits.subscribes_per_minuteintegertopicsTopics attached a minute per key.
neg_token.neg, .expires_in_sstring, integersecondsThe token for the socket's neg; good for 300 s, for any socket of this key. It is not a credential: the key still goes in token.
/v1/stream/ticketA one-use ticket, good for 60 seconds, so a web page can open the socket without ever holding your key. Your server asks for it and hands the page the ticket with the negotiation's neg.
ticketstringOpaque, starts dft_tk_; one use; send it as the socket's token.
expires_inintegersecondsThe ticket's life from now: 60.
| Family | Topic | What it sends |
|---|---|---|
| levels | levels:SPY:exp4:- | Key levels, walls, totals and the flow summary, as /levels answers them. Scope latest for the nearest expiration, or expN for the nearest N: 4, 5, 10, 20 or 50. |
| bars | bars:SPY:-:- | Spot bars: the current minute's, then each change. When the minute turns, the finished bar, then the new one. |
| flow | flow:SPY:all:- | Flow totals, the current one-minute bucket and the classification. |
| tape | tape:SPY:all:- | Option prints as they arrive, each one once. |
| sweeps | sweeps:SPY:all:- | Sweeps as they're found, with any new unusual prints. |
| quotes | quotes:AAPL:-:- | A stock's, ETF's or index's quote when it changes. Up to 50 per connection on every plan, apart from the plan's symbols. |
levels:SPY:exp4:-
What it sends: Key levels, walls, totals and the flow summary, as /levels answers them. Scope latest for the nearest expiration, or expN for the nearest N: 4, 5, 10, 20 or 50.
bars:SPY:-:-
What it sends: Spot bars: the current minute's, then each change. When the minute turns, the finished bar, then the new one.
flow:SPY:all:-
What it sends: Flow totals, the current one-minute bucket and the classification.
tape:SPY:all:-
What it sends: Option prints as they arrive, each one once.
sweeps:SPY:all:-
What it sends: Sweeps as they're found, with any new unusual prints.
quotes:AAPL:-:-
What it sends: A stock's, ETF's or index's quote when it changes. Up to 50 per connection on every plan, apart from the plan's symbols.
Also profile, contracts, surface, drift, imap and core. The negotiation lists every family with its scopes and the REST route its body comes from.
Your plan sets the symbols per connection and the connections at once. A symbol counts once however many families carry it, and quotes don’t count.
| Plan | Symbols per connection | Connections at once |
|---|---|---|
| Free | Quotes only | 1 |
| Basic | 5 symbols | 1 |
| Trader | 25 symbols | 2 |
| Developer | 50 symbols | 2 |
| Quant | 50 symbols | 5 |
Quotes only
Connections at once: 1
5 symbols
Connections at once: 1
25 symbols
Connections at once: 2
50 symbols
Connections at once: 2
50 symbols
Connections at once: 5
| Close code | Reason | Reconnect |
|---|---|---|
| 4001 | unauthorized: no key, or an unknown one. | No |
| 4001 | invalid_ticket: a ticket unknown, already used, or older than 60 s. | With a new ticket |
| 4001 | key_revoked, plan_ended: the key was revoked, or the plan ended. | Once restored or renewed |
| 4003 | attestation_required, attestation_pending: no current OPRA attestation, or support's grace. | Once attested |
| 4003 | negotiation_required, negotiation_principal: no current neg, or one that another key negotiated. | After negotiating |
| 4003 | connect_rate, connection_limit: too many connection attempts, or too many connections at once. | Later, or after closing one |
| 4400 | invalid_api, invalid_encoding, invalid_compact: a query value that isn't accepted. | Once fixed |
| 4408 | no_topics: nothing subscribed for two minutes. | When you have a topic |
| 4429 | message_flood: your messages faster than the socket takes them. | Yes, backing off |
| 1009 | A message of yours over 64 KiB. | Yes |
| 1011 | backend_unavailable: the socket couldn't be registered. | Yes, backing off |
| 1013 | slow_consumer: messages to you went undrained too long. | Yes, backing off |
unauthorized: no key, or an unknown one.
Reconnect: No
invalid_ticket: a ticket unknown, already used, or older than 60 s.
Reconnect: With a new ticket
key_revoked, plan_ended: the key was revoked, or the plan ended.
Reconnect: Once restored or renewed
attestation_required, attestation_pending: no current OPRA attestation, or support's grace.
Reconnect: Once attested
negotiation_required, negotiation_principal: no current neg, or one that another key negotiated.
Reconnect: After negotiating
connect_rate, connection_limit: too many connection attempts, or too many connections at once.
Reconnect: Later, or after closing one
invalid_api, invalid_encoding, invalid_compact: a query value that isn't accepted.
Reconnect: Once fixed
no_topics: nothing subscribed for two minutes.
Reconnect: When you have a topic
message_flood: your messages faster than the socket takes them.
Reconnect: Yes, backing off
A message of yours over 64 KiB.
Reconnect: Yes
backend_unavailable: the socket couldn't be registered.
Reconnect: Yes, backing off
slow_consumer: messages to you went undrained too long.
Reconnect: Yes, backing off