Errors and retries
Relay returns stable JSON envelopes for application errors. Use the HTTP status for transport behavior, error.code for program logic, and meta.requestId for investigation.
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "One or more fields are invalid.",
"details": [
{
"field": "useremail",
"code": "isEmail"
}
]
},
"meta": {
"requestId": "ab16ed04-fceb-4415-a609-967bc77972fa"
}
}HTTP status codes#
| Status | Default code | Meaning | What to do |
|---|---|---|---|
400 | INVALID_REQUEST or VALIDATION_FAILED | The body, query, asset, amount, or workflow rule is invalid | Correct the request; do not retry unchanged |
401 | UNAUTHORIZED | The accesskey is missing, invalid, or inactive | Fix or rotate credentials |
403 | FORBIDDEN | The authenticated actor is not allowed to perform the operation | Review account access; do not retry unchanged |
404 | NOT_FOUND or a resource code | The tenant-scoped resource does not exist | Check the resource identifier |
409 | CONFLICT | The request conflicts with current resource state | Retrieve current state before deciding whether to retry |
422 | VALIDATION_FAILED | Semantically invalid input on endpoints that use this status | Correct the fields shown in details |
429 | RATE_LIMITED | A plan or protection limit was reached | Respect Retry-After when present and back off |
500 | INTERNAL_ERROR | Relay encountered an unexpected error | Retry with backoff; contact Relay if it persists |
Relay does not publish one global request quota. Limits may vary by environment or commercial plan. Build clients that can safely handle 429 without assuming a fixed requests-per-minute value.
Business validation errors#
Some 400 responses have a message specific to the business rule. Common examples include:
| Message or code | Cause |
|---|---|
Invalid Crypto Token | The network/token pair is not active for payment intents |
INVALID_PAYMENT_AMOUNT | The amount is non-positive or cannot fit the token precision |
External Wallet Address is Required | external_wallet was selected without a destination |
Asset is unavailable | A customer-wallet asset is inactive or missing its contract configuration |
Customer wallet fees are not configured for this asset | Relay has not configured a fee policy for the organization and asset |
PAYMENT_NOT_FOUND | No payment intent with that txId belongs to the organization |
Messages add context but may become more specific. Prefer branching on error.code where a dedicated code exists.
Retry policy#
Retry only errors that can reasonably recover without changing the request:
- network failures before you receive a response;
429responses, honoring a boundedRetry-After; and500responses with exponential backoff and jitter.
Do not automatically retry 400, 401, 403, 404, or ordinary 409 responses.
const retryable = response.status === 429 || response.status >= 500;
const retryAfter = Number(response.headers.get("retry-after") ?? 0) * 1000;
const delay = Math.max(retryAfter, Math.min(30_000, 500 * 2 ** attempt));Creation and idempotency#
txRef is your merchant reference; it is not an HTTP idempotency key. The current payment-intent creation routes do not accept a documented idempotency header. A repeated creation request can create another intent.
For ambiguous timeouts:
- record whether Relay returned a
txIdbefore retrying; - use a durable local state machine around each order;
- avoid blindly sending the same create request again; and
- reconcile with your stored Relay resource or contact Relay using the request ID when the outcome is uncertain.
Read endpoints are safe to retry. Webhook deliveries are explicitly at-least-once and keep the same event ID across retries.