Symptom → cause → fix, grouped by where you are in a call: reaching the hub, credentials and origins, quoting, submitting and following an order. Every failure comes in the response envelope — branch on error_code, read error_reference and error_params for the machine detail, and keep request_id for a report. The hub’s own failures are HTTP 400; the gateway in front of it answers its refusals with a real status (400, 401, 403, 404, 413, 429, 502, 503, 504) and an EPX code (Errors — gateway codes). Read Handling errors first for the general rule this page applies row by row. The Where column links to the page that carries the fact.

Reaching the hub

Credentials and origins

Quoting

Submitting

Following the order

Walk-throughs

Every call below sends your secret key in x-api-key. Values follow the same example world as the rest of this reference.

1. The answer to POST /order/openfor never arrived

This covers your client timing out or losing the connection, and the gateway’s availability answers on a submission: EPX0050, EPX0051, and EPX0052 (504) when the hub took longer than the gateway’s 10 s. None of them says whether the order was opened. (EPX0053 is different: the call never reached the hub, so re-post it as it is.) Step 1 — derive hubOrderId. You already hold the quoteId from the quote; the hub’s durable alias for the order is a public, client-computable hash of it, so you don’t need the lost ack to find the order:
Step 2 — read the status by that alias.
The orderId this route echoes is always the upstream’s id, whichever alias you queried it by. Found it — you’re done; watch it as usual. Step 3 — if it answers EIN0009 instead. A not-found here does not prove nothing was submitted: in the narrow window right after a crash the alias record can lag the acceptance it points at, and a forward that was cut short reads as not-found too (below):
Re-post the identical openFor body — same quoteRequestId, quoteId and signature — never re-quote and never ask the user to sign again:
When the first forward completed, this is what comes back: the request_id is new; submittedAt is not — this is the original recorded ack replayed, not a second order. The re-post can also answer two other ways:
  • A fresh ack, with its own submittedAt: the first call was cut before the hub forwarded anything, so this re-post is the first forward. Still one order.
  • EIN0052, on this and every later re-post: the hub’s call was cut while it was forwarding the order, by the gateway’s 10 s limit (the EPX0052 you got). The forward is tied to that call, so cutting it cancels the upstream call too, and its outcome stays unknown until an operator reconciles it (known issue). Go on with walk-through 2.
Your own client giving up does not cause the last case: the gateway keeps its call to the hub running when your client times out or disconnects, so the order goes on and the read in step 2 finds it. Still give openFor a client timeout longer than the gateway’s 10 s, so that you get the gateway’s answer rather than none. The SDK waits 15 s on submissions (35 s through a partner proxy). Step 4 — the POST /order variant. The same rule holds for the open flow: re-post the identical request (the same quoteRequestId, quoteId, artifactHash, signature, signer, scheme), and a quote that already has an order answers with that order’s ack — even past expiresAt:
Never resend the transaction itself — only the hub call. See Submit order (openFor), Handling errors — a lost answer and the SDK’s deriveHubOrderId.

2. EIN0052 — outcome unknown

Step 1 — the body. The same POST /order/openfor call can come back like this instead of an ack:
Step 2 — what it means. The hub forwarded the signed order and got no readable answer back — for example because the gateway cut an earlier call at its 10 s while the hub was still forwarding it, which cancels the forward (known issue). The reservation is kept, because the upstream may have opened it. Re-posting the identical body is safe — a re-post never opens a second order — but it will keep answering EIN0052 for as long as the reservation stays open. Step 3 — stop, and surface it. There is no client route that clears this reservation. Stop retrying in a loop, show the request_id to the operator, and let them reconcile it directly — a retry loop only produces more EIN0052s to sort through. Step 4 — an EVM-gasless-only shortcut (advisory, not contract). On that lane specifically, the Permit2 nonce inside the signPayload the user signed being spent on-chain is proof the order opened. Until you can check that, do not ask the user to sign for the same payment again. Tip. Check the code for EIN0052 before consulting HubError.retryable — EIN0052 sits in the availability block (EIN0050–EIN0079) and so reads as retryable, but looping on it is exactly the wrong instinct; it needs an operator, not a retry. See Submit order (openFor), Errors — the blocks and SDK errors — Retrying.

3. EIN0005 — quote expired

Step 1 — compare the times. The quote from POST /quote carried:
Thirty seconds of validity, never refreshed. Step 2 — which rule applied. On a signed lane, a submission at or after expiresAt is EIN0005, even while the quoting process still holds the template. On a proxy lane, the rule is 20 minutes after the quote was issued, not expiresAt itself — but the aggregator already refuses the registration once expiresAt has passed (EIN0007), and the deposit then completes unregistered (known issue). A prepared quote’s stored expiry is never extended by a retry. A non-prepared quote submitted to a different replica than the one that quoted it looks expired there too, even before its real expiresAt. Had the user signed too late, the first openFor for this quote, sent after 12:00:45Z, would have answered as below. (In the quickstart this quote was accepted at 12:00:20Z, so posting it now replays that ack instead: see step 4.)
Step 3 — recover. Re-quote (a new preparationId if you were preparing), have the user sign the new disclosure, and submit that. The old signature is dead — do not resend it. On a proxy lane whose transaction is already sent, don’t re-quote: a new quote means a second deposit. Follow the first one on chain instead. Step 4 — check first if you already submitted. A recorded submission is still replayed after expiresAt (the idempotency check runs before the expiry check). If you’re not sure whether an earlier attempt landed, check before you re-quote: read the status by the hubOrderId alias (walk-through 1) and look for the order in GET /orders. A re-quote is a new quote with its own quoteId, so submitting it opens a second order, and the user pays twice. See Submit order (openFor), Get quote and Limits.

4. EIN0080 — rate limited

Step 1 — the body. At the defaults (tenantRps: 5) every shed on every route carries the same back-off:
error_params[0] is floor(1000 / tenantRps) milliseconds, at least 1 — the same value on every shed, on /quote, /quote/preview and /quote/prepare alike (they share one bucket), and separately on POST /order//order/openfor. Step 2 — which bucket. The three quote routes share a bucket; POST /order and /order/openfor share another; both are per organization: every call of your organization, with a key or with its project id, draws on them. A 429 with an EPX code (EPX0080, EPX0082) is the gateway’s limit instead, in front of the hub: the same rule applies, and its back-off also comes in a Retry-After header. Step 3 — recover. On the order routes nothing was reserved or forwarded by a shed, so re-posting the identical body after error_params[0] ms (HubError.retryAfterMs) is exactly right — it is not the EIN0052 situation. Step 4 — stay under it. Debounce POST /quote/preview while the user is still typing and pause it while a wallet prompt is open; call POST /quote/prepare once per confirmation, not once per render; poll the status views every 2–3 s (both are cached 2 s, so polling faster buys nothing). Behind your own backend, meter your users yourself: they all share your organization’s buckets. See Get quote, Limits and Handling rate limits.

See also