{
  "asyncapi": "3.0.0",
  "info": {
    "title": "Zodia Markets RFS WebSocket API",
    "version": "3",
    "description": "Client-facing WebSocket protocol for the Zodia Markets Request-For-Stream (RFS) brokerage service: streaming two-way prices, order execution on a streamed quote, and account-group lookup.\n\nA single connection carries every message, in both directions. Each message names itself in its `messageType` property — pinned here with `const` — and that property, not the order or the timing of arrival, is what tells one message from another. Dispatch on it.\n\nVersioning follows the REST API that issues the connection token: this document describes version 3. Within a version, change is additive — a later release may introduce a message type, or a property on an existing message — so a connection may carry more than this document describes.\n\nScope: the messages the published reference pages document. A connection may carry frames that are not described here; those are not part of this contract and may change or disappear without notice.",
    "contact": {
      "name": "Zodia Markets",
      "url": "https://docs.zodiamarkets.com"
    }
  },
  "servers": {
    "production": {
      "host": "trade-uk.zodiamarkets.com",
      "protocol": "wss",
      "description": "Production, UK entity. Replace `trade-uk` with `trade-ame` if you are onboarded with the AME entity."
    },
    "sandbox": {
      "host": "trade-uk.sandbox.zodiamarkets.com",
      "protocol": "wss",
      "description": "Sandbox, UK entity."
    }
  },
  "channels": {
    "wsClient": {
      "address": "/zm/ws/ws-client",
      "title": "RFS client channel",
      "description": "The single client-facing WebSocket endpoint. Every message in either direction travels over this one connection and is identified by its `messageType`.\n\nTwo query parameters are relevant to a client integration:\n\n- `token` (required) — a single-use token from `POST /api/3/zm/rest/auth/token`. Presenting it consumes it, so every connection and reconnection needs a freshly requested one, and it expires seconds after issue (30 seconds in the shipped configuration). Request it immediately before connecting.\n- `sessionId` (optional) — the session this connection joins. Supply it to make order outcomes recoverable across a reconnect; see [Order Sessions and Recovery](https://docs.zodiamarkets.com/reference/order-subscriptions). 1–64 characters, and it must not contain `/`, whitespace or control characters; dots are allowed and the value is percent-decoded before it is validated. A value that breaks those rules refuses the connection: one `error` frame, then the socket closes. Omitted or empty, the gateway mints a fresh identifier for the connection, so no WebSocket redelivery is possible across a reconnect. The outcome itself is not lost: it stays readable for 24 hours from the REST order-state endpoints. Connecting again with a `sessionId` that is already live for your account closes the earlier connection.",
      "messages": {
        "subscribeRequest": {
          "$ref": "#/components/messages/subscribeRequest"
        },
        "subscribeResponse": {
          "$ref": "#/components/messages/subscribeResponse"
        },
        "priceStreamResponse": {
          "$ref": "#/components/messages/priceStreamResponse"
        },
        "unsubscribeRequest": {
          "$ref": "#/components/messages/unsubscribeRequest"
        },
        "unsubscribeResponse": {
          "$ref": "#/components/messages/unsubscribeResponse"
        },
        "streamStopResponse": {
          "$ref": "#/components/messages/streamStopResponse"
        },
        "orderRequest": {
          "$ref": "#/components/messages/orderRequest"
        },
        "orderResponse": {
          "$ref": "#/components/messages/orderResponse"
        },
        "orderUnsubscribeRequest": {
          "$ref": "#/components/messages/orderUnsubscribeRequest"
        },
        "orderUnsubscribeResponse": {
          "$ref": "#/components/messages/orderUnsubscribeResponse"
        },
        "errorResponse": {
          "$ref": "#/components/messages/errorResponse"
        },
        "accountGroupsRequest": {
          "$ref": "#/components/messages/accountGroupsRequest"
        },
        "accountGroupsResponse": {
          "$ref": "#/components/messages/accountGroupsResponse"
        }
      }
    }
  },
  "operations": {
    "sendClientMessage": {
      "action": "send",
      "channel": {
        "$ref": "#/channels/wsClient"
      },
      "title": "Client to Zodia Markets",
      "summary": "Messages a client sends on the connection.",
      "messages": [
        {
          "$ref": "#/channels/wsClient/messages/subscribeRequest"
        },
        {
          "$ref": "#/channels/wsClient/messages/unsubscribeRequest"
        },
        {
          "$ref": "#/channels/wsClient/messages/orderRequest"
        },
        {
          "$ref": "#/channels/wsClient/messages/orderUnsubscribeRequest"
        },
        {
          "$ref": "#/channels/wsClient/messages/accountGroupsRequest"
        }
      ]
    },
    "receiveClientMessage": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/wsClient"
      },
      "title": "Zodia Markets to client",
      "summary": "Messages Zodia Markets sends on the connection.",
      "messages": [
        {
          "$ref": "#/channels/wsClient/messages/subscribeResponse"
        },
        {
          "$ref": "#/channels/wsClient/messages/priceStreamResponse"
        },
        {
          "$ref": "#/channels/wsClient/messages/unsubscribeResponse"
        },
        {
          "$ref": "#/channels/wsClient/messages/streamStopResponse"
        },
        {
          "$ref": "#/channels/wsClient/messages/orderResponse"
        },
        {
          "$ref": "#/channels/wsClient/messages/orderUnsubscribeResponse"
        },
        {
          "$ref": "#/channels/wsClient/messages/errorResponse"
        },
        {
          "$ref": "#/channels/wsClient/messages/accountGroupsResponse"
        }
      ]
    }
  },
  "components": {
    "messages": {
      "subscribeRequest": {
        "name": "subscribe",
        "title": "Subscribe request",
        "summary": "Request a continuous stream of prices for a specific currency pair and quantity.",
        "payload": {
          "type": "object",
          "required": [
            "messageType",
            "instrument",
            "quantity",
            "currency",
            "accountGrpUuid",
            "tenor"
          ],
          "properties": {
            "messageType": {
              "type": "string",
              "const": "subscribe",
              "description": "Must be `subscribe`. Every message on this connection is routed by `messageType`; it is the only thing that tells one message from another. A value outside the protocol's set is discarded before any handler sees it — no `error` frame, no reply at all — so a misspelling here is indistinguishable from a lost message."
            },
            "instrument": {
              "type": "string",
              "description": "The pair to price, written `BASE.QUOTE` — `USDC.AED`, `BTC.USD`. Matched against the streaming instruments by exact, case-sensitive comparison: a lower-case or otherwise reformatted pair is not rejected as malformed, it simply matches nothing and the subscription fails with `RFS100011`. `currency` below decides which of the two halves `quantity` is denominated in."
            },
            "quantity": {
              "type": "string",
              "description": "The size to be priced, as a decimal string, denominated in `currency`. Prices are quoted for this size specifically — a different size is a different subscription, not a different view of the same one. Send no more than 8 decimal places: beyond that the value is rounded, and the quantity echoed back can sit one unit below the size actually being priced."
            },
            "currency": {
              "type": "string",
              "description": "Which half of `instrument` `quantity` is expressed in. Must be exactly one of the pair's two currencies, or the request is refused with `RFS100008`. The choice changes the shape of every price update: name the base currency and `quantity` is fixed on both sides while the quote amounts differ; name the quote currency and `quoteAmount` is fixed while the base quantities differ between bid and offer."
            },
            "accountGrpUuid": {
              "type": "string",
              "description": "The account group to price against, from the [Account Groups](https://docs.zodiamarkets.com/reference/account-group-1) message. It selects the spread, credit limit and trading permissions applied to this subscription, so the same request against two account groups can return two different prices — or be refused on one and accepted on the other. Blank is refused with `RFS600004`. Mandatory when `beneficiaryDetails` is present."
            },
            "tenor": {
              "type": "string",
              "description": "Settlement tenor. `T` (closest available day) and `T1` (closest available day + 1) are the values to use; both are composed by Zodia Markets from the underlying venue tenors, which are themselves reachable by name — `TOD`, `TOM`, `SP`. The field is not checked against that list: any non-blank string is accepted, and a value with no live price behind it fails the subscription with `RFS100011` rather than a format error. Blank is refused with `RFS100021`."
            },
            "tag": {
              "type": "string",
              "description": "A label of your choosing, echoed unchanged on this subscription's `subscribe`, `pricestream` and `streamStop` messages. Price updates carry no `subscriptionId`, so where you hold several subscriptions on one connection the tag is what tells their updates apart — set it to something you can route on. Omitted, it comes back as an empty string."
            },
            "beneficiaryDetails": {
              "type": "object",
              "x-requirement": "conditional",
              "description": "Third-party delivery block. Send it only when the trade settles to a beneficiary's wallet instead of your own — see [3rd Party Beneficiary Delivery Price Channel](https://docs.zodiamarkets.com/reference/third-party-settlement-price-subscription). Cannot be combined with `senderDetails`.",
              "properties": {
                "beneficiaryId": {
                  "type": "string",
                  "x-requirement": "conditional",
                  "description": "UUID of pre-configured beneficiary. [Get Beneficiaries](https://docs.zodiamarkets.com/reference/get-beneficiary-list) on how to retrieve list of beneficiary UUID"
                },
                "payoutCurrency": {
                  "type": "string",
                  "x-requirement": "conditional",
                  "description": "Payout currency. Must be a currency that is contained within the instrument being subscribed to."
                },
                "networkId": {
                  "type": "string",
                  "description": "Not usually required unless using a specific network. Contact your Relationship Manager for more info."
                }
              }
            },
            "senderDetails": {
              "type": "object",
              "x-requirement": "conditional",
              "description": "Third-party collection block. Send it only when Zodia Markets collects the crypto funds from a verified third party instead of from you — see [3rd Party Sender Collection Price Channel](https://docs.zodiamarkets.com/reference/3rd-party-sender-collection-price-channel). Cannot be combined with `beneficiaryDetails`.",
              "properties": {
                "senderId": {
                  "type": "string",
                  "x-requirement": "conditional",
                  "description": "UUID of pre-configured Sender. [Get Senders](https://docs.zodiamarkets.com/reference/get-sender-list) on how to retrieve list of Sender UUID"
                },
                "payinCurrency": {
                  "type": "string",
                  "x-requirement": "conditional",
                  "description": "Pay in currency. Must be a currency that is contained within the instrument being subscribed to. Only Crypto Currencies are supported for third party collection."
                },
                "networkId": {
                  "type": "string",
                  "description": "Not usually required unless using a specific network. Contact your Relationship Manager for more info."
                }
              }
            }
          }
        },
        "examples": [
          {
            "name": "Request",
            "payload": {
              "messageType": "subscribe",
              "instrument": "USDC.AED",
              "quantity": "100000",
              "currency": "AED",
              "accountGrpUuid": "a6898bdd-856b-4259-b6e2-6ef66f2282e1",
              "tenor": "T"
            }
          }
        ]
      },
      "subscribeResponse": {
        "name": "subscribe",
        "title": "Subscribe response",
        "summary": "Confirms your subscription was successful and provides a subscription ID.",
        "payload": {
          "type": "object",
          "required": [
            "chanId",
            "timestamp",
            "messageType",
            "success",
            "message",
            "subscriptionId",
            "instrument",
            "quantity"
          ],
          "properties": {
            "chanId": {
              "type": "string",
              "description": "The connection identifier the gateway stamps on every response; echoed from the connection, not client-supplied."
            },
            "timestamp": {
              "type": "integer",
              "format": "int64",
              "description": "Unix epoch milliseconds, UTC — the moment Zodia Markets built this frame, not the moment your request arrived."
            },
            "messageType": {
              "type": "string",
              "const": "subscribe",
              "description": "Always `subscribe`. A `subscribe` request is always answered by exactly one of these, whether or not the subscription was created — read `success`, not the arrival of the message, to find out which happened."
            },
            "success": {
              "type": "boolean",
              "description": "Whether the subscription now exists. Failures are reported here, in band: a rejected `subscribe` returns `success: false` with a `code`, never a separate `error` frame, and never both."
            },
            "message": {
              "type": "string",
              "description": "`Subscribed` on success. On failure, the reason in plain text, usually naming the offending value. Intended for a human reading a log — branch on `code`, not on this."
            },
            "subscriptionId": {
              "type": "string",
              "description": "UUID naming this subscription; the value `unsubscribe` takes. Present on failures too, because it is minted before the request is validated — on a `success: false` response it names nothing and unsubscribing it is silently ignored. Key off `success` before storing it."
            },
            "instrument": {
              "type": "string",
              "description": "The pair now streaming, echoed from the request."
            },
            "quantity": {
              "type": "string",
              "description": "Your submitted `quantity`, echoed in the currency you submitted it in and normalised to 8 decimal places. It is not converted to the base currency: subscribe for 100000 AED on `USDC.AED` and this reads `100000.00000000` AED, not a USDC amount. On a rejected request the raw string you sent is echoed back unparsed, so it is not guaranteed to be a well-formed decimal when `success` is `false`."
            },
            "code": {
              "type": "string",
              "description": "Empty on success. On failure, what was wrong: `RFS100004`/`RFS100005` instrument, `RFS100008` currency, `RFS100009` quantity, `RFS100021` tenor, `RFS600004` account group, `RFS100017` you already hold this subscription, `RFS100011` no price at that size, `PRE200035` pair disabled for your account, `RFS100013` an unexpected failure our side. The full list is in the [Response / Error Code Reference](https://docs.zodiamarkets.com/reference/code-reference)."
            },
            "tenor": {
              "type": "string",
              "description": "The tenor now streaming, echoed from the request."
            },
            "settleDate": {
              "type": "string",
              "description": "Always present on the acknowledgement and always empty - the settlement date is carried on `pricestream`, not on the ack."
            },
            "tag": {
              "type": "string",
              "description": "Your `tag`, echoed. Empty string if you sent none."
            },
            "networkId": {
              "type": "string",
              "description": "The network this subscription's trade will settle over. Set only when the request carried `beneficiaryDetails` or `senderDetails`; `null` on an ordinary subscription. It echoes the `networkId` you supplied if you supplied one; otherwise Zodia Markets resolves it — `ZM_TRANSFER` for a crypto beneficiary payout, `ZM_SENDER` for a sender collection, and for a fiat payout the network chosen for that corridor, which is neither of those."
            }
          }
        },
        "examples": [
          {
            "name": "Response",
            "payload": {
              "timestamp": 1718110925811,
              "messageType": "subscribe",
              "success": true,
              "message": "Subscribed",
              "subscriptionId": "13f07bc9-055f-4054-bb78-73fe9f325ee6",
              "tag": "test-tag",
              "instrument": "USDC.AED",
              "quantity": "10000.000000",
              "code": "",
              "tenor": "T"
            }
          }
        ]
      },
      "priceStreamResponse": {
        "name": "pricestream",
        "title": "Price update",
        "summary": "A live two-way price for a subscription, carrying the quote ID an order executes on.",
        "payload": {
          "type": "object",
          "required": [
            "chanId",
            "timestamp",
            "messageType",
            "instrument",
            "quoteId",
            "offer",
            "bid",
            "tenor"
          ],
          "properties": {
            "chanId": {
              "type": "string",
              "description": "The connection identifier the gateway stamps on every response; echoed from the connection, not client-supplied."
            },
            "timestamp": {
              "type": "integer",
              "format": "int64",
              "description": "Unix epoch milliseconds, UTC — when this update was produced. Updates are pushed on a fixed cadence rather than on every market tick, so consecutive prices for one subscription are evenly spaced; compare this field rather than arrival order to tell which of two updates is the later."
            },
            "messageType": {
              "type": "string",
              "const": "pricestream",
              "description": "Always `pricestream`."
            },
            "instrument": {
              "type": "string",
              "description": "The pair being priced, `BASE.QUOTE`, echoed from the subscription."
            },
            "quoteId": {
              "type": "string",
              "description": "The token an order executes against — copy it onto `order` unchanged. It is opaque and not parseable, and it already carries the price, size, tenor and settlement date of this update, which is why an order need not repeat them (and why any it does send are ignored). It is bound to your user and expires ten minutes after issue: executing one issued to another user is refused with `RFS100020`, a stale one with `RFS100019`. It is not single-use — several orders may execute against one quote until their combined size reaches the quoted quantity, after which further orders are refused with `RFS600021`."
            },
            "offer": {
              "type": "object",
              "description": "The side you BUY on. Execute against it with `side: BUY` to buy the base currency from Zodia Markets. `offer.price` is always the higher of the two prices — the spread is applied outward from the market, raising the offer and lowering the bid.",
              "required": [
                "price",
                "quantity",
                "quoteAmount"
              ],
              "properties": {
                "price": {
                  "type": "string",
                  "description": "The price you buy at, in quote currency per one unit of base currency, as a decimal string. Parse it as an arbitrary-precision decimal: the number of decimal places is not fixed, and very small prices are rendered in scientific notation (`1E-8`)."
                },
                "quantity": {
                  "type": "string",
                  "description": "The base-currency amount on this side. Where the subscription was made in the base currency this is that amount, identical to `bid.quantity`; where it was made in the quote currency it is the quote amount divided by `offer.price`, rounded down, and therefore differs from `bid.quantity`."
                },
                "quoteAmount": {
                  "type": "string",
                  "description": "The quote-currency amount you pay for `quantity`. Where the subscription was made in the quote currency this is that amount, identical to `bid.quoteAmount`; where it was made in the base currency it is `quantity × price`, and therefore differs from `bid.quoteAmount`. Rounded to the quote currency's own precision — not to a fixed 8 places — and always in plain notation."
                }
              }
            },
            "bid": {
              "type": "object",
              "description": "The side you SELL on. Execute against it with `side: SELL` to sell the base currency to Zodia Markets. `bid.price` is always the lower of the two prices.",
              "required": [
                "price",
                "quantity",
                "quoteAmount"
              ],
              "properties": {
                "price": {
                  "type": "string",
                  "description": "The price you sell at, in quote currency per one unit of base currency, as a decimal string. Same parsing caveats as `offer.price`."
                },
                "quantity": {
                  "type": "string",
                  "description": "The base-currency amount on this side. Identical to `offer.quantity` when the subscription was made in the base currency; the quote amount divided by `bid.price` when it was made in the quote currency."
                },
                "quoteAmount": {
                  "type": "string",
                  "description": "The quote-currency amount you receive for `quantity`. Identical to `offer.quoteAmount` when the subscription was made in the quote currency; `quantity × price` when it was made in the base currency."
                }
              }
            },
            "tenor": {
              "type": "string",
              "description": "The tenor this price settles on, echoed from the subscription. `settleDate` is the calendar date it currently resolves to."
            },
            "settleDate": {
              "type": "string",
              "description": "The date this price settles on, `YYYYMMDD`. It is a property of the update, not of the subscription: as the day's cut-offs pass, a `T` subscription rolls onto a later date and this field moves with it, without the subscription being interrupted."
            },
            "tag": {
              "type": "string",
              "description": "Your `tag`, echoed from the subscription. This message carries no `subscriptionId`, so where a connection holds several subscriptions the tag — with `instrument` and `tenor` — is what routes an update to the right one."
            }
          }
        },
        "examples": [
          {
            "name": "Price Update",
            "payload": {
              "timestamp": 1729156597244,
              "messageType": "pricestream",
              "instrument": "USDC.AED",
              "quoteId": "cmEK+SwelROidy4Sn63WoWU2UJSAFOPy9Xi5UDpnCJPG5oH8ABAFv6fB...",
              "tag": "a5cf32f7-ae66-4edc-8538-f015020d1952",
              "offer": {
                "price": "3.673050",
                "quantity": "2722.533045",
                "quoteAmount": "2722.533045"
              },
              "bid": {
                "price": "3.672950",
                "quantity": "2722.607169",
                "quoteAmount": "2722.533045"
              },
              "tenor": "T",
              "settleDate": "20251021"
            }
          }
        ]
      },
      "unsubscribeRequest": {
        "name": "unsubscribe",
        "title": "Unsubscribe request",
        "summary": "Stop receiving price updates for a subscription.",
        "payload": {
          "type": "object",
          "required": [
            "messageType",
            "subscriptionId"
          ],
          "properties": {
            "messageType": {
              "type": "string",
              "const": "unsubscribe",
              "description": "Must be `unsubscribe`."
            },
            "subscriptionId": {
              "type": "string",
              "description": "The `subscriptionId` from the `subscribe` response that opened this stream. It is matched against the subscriptions held on this connection only — a value from another connection, one already cancelled, or one taken from a failed `subscribe` matches nothing, and an unmatched request draws no reply of any kind. Time out rather than waiting indefinitely for an acknowledgement."
            }
          }
        },
        "examples": [
          {
            "name": "Request",
            "payload": {
              "messageType": "unsubscribe",
              "subscriptionId": "13f07bc9-055f-4054-bb78-73fe9f325ee6"
            }
          }
        ]
      },
      "unsubscribeResponse": {
        "name": "unsubscribe",
        "title": "Unsubscribe response",
        "summary": "Confirms the subscription was cancelled.",
        "payload": {
          "type": "object",
          "required": [
            "chanId",
            "timestamp",
            "messageType",
            "success",
            "subscriptionId",
            "message",
            "instrument",
            "quantity",
            "tenor"
          ],
          "properties": {
            "chanId": {
              "type": "string",
              "description": "The connection identifier the gateway stamps on every response; echoed from the connection, not client-supplied."
            },
            "timestamp": {
              "type": "integer",
              "format": "int64",
              "description": "Unix epoch milliseconds, UTC — when the subscription was cancelled."
            },
            "messageType": {
              "type": "string",
              "const": "unsubscribe",
              "description": "Always `unsubscribe`. This message is sent only when a subscription was actually cancelled; there is no negative form of it."
            },
            "success": {
              "type": "boolean",
              "description": "Always `true`. The stream is closed and no further `pricestream` updates will arrive for this `subscriptionId`. A request that matched nothing produces no message at all rather than `success: false`, so the arrival of this message is itself the confirmation."
            },
            "subscriptionId": {
              "type": "string",
              "description": "The `subscriptionId` from your request, now cancelled and not reusable."
            },
            "message": {
              "type": "string",
              "description": "A diagnostic line naming the cancelled subscription and its size. Its wording is not part of the contract and can change without notice — log it, but do not parse it or match on it."
            },
            "instrument": {
              "type": "string",
              "description": "The pair that was streaming, from the cancelled subscription."
            },
            "quantity": {
              "type": "string",
              "description": "The size that was being priced, in the currency the subscription was made in. It may carry more trailing zeros than the value you sent; compare it as a decimal, not as a string."
            },
            "tenor": {
              "type": "string",
              "description": "The tenor that was streaming, from the cancelled subscription."
            }
          }
        },
        "examples": [
          {
            "name": "Response",
            "payload": {
              "timestamp": 1718111157195,
              "messageType": "unsubscribe",
              "success": true,
              "subscriptionId": "2ffc8d0c-21f5-4364-b2a6-4007218e57ee",
              "message": "Cancelled subscription 2ffc8d0c-21f5-4364-b2a6-4007218e57ee...",
              "instrument": "USDC.AED",
              "quantity": "100000.000000",
              "tenor": "T"
            }
          }
        ]
      },
      "streamStopResponse": {
        "name": "streamStop",
        "title": "Stream stop message",
        "summary": "The price stream for a tenor is no longer available.",
        "payload": {
          "type": "object",
          "required": [
            "chanId",
            "timestamp",
            "messageType",
            "instrument",
            "tenor",
            "code",
            "message"
          ],
          "properties": {
            "chanId": {
              "type": "string",
              "description": "The connection identifier the gateway stamps on every response; echoed from the connection, not client-supplied."
            },
            "timestamp": {
              "type": "integer",
              "format": "int64",
              "description": "Unix epoch milliseconds, UTC — when this message was produced. It is not the moment pricing stopped: the message repeats on each price broadcast for as long as the condition holds, so consecutive frames carry advancing timestamps for one continuous outage."
            },
            "messageType": {
              "type": "string",
              "const": "streamStop",
              "description": "Always `streamStop`. It replaces the `pricestream` update for this subscription rather than ending it: the subscription stays open, no `unsubscribe` is needed, and pricing resumes on its own — as `pricestream` messages — once the cause clears."
            },
            "instrument": {
              "type": "string",
              "description": "The pair that is no longer priced, from the affected subscription."
            },
            "tenor": {
              "type": "string",
              "description": "The tenor that is no longer priced, from the affected subscription. Only this tenor is affected; other tenors on the same pair may still be streaming."
            },
            "code": {
              "type": "string",
              "enum": [
                "CUT_OFF_TIME_REACHED",
                "STREAM_UNAVAILABLE"
              ],
              "description": "Why pricing stopped, and the only field that distinguishes the two causes. `CUT_OFF_TIME_REACHED` — the settlement cut-off for this tenor has passed, so it can no longer be dealt today; it reaches same-day tenors only, and re-subscribing to a later tenor is the way forward. `STREAM_UNAVAILABLE` — this pair is not currently available to your account, which is a permissioning or market-state matter specific to you rather than a market-wide outage."
            },
            "message": {
              "type": "string",
              "description": "Fixed text, identical for both codes: `Prices unavailable — market closed or stream inactive.` It carries no information beyond `code`; branch on `code`."
            },
            "tag": {
              "type": "string",
              "description": "Your `tag`, echoed from the subscription — the same routing handle as on `pricestream`."
            }
          }
        },
        "examples": [
          {
            "name": "Stream Stop",
            "payload": {
              "instrument": "USDC.GBP",
              "tenor": "TOD",
              "tag": "c8c423ff-6c6c-4cc0-a63d-a2a1251d0bbe",
              "timestamp": 1747916826063,
              "messageType": "streamStop",
              "code": "CUT_OFF_TIME_REACHED",
              "message": "Prices unavailable — market closed or stream inactive."
            }
          }
        ]
      },
      "orderRequest": {
        "name": "order",
        "title": "Order request",
        "summary": "Execute an order using a quote ID from a price stream update.",
        "payload": {
          "type": "object",
          "required": [
            "messageType",
            "quoteId",
            "side",
            "clientRequestId"
          ],
          "properties": {
            "messageType": {
              "type": "string",
              "const": "order",
              "description": "Must be `order`. A value outside the protocol's set is discarded silently, with no reply — on the order path that is indistinguishable from a message that never arrived, so validate before sending."
            },
            "quoteId": {
              "type": "string",
              "description": "The `quoteId` from the `pricestream` update you are dealing on, copied unchanged. It carries the price, size, tenor and settlement date of that update, so those are taken from it and not from this request — an `instrument`, `tenor` or `settleDate` sent here is replaced. It expires ten minutes after issue (`RFS100019`) and is bound to your user (`RFS100020`)."
            },
            "side": {
              "type": "string",
              "enum": [
                "BUY",
                "SELL"
              ],
              "description": "`BUY` (execute offer) or `SELL` (execute bid)"
            },
            "clientRequestId": {
              "type": "string",
              "description": "Your unique identifier for this order — 1–64 characters with no control characters, or the order is refused with `RFS100012`. This is the key the [order-state read](https://docs.zodiamarkets.com/reference/get-order-state) looks orders up by, and it becomes `clientRef` on the trade."
            },
            "accountGrpUuid": {
              "type": "string",
              "x-requirement": "conditional",
              "description": "[Account group UUID →](https://docs.zodiamarkets.com/reference/account-group-1). Please check with your Account Manager your Account Group setup"
            },
            "quantity": {
              "type": "string",
              "description": "How much to deal, as a decimal string — not a JSON number. It is denominated in the `currency` named on the subscription behind this quote, not necessarily the base currency of the pair, and this message carries nothing that says which: the client that made the subscription is the one that knows. Omit it to deal the full quoted size. More than the quoted size is refused with `RFS100009`; less is dealt in full at the quoted price. Orders are all-or-nothing — there is no partial fill, and the `quantity` on the response is always the quantity you asked for. You may place several orders against the same `quoteId`; their quantities accumulate, and an order is refused with `RFS600021` once the cumulative quantity taken against that quote would exceed the quoted size."
            },
            "idempotencyKey": {
              "type": "string",
              "description": "Makes a retry of this order safe. A later order carrying the same key is not executed — the original order's outcome is replayed instead, on the original's `transactionId`. 1–64 characters, no whitespace and no control characters; dots and colons are allowed. Scoped to your user and valid for 24 hours from first use. See [Idempotency](https://docs.zodiamarkets.com/reference/subscribe-to-order-channel#idempotency)."
            },
            "paymentReason": {
              "type": "string",
              "x-requirement": "conditional",
              "description": "ISO payment-reason code. Required when executing on a third-party beneficiary or third-party sender price stream; see the code tables on [3rd Party Beneficiary Delivery Price Channel](https://docs.zodiamarkets.com/reference/third-party-settlement-price-subscription)."
            },
            "autoSubscribe": {
              "type": "boolean",
              "default": false,
              "description": "Opt in to session-bound delivery of this order's outcome. `false` (the default) delivers one terminal `order` response on the connection that submitted the order, and nothing survives a disconnect. `true` returns `PENDING` immediately, delivers the terminal to the session rather than the connection, and redelivers it on reconnect until you send `orderUnsubscribe` — see [Order Sessions and Recovery](https://docs.zodiamarkets.com/reference/order-subscriptions)."
            },
            "signedIntent": {
              "type": "object",
              "description": "Optional cryptographic proof of order intent — see [Subscribe to Order Channel with Signed Payload](https://docs.zodiamarkets.com/reference/order-channel-signed).",
              "properties": {
                "payload": {
                  "type": "string",
                  "description": "Base64-encoded JSON containing order parameters"
                },
                "signature": {
                  "type": "string",
                  "description": "Base64-encoded RSA signature of the payload"
                }
              }
            }
          }
        },
        "examples": [
          {
            "name": "Request",
            "payload": {
              "messageType": "order",
              "quoteId": "YdB4DodjOC5FkBDrYmKLW/Dwnz+x9bVbXyshrwz8yntjA+kZ...",
              "side": "BUY",
              "clientRequestId": "TESTCLIENT1",
              "idempotencyKey": "ORDER-2026-04-20-001-USDC-AED",
              "accountGrpUuid": "7687282f-1073-441b-9ff4-694e6b49effe",
              "quantity": "100000"
            }
          }
        ]
      },
      "orderResponse": {
        "name": "order",
        "title": "Order response",
        "summary": "The outcome of an order. Returned whether or not the order filled — `orderStatus` is the outcome field.",
        "payload": {
          "type": "object",
          "required": [
            "chanId",
            "messageType",
            "timestamp",
            "message",
            "code",
            "instrument",
            "side",
            "price",
            "quantity",
            "clientRequestId",
            "orderStatus",
            "quoteId",
            "transactionId",
            "settleDate",
            "tenor"
          ],
          "properties": {
            "chanId": {
              "type": "string",
              "description": "The connection identifier the gateway stamps on every response; echoed from the connection, not client-supplied."
            },
            "messageType": {
              "type": "string",
              "const": "order",
              "description": "Always `order`, whatever the outcome. Do not treat its arrival as success — `orderStatus` carries that. Note also that a failure can reach you as either an `order` message with a non-`SUCCESS` `orderStatus` or a separate `error` message, depending on where in the chain it happened and whether it is first delivery or recovery; a client that branches only on `messageType` will mishandle one of the two."
            },
            "timestamp": {
              "type": "integer",
              "format": "int64",
              "description": "Unix epoch milliseconds, UTC — when this message was built, which is not necessarily when the order was dealt. A terminal redelivered after a reconnect is stamped at redelivery, and a replayed idempotent order carries the stamp of the original. Treat it as a diagnostic, not as a sequence number: it is not monotonic across the messages for one order."
            },
            "message": {
              "type": "string",
              "description": "Empty on `SUCCESS` and `PENDING`. On every failure it is the same fixed sentence — `Please contact your desk support.` — never a description of what went wrong. `code` is the only field that distinguishes one failure from another."
            },
            "code": {
              "type": "string",
              "description": "Empty on `SUCCESS` and `PENDING`. On failure: `RFS600001` the order did not fill and the quote can no longer be dealt; `RFS600002` the outcome is unknown; `RFS600003` an unexpected internal state. A terminal recovered after a reconnect may instead carry the code recorded when the order originally failed."
            },
            "instrument": {
              "type": "string",
              "description": "The pair dealt, `BASE.QUOTE`, taken from the executed quote."
            },
            "side": {
              "type": "string",
              "enum": [
                "BUY",
                "SELL"
              ],
              "description": "Which way the order went, echoed from the request. `BUY` — you bought the base currency at the quote's `offer.price`. `SELL` — you sold it at the `bid.price`."
            },
            "price": {
              "type": "string",
              "description": "The price dealt, in quote currency per one unit of base currency — the price sealed into the quote you executed, not a separately derived fill price.",
              "x-note": "This field can be an empty string on a terminal recovered after a reconnect or replayed under an idempotency key, including a successful one. Treat `\"\"` as \"not carried on this message\", never as zero — the dealt price is on the [trade record](https://docs.zodiamarkets.com/reference/get-trades-list)."
            },
            "quantity": {
              "type": "string",
              "description": "The size dealt, in the currency the underlying subscription was made in — not necessarily the base currency. It always equals the quantity you asked for: orders are all-or-nothing, so this is never a partially filled amount and never less than the request."
            },
            "clientRequestId": {
              "type": "string",
              "description": "Your own identifier, echoed from the request. It is the key the [order-state read](https://docs.zodiamarkets.com/reference/get-order-state) looks orders up by, and it becomes `clientRef` on the resulting trade."
            },
            "orderStatus": {
              "type": "string",
              "enum": [
                "SUCCESS",
                "FAILED",
                "INDETERMINATE",
                "PENDING"
              ],
              "description": "Outcome of the order, and the only field that carries it. `SUCCESS` filled; `FAILED` did not fill; `INDETERMINATE` means the outcome is genuinely unknown and the order may have filled — do not re-place it, reconcile it; `PENDING` is accepted-not-yet-known and is sent only to clients that set `autoSubscribe`. Read this field and nothing else to classify an order."
            },
            "quoteId": {
              "type": "string",
              "description": "The `quoteId` that was executed, echoed from the request."
            },
            "transactionId": {
              "type": "string",
              "description": "Zodia Markets' reference for this order: 32 lowercase hexadecimal characters, a UUID with the hyphens removed. Assigned before the order is sent onward, so it exists even on orders that never dealt, and it is the identifier that ties the `PENDING`, the terminal, every redelivery of that terminal and your `orderUnsubscribe` together. Correlate on it. It is usually absent from `error` messages, which is why a rejected order can leave you with nothing to key on but `clientRequestId`."
            },
            "settleDate": {
              "type": "string",
              "description": "The date the trade settles, `YYYYMMDD`, taken from the executed quote — a `settleDate` sent on the request is ignored. Empty on a terminal recovered after a reconnect or replayed under an idempotency key."
            },
            "tenor": {
              "type": "string",
              "description": "The tenor dealt, taken from the executed quote — a `tenor` sent on the request is ignored. Empty on a terminal recovered after a reconnect or replayed under an idempotency key."
            },
            "subscriptionStatus": {
              "type": "string",
              "enum": [
                "NOT_SUBSCRIBED",
                "SUBSCRIBED",
                "UNSUBSCRIBED"
              ],
              "description": "Present only on responses to orders sent with `autoSubscribe: true`, and on those it only ever reads `SUBSCRIBED` — this message is not the place you observe an order becoming unsubscribed. Once you acknowledge with `orderUnsubscribe`, no further `order` message arrives for that order at all. Absent from the wire, not null, for orders sent without `autoSubscribe`. The other two values appear on the [order-state read](https://docs.zodiamarkets.com/reference/get-order-state)."
            },
            "sessionId": {
              "type": "string",
              "description": "The session this order's outcome is delivered to, and the value to reconnect under to recover it. It is the `sessionId` from the WebSocket handshake where you supplied one; where you did not, Zodia Markets assigns one and this is the only place it is returned to you. Present only on responses to orders sent with `autoSubscribe: true`."
            }
          }
        },
        "examples": [
          {
            "name": "Success Response",
            "summary": "The order dealt. `orderStatus` is `SUCCESS`, `message` and `code` are empty, and the economics are all present.",
            "payload": {
              "timestamp": 1723196525811,
              "messageType": "order",
              "message": "",
              "code": "",
              "instrument": "USDC.AED",
              "side": "BUY",
              "price": "3.673050",
              "quantity": "100000",
              "clientRequestId": "TESTCLIENT1",
              "orderStatus": "SUCCESS",
              "quoteId": "YdB4DodjOC5FkBDrYmKLW/Dwnz+x9bVbXyshrwz8yntjA+kZ...",
              "transactionId": "774722250a694498b916b7bac07bb48e",
              "tenor": "T",
              "settleDate": "20240811"
            }
          },
          {
            "name": "Failed Response",
            "summary": "The order did not deal and nothing was booked. `RFS600001` — the quote can no longer be dealt; re-quote and try again.",
            "payload": {
              "timestamp": 1723196658204,
              "messageType": "order",
              "message": "Please contact your desk support.",
              "code": "RFS600001",
              "instrument": "USDC.AED",
              "side": "BUY",
              "price": "3.673050",
              "quantity": "100000",
              "clientRequestId": "TESTCLIENT2",
              "orderStatus": "FAILED",
              "quoteId": "YdB4DodjOC5FkBDrYmKLW/Dwnz+x9bVbXyshrwz8yntjA+kZ...",
              "transactionId": "4f05e316c5214ea8a6cc88889b711615",
              "tenor": "T",
              "settleDate": "20240811"
            }
          },
          {
            "name": "Pending Response",
            "summary": "Sent only when the order carried `autoSubscribe: true`. Accepted, outcome not yet known — note `subscriptionStatus` and `sessionId`, which are absent entirely without `autoSubscribe`.",
            "payload": {
              "timestamp": 1723197093017,
              "messageType": "order",
              "message": "",
              "code": "",
              "instrument": "USDC.AED",
              "side": "BUY",
              "price": "3.673050",
              "quantity": "100000",
              "clientRequestId": "TESTCLIENT3",
              "orderStatus": "PENDING",
              "quoteId": "YdB4DodjOC5FkBDrYmKLW/Dwnz+x9bVbXyshrwz8yntjA+kZ...",
              "transactionId": "8ed14cd3bce249eda980f606b0796909",
              "tenor": "T",
              "settleDate": "20240811",
              "subscriptionStatus": "SUBSCRIBED",
              "sessionId": "trading-desk-1"
            }
          },
          {
            "name": "Indeterminate Response",
            "summary": "The terminal for the order above, recovered after a reconnect. The outcome is unknown — reconcile it, do not re-place it. Note that `price`, `tenor` and `settleDate` come back empty on a recovered terminal.",
            "payload": {
              "timestamp": 1723198007902,
              "messageType": "order",
              "message": "Please contact your desk support.",
              "code": "RFS600002",
              "instrument": "USDC.AED",
              "side": "BUY",
              "price": "",
              "quantity": "100000",
              "clientRequestId": "TESTCLIENT3",
              "orderStatus": "INDETERMINATE",
              "quoteId": "YdB4DodjOC5FkBDrYmKLW/Dwnz+x9bVbXyshrwz8yntjA+kZ...",
              "transactionId": "8ed14cd3bce249eda980f606b0796909",
              "tenor": "",
              "settleDate": "",
              "subscriptionStatus": "SUBSCRIBED",
              "sessionId": "trading-desk-1"
            }
          }
        ]
      },
      "orderUnsubscribeRequest": {
        "name": "orderUnsubscribe",
        "title": "Order unsubscribe request",
        "summary": "Acknowledge a subscribed order's terminal result and stop it being redelivered.",
        "payload": {
          "type": "object",
          "required": [
            "messageType",
            "transactionId"
          ],
          "properties": {
            "messageType": {
              "type": "string",
              "const": "orderUnsubscribe",
              "description": "Must be `orderUnsubscribe`."
            },
            "transactionId": {
              "type": "string",
              "description": "The `transactionId` of the order you are acknowledging, taken from its `order` response. Only an order whose terminal has already been delivered may be acknowledged — sending this while the order is still in flight is refused, because it would cut you off from the outcome. Orders are tracked for 24 hours; after that the reference is no longer known."
            }
          }
        },
        "examples": [
          {
            "name": "Request",
            "payload": {
              "messageType": "orderUnsubscribe",
              "transactionId": "774722250a694498b916b7bac07bb48e"
            }
          }
        ]
      },
      "orderUnsubscribeResponse": {
        "name": "orderUnsubscribe",
        "title": "Order unsubscribe response",
        "summary": "The result of an unsubscribe request.",
        "payload": {
          "type": "object",
          "required": [
            "chanId",
            "messageType",
            "timestamp",
            "success",
            "message",
            "transactionId",
            "code"
          ],
          "properties": {
            "chanId": {
              "type": "string",
              "description": "The connection identifier the gateway stamps on every response; echoed from the connection, not client-supplied."
            },
            "messageType": {
              "type": "string",
              "const": "orderUnsubscribe",
              "description": "Always `orderUnsubscribe`. Unlike `unsubscribe`, this message is sent for every request, refusals included — read `success`."
            },
            "timestamp": {
              "type": "integer",
              "format": "int64",
              "description": "Unix epoch milliseconds, UTC — when this message was built."
            },
            "success": {
              "type": "boolean",
              "description": "`true` when the order is now acknowledged and will not be redelivered — including when it already was, so a repeat is a success rather than an error. `false` means nothing changed: the order is still in flight, unknown to us, or not yours. No further `order` message arrives for an acknowledged order."
            },
            "message": {
              "type": "string",
              "description": "Which of the five outcomes this was. `Unsubscribed` — done. `Order already unsubscribed` — a repeat, also a success. `Order is still in flight; unsubscribe is only allowed once a terminal result is delivered` — wait for the terminal and retry. `Unknown order for transactionId=…` — no such order, or it is not yours, or it has aged out of the 24-hour tracking window; the three are deliberately indistinguishable so that probing cannot confirm another counterparty's order exists. `Failed to persist unsubscribe for transactionId=…` — a transient failure our side, safe to retry."
            },
            "transactionId": {
              "type": "string",
              "description": "The `transactionId` from your request, echoed."
            },
            "code": {
              "type": "string",
              "description": "Empty on success — including on a repeat acknowledgement, which is a success. `RFS100012` on a refusal; `message` says which of the three refusals it was."
            }
          }
        },
        "examples": [
          {
            "name": "Response",
            "payload": {
              "timestamp": 1728374758421,
              "messageType": "orderUnsubscribe",
              "success": true,
              "message": "Unsubscribed",
              "transactionId": "774722250a694498b916b7bac07bb48e",
              "code": ""
            }
          }
        ]
      },
      "errorResponse": {
        "name": "error",
        "title": "Error response",
        "summary": "Returns error details when a request fails.",
        "payload": {
          "type": "object",
          "required": [
            "chanId",
            "timestamp",
            "messageType",
            "message",
            "code"
          ],
          "properties": {
            "chanId": {
              "type": "string",
              "description": "The connection identifier the gateway stamps on every response; echoed from the connection, not client-supplied."
            },
            "messageType": {
              "type": "string",
              "const": "error",
              "description": "Always `error`. This message carries failures that happen before a request becomes a typed exchange — a malformed or refused order, a rate limit, a request type this connection cannot serve. A `subscribe` never fails this way (it reports failure on its own `subscribe` response), and an order that reached the market and then failed comes back as an `order` message instead."
            },
            "timestamp": {
              "type": "integer",
              "format": "int64",
              "description": "Unix epoch milliseconds, UTC — when the error was raised."
            },
            "message": {
              "type": "string",
              "description": "What went wrong, in plain text. It is chosen independently of `code`, so the same code can arrive with different wording and the wording can change without notice — branch on `code`, and log this. Some messages contain an unsubstituted `%s` placeholder where a value was meant to be interpolated; that is cosmetic and does not change the meaning of `code`."
            },
            "code": {
              "type": "string",
              "description": "Error code — see the [Response / Error Code Reference](https://docs.zodiamarkets.com/reference/code-reference)"
            },
            "clientRequestId": {
              "type": "string",
              "description": "Your identifier from the request that failed (empty when the failure is not tied to one)"
            },
            "quoteId": {
              "type": "string",
              "description": "Quote ID from the failed request (empty when not applicable)"
            },
            "transactionId": {
              "type": "string",
              "description": "System-generated transaction reference, when one had already been assigned (empty otherwise)"
            }
          }
        },
        "examples": [
          {
            "name": "Error Response",
            "summary": "An order sent on a quote that had already expired. The failure happened before the order was tracked, so `transactionId` is empty and `clientRequestId` is the only handle on it.",
            "payload": {
              "timestamp": 1718113605032,
              "messageType": "error",
              "message": "quoteId is Expired",
              "clientRequestId": "TESTCLIENT1",
              "code": "RFS100019",
              "quoteId": "YdB4DodjOC5FkBDrYmKLW/Dwnz+x9bVbXyshrwz8yntjA+kZ...",
              "transactionId": ""
            }
          }
        ]
      },
      "accountGroupsRequest": {
        "name": "accountGroups",
        "title": "Account groups request",
        "summary": "Send this message to request your account groups.",
        "payload": {
          "type": "object",
          "required": [
            "messageType"
          ],
          "properties": {
            "messageType": {
              "type": "string",
              "const": "accountGroups",
              "description": "Must be `accountGroups`. The message carries nothing else — the groups returned are those of the authenticated user on this connection, and there is no way to ask for another user's."
            }
          }
        },
        "examples": [
          {
            "name": "Request",
            "payload": {
              "messageType": "accountGroups"
            }
          }
        ]
      },
      "accountGroupsResponse": {
        "name": "accountGroups",
        "title": "Account groups response",
        "summary": "The account groups (sub-accounts) available to the authenticated user.",
        "payload": {
          "type": "object",
          "required": [
            "chanId",
            "timestamp",
            "messageType",
            "message",
            "code"
          ],
          "properties": {
            "chanId": {
              "type": "string",
              "description": "The connection identifier the gateway stamps on every response; echoed from the connection, not client-supplied."
            },
            "timestamp": {
              "type": "integer",
              "format": "int64",
              "description": "Unix epoch milliseconds, UTC — when this message was built. The underlying list is cached briefly, so a group added moments ago may not appear immediately."
            },
            "messageType": {
              "type": "string",
              "const": "accountGroups",
              "description": "Always `accountGroups`."
            },
            "message": {
              "type": "string",
              "description": "Empty on success. On failure, the fixed sentence `Please contact your desk support.` — the cause is not disclosed here."
            },
            "code": {
              "type": "string",
              "description": "Empty on success, `RFS100016` on failure. Because an empty list is itself treated as a failure, this is the field to check: if it is set, the list below is empty and you should not conclude from it that you have no account groups."
            },
            "accountGroups": {
              "type": "array",
              "description": "Your account groups. Always present — an empty array on failure, never omitted. An empty array is never a successful answer: if we cannot reach the source, or it returns nothing, the response is marked failed with `RFS100016` and the array comes back empty, so the two cases are indistinguishable from the wire. Retry rather than treating an empty list as authoritative.",
              "items": {
                "type": "object",
                "required": [
                  "uuid",
                  "name"
                ],
                "properties": {
                  "uuid": {
                    "type": "string",
                    "description": "The account group's UUID — the value to send as `accountGrpUuid` on `subscribe` and `order`. It selects the spread, credit limit and trading permissions applied, so it decides both the price you are quoted and whether the request is permitted at all."
                  },
                  "name": {
                    "type": "string",
                    "description": "The label configured for the group, for display and reconciliation. It has no meaning to the protocol — never send it where a `uuid` is expected — and it can be changed without the `uuid` changing."
                  }
                }
              }
            }
          }
        },
        "examples": [
          {
            "name": "Response",
            "payload": {
              "timestamp": 1728374758421,
              "messageType": "accountGroups",
              "message": "",
              "code": "",
              "accountGroups": [
                {
                  "uuid": "dcbb114e-8dfe-4eb8-9a6a-f267b7980b34",
                  "name": "Default"
                },
                {
                  "uuid": "7f6d661d-f45f-4dde-8633-749c709a724e",
                  "name": "Subaccount_1"
                }
              ]
            }
          }
        ]
      }
    }
  }
}
