Streaming FX Quotes
Subscribe to a stream of FX Quotes via a secure WebSocket connection
Use the FX WebSocket to subscribe to streaming FX quotes and execute Market Orders over a secure WebSocket connection.
This integration is intended for more sophisticated FX workflows and is more complex to implement than our REST APIs. We recommend starting with the REST APIs before choosing a WebSocket integration.
Overview
Using the FX WebSocket, you can subscribe to a continuous stream of FX quotes for all EUR- and USD-based currency pairs, plus a small number of additional pairs. These quotes are indicative (non-firm), but you can trade on them using Market Orders subject to symmetric price improvement.
The FX WebSocket uses the same authentication scheme as our REST APIs, so you must obtain an access token through the Authorization API before you connect. Because WebSocket sessions are long-lived, you must also renew the token before it expires to avoid disconnection.
A typical connection lifecycle looks as follows:
- Obtain an access token through our Authorization API and establish a WebSocket connection using that token.
- Subscribe to pairs such as EUR/USD and GBP/USD by sending a MarketDataRequest.
- Banking Circle starts streaming real-time quotes for the subscribed pairs. The stream continues until you unsubscribe, the access token expires, or the WebSocket disconnects.
- Send TokenRefresh messages before token expiry to avoid intraday session disconnects.
- Execute Market Orders over the WebSocket and optionally specify a target price and slippage. Banking Circle responds with ExecutionReport messages, as it does in our REST APIs.
- Unsubscribe from the pairs you no longer need, or disconnect from the WebSocket.
Using Our WebSocket
Once your API user has been configured for streaming FX quotes, you can establish a WebSocket connection in both the Sandbox and Production environments:
- Sandbox: wss://sandbox.bankingcircleconnect.com/api/v1/fx/websockets
- Production: wss://www.bankingcircleconnect.com/api/v1/fx/websockets
To connect, initiate a standard WebSocket handshake. The process starts with an HTTP GET request that is upgraded to a WebSocket connection.
The Handshake Request
Your initial request must include the following headers to comply with the RFC 6455 protocol.
| Header | Value | Description |
|---|---|---|
| Connection | Upgrade | Signals the transition from HTTP to another protocol |
| Host | sandbox.bankingcircleconnect.com | Specifies the server domain |
| Upgrade | websocket | Specifies the target protocol |
| Sec-WebSocket-Key | Random Base64 | A security key generated by your client |
| Sec-WebSocket-Version | 13 | Specifies the WebSocket protocol version to use |
| Authorization | Bearer access_token | Your Banking Circle access token |
Replace access_token with your actual token value. Do not include the angle brackets in the header you send.
Messaging
Each inbound and outbound message inherits from BaseMessage, which contains a required type field. Parse this field first to determine how to handle the rest of the payload.
{
"type" : "<MessageType>"
}Refreshing Your Token
Authentication happens during the initial handshake. If the Authorization header is missing or the token is invalid, the server returns 401 Unauthorized and the connection is not established.
Token Refresh Logic
Because the WebSocket connection is long-lived, your initial access token may expire while the socket is still open. If you do not renew the token and send a TokenRefresh message before expiry, Banking Circle disconnects your client.
Check your access token TTL (time-to-live) and obtain a new token from the Authorization API before the current token expires.
Once you have renewed your token, you must send a TokenRefresh message:
{
"type" : "TokenRefresh",
"bearerToken" : "<access_token>"
}
Subscribing To Market Data
Use MarketDataRequest to subscribe to or unsubscribe from a stream of real-time quotes.
| Field | Type | Required | Description |
|---|---|---|---|
| type | String | Yes | Must be set to: MarketDataRequest |
| customerId | String | Yes | Your Banking Circle customer id provided to you during onboarding |
| mdRequestId | String | Yes | A unique market data request ID identifying this subscription |
| subscriptionType | String | Yes | Set to: Subscribe if you want to receive quotes, or Unsubscribe if you want to stop an existing stream |
| currencyPair | String | Yes | Set to e.g. EUR/USD or another currency pair |
| tenor | String | Yes | Set to e.g. SPOT (ON for value Today, and TN for value Tomorrow) |
| level | Integer | No | Optional. If not specified, 0 is assumed. 0: All available levels. 1: Top of book only. |
Send a subscribe request to start streaming quotes for a currency pair:
{
"type" : "MarketDataRequest",
"customerId" : "000012345",
"mdRequestId" : "1aac349acde611ebb8bc0242ac130003",
"subscriptionType" : "Subscribe",
"currencyPair" : "EUR/USD",
"tenor" : "SPOT", // Also supports ON or TN
"level" : 0 // Optional
}
If your MarketDataRequest is not successful, Banking Circle will reject your request with a MarketDataRequestReject message indicating why your subscription was rejected:
{
"type" : "MarketDataRequestReject",
"mdReqId" : "1aac349acde611ebb8bc0242ac130003",
"rejectReason" : "InvalidInput",
"text": "Customer id is not mapped to Streaming SPOT"
}If your request is successful, Banking Circle will start streaming quotes using IncrementalRefresh messages:
{
"type":"IncrementalRefresh",
"mdReqId" : "1aac349acde611ebb8bc0242ac130003",
"currencyPair" : "EURUSD",
"tenor" : "SPOT",
"size" : 5000000,
"bid" : 1.1476716,
"ask" : 1.1598708,
"timestamp" : "2026-03-12T12:49:37.5129172+00:00"
}In this example, the quoted size supports trading up to €5 million. If you subscribe to the full book, you receive one IncrementalRefresh message for each available level for that currency pair.
To stop the stream, send another MarketDataRequest with the same mdRequestId and set subscriptionType to Unsubscribe:
{
"type" : "MarketDataRequest",
"customerId" : "000012345",
"mdRequestId" : "1aac349acde611ebb8bc0242ac130003",
"subscriptionType" : "Unsubscribe",
"currencyPair" : "EUR/USD",
"tenor" : "SPOT"
}Banking Circle will then stop streaming further quote updates for this currency pair.
Executing Trades
You execute trades by sending Market Orders. Because quotes from the stream are indicative (non-firm), the final execution price depends on the market at the time your order is processed.
The table below summarizes the three supported order styles.
| Type | Behaviour |
|---|---|
| Market Order | Executes at the current market price. It does not use a target price or slippage limit. |
| Market Order with target price | Executes only at your target price or better. |
| Market Order with slippage | Executes at your target price or better, or within your allowed slippage if the market moves against you. |
Example scenario
Assume you have seen EUR/USD at 1.14 / 1.15 on the stream and want to buy EUR. By the time you send the order, the market has moved to 1.13 / 1.16.
| Target Price | Slippage | Result | Description |
|---|---|---|---|
| — | — | Filled | Fills at 1.16 according to the current market price. |
| 1.17 | — | Filled | Fills at 1.16 because the market has moved in your favour. |
| 1.15 | — | Rejected | The market has moved against your target, so the order is rejected. |
| 1.17 | 0.01 | Filled | Fills at 1.16. Slippage is not used because the market has moved in your favour. |
| 1.15 | 0.01 | Filled | Fills at 1.16 because the current market price is within your allowed slippage. |
| 1.14 | 0.01 | Rejected | Rejected because the market is at 1.16 and your target price plus slippage is below that level. |
To execute a trade, include the following fields in a MarketOrder message:
Field | Type | Required | Description |
|---|---|---|---|
type | String | Yes | Must be set to: MarketOrder |
clientOrderId | String | Yes | Unique reference on your side identifying this order. Maximum 35 characters. We recommend using UUID without dashes. |
customerId | String | Yes | Your Banking Circle customer id provided to you during onboarding |
buyCurrency | String | Yes | Specify the currency to be bought e.g. EUR |
sellCurrency | String | Yes | Specify the currency to be sold e.g. USD |
amountCurrency | String | Yes | Specify whether the order amount refers to the currency being bought or currency being sold. |
amount | Decimal | Yes | Specify the order amount. Minimum order amount is 0.01 in EUR equivalent (or 1, if the currency does not support decimals) |
tenor | String | Yes | Tenor indicating settlement date for the order. |
targetPrice | Decimal | No | Optionally specify the target price for this order. Order will be rejected if current market price is against you and no slippage is provided. This field is required if slippage is provided. |
maxSlippage | Decimal | No | Optionally specify the maximum slippage (adverse market movement) you are willing to accept. Order will be rejected if current market price is against you even if slippage is taken into consideration |
buyAccount | String | No | Optionally specify the bank account to be used for the buy amount |
sellAccount | String | No | Optionally specify the bank account to be used for the sell amount |
note | String | No | Optionally specify a custom note that is visible in your FX Transactions |
Here is an example MarketOrder specifying both target price and slippage:
{
"type" : "MarketOrder",
"clientOrderId" : "my-unique-client-order-id", // Use UUID without dashes for uniqueness
"customerId" : "000012356",
"buyCurrency" : "EUR",
"sellCurrency" : "USD",
"amountCurrency" : "EUR",
"amount" : 3000000,
"tenor" : "SPOT",
"note" : "Q2 Risk Exposure", // Optional
"buyAccount" : "DK9389000000000214", // Optional - if not provided, default will be used
"sellAccount" : "DK7789000000000114", // Optional - if not provided, default will be used
"targetPrice" : 1.1598708, // Optional
"maxSlippage" : 0.001 // Optional
}Banking Circle always responds with two execution reports. The first confirms receipt of the order. The second confirms whether the order was filled or rejected.
Below are the fields you can expect in an ExecutionReport:
| Field | Type | Required | Description |
|---|---|---|---|
| type | String | Yes | Set to: ExecutionReport |
| execReportType | String | Yes | Set to either "New", "Filled", or "Rejected". |
| clientOrderId | String | Yes | Echoes the value sent in your MarketOrder |
| bankingCircleRef | String | Yes | A unique identifier for this trade on Banking Circle side. |
| tradeStatus | String | Yes | Set to either: "Pending", "Filled" or "Rejected". |
| buyCurrency | String | Conditional: | The currency that was bought. Present only for Filled trades. |
| buyAmount | Decimal | Conditional | The amount that was bought. Present only for Filled trades |
| sellCurrency | String | Conditional | The currency that was sold. Present only for Filled trades |
| sellAmount | Decimal | Conditional | The amount that was sold. Present only for Filled trades |
| currencyPair | String | Conditional | The currency pair used, for example EURUSD. Present only for Filled trades |
| allInRate | Decimal | Conditional | The exchange rate used for this deal. Present only for Filled trades |
| settleDate | Date | Conditional | Settlement date for the trade in yyyy-MM-dd format. Present only for Filled trades |
| tradeExecutionTime | ISO 8601 UTC | Conditional | UTC timestamp for when the trade was executed. Present only for Filled trades |
| rejectReason | String | Conditional | Rejection reason for your trade. Only present for Rejected trades |
Here is the initial execution report for an accepted order:
{
"type" : "ExecutionReport",
"clientOrderId" : "my-unique-client-order-id",
"bankingCircleRef" : "OX68YAEBCBQ2",
"tradeStatus" : "Pending", // Indicating the trade is being processed
"execReportType" : "New"
}
If the order is filled, Banking Circle then responds with a filled execution report:
{
"type" : "ExecutionReport",
"execReportType" : "Filled",
"clientOrderId" : "my-unique-client-order-id",
"bankingCircleRef" : "OX68YAEBCBQ2",
"tradeStatus" : "Filled",
"buyCurrency" : "EUR",
"buyAmount" : 3000000,
"sellCurrency" : "USD",
"sellAmount" : 3479612.40,
"currencyPair" : "EURUSD",
"allInRate" : 1.1598708,
"settleDate" : "2025-09-24",
"tradeExecutionTime" : "2025-09-22T08:34:24.1630245+00:00",
}
If the order is rejected, Banking Circle responds with a rejected execution report instead:
{
"type" : "ExecutionReport",
"execReportType" : "Rejected",
"clientOrderId" : "my-unique-client-order-id",
"bankingCircleRef" : "OX68YAEBCBQ2",
"tradeStatus" : "Rejected",
"rejectReason" : "Outside of our opening hours"
}All executed orders are visible through our FX Transactions API and follow the FX Transaction Lifecycle, just as they do in our REST APIs.
Error Messages
Banking Circle may send an error message in response to invalid input, validation failures, or unexpected data. The format is as follows:
| Field | Type | Required | Description |
|---|---|---|---|
| type | String | Yes | Set to: "ErrorMessage" |
| errorCode | String | Yes | Machine-readable error code such as InvalidInput, UnsupportedCurrencyPair. |
| errorDescription | String | Yes | A human-readable explanation of the error |
The following example shows the error message returned when you send a message with type WrongMessage.
{
"type" : "ErrorMessage",
"errorCode" : "InvalidInput",
"errorDescription": "Message format is not correct. Make sure you include all fields. Error: Unsupported message type: WrongMessage"
}
Updated about 18 hours ago
