# Errors

Before upgrade, authentication and connection/admission errors are HTTP responses. After
upgrade, application errors arrive as `error` envelopes. A transport failure or a forced close
can occur without a final application error message.

## Before the Upgrade

| HTTP status | Meaning |
|  --- | --- |
| `401` | Missing or invalid API key |
| `429` | Rate limit or per-API-key connection limit reached |
| `503` | Server connection capacity unavailable |


`X-WS-Error` carries a short description. A simultaneous connection race can reject a socket
just after upgrade with `PolicyViolation`, without an `error` envelope; wait and check existing
connections before retrying. Do not assume every policy close has the same cause.

## Validation and Correlation

An account request can report several field errors together:


```json
{
  "msg_type": "error",
  "id": "msg_675d3f4bbc7e4f27aadd0db8444f",
  "request_id": "bad-account-sub",
  "timestamp": "2026-10-26T15:00:00Z",
  "v": 4,
  "payload": {
    "errors": [
      {
        "code": "MANDATORY_PARAM_EMPTY_OR_MALFORMED",
        "field": "payload.account",
        "message": "Missing parameter"
      },
      {
        "code": "INVALID_PARAMETER",
        "field": "payload.broker_id",
        "message": "Invalid parameter"
      }
    ]
  }
}
```

A single error uses `payload.code`, optional `field` and `message`. Multiple errors use the
`payload.errors` array. Echoed `request_id` associates a rejection with its request even while
other data and heartbeats are arriving.

| Account validation | Result |
|  --- | --- |
| Missing/blank `payload.account` | `MANDATORY_PARAM_EMPTY_OR_MALFORMED` |
| Account longer than 45 characters | `INVALID_PARAMETER` |
| Missing, zero or negative `payload.broker_id` | `INVALID_PARAMETER` |
| Explicit zero or negative `payload.person_id` | `INVALID_PARAMETER` |
| Any account `payload.resume_from` | `INVALID_PARAMETER` because account replay is unsupported |
| Unknown action/channel, malformed envelope or timestamp outside one minute of UTC | Validation error on the affected field |


Omitted `person_id` uses the authenticated session person; an explicit value does not grant
access to someone else's account. Authorization checks the selected exact account/broker owner
and current channel permissions/site authority.

## Subscription Errors

| Code | Meaning and action |
|  --- | --- |
| `FORBIDDEN` | Account/strategy authority denied. Correct access before retrying |
| `ALREADY_EXISTS` | The channel and physical identity are already subscribed or pending. A different person override does not create a second account subscription |
| `NOT_FOUND` | Unsubscribe requested an identity not currently subscribed |
| `TOO_MANY_SUBSCRIPTIONS` | The relevant account or strategy limit was reached. Unsubscribe unused identities first |
| `SERVICE_UNAVAILABLE` | The initial account snapshot or server connection capacity is unavailable. Retry the subscription with backoff |
| `TOO_MANY_REQUESTS` | Request rate exceeded. After upgrade, delay and retry the request while keeping the healthy connection |
| `FRAGMENT` | Unsupported WebSocket fragmentation or invalid JSON |
| `UNKNOWN` | Unexpected server error. Retain diagnostic IDs and contact API support |


An unsubscribe acknowledgement follows a successful explicit unsubscribe. A pending account
subscription can fail before its acknowledgement if the initial snapshot is unavailable.
A missing update does not clear existing orders or positions.

## Access Revocation

Changes to API-key or account access can close the connection. If an active account subscription
is no longer authorized, the server
removes it and sends an unsolicited `subscription.ack` with `action: "unsubscribe"`, its channel,
account, broker ID and person ID. It has no `request_id`. Stop using that channel's
cached state; do not automatically resubscribe in a tight loop. A denial before activation
returns a correlated `FORBIDDEN` error instead.

Other subscriptions can remain active when one channel is removed. Correlate requested
acknowledgements by `request_id` and handle unsolicited ones separately.

## Heartbeat, Slow Consumers and Transport Failures

A missed application heartbeat can close with `NormalClosure` and reason `Ping timeout`.
Slow consumption can produce that same reason, so it does not uniquely identify the cause.
Do not infer an exact close deadline from the close code or reason.

The server can close a slow connection with reason
`Outbound queue full. Reconnect and resubscribe`. Transport failures or server shutdown can
end a connection without an application error. A normal close code can still require recovery.

Process incoming messages promptly, acknowledge JSON heartbeats, and move blocking REST,
storage or computation work outside the receive loop. Protocol Ping/Pong does not replace
`heartbeat.ack`.

## Reconnection Guidance

| Situation | Action |
|  --- | --- |
| Invalid fields or `FORBIDDEN` | Correct the cause; keep other healthy subscriptions running |
| Duplicate subscription | Use the existing subscription or unsubscribe it before replacing it |
| Snapshot `SERVICE_UNAVAILABLE` | Retry that subscription with backoff; connection may remain usable |
| Rate limit after upgrade | Delay the request. Reconnecting is unnecessary |
| Transport close or eviction | Reconnect with exponential backoff and jitter, then use the channel-specific rule below |
| Authority revoked | Mark affected state unavailable and obtain corrected authority before resubscribing |


For `strategy.signal` and `strategy.live_fills`, retain separate successfully processed cursors
and request bounded replay. For `strategy.positions`, resubscribe and replace the full collection.
For every `account.*` channel, resubscribe **without** `resume_from` and replace state from the new
initial snapshot. Clear stale account state rather than merging the old socket's baseline.
See [Recovery and Gap Fill](/guides/websocket-gap-fill).