4xx (a malformed request that fails the same way every time) and from a 429 rate limit (you exceeded your budget). A transient failure means the request did not get through this time — retry it.
The catch on a write: you may not learn whether it applied. Retry with the same request_id so the exchange can deduplicate it.
Transient HTTP responses
Three status families signal that the request did not complete and is worth retrying.| Status | code | Meaning to the caller |
|---|---|---|
503 Service Unavailable | at_capacity | The exchange is at capacity. Not processed. |
503 Service Unavailable | request_dropped | The request was dropped before processing. Not processed. |
503 Service Unavailable | service_unavailable | The exchange is briefly unavailable. Not processed. |
503 Service Unavailable | retry_required | The request needs to be retried. Not processed. |
504 Timeout | ack_timeout | The exchange did not confirm before the deadline. The write may or may not have applied. |
500 Internal Server Error | ack_failed | The exchange did not return a confirmation. The write may or may not have applied. |
503, the request was rejected before it reached the engine, so a write did not apply. On 500 and 504, the request may have reached the engine before the failure — the outcome is unknown. Treat both the same way: retry with the same request_id.
These codes arrive in the application/problem+json body. Branch on code, not on the human-readable detail. The full table is in error codes.
Transient RequestAck statuses
A signed write returns200 OK with a RequestAck. Two status values there are transient: the request reached the exchange but was not processed.
status | Meaning |
|---|---|
request_dropped | The exchange was busy and dropped the request before processing. |
retry_required | The request needs to be retried. |
request_id. Aside from duplicate_request_id — the success signal for an already-applied retry — every other non-request_completed status is a permanent rejection (bad price, insufficient margin, market halted), where fixing the request, not retrying it, is the answer.
Retry safely with the same request_id
Therequest_id is a UUIDv7 and the idempotency key. When you do not know whether a write applied — a 500, 503, 504, a transient RequestAck status, or a dropped connection — resubmit the same request_id:
- If the original applied, the retry returns
duplicate_request_id(a success signal, not an error). Stop. - If it did not, the retry is processed once and returns
request_completed.
400 request_timestamp_skew — at that point generate a fresh UUIDv7 and accept that you no longer have idempotency protection for that attempt.
Back off, don’t hammer
A retry storm against an exchange that is already at capacity makes the outage worse. Space your retries out.Wait before the first retry
Start from a base delay (for example 200 ms) rather than retrying immediately.
Grow the delay each attempt
Double the wait on each successive failure — 200 ms, 400 ms, 800 ms — up to a ceiling (for example a few seconds). Add a small random jitter so concurrent clients do not retry in lockstep.
Not the same as a rate limit
A429 is not a degraded mode. It means you exceeded your own rate limit, not that the exchange is unhealthy. Both warrant backoff, but the cause and the fix differ: a 429 clears when you slow down, a 503 clears when the exchange recovers. As with any write retry, reuse the original request_id on a 429 resend so it is deduplicated.