Every Polyester interface reports a failure with a stable, machine-readable error code. Branch on
that code. Message text such as detail or message is for logs and people, and it can change
without notice.
Each interface carries the code in its own way, but the codes are shared. An order rejected over REST, ConnectRPC, or a trade WebSocket reports the same code. The full list, with each code's HTTP status and Connect code, is on Error codes. The API reference lists the errors of each operation.
Error categories
The HTTP status and the Connect code tell you the category of a failure before you look at the specific code. Use the category to decide what to do next.
- Invalid request · HTTP 400 ·
invalid_argument
Fix the request. Sending it again unchanged fails the same way. - Request too large · HTTP 413 ·
invalid_argument
Send a smaller body or a smaller batch. - State does not allow the request · HTTP 400 or 412 ·
failed_precondition
Change the account, order, or market state first, or wait for it to change. - Not authenticated · HTTP 401 ·
unauthenticated
Sign again or refresh the session; check the API key and the clock. - Not permitted · HTTP 403 ·
permission_denied
Check the role, the API key's scope, and its policy. Do not retry unchanged. - Not found · HTTP 404 ·
not_found
Check the identifier and the selected Subaccount. - Duplicate or reused key · HTTP 409 ·
already_exists
Reconcile with the existing resource; do not create a new one blindly. - Concurrent change · HTTP 409 ·
aborted
Read the current state, then repeat the request if it still applies. - Rate limit or capacity · HTTP 429 ·
resource_exhausted
Wait for the retry delay, then retry. Some capacity codes say retrying does not help. - Client closed the request · HTTP 499 ·
canceled
The client gave up before an answer; reconcile writes. - Internal error · HTTP 500 ·
internal
Reconcile writes, then contact support if it repeats. - Not supported here · HTTP 501 ·
unimplemented
Change the request; this combination is not offered. - Upstream error · HTTP 502 ·
unavailable
Retry with backoff; reconcile writes first. - Temporarily unavailable · HTTP 503 ·
unavailable
Retry with backoff and the same idempotency identity. - Outcome unknown · HTTP 504 ·
deadline_exceeded
Read the resource before retrying. The request may have been applied.
Requests whose account or market state does not allow them answer HTTP 400, except account,
API key, and security endpoints, which answer 412. One code breaks the pattern: OVERLOADED, too
many order requests in flight for the account, is resource_exhausted on ConnectRPC but HTTP 503 on REST, with no rate-limit headers.
REST
REST errors use an RFC 9457 problem details body with
content type application/problem+json:
{
"type": "https://polyester.dev/problems/unknown-symbol",
"title": "Unknown Symbol",
"status": 404,
"detail": "The requested trading pair symbol was not found.",
"code": "UNKNOWN_SYMBOL",
"symbol": "NOPE-USDT"
}| Field | Meaning |
|---|---|
code | The stable error code. Branch on this |
status | The HTTP status, repeated in the body |
title | A short summary of the problem |
detail | A human-readable explanation of this occurrence. Do not parse it |
type | A URI that identifies the problem type. Treat it as an identifier; it is not a web page |
type follows one rule on every endpoint: https://polyester.dev/problems/ followed by code in
lowercase, with hyphens for underscores. AUTH_SESSION_KIND_NOT_ALLOWED is https://polyester.dev/problems/auth-session-kind-not-allowed wherever it is answered. Match on code; type is its URI form.
Some errors add fields to the body:
| Field | When |
|---|---|
existingOrderId | CONFLICT_DUPLICATE_CLIENT_ORDER_ID: the public ID of the order that holds the client order ID, when known |
rate_limit | RATE_LIMIT_EXCEEDED (429) and RATE_LIMIT_AUTHORITY_UNAVAILABLE (503): the quota that refused, and retry_after_ms on a 429 |
swapId | QuickSwap errors that concern an existing QuickSwap |
minDepositAmount, maxDepositAmount | QuickSwap AMOUNT_OUT_OF_RANGE: the route's current limits, when known |
symbol, depth | Order book UNKNOWN_SYMBOL and INVALID_DEPTH: the value you sent |
The rate_limit object uses snake_case field names, unlike the rest of the body. How to read it is
on Rate limits.
A request that fails validation answers 400 with a validation code, such as VALIDATION_ERROR or INVALID_ARGUMENT.
On order endpoints the problem body also carries violations: each item has a fieldPath that names
the REST request field, such as order.baseQty or items[2].limitGtc.price, a ruleId, and a message. BATCH_TOO_LARGE carries an items violation the same way. A REST order decimal with a
nonzero digit beyond its scale answers the code an off-grid value answers: QTY_STEP_SIZE for a baseQty finer than the Pair's base scale, and PRICE_TICK_SIZE for a limit or attached-leg price
with more than 9 fractional digits. A create answers it in the problem body, a modify or batch
replace answers 400 PRICE_TICK_SIZE with the violation (such as newPrice or items[1].newPrice), a batch create rejects only that item, and a preview reports it as a rejection.
Other decimals, such as maxQuoteDebit and clientRefPrice, answer INVALID_ARGUMENT. Zeros beyond
the scale do not change the value and are accepted: a baseQty of "1.500" on a Pair with base
scale 2 is read as 1.5. See REST getting started.
A body over the 256 KiB size limit of an order or trigger write route answers 413 with REQUEST_TOO_LARGE, for API-key and session callers alike, and nothing is applied. Send a smaller
body or split the batch. A body or query value that cannot be parsed answers 400 with BAD_REQUEST. Credentials are checked first, so an unsigned request answers 401 whatever its body.
Per-item results are different. Each failed item of a batch order response, and the rejection of
a preview order, carries an error object
with code, violations, rateLimit, and, for a create, existingOrderId. Each violation has a fieldPath, a ruleId for programmatic matching, and a message.
401 with "code": "UNAUTHENTICATED" and "type": "https://polyester.dev/problems/unauthenticated". The
per-IP limit on order and trigger writes answers a plain 429 with no Polyester body. A path or
method the API does not serve answers 404 with NOT_FOUND, or a Cloudflare 403 challenge for
some non-browser clients; see Troubleshooting.
Read the HTTP status first, then parse the body according to its content type.ConnectRPC
A ConnectRPC error has a non-200 HTTP status and a JSON body with a Connect code, a message,
and details. Each detail has a fully qualified protobuf type and its base64-encoded value, and
can also include a debug JSON rendering. Generated clients and the SDKs decode details for you.
This is a validation failure, with its debug field left out:
{
"code": "invalid_argument",
"message": "validation error: symbol_id: must be greater than 0",
"details": [
{
"type": "buf.validate.Violations",
"value": "ClASCXVpbnQzMi5ndBoWbXVzdCBiZSBncmVhdGVyIHRoYW4gMCoRCg8IARIJc3ltYm9sX2lkGA0yGAoMCAUSBnVpbnQzMhgLCggIBBICZ3QYDQ"
}
]
}The Polyester error code is in a domain detail. Read the detail of the service you called:
orders.v1.ErrorDetail· Orders, triggers, order and trade reads
Carriescode,violations,rate_limit,existing_order_id.auth.v1.AuthErrorDetail· Accounts, API keys, MFA, refused credentials
Carriescode,message.auth.v1.ProfileErrorDetail· Profiles
Carriescode,field,message.chain.withdraw.v1.ErrorDetail· Withdrawals
Carriescode.transfer.v1.ErrorDetail· Internal transfers
Carriescode.ledger.read.v1.ErrorDetail· Balances and transfer history
Carriescode.marketoverview.v1.ErrorDetail· Market overview
Carriescode.claims.v1.ErrorDetail· Daily claim
Carriescode.swap.v1.ErrorDetail· Swap
Carriescode,swap_id,min_amount_e18,max_amount_e18.swap.quickswap.v1.ErrorDetail· QuickSwap broker API
Carriescode,swap_id,min_deposit_amount,max_deposit_amount.polyester.ratelimit.v1.RateLimitDetail· Any rate-limited method
Carriesreason,limit,remaining,retry_after_ms, and the quota that refused.buf.validate.Violations· Any method, on request validation
Carries the failed fields and rules.
On order methods, orders.v1.ErrorDetail.violations lists the failed fields as field_path, rule_id, and message. Other validation failures carry the standard buf.validate.Violations detail instead.
Not every Connect error has a domain detail. Public market-data reads answer a bad symbol, or a body
they cannot parse, with code and message only. GetOrderBook with an unknown symbolId, for example, answers HTTP 404:
{ "code": "not_found", "message": "The requested trading pair symbol was not found." }A procedure that does not exist answers 404 with a REST problem body, code NOT_FOUND.
Orders, triggers, order and trade reads, and balance reads answer a ConnectRPC call refused for its
credentials with a Connect error: unauthenticated with an auth.v1.AuthErrorDetail whose code
names the reason, such as AUTH_SIGNATURE_INVALID, AUTH_TIMESTAMP_SKEW, or AUTH_IP_NOT_ALLOWED.
A call with no credentials, or with a missing or expired session token, gets AUTH_AUTHENTICATION_REQUIRED. An account API key on a method that accepts only a session token is
answered permission_denied (HTTP 403) with AUTH_SESSION_KIND_NOT_ALLOWED. Other services can
still answer a call refused before it reaches the API with a problem details body or a minimal JSON
body; Connect clients then derive the code from the HTTP status, so 401 surfaces as unauthenticated.
Trade WebSockets
The trade WebSockets answer a rejected command with a CommandReject and keep the connection open.
Order codes use the same numbers as the orders.v1.ErrorCode enum.
CommandReject carries seq, correlationId, the numeric code, a detail string, rate-limit
fields for RATE_LIMIT_EXCEEDED, and existingOrderId for a duplicate client order ID. The SBE
reject codes add SEQUENCE_OUT_OF_ORDER (1001) for a sequence gap on the connection.
An outcome-unknown code, or an UNAVAILABLE for a command that could not be sent, closes the
connection with WebSocket close code 1013 after the reject.
Read Errors and rate limits for every
group of codes and what to do.
CommandReject carries the reject code as a string in code and a client-safe detail. For
rate-limit and duplicate client order ID rejects it also sets orders.v1.ErrorDetail in error,
with the numeric code and any rate-limit details; other rejects leave error empty. An
outcome-unknown code, or an UNAVAILABLE for a command that could not be sent, closes the connection
with WebSocket close code 1013 after the reject, as on SBE. See Protobuf trade WebSocket.
Private and realtime streams
The private WebSocket at /v1/ws/private answers a failed request with a RequestError frame. It
carries the request id, a code such as FORBIDDEN or CURSOR_INVALID, and a message. The
connection stays open. The codes are listed under Private WebSocket requests.
Connection-level problems close the connection with a close code:
| Close code | Meaning | What to do |
|---|---|---|
1001 | The server is draining | Reconnect, then resubscribe with your cursors |
4400 | A malformed frame, an unknown operation, or a frame of the other encoding | Fix the client |
4401 | The connect token expired without a refresh | Reconnect with a new token |
4403 | A bad connect token, another user's token, or no auth frame within 5 seconds | Get a new token and authenticate in time |
4408 | The client did not read its frames fast enough | Reconnect, then resubscribe with your cursors |
Back off reconnects from 0.5 seconds, doubling up to 10 seconds, with jitter. Unavailable, ResyncRequired, and Unsubscribed frames are not errors: they describe the state of a
subscription, which recovers by itself. Token handling for the private WebSocket and the realtime
WebSocket is on WebSocket session model.
Retries and idempotency
A failed write can mean three different things: nothing changed, the outcome is unknown, or the
request changed something before it failed. The code tells you which. Many order codes say
"Nothing changed" in their meaning; outcome-unknown codes end in _OUTCOME_UNKNOWN.
Classify the failure
Do not retry an invalid request, a permission error, or a state error unchanged. Retry rate limits, temporary unavailability, and codes whose meaning says "retry shortly" or "retry later".
Reconcile an unknown outcome
For 504, deadline_exceeded, a _OUTCOME_UNKNOWN code, a lost response, or a dropped
connection, read the order, trigger, or transfer before sending anything new. The request may have
been applied.
Wait
On a rate limit, wait at least the delay the error gives: retry_after_ms in the body or detail,
the Retry-After header, or rateLimitRetryAfterNs on SBE. Otherwise use bounded exponential
backoff with jitter.
Retry with the same identity
Keep the client order ID, client trigger ID, request ID, or idempotency key, and the payload, unchanged. Sign an API-key request again with a fresh timestamp. A new identifier makes a new logical request and can create a second order or transfer.
How each identifier deduplicates a retry, and for how long, is on Client order IDs and idempotency. Quotas, retry headers, and limits that are not your quota are on Rate limits.