Skip to main content
This page is the REST and WebSocket contract for the RFQ Network: how to authenticate, which scope each endpoint needs, how to create and accept RFQs, what states a resource moves through, and how to consume the replayable event stream. Read Overview first for the two roles and the consistency rules; use FIX protocol instead if you connect over FIX. Use REST for commands and snapshots and WebSocket for cursor-replayable lifecycle events. The production base URL is https://rfq.kairos.trade.

Authentication

Two credential forms are accepted. API credentials use all three headers together:
Bearer JWTs are also accepted: RS256, issuer kairos.trade, audience kairos-api, with sid and ver: 1 claims and a required exp. Every JWT is checked for revocation on connect and, on the WebSocket, again every 30 seconds. Authentication fails closed with 503 auth_dependency_unavailable when revocation status cannot be verified. API-credential principals carry the scopes stored on the credential, and an ipWhitelist on the credential is enforced against the client IP.
Gotcha: an IP-allowlist miss is a 403, not a 401. A non-allowlisted source gets 403 forbidden, which is easy to misread as a scope problem. Check the source address before you re-issue credentials.

Scopes

The scope * satisfies every check.
Gotcha: a JWT with no scopes claim cannot quote. It is granted rfq:read, rfq:create, and rfq:accept — not rfq:quote. Market makers need a credential that carries rfq:quote explicitly.

Endpoint index

/v1/venues/capabilities needs authentication but no particular scope.

Rules that apply to every request

The requester flow

  1. Check venue capabilities. GET /v1/venues/capabilities tells you what each venue currently supports. Do this before selecting venues — see Create an RFQ for what happens when a venue cannot accept requester fanout.
  2. Create the RFQ. POST /v1/rfqs with a fresh Idempotency-Key returns 201 and an rfq_id.
  3. Watch for quotes. Connect to GET /v1/stream and consume quote.created / quote.revised / quote.withdrawn, or poll GET /v1/rfqs/{rfq_id}/quotes.
  4. Accept one. POST /v1/quotes/{quote_id}/accept returns 202 and an acceptance in pending_routing. This reserves quantity; it does not fill.
  5. Follow the acceptance to a terminal state via GET /v1/executions/{acceptance_id} or the acceptance.* events. executed also emits execution.normalized.v1.
  6. Cancel what you no longer want. POST /v1/rfqs/{rfq_id}/cancel moves the RFQ to cancelled and its live quotes to withdrawn.

The market-maker flow

  1. Confirm your credential carries rfq:quote. A scopeless JWT does not.
  2. Discover open venue RFQs with GET /v1/exchange-rfqs, or consume the maker broadcast stream, which callers holding rfq:quote receive on GET /v1/stream.
  3. Quote it. POST /v1/quotes returns 201.
  4. Revise or withdraw. PUT /v1/quotes/{quote_id} requires the new revision to be exactly current + 1; DELETE /v1/quotes/{quote_id} withdraws.
  5. Watch quote.disposition and the acceptance.* events for the outcome.

Create an RFQ

Creates a requester RFQ and fans it out to the selected venues. Returns 201. Scope: rfq:create. Requires an Idempotency-Key header.

Example

Validation

A request that breaks any of these is rejected 400 invalid_request. Decimal quantities and prices are strings.

Gotchas

Idempotency ignores the body. Keys are (principal, operation, key). Replaying a key returns the resource the first call created and never performs a second mutation — so a replay with a different body still returns the original resource. Treat a key as bound to one command.
Requester fanout is not live yet. Every venue adapter currently reports requester_rfq: false, so each entry in venues comes back as a targets entry with state: "excluded" and a reason, plus a mapping_warnings entry. The 201 is not evidence that anything reached a venue. Read /v1/venues/capabilities before relying on fanout.

List your RFQs

Scope: rfq:read.

Discover venue RFQs

Lists the venue-originated RFQs currently open for you to quote. Scope: rfq:quote.

Example

Response

Results are expiry ordered and include an opaque next_cursor plus observed_at, which reports the local projection’s last mutation time.

Errors

Lifecycle states

RFQ

Quote

Acceptance

Gotcha: acceptance is a reservation, not a fill. It binds quote_revision, side, quantity, expected_price, and mapping_snapshot_id atomically and holds reserved_quantity on both the RFQ and the price level until the acceptance reaches a terminal state. Do not book a trade on the 202.

Errors

Errors are JSON objects whose only field is error, except where noted.

Stream recovery

Scope: rfq:read. Upgrades to a WebSocket (101).
  1. Connect to /v1/stream.
  2. Persist the sequence of the last event you processed.
  3. On reconnect, pass after=<sequence> to replay missed durable events.
Events are delivered as JSON text frames with sequence, event_id, resource_id, type, occurred_at, and data. The gateway polls roughly every 250 ms and sends at most 250 events per poll.
Gotcha: the stream is server-push only. There are no client-to-server messages, and after is the sole subscription control — you cannot filter or subscribe selectively once connected.
Gotcha: always process events idempotently. A reconnect can redeliver an event that was received immediately before the disconnect.

Event types

Callers holding rfq:quote additionally receive the maker broadcast stream of venue-originated RFQ events.

Close codes

Scope and authentication failures happen before the upgrade and surface as an ordinary HTTP error rather than a close frame.

Machine-readable contract

The complete machine-readable contract lives in the RFQ service’s spec/openapi.yaml and spec/asyncapi.yaml files.