Errors
What each code means and which errors are worth retrying.
Every error has the same shape.
{
"error": {
"type": "invalid_request",
"code": "route_unsupported",
"message": "ARG/ARS/BANK_ACCOUNT/SPEI is not a live route.",
"param": "rail",
"request_id": "req_..."
}
}type is the category. code is the specific reason and the thing to branch on. param names the offending field where one applies. request_id is what to quote when you ask us about it, so log it.
Branch on the code field. The message field is written for humans and will change, so don't depend on it.
Types
type | HTTP | Means |
|---|---|---|
invalid_request | 400, 422 | Something about the request is wrong |
authentication_error | 401 | Credentials missing or bad |
permission_error | 403 | Authenticated but not allowed to do this |
not_found | 404 | No such resource, or not on this host |
conflict | 409 | Valid request, wrong state |
rate_limited | 429 | Too many requests |
api_error | 5xx | A fault on our side |
Codes worth knowing before you hit them
Permission and eligibility
| Code | What went wrong | Do this |
|---|---|---|
endorsement_not_enabled | The customer doesn't hold the endorsement this needs, or it isn't APPROVED yet | Request it, wait for the event, then retry. See Endorsements. |
customer_type_ineligible | This customer type can't do this at all. A SUB_CUSTOMER opening a Corporate USD Account, for instance | Nothing to retry. |
third_party_not_permitted | Paying a destination the customer doesn't own, without stablecoin_payout.third_party | Request the endorsement. |
Routes and rails
| Code | What went wrong | Do this |
|---|---|---|
route_unsupported | The combination of country, currency, destination type and rail isn't LIVE | Read GET /v1/countries/{country}. Don't retry the same request. |
rail_mismatch | The rail doesn't match the destination's supportedRails[]. A SPEI quote can't fund a PIX account | Re-quote on a rail the destination supports. |
currency_mismatch | The destination's currency isn't the quote's toCurrency | Fix one or the other. |
All three are refused before anything is locked, so no balance is held while you work out what to change.
Quotes
| Code | What went wrong | Do this |
|---|---|---|
quote_expired | Execution landed outside expiresAt | Re-quote. Don't retry. |
quote_consumed | This quote already funded something | Get a new one. |
pair_shape_violation | A conditional field was supplied where the pair forbids it, omitted where it requires it, or both amounts were set | Check param. See Quotes and pricing. |
State
| Code | What went wrong | Do this |
|---|---|---|
insufficient_balance | Not enough in available | Read the balance. Don't retry blind. |
parent_terminal | You raised an action against a transaction that's COMPLETE, FAILED, CANCELLED or BLOCKED | Nothing reaches back into a terminal transaction. A new transaction in the opposite direction is the only route. |
not_returned | You raised a correction against a payout whose leg isn't RETURNED | Use /reprocess if it failed without returning. |
recipient_not_linked | The destination isn't VERIFIED | Wait on linked_account.updated. |
submission_frozen | The customer's draft is already under verification | Use /reevaluate. |
preflight_failed | The draft wouldn't pass. error.details lists what's outstanding | Fix what it names, then submit. |
Requests
| Code | What went wrong |
|---|---|
idempotency_conflict | Same key, different body |
validation_error | A field is missing, the wrong type, or not in the enum. param names it |
file_too_large | Over the 10 MB per-file limit |
unsupported_file_type | Not PDF, JPEG or PNG |
What to retry
Retry: a 429, waiting for the interval in the Retry-After header. A 5xx, with backoff.
Don't retry: anything 4xx other than 429. The request will fail the same way until you change it.
Careful: replaying a POST needs the same Idempotency-Key as the original. A new key on a retry can double-execute. The same key with a changed body returns idempotency_conflict instead of executing.
Rate limits
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 carries Retry-After.
Read the headers instead of hardcoding a number. X-RateLimit-Remaining is the only figure that's true at the moment you read it, and a limit copied into your code stops being accurate the first time yours is adjusted.
Simulator paths on production
Simulator paths under /v1/test/ exist on the sandbox host only. On production they return 404 not_found. That is intended behavior and doesn't indicate a missing route.
Updated 5 days ago

