Error Handling

Understand error responses and handle them gracefully in your integration.

Error response format

All API errors return a consistent JSON structure:

{
  "error": "Invoice with ID inv_abc123 not found",
  "code": "INV001"
}

The error field is a human-readable message and may change - don't parse it programmatically. The code field is a stable machine-readable module code (e.g. INV001) you can match on.

HTTP status codes

4xx Client errors

CodeMeaningWhat 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.
412 Precondition Failed The resource is not in a state that permits this operation - a business-rule or state-machine precondition is not met (e.g., settling an invoice that has not been approved).
429 Rate Limited Too many requests. Back off and retry after the time indicated in X-RateLimit-Reset.

5xx Server errors

CodeMeaningWhat 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.

Common error codes

Invoice errors

Error codeStatusDescription
INV001404Invoice ID doesn't exist or isn't accessible
INV004412Action not allowed in current invoice status
INV003409Invoice number already exists for this connection
INV002400Generic invoice validation error (e.g., invalid amount or missing field)
CONN003404Connection ID doesn't exist or isn't accessible
CONN010412Connection is not in the expected status (e.g., suspended or archived)

Payment errors

Error codeStatusDescription
TREAS007412Not enough funds to complete the payment

Authentication errors

All API-key authentication failures - invalid key, invalid secret, revoked key - return the same response, with no distinguishing code:

{
  "error": "Unauthorized",
  "message": "Invalid API key or secret"
}

An API key is bound to its environment, so you never select one - omit any environment header and the key decides. The optional X-Environment header exists for dashboard sessions; sending it with an API key is only valid when it names that key's own environment. Unlike the auth failures above, these rejections do carry a code: 400 GW_UNKNOWN_ENVIRONMENT (value is not PRODUCTION or SANDBOX), 403 GW_ENVIRONMENT_KEY_MISMATCH (the key belongs to the other environment), and 503 GW_ENVIRONMENT_UNAVAILABLE (no deployed instances).

Retry strategy

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)
Don't retry 4xx errors Client errors (except 429) indicate a problem with the request itself. Retrying without changing the request will always fail. Fix the request first.

Idempotency and safe retries

Idempotency via the requestId field is enforced only for invoice create/submit and treasury balance/withdrawal operations. If you include a requestId on one of these and the original request succeeded, retrying returns the original response - no duplicate side effects. The field is accepted across the money-moving and resource-creating surface - invoices, bookings, connections and treasury operations. Do not infer the set from this prose: the endpoint's own request schema in openapi.json is authoritative - if it declares a requestId property, send one. Where an endpoint does not declare it, a retry may create a duplicate.

// Safe retry pattern (invoice create)
POST /api/v1/invoices
{
  "requestId": "your-unique-id-123",  // same ID on retry
  "sellerId": "550e8400-e29b-41d4-a716-446655440001",
  "buyerId": "550e8400-e29b-41d4-a716-446655440002",
  "amount": "1500.00",
  "dueDate": "2026-05-01"
}

This is especially important for payment operations where duplicate processing would be costly.

Debugging tips