{"$comment":"The feed stream's protocol, version 1: what a client sends, what the stream sends besides the frames of feed_frame.v1.json, the close codes, the limits, how a client keeps a correct copy of the feed, and the words the reference uses (glossary). The messages' JSON Schemas are feed_stream.v1.schema.json; the reference is https://api.quanor.com/api/docs/stream. Every message is one JSON text frame. A reader ignores fields, frame types, item kinds and part kinds it does not know, reads a value it does not know, of a field that names a kind, a state, a reason or a code, as unknown, and reconnects with backoff on a close code it does not know.","endpoints":{"api":"`wss://api.quanor.com/v1/stream/feed`, with `Authorization: Bearer <API key>` on the upgrade, from a server: a browser cannot set that header on a WebSocket. The same endpoint answers at `wss://api.quanor.com/api/v1/stream/feed`, for good: a client connected there changes nothing. An HTTP answer to the upgrade (503 while the stream restarts or is full, another status while no stream answers) or none at all: retry with backoff."},"client":{"subscribe":{"$comment":"First after the socket opens, and again after catch_up or resync: from `since` (a feed cursor, its position at most 9223372036854775807; null for the present), the mirrors' selections, the served language. Answered by ready; or in its place by catch_up or resync, when the stream cannot send the changes since the cursor; or by a close 1012 (positions_unknown), when it cannot answer the cursor yet. Send `lang` en or sv, the values the schema names: the stream serves any other in English rather than refusing it (GET /v1/feed refuses one, 422). A socket keeps the language of its first subscribe, the one its access was resolved in: a later subscribe with another `lang` is refused (4400), and one without `lang` is read as en. Send `v`, the protocol's version, 1, as the schema requires: the stream reads a subscribe without it as version 1, and refuses one naming a version it does not speak (4400, `unsupported_version`).","example":{"type":"subscribe","v":1,"since":"1.1044","lang":"sv","selections":{"feed":{"companies":[28,706],"teasers":true},"home":{"item_kinds":["announcement"]}}}},"selection":{"$comment":"A mirror's selection set, changed or (null) removed on the same socket; answered by selection_ack in order. Changes for the mirror are sent from its ack on: a client that read the mirror's window before the ack reads, after the ack, the changes from that read's through to at least the ack's cursor (POST /v1/feed/since, that mirror alone), so none is missed.","example":{"type":"selection","mirror_id":"company:28","selection":{"companies":[28]}}},"ping":{"$comment":"Answered by pong with the same t, a JSON number (any other t is refused, 4400), after the frames queued before it: a client that takes any message as the socket alive never drops a socket that is delivering a burst.","example":{"type":"ping","t":1791439215123}},"pong":{"$comment":"The answer to the stream's ping.","example":{"type":"pong","t":1791439230456}}},"stream":{"ready":{"$comment":"After subscribe: the cursor the frames that follow continue, the listener's access version, the stream's clock, the processing rows it holds, each with m, the mirrors it is for, as a frame carries (processing_complete false until the stream holds every row pending: a client keeps its own rows), and from here on the changes its mirrors take. After ready come the changes above its cursor that the stream's memory holds and its mirrors take — none from the head, and from the present at most a change held above a gap — then through: what comes between a ready and its through is the stream's memory answering the cursor (a resume); what follows is live. In place of that through comes catch_up, when the stream's memory let go of changes the resume was still owed, or resync, when the database was restored meanwhile. v is the protocol version the stream speaks on this socket; lang the language served, the one its access was resolved in (en for a language the feed is not written in, whatever the subscribe named); ping_interval_ms how often the stream pings it (stream.ping), which a client's own liveness deadline is read from.","example":{"type":"ready","v":1,"cursor":"1.1044","access_version":"b4d934cf","lang":"sv","ping_interval_ms":30000,"server_time":"2026-10-08T06:00:00.120000Z","processing":[],"processing_complete":true,"n":1}},"selection_ack":{"$comment":"The mirror's selection is in force from here on: what is sent after the ack is the new selection's. cursor is the highest position the stream had heard as it acknowledged, at or above the cursor its frames carry: a change heard above a gap is sent at once, under the selections then in force, so a read of that mirror alone (POST /v1/feed/since), issued after the ack from the mirror window's through, reads at least to it (client.selection). Absent only while the stream has heard no head, before any subscribe is answered.","example":{"type":"selection_ack","mirror_id":"company:28","cursor":"1.1046","n":2}},"through":{"$comment":"Every change at or below the cursor is accounted for. Sent after a ready, once the changes the stream's memory holds above its cursor are sent (or there were none), closing the resume, unless catch_up or resync ends the resume first; and to a listener that has had no frame for 30 s while the cursor moved.","example":{"type":"through","cursor":"1.1050","n":3}},"catch_up":{"$comment":"The stream cannot send the changes since the cursor (below its memory, or another epoch it still knows): read them (POST /v1/feed/since) up to through and beyond, then subscribe again from the read's own through. A client holding no selection reads nothing and subscribes from through, whatever its epoch. No change is sent until then (status and access_changed still are).","example":{"type":"catch_up","through":"1.1050","n":2}},"resync":{"$comment":"The cursor cannot be answered at all (a restored database; or a cursor beyond every position the stream has heard, still so after its next head): read the windows again and subscribe from their cursor. No change is sent until then (status and access_changed still are).","example":{"type":"resync","n":2}},"access_changed":{"$comment":"The listener's access changed: frames and pages read under an older access version are superseded; read the windows again. Its processing rows are superseded too: drop them here, and the processing frames that follow are the rows held that the new access is sent (none for a level sent no processing rows). A ready still queued when the access changed is sent with no processing rows.","example":{"type":"access_changed","access_version":"0ae8787e","n":7}},"status":{"$comment":"The builder — the Quanor process that writes the feed's changes — is silent (nothing heard from it for 5 s), or live again when it next speaks. Sent to every listener, whatever it is waiting for, each time the builder falls silent or speaks again. Every ready resets a client to live, so a ready sent while the builder is silent is followed by status silent.","example":{"type":"status","builder":"silent","n":8}},"refetch":{"$comment":"An item changed but its frame was too large to send: read it.","example":{"type":"refetch","feed_item_id":"announcement:88001","n":9}},"reconnect":{"$comment":"The stream closes this connection 1012 next and suggests waiting reconnect_after_ms first, as the close's reason says too: sent last before every 1012 that suggests a wait (the stream draining its sockets, a connection's lifetime, a change of access that starts or stops flash rows or processing rows), when the listener's queue has room for it. Some WebSocket stacks (Safari's, and every browser's on iOS) report a 1012 as 1005 with no reason, so a client reads the wait here; one that does not know the frame ignores it, and the close's reason is unchanged.","example":{"type":"reconnect","reconnect_after_ms":1500,"n":12}},"ping":{"$comment":"Every 30 s. A listener that sends nothing for 75 s, not even the pong, is closed 4408 (idle) and dropped. t is the stream's clock as it queued the ping, in ms since the epoch: a sample of that clock which is late, never early, by the frames queued before it (a client can estimate the stream's clock from it, as from ready's server_time).","example":{"type":"ping","t":1791439230456}},"pong":{"example":{"type":"pong","t":1791439215123}}},"per_listener_fields":{"$comment":"Added by the stream to every frame of feed_frame.v1.json it sends: n (this connection's counter, contiguous), cursor (every position at or below it is accounted for once this frame is — sent, not for this listener, or replaced by a later change of its item: the watermark after this frame, which can be below, at or above its seq; a frame sent from the stream's memory after a subscribe carries its own place, the lower of its seq and the watermark), m (the mirrors the frame is for), access_version.","example":{"type":"item","op":"new","feed_item_id":"announcement:88001","seq":1047,"n":4,"cursor":"1.1047","m":["feed"],"access_version":"b4d934cf"}},"close_codes_for_api_keys":{"$comment":"What an API key's client does on each close. One that should stop never reconnects with the same key: every attempt is refused again. On a close code this table does not name: reconnect with backoff.","1012":"reconnect after the wait the reason suggests (reconnect_after_ms=<n>, also sent just before the close as a reconnect frame) or the backoff's next wait, whichever is longer, and subscribe from the cursor held","4008":"reconnect at once and subscribe from the cursor held; take frames faster (read the socket on its own task)","4400":"a message the stream will not read (the reason says which; `unsupported_version`: speak a version the stream does): stop and fix it — reconnecting sends it again. The one reason that names no message is `no_subscribe` (no subscribe within limits.subscribe_within_s of the socket opening): reconnect with backoff, and send subscribe as the socket opens","4401":"no key was presented, or it is unknown, revoked or expired: stop and check the key","4403":"the key was revoked, rotated or expired while connected, or it may not stream (can_websocket): stop and check the key","4408":"reconnect with backoff and subscribe from the cursor held; answer the stream's ping within limits.idle_after_s (read the socket on its own task)","4429":"too many sockets for this key, or messages sent too fast: reconnect with backoff, and hold fewer sockets","4503":"the stream cannot confirm the key's access now: reconnect with backoff"},"limits":{"$comment":"The bounds a listener meets. A company or market id above id_max is refused, as is a selection naming too many. A socket that sends no subscribe within subscribe_within_s of opening is closed 4400 (no_subscribe). A message over message_bytes is refused (4400, too_large). A listener that does not take its frames is closed 4008: when a frame queued for it has waited send_deadline_s unwritten (send_deadline), or when more than queue_bytes are queued for it (queue_full). No item frame the stream sends is over frame_bytes, before the fields it adds (per_listener_fields). A processing row nothing has resolved processing_row_s after it was first sent is resolved `expired` (processing_resolved). A client drops a part sent ahead that no item frame accounts for within ahead_kept_s of hearing a live builder (reading.order): the stream holds no part sent ahead. What a socket sends is counted in four budgets, each refilled per second up to its burst — bytes, commands (subscribe and selection), the ids their selections name (each company and market id, the first subscribe's included) and other messages (ping, pong, any the stream ignores, and each of WebSocket's own ping and pong frames, as a client library's keepalive sends them) — and a socket past any is closed 4429 (rate): a client reconnects with backoff.","subscribe_within_s":5,"ping_every_s":30,"idle_after_s":75,"send_deadline_s":10,"queue_bytes":4194304,"lifetime":"under 24 h: the stream ends a connection with 1012 and a reconnect hint before then","lifetime_under_s":86400,"reconnect_after_ms_max":5000,"mirrors_per_socket":8,"mirror_id_bytes":128,"companies_per_selection":2000,"markets_per_selection":64,"id_max":2147483647,"message_bytes":221632,"frame_bytes":800000,"lang_bytes":16,"inbound_bytes_per_s":65536,"inbound_bytes_burst":886528,"commands_per_s":2,"commands_burst":16,"selection_ids_per_s":2064,"selection_ids_burst":33024,"other_messages_per_s":4,"other_messages_burst":64,"processing_row_s":120,"ahead_kept_s":600,"sockets_per_key":"at most the number a key may hold at once — the reference page's Connect section says how many; one more is closed 4429","history":"a key with a history window (history_days) is sent nothing of an item that sorts before it — live, on a resume or as a part — as /v1 reads nothing of it","frame_too_large":"an item whose frame is too large to send is announced by refetch: read it (GET /v1/feed/items/{feed_item_id})"},"reading":{"$comment":"How a client keeps a correct copy of the feed from the frames.","start":"Read each selection's first page (GET /v1/feed, a read for each selection), and older pages as wanted (next_cursor); then subscribe, with those selections, from the lowest through of the reads the copy was made from. Each page holds its items as of its own through, so every change past the lowest reaches the client, and one a later read already held changes nothing (order). Two feed cursors order by epoch, then by seq, each as an integer: 1.9 is before 1.10, and 1.10 before 2.0. A client that keeps no history subscribes with since null: from the present.","order":"Apply an item frame only when its seq is higher than the position the copy holds the item at, or last removed it at (hidden, withdrawn): the stream sends a position as soon as it is accounted for, so an item's frames can arrive out of order. Apply a part frame (provisional) only when its part_version is higher than the part held: of two copies of a part the higher part_version is the later, so an item frame keeps a part held at a later version than its own. A part frame has no seq, being at no position of the feed, and carries cursor as every frame the stream sends does (cursor). A part frame of an item not held makes the item from the frame's head, unless the item was removed. A part the stream sent ahead (a part frame) stays in its item until an item frame holds it at its part_version or later, or names it in superseded_parts: an item frame built before the part was written arrives after it and says nothing of it, so the part is kept beside the parts that frame holds. Drop the parts an item names in superseded_parts, and a part frame whose part_id the item held names there. A part no item frame has accounted for within limits.ahead_kept_s of hearing a live builder is dropped: one its item's builds never included (while the socket is down, before a ready and while status is silent, its item frame is late, not missing).","cursor":"Keep the cursor of the last frame (or `through`) received — never a `selection_ack`'s, a bound to read to — and subscribe again from it after any reconnect.","resume":"What comes between a ready and its through happened while the client was away (the stream's memory answering its cursor); what follows is live. A ready is followed by its through (exchanges.resume), or by catch_up or resync in its place (stream.ready): do as that frame says.","catch_up":"Read POST /v1/feed/since with the cursor held, the selections subscribed with and the filters the windows were read with in its body ({\"since\": cursor, \"selections\": selections, ...filters}), page by page until complete, applying each frame; then subscribe from the last through. A change written quietly (an item older than a day made again with nothing changed at its source — a new rendering, a company's new name — which changes no one's access) is not among them: a read of GET /v1/feed carries it. A correction or an update of what an item shows is, however old the item.","resync":"Read GET /v1/feed again for each selection, replace the copy, and subscribe from the lowest through of those reads (start).","access_changed":"Everything read under an older access_version is superseded: read the windows again, replacing the copy and the removals it remembered (a seq is a position of its own access's rendering); drop processing rows. Keep the socket and the cursor held: the stream goes on under the new access, and is not subscribed to again. A ready naming another access_version than the copy was read under (its pages' access_version) says the same of a change while the client was away, or since its read: do the same, as an item that did not change since is not sent again. A window or a since page naming another access_version than the copy's was read across a change, as were windows that name different ones: do the same.","hidden":"Drop the item and remember its seq (order): only a later item frame brings it back; withdrawn: the same, and its replaced_by names the item it was merged into or replaced by (a feed_item_id; null: gone), an item of its own that is sent and read as any other. A hidden is for the selections it names (m): an item that leaves one or more of a client's selections while another takes it, newly or still, is sent hidden for those it left and, at that same seq, the item for those that take it (a refetch in its place, for a change too large to send). A hidden can also come again, at a later seq, for a selection the item had already left, with nothing for those that still take it. A copy for each selection needs nothing more. A client keeping one copy for several remembers which selections hold each item (an item frame's m, the selection a window was read for), drops it only when none does, and applies an item frame at the seq a hidden removed it at when the frame names a selection the hidden did not.","refetch":"Read the item (GET /v1/feed/items/{feed_item_id}) and apply its frame by seq, before keeping a cursor sent after the refetch: its change is accounted for, and a resume from a later cursor does not send it again. A refetch names no mirror: the frame is for each selection that takes the item (glossary: selection). A 404 under the access_version the copy was read under says the key is no longer shown the item: drop it. A 404 naming another access_version says the key's access changed: do as access_changed says.","status":"The builder is silent (stream.status): changes are late, none is lost. Nothing to do.","processing":"With can_pending, ready.processing holds the rows pending at connect; processing frames add one and processing_resolved drops it, whatever its resolution; a row nothing resolves is resolved `expired` after limits.processing_row_s. A resolution can be lost on its way, so a client also drops a row it has held longer than limits.processing_row_s. A row's item is the one whose head.edition_ids holds its edition_id.","unknown":"Ignore fields, frame types, item kinds and part kinds this version does not name. Read a value it does not name, of a field that names a kind, a state, a reason or a code, as unknown, never as an error; on a close code it does not name, reconnect with backoff."},"glossary":{"$comment":"The words the reference uses, as an API key's client meets them.","feed item":"One thing in the feed, held at its latest state and named by `feed_item_id`: for an API key, an announcement (its flash rows, then its story; a report's is its report story) or a pre-earnings analysis.","part":"One part of an announcement: a flash row (`flash`) or its story (`story`; a report's is its report story), named by `part_id`. Of two copies of a part, the higher `part_version` is the later.","flash row":"A headline and its detail from Quanor's first reading of an announcement, written within seconds of it; an announcement can have several.","part sent ahead":"A flash row sent alone, as a part frame (`provisional`), the moment it is written: before the item frame that holds it.","feed position":"A change's place in the feed (`seq`). Every position is accounted for (its change sent, not for the listener, or replaced by a later change of its item), so a client that holds every one up to N holds every change up to N. Positions compare within one epoch, and an item's within the access version it was rendered under: a change of access replaces the copy, not the cursor (reading.access_changed).","epoch":"The numbering positions are in: a feed cursor's first number. A restored database starts a new one, and the stream says `resync`.","feed cursor":"<epoch>.<seq>: a watermark, every change at or below seq accounted for. What a client resumes from: a bound on positions, not a frame's own position, though it can equal one.","through":"The feed cursor a page or a catch-up read accounts for; and the stream's message that says the same of what it has sent.","selection":"What one mirror asks the feed for: companies, markets, item kinds, part kinds, teasers. It takes an item that passes each field it names (`company.company_id` among companies, `company.market_exchange_id` among markets, `item_kind` among item_kinds, an announcement with a part of one of part_kinds; a teaser has no parts), and a teaser (`access: teaser`) only when it asks for teasers.","mirror":"One selection on a socket, under a mirror id the client chooses. Each frame names the mirrors it is for (`m`).","listener":"One open subscription to the feed stream (a socket, from its subscribe on), with its own access, cursor and counter (`n`).","window":"One selection's items as `GET /v1/feed` reads them: page by page from the newest arrival, each page as of its through.","page cursor":"`next_cursor`: where the next, older page of a window starts. Opaque: pass it back unchanged, never build one.","copy":"The items a client holds, read from the windows, then kept current by the frames: a copy for each selection, or one for several (reading.hidden).","access version":"The access a key's frames and pages are rendered under (`access_version`). When it changes, what was read under the older one is read again (`access_changed`).","access level":"What a key is sent of an item: `api` with `can_access_detail`, `api_list` without it, `api_teaser` as a teaser. The examples name theirs (`item.announcement.api_list.en`).","teaser":"An item of a company outside the key's `allowed_company_ids`, sent to a selection that asks for teasers: that it exists, its company, kind and time (`access: teaser`).","announcement head":"What an announcement shows around its parts (`head`): its title, kind, route, inside-information mark, editions and times.","feed head":"The highest feed position written, which Quanor's builder announces to the stream every second; a client is not sent it. Not an announcement's head.","edition":"An announcement as one provider and one distributor delivered it, in one or more languages: the document readers see. `head.edition_ids` lists an announcement's editions; a processing row names one.","processing row":"An announcement Quanor is reading before its first flash row, sent to a key with `can_pending` (`processing`, `processing_resolved`).","builder":"The Quanor process that writes the feed's changes. The stream says when it falls silent and when it speaks again (`status`).","the stream's memory":"The latest changes the stream holds, from which it answers a subscribe's cursor (a resume). A cursor below it is told `catch_up`."},"filters":{"$comment":"How a client applies the filters to the items it holds. GET /v1/feed and POST /v1/feed/since take them by these names; the stream never applies them. A client that read its windows with them holds each item to them as frames of it come — live, on a resume, from a catch-up or a refetch: one that fails a filter is kept in the copy and not shown, never applied as hidden, and its next frame — an item frame, or a part sent ahead of one — decides again. Only a hidden or a withdrawn frame the client is sent removes an item (a catch-up read with the filters sends one that no longer passes them as hidden). Each filter reads the item as the client holds it: its latest item frame's fields, less the parts that frame names in superseded_parts, with the parts sent ahead of it (reading.order). Items of another item_kind pass relevance_floor, story_treatment and reports exclude, and fail reports only, inside_information and announcement_kind. A teaser (access: teaser) shows none of what they read and fails each one set (reports include is none). The reference client's passes() is this table, held by the API's tests to the cases the feed's own filters are held to.","relevance_floor":"Above 1, the highest relevance of the item's parts reaches it — its flash rows (every one, or with the list, flash_count present, the first written) and its story — or its story is a report story (its report set), which passes any floor, as /v1/analyses keeps every report as material. A relevance of null is none, and an announcement with none known fails until a frame brings one.","announcement_kind":"head.announcement_kind is one of those listed, or null (not known yet).","story_treatment":"The story part's treatment is one of those listed, or there is no story part (none written yet), or the story is a report story (its report set), which is shown as its report, never as a Story or a Brief. Both full and brief is no filter.","reports":"include (the default) is no filter; only: head.route is report; exclude: head.route is not report (null, not known yet, passes).","inside_information":"An announcement that is not a report: head.inside_information is true; false and null (not known yet) fail, and a later frame decides again. A report (head.route is report) passes, and follows reports alone, because a report's own flag is unreliable: with reports include this keeps inside information and every report, with exclude inside information that is no report."},"exchanges":{"$comment":"What a client meets, in order: each message the client sends (from client) or the stream sends (from stream), with the fields that say the order; n, access_version, server_time and the frames' content are left out.","resume":{"$comment":"A subscribe from a cursor the stream still remembers, the feed's head at 1.1046: its ready, the changes above the cursor its mirrors take, then through, which closes the resume; what follows is live.","messages":[{"from":"client","type":"subscribe","since":"1.1044"},{"from":"stream","type":"ready","cursor":"1.1044"},{"from":"stream","type":"item","seq":1045,"cursor":"1.1045"},{"from":"stream","type":"item","seq":1046,"cursor":"1.1046"},{"from":"stream","type":"through","cursor":"1.1046"}]},"at_the_head":{"$comment":"A subscribe from the head: nothing above it to send, and its through all the same.","messages":[{"from":"client","type":"subscribe","since":"1.1046"},{"from":"stream","type":"ready","cursor":"1.1046"},{"from":"stream","type":"through","cursor":"1.1046"}]}}}