Understand error responses and handle them gracefully in your integration.
All API errors return a consistent JSON structure:
{
"error": "INVOICE_NOT_FOUND",
"message": "Invoice with ID inv_abc123 not found",
"details": {}
}
The error field is a stable machine-readable code you can match on.
The message is human-readable and may change - don't parse it programmatically.
| Code | Meaning | What to do |
|---|---|---|
400 |
Bad Request | Check request body and query parameters. The message field describes the specific validation failure. |
401 |
Unauthorized | API key or secret is invalid, expired, or missing. Verify your X-API-Key and X-API-Secret headers. |
403 |
Forbidden | Your API key doesn't have the required permissions for this endpoint. Check your key's permission scope. |
404 |
Not Found | The requested resource doesn't exist or belongs to a different organization. |
409 |
Conflict | The action can't be performed in the current state. For example, approving an already-settled invoice. |
422 |
Unprocessable Entity | The request body is valid JSON but violates business rules (e.g., negative amounts, invalid currency). |
429 |
Rate Limited | Too many requests. Back off and retry after the time indicated in X-RateLimit-Reset. |
| Code | Meaning | What to do |
|---|---|---|
500 |
Internal Error | An unexpected error occurred. Retry with exponential backoff. If it persists, contact support. |
502 |
Bad Gateway | A downstream service is temporarily unavailable. Retry after a short delay. |
503 |
Service Unavailable | The API is temporarily down for maintenance. Check the status page. |
504 |
Gateway Timeout | Request took too long. Retry - for bulk operations, consider smaller batch sizes. |
| Error code | Status | Description |
|---|---|---|
INVOICE_NOT_FOUND | 404 | Invoice ID doesn't exist or isn't accessible |
INVOICE_INVALID_STATUS | 409 | Action not allowed in current invoice status |
INVOICE_DUPLICATE_NUMBER | 409 | Invoice number already exists for this connection |
INVOICE_INVALID_AMOUNT | 400 | Amount must be positive with max 2 decimal places |
CONNECTION_NOT_FOUND | 404 | Connection ID doesn't exist or isn't active |
CONNECTION_NOT_ACTIVE | 409 | Connection is suspended or archived |
| Error code | Status | Description |
|---|---|---|
INSUFFICIENT_BALANCE | 409 | Not enough funds to complete the payment |
PAYMENT_ALREADY_SETTLED | 409 | This invoice has already been paid |
PAYMENT_FAILED | 500 | Payment processing failed - check details and retry |
| Error code | Status | Description |
|---|---|---|
INVALID_API_KEY | 401 | API key is invalid or doesn't exist |
INVALID_API_SECRET | 401 | API secret doesn't match the key |
API_KEY_REVOKED | 401 | This API key has been revoked |
ENVIRONMENT_MISMATCH | 403 | Sandbox key used against production or vice versa |
For transient errors (429, 500, 502, 503, 504),
implement exponential backoff with jitter:
// Pseudocode
maxRetries = 3
for attempt in 0..maxRetries:
response = makeRequest()
if response.status < 500 and response.status != 429:
return response
delay = min(2^attempt * 1000, 30000) // 1s, 2s, 4s... max 30s
jitter = random(0, delay * 0.1)
sleep(delay + jitter)
429) indicate a problem with the request itself.
Retrying without changing the request will always fail. Fix the request first.
All write operations support the requestId field. If you include a requestId
and the original request succeeded, retrying returns the original response - no duplicate side effects.
// Safe retry pattern
POST /api/v1/invoices
{
"requestId": "your-unique-id-123", // same ID on retry
"connectionId": "conn_abc",
"amount": "1500.00",
...
}
This is especially important for payment operations where duplicate processing would be costly.
exp_sand_ keys) to test error handling without real datamessage field often contains the specific validation failure - log it for debugging409 Conflict errors, fetch the current resource state before retrying the action