Errors
Error codes, the error envelope, and how to handle retryable vs non-retryable failures.
All error responses use the same envelope. Use error.code for programmatic handling and include meta.request_id when contacting support.
{
"error": {
"code": "not_found",
"message": "invoice not found",
"details": []
},
"meta": {
"request_id": "req_0000000000000001"
}
}Error codes
| Code | HTTP | Description |
|---|---|---|
validation_error | 400 | One or more request fields failed validation. Check details for field-level messages. |
invalid_address | 400 | The destination address is not valid for the specified currency or network. |
unauthorized | 401 | API key is missing, malformed, revoked, or from the wrong environment. |
forbidden | 403 | API key is valid but lacks the required permission for this operation. |
not_found | 404 | The requested resource does not exist. |
invoice_not_found | 404 | The specified invoice does not exist or does not belong to this merchant. |
payout_not_found | 404 | The specified payout does not exist or does not belong to this merchant. |
conflict | 409 | The request conflicts with current state (e.g. cancelling a non-cancellable invoice). |
idempotency_key_mismatch | 422 / 409 | The request body does not match the body of a prior request that used the same idempotency key. Returns 422 on Create Invoice; 409 on Create Refund. Use a new idempotency key or resend the original body. |
insufficient_balance | 422 | The merchant balance is too low to create this payout or refund. |
gate_offline | 503 | The blockchain processor for this currency is temporarily unavailable. Retry with backoff. |
rate_limit_exceeded | 429 | Too many requests. Back off using the X-RateLimit-Reset header. |
internal_error | 500 | Unexpected server error. Retry once; if it persists, contact support with meta.request_id. |
Retryable vs non-retryable
Retryable: gate_offline, rate_limit_exceeded, internal_error (once). Use exponential backoff.
Non-retryable without changing the request: validation_error, invalid_address, unauthorized, forbidden, not_found, invoice_not_found, payout_not_found, conflict, idempotency_key_mismatch, insufficient_balance.
Retry 429 and temporary 5xx errors with backoff. Do not retry validation errors without correcting the request. Do not show raw internal_error details to end users.
Handling 429 responses
On 429, the body contains rate_limit_exceeded. Use the X-RateLimit-Reset header (Unix seconds) to determine when to retry — there is no Retry-After header on this API.
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err: unknown) {
const e = err as { status?: number; headers?: Record<string, string> };
if (e.status !== 429 || attempt === maxRetries) throw err;
const resetAt = parseInt(e.headers?.['x-ratelimit-reset'] ?? '0', 10);
const waitMs = resetAt > 0 ? (resetAt * 1000 - Date.now()) : 1000;
await new Promise((r) => setTimeout(r, Math.max(waitMs, 100)));
}
}
throw new Error('unreachable');
}Support
Always include meta.request_id in support requests. It lets the halfin team find the server trace immediately.