Errors
Chain uses standard HTTP status codes and returns structured error responses. This guide covers error formats, common codes, and best practices for handling them.
Error Response Format
All error responses return a consistent JSON object with an error key.
{
"error": {
"code": "insufficient_funds",
"message": "Wallet wal_xyz789 has insufficient funds for this transaction.",
"status": 422,
"param": "amount",
"request_id": "req_abc123"
}
}HTTP Status Codes
| Code | Meaning | Action |
|---|---|---|
200 | OK | Request succeeded. |
201 | Created | Resource created successfully. |
400 | Bad Request | Check request body for validation errors. |
401 | Unauthorized | Verify your API key and Authorization header. |
403 | Forbidden | Your API key does not have access to this resource. |
404 | Not Found | The requested resource does not exist. |
409 | Conflict | Request conflicts with current state (e.g. duplicate idempotency key). |
422 | Unprocessable | Request is valid but cannot be processed (e.g. insufficient funds). |
429 | Rate Limited | Too many requests. Back off and retry with exponential delay. |
500 | Server Error | Something went wrong on our end. Retry or contact support. |
Error Codes
Authentication
| Code | Description |
|---|---|
invalid_api_key | The API key provided is invalid or revoked. |
expired_api_key | The API key has expired. Generate a new one. |
missing_authorization | No Authorization header provided. |
insufficient_permissions | Your key does not have the required permissions. |
Validation
| Code | Description |
|---|---|
invalid_parameter | A request parameter is invalid. Check the param field. |
missing_parameter | A required parameter is missing. |
invalid_currency | The specified currency is not supported. |
amount_too_small | Amount is below the minimum transaction size. |
amount_too_large | Amount exceeds the maximum transaction size. |
Payment
| Code | Description |
|---|---|
insufficient_funds | Wallet does not have enough funds for this transaction. |
bank_account_not_linked | The specified bank account has not been linked. |
payee_not_verified | Payee has not completed verification. |
payment_limit_exceeded | Transaction exceeds your account or card limits. |
duplicate_transaction | A transaction with this idempotency key already exists. |
Card
| Code | Description |
|---|---|
card_frozen | The card is frozen and cannot process transactions. |
card_cancelled | The card has been cancelled. |
card_limit_exceeded | Transaction exceeds card spending limits. |
mcc_not_allowed | Merchant category is not allowed for this card. |
Business & Compliance
| Code | Description |
|---|---|
business_not_verified | Business has not completed KYB verification. |
business_suspended | Business account has been suspended. |
compliance_hold | Transaction held for compliance review. |
Rate Limiting
| Code | Description |
|---|---|
rate_limit_exceeded | Too many requests. Retry after the delay in the Retry-After header. |
concurrent_request_limit | Too many concurrent requests. Reduce parallel calls. |
Best Practices
Use idempotency keys
Include X-Idempotency-Key headers on mutating requests to safely retry without creating duplicate resources.
Implement exponential backoff
When you receive a 429 or 5xx error, wait with exponential backoff before retrying. Our SDKs handle this automatically.
Check the request_id
Every error response includes a request_id. Include this when contacting support for faster resolution.
Handle specific error codes
Don't just check HTTP status codes. Use the code field to handle specific error scenarios and provide better error messages to your users.
Example: Handling Errors
import Chain, { ChainError } from '@chain/node';
const chain = new Chain('sk_live_...');
try {
const payout = await chain.payouts.create({
amount: 50000,
currency: 'USDC',
payee_id: 'payee_abc',
wallet_id: 'wal_xyz789',
});
} catch (err) {
if (err instanceof ChainError) {
switch (err.code) {
case 'insufficient_funds':
// Show user a friendly message
showError('Not enough funds. Please add more to your wallet.');
break;
case 'payee_not_verified':
// Redirect to verification
redirect('/payees/payee_abc/verify');
break;
case 'rate_limit_exceeded':
// Retry after delay
await sleep(err.retryAfter * 1000);
break;
default:
// Log and report
logger.error(`Chain API error: ${err.code}`, {
requestId: err.requestId,
status: err.status,
});
}
}
}