Errors
One envelope, snake_case, and a stable machine-readable code you switch on.
Every error on /v1 renders the same shape:
{
"status_code": 400,
"message": "That network cannot be received on",
"error": "Bad Request",
"details": { "code": "unsupported_chain" }
}details.code is the contract
message is prose written for a human reading a log, and it may be reworded at any
time. details.code is stable. Switch on details.code and nothing else.
This is also why every authentication failure carries its own distinct code rather than a
shared unauthorized. The difference matters at 2am: timestamp_skew tells your on-call
their clock drifted, where a generic failure sends them to rebuild a canonical string
that was fine all along.
Handle unknown codes as failures
The code set grows. A client that treats an unrecognised code as success will one day treat a refusal as a completed payment. Default to failure and alert.
Casing
Bodies on this surface are snake_case throughout, matching the webhook contract, because that is what your generated types expect. The first-party dashboard API uses camelCase - if you are reading both, they genuinely differ, and that split is deliberate rather than an oversight.
Retrying
| Class | Retry? |
|---|---|
4xx other than 429 | No. The request is wrong; retrying changes nothing. |
409 address_limit_reached | No. Talk to us. |
429 | Yes, after the indicated delay. |
503 service_unavailable | Yes, with exponential backoff and jitter. |
5xx | Yes, with backoff. Where the operation is idempotent this is always safe. |
Address creation is get-or-create on your reference, so retrying it after a timeout is
safe by construction - you get the same address, never a second one.