How to read an error body, what a retry actually means, and which failures need more than a retry loop.

Read the code, not the status

The hub answers 200 for success and 400 for every failure of its own — availability and busy codes included — and the gateway in front of it passes those answers through unchanged. The gateway answers its own refusals in the same envelope with a real status (400, 401, 403, 404, 413, 429, 502, 503, 504) and an EPX code. Either way the status never tells a retryable failure from a terminal one: branch on the code. An answer without the envelope (an HTML page, a plain-text status) did not come from a route; treat it as the service being unavailable. See Conventions — the response envelope for the envelope shape itself (not restated here).

The three blocks, and what a retry means

The codes fall into three blocks — terminal, availability, busy — and the hub’s EIN codes and the gateway’s EPX codes share them. Terminal codes need a changed request (or, for the gateway, a fixed credential or origin), availability codes a later retry, busy codes a back-off (EIN0080, EPX0080 and EPX0082: error_params[0] ms; EPX0081: Retry-After seconds) — the table on Errors is the spec. The TypeScript SDK encodes the block rule as HubError.retryable and retryAfterMs (SDK errors). On any route, an EIN0050 whose error_params[0] is upstreamAuth means the upstream refused the hub’s own credential — a configuration problem an operator has to clear, and no client-side back-off fixes it. A refused key or origin (401, 403) is the same kind of problem on your side: fix the configuration, don’t retry. Retrying a refused credential makes it worse: 20 of them within a minute lock the caller’s IP out of every call for the rest of that minute (EPX0081).

The request id

Every answer carries a request id, in the body’s request_id and in the X-Request-Id header. The gateway makes a fresh one for every call it sends to the hub, retries included: you never send one, and nothing about a retry depends on it. Log it next to the call, and quote it when you report a problem.

Terminal codes on the happy path

A few terminal codes show up in normal flow, not just on mistakes:
  • EIN0005 (quote expired) — normal flow, not a bug: re-quote and re-sign. Not once the quote’s own transaction is sent (the open flow, or a proxy lane — see the known issue): the funds have already moved, and a new quote would move them a second time.
  • EIN0008 (no viable route) — a fact about the amount or the destination, not the form; change the request.
  • EIN0010 (bad signature) — covers an unknown quote too (one code on purpose); re-sign the exact bytes disclosed, never a re-derived version. On POST /order/openfor a bad EVM escrow signature is not checked by the hub and comes back as EIN0021 instead.
  • EIN0001 on the winner’s own kind (“the winner completes via POST /order”) — a bridge winner posted to the wrong route; the fix is the other route, not different input.
Links to the route pages carry the full error tables; not restated here.

Submissions: re-post, never re-quote

A re-post of the identical body never opens a second order, and it is safe — except after EIN0021: a declined signed order must be re-quoted and signed again, because a re-post answers EIN0050 and then EIN0052. This is the client rule behind two route-level specs — the reservation mechanics on Submit order (openFor) — Behavior and the idempotency bullet on Create order — Behavior — and it’s worth stating once, in plain terms:
  • EIN0052 means an earlier submission of this quote went unanswered and may have opened the order. Stop. Never re-quote, never ask the user to sign again. Re-posting the identical body is safe and keeps answering EIN0052 until an operator reconciles the reservation — surface request_id to them; there is no client-side fix. This rule is for a submission that went unanswered. After an explicit EIN0021 nothing was opened: do not re-post (that only answers EIN0050 and then EIN0052) — re-quote and sign again, as above.
  • A bare EIN0050 or EIN0081 on POST /order/openfor can mean two different things: nothing was sent (back off and re-post — it forwards again), or something was sent and the hub is still waiting (the reservation is kept, and the next re-post answers EIN0052, which tells you which case it was). You can’t tell which one happened from the first error alone — only from what the re-post answers.
  • The gateway’s availability codes on a submission (EPX0050, EPX0051 and EPX0052, a 502, 503 or 504) say nothing about whether the order was opened: treat them as a lost answer (below). EPX0053 is the exception: the gateway could not check the credential, and the call never reached the hub.
  • POST /order replays the first ack even after expiresAt, for a quote that already has an order — so a lost POST /order answer is recovered the same way.

A lost answer is not a lost order

If the POST /order/openfor answer never arrives, or comes back as one of the gateway’s availability codes, compute hubOrderId = hex(sha256("infinity:v1:order:" + quoteId)) and read GET /order/{hubOrderId}/status. The SDK’s deriveHubOrderId does the hashing. A not-found in the narrow window right after a crash does not prove there was no submission — the alias record is written best-effort after upstream acceptance, so retry the read once, then, if it’s still not-found, re-post the identical request. It never opens a second order. The gateway keeps an order submission running at the hub when your client times out or disconnects, so a client that gives up only loses the answer: the read above finds the order. The gateway does cut the hub’s call at its own 10 s (EPX0052), and a submission cut while the hub is forwarding it is cancelled with it: the read then answers EIN0009, and every re-post EIN0052 until an operator reconciles it (known issue). Give submissions a client timeout longer than the gateway’s 10 s, so that you get the gateway’s answer rather than none. If the POST /order answer never arrives, re-post the identical request: a quote that already has its order answers with that order’s ack, even after expiresAt. Never send the transaction again.

Example error responses

Illustrative values; formats follow Conventions — the response envelope. EIN0008 is documented on Conventions already, so these four use different codes. EIN0001 — a form field, here amount on POST /quote. Terminal codes carry the top-level message:
EIN0080 — the hub’s rate limit on POST /order/openfor; error_params[0] is the suggested back-off in milliseconds (floor(1000 / tenantRps), at least 1; 200 at the defaults). The hub’s busy codes carry no top-level message, but the hub always sets error_reference on this code (the same informational text as the RateLimitShed error, not a hint to parse):
EIN0052 — outcome unknown, on a different POST /order/openfor submission (an earlier, unanswered submission of the same quote is what produced this outcome, not a retry of the EIN0080 call above). Availability codes also carry no top-level message:
EPX0080 — the gateway’s rate limit: your pool is spent. It comes with HTTP 429 and a Retry-After header in seconds; error_params[0] is the same back-off in milliseconds:

See also