# Connection and authentication

Sign the SBE WebSocket upgrade, keep the session alive, sequence and pipeline commands, and reconnect after a close.

A trading session is one WebSocket connection authenticated by one Ed25519 API key. It trades for exactly one account, numbers its commands from `1`, and ends when the socket closes. Nothing carries over to the next connection except the effects of the commands it sent.

> **Current server settings**
>
> The limits and timeouts on this page are the current server settings and can change. Handle the documented `429` responses and close codes instead of hard-coding the values.

## Sign the upgrade

The server authenticates the HTTP upgrade request itself. There is no login message after the socket opens, and JWT sessions are not accepted: the upgrade must carry API-key headers and no `Authorization` header. Send each header once.

Send the three headers from [Ed25519 API keys](https://testnet.polyester.com/docs/developer-docs/authentication-security/ed25519-api-keys) on the `GET` upgrade request:

| Header            | Value                                                                                                                           |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `X-API-KEY-ID`    | The key ID returned by [`CreateApiKey`](https://testnet.polyester.com/docs/api-docs/connect/auth.v1.ApiKeyService/CreateApiKey) |
| `X-API-TIMESTAMP` | Current Unix time in milliseconds, within 10 seconds of Polyester time                                                          |
| `X-API-SIGNATURE` | Ed25519 signature of the canonical string, as hex or standard padded Base64                                                     |

The canonical string for the upgrade has an empty query and the SHA-256 of an empty body:

```text
1762171200000
GET
/ws/trade.sbe

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```

The first line is your `X-API-TIMESTAMP` value. The fourth line is empty because the upgrade URL has no query. Sign every connection attempt with a fresh timestamp: the server remembers recent signatures and rejects a repeated signed upgrade as `SIGNATURE_INVALID`.

If the key has an IP allowlist, the address you connect from must be on it. See [API key IP allowlists](https://testnet.polyester.com/docs/developer-docs/authentication-security/api-key-ip-allowlists).

### Which account the session trades for

The session acts for the account the key is bound to:

- a Main Account key trades for the Main Account
- a Subaccount-bound key trades for that Subaccount

Commands carry no account or Subaccount field, and the session receives private events of that one account only. To trade several Subaccounts, open one connection per Subaccount with a key bound to each. Self-trade prevention still applies across the Main Account and all its Subaccounts; see [Self-trade prevention](https://testnet.polyester.com/docs/developer-docs/shared-concepts/self-trade-prevention).

### Upgrade failures

A refused upgrade is a plain HTTP response, not a WebSocket close. Its body is the code as text, followed by a newline.

- **`UNAUTHENTICATED`** · HTTP 401\
  No API-key headers, or an `Authorization` header. Send only the three API-key headers.
- **`MISSING_HEADERS`** · HTTP 401\
  A key ID without a timestamp or signature. Send all three headers.
- **`TIMESTAMP_SKEW`** · HTTP 401\
  Timestamp malformed or more than 10 seconds from server time. Sync the clock and sign again.
- **`SIGNATURE_INVALID`** · HTTP 401\
  Unknown key, bad signature, or a repeated signed upgrade. Fix the canonical string and sign a fresh one.
- **`KEY_DISABLED`** · HTTP 401\
  The key is disabled or revoked. Use an active key.
- **`KEY_EXPIRED`** · HTTP 401\
  The key has expired. Use an active key.
- **`IP_NOT_ALLOWED`** · HTTP 401\
  Your address is outside the key's IP allowlist. Connect from an allowed address.
- **`TOO_MANY_SESSIONS_API_KEY`** · HTTP 429\
  The key holds its maximum of concurrent sessions. Close a session of the key, or use another key.
- **`TOO_MANY_SESSIONS_ACCOUNT`** · HTTP 429\
  The account holds its maximum of concurrent sessions. Close one of the account's sessions.
- **Empty or non-Polyester body** · HTTP 429\
  Too many open connections from your IP address. Close connections before opening more.

## Connection limits

Up to 16 concurrent sessions per API key and 64 per account are always accepted. Above those numbers an upgrade may be refused with `429`. The gateway also limits one IP address to 200 open connections to this endpoint.

A market maker typically spreads strategies over a few sessions per key and a few keys per account. Sessions do not share sequence numbers or subscriptions: each is independent.

## Frames

- Send exactly one SBE message per binary WebSocket frame. A text frame closes the connection with `1003`.
- A message may not exceed 64 KiB. A larger frame closes the connection with `1009`. The largest valid command, a full `BatchCreateOrders`, is far below that.
- WebSocket compression is not negotiated. Send uncompressed frames.

[Schemas and codecs](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/schemas-and-codecs) describes the message inside each frame.

## Sequence numbers

Every command carries `seq`, the connection's client sequence number. The first command on a connection uses `1`, and each command the server takes uses the next number: `2`, `3`, and so on. A new connection starts again at `1`.

A command whose `seq` is not the next expected number is answered with a `CommandReject` with code `SEQUENCE_OUT_OF_ORDER` (`1001`) and is not processed. The connection stays open, and the expected number does not change.

A command rejected before sequencing does not use its number either. That covers a frame the server cannot decode and a command that fails its structural checks, such as a missing required enum, a `0` sequence, or a batch above its item limit. Every other answer, an acknowledgement or a reject, uses the number.

When you are unsure whether a rejected command used its number `N`, send a `Ping` with `seq` `N` while nothing else is in flight. Either it is answered with `Pong` and uses `N`, or it is rejected with `SEQUENCE_OUT_OF_ORDER` because `N` was already used. In both cases your next command uses `N + 1`.

### Correlation IDs

`correlationId` is your own 64-bit token, copied unchanged into the answer. Use it to match answers to commands. For `ModifyOrder`, `CancelAllOrders`, and the three batch commands it is also the command's idempotency identity: it must be nonzero, and a retry keeps it while a new operation uses a new one. See [Order entry](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/order-entry).

## Pipelining and backpressure

You do not need to wait for an answer before sending the next command. The server takes commands in sequence order, dispatches each as soon as it is admitted, and answers them in the order it read them.

Up to 1,024 commands can be unanswered on one connection. At that point the server stops reading the socket until an answer is written, so TCP flow control holds your sends back. Nothing is rejected for this reason.

The account also has a limit of outstanding order requests shared by all its connections. A command above it is rejected with `OVERLOADED`. See [Errors and rate limits](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/errors-and-rate-limits).

## Keepalive

The server sends a WebSocket ping control frame every 20 seconds. Standard WebSocket libraries answer it automatically with a pong. If the server neither receives a pong nor completes a write to you within 60 seconds, it closes the connection with `1008` (`pong_timeout`). A write to you that cannot complete within 10 seconds closes it with `1008` (`write_timeout`).

Keep reading the socket continuously, even when you are not sending. Reading is also how the server's pings and your private events drain.

To measure round-trip time or check liveness from your side, send the SBE `Ping` command. Its `Pong` echoes `seq`, `correlationId`, and up to the frame limit of opaque payload bytes. A `Ping` uses a sequence number like any other command.

## Close codes

Branch on the close code. The close reason is diagnostic text and may change.

- **`1003`** · `binary frames required`\
  You sent a text frame. Fix the client; send binary frames only.
- **`1008`** · `pong_timeout; reconnect and resync required`\
  No pong or completed write within 60 seconds. Reconnect and reconcile.
- **`1008`** · `write_timeout; reconnect and resync required`\
  Your client stopped reading. Read faster, then reconnect and reconcile.
- **`1008`** · `private event queue overflow; resync required`\
  Your private events backed up because your client read too slowly. Read faster, then reconnect and reconcile.
- **`1009`** · library-defined reason text\
  A frame exceeded 64 KiB. Fix the client.
- **`1012`** · `private event stream gap; resync required`\
  Private events of your account, or of a Pair without `marketResync`, may be missing. Reconnect and reconcile.
- **`1012`** · `private event source unavailable; resync required`\
  Private event delivery is temporarily unavailable. Reconnect with backoff and reconcile.
- **`1013`** · `command without a definite outcome; reconcile before resending`\
  A command was answered with an unknown outcome, or could not be sent. Reconcile every unanswered command, then reconnect.
- **`1006`** · no close frame, so no reason text\
  Network failure or server restart. Reconnect with backoff and reconcile.

Without `marketResync`, a `1012` can follow a missed event of any account on a Pair, including a Pair you do not trade. With `marketResync` enabled, a `1012` still arrives when events of your own account may be missing, and when private event delivery is unavailable. Handle `1012` on every connection. See [Responses and events](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/responses-and-events#recovery).

### Outcome-unknown close

When the server sends a command but cannot confirm its outcome, it answers that command with an outcome-unknown `CommandReject` and then closes with `1013`. A command it could not send is answered with `UNAVAILABLE` and closes the connection the same way. It does not send or answer any command it read after that one. Commands you pipelined behind it get no answer and may or may not have been applied.

> **Reconcile before resending**
>
> After `1013`, read the state of every command that has no answer before you resend anything: read each order by client order ID with [`GetOrder`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersReadService/GetOrder), or read all [open orders](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/orders/open). Resending blindly can place an order twice. Batch commands can be resent with the same `correlationId` and items to learn each item's result.

## Reconnect

1. Wait with backoff

   Reconnect after a short delay that grows with each failed attempt, with jitter. For example, start at 0.5 seconds and double up to 10 seconds. Do not retry a `401` until its cause is fixed.

2. Sign a fresh upgrade

   Use a new timestamp and signature. Start the new connection at `seq` `1`.

3. Subscribe again

   Subscriptions do not survive the connection. Send `Subscribe` before relying on events.

4. Reconcile

   Read open orders with [`GET /v1/orders/open`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/orders/open) and recent executions with [`GET /v1/spot/trades`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/trades), or their ConnectRPC equivalents [`GetOpenOrders`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersReadService/GetOpenOrders) and [`GetUserTrades`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.OrdersReadService/GetUserTrades). Resolve every command that was unanswered when the connection closed. Then apply new events, ignoring order events whose `version` is not newer than the state you loaded.

5. Resume trading

   Resend only the commands your reconciliation shows did not take effect. Keep the same `correlationId` for a retried modify, cancel-all, or batch.
