When a request fails, the API returns a non-2xx HTTP status and an error field. The HTTP status tells you the category of failure; the body tells you the specific reason.

Error envelope

The error field is either a plain string (most endpoints) or a structured { code, message, details? } object (webhooks, partner onboarding URLs, newer endpoints). Validation failures use a third hybrid: a string error with a sibling top-level details array. See Response format → Error responses for the full shape contract and a Node normalizer you can drop into your client.
error
string | object
required
String for most endpoints; { code, message, details? } for newer endpoints. Always present on a non-2xx response.
error.code
string
Structured form only. Stable, machine-readable identifier (e.g. VALIDATION_ERROR, NOT_FOUND). Branch on this, not on the message text.
error.message
string
Structured form only. Human-readable description. Safe to log; do not parse.
error.details
object
Structured form only. Optional context (e.g. Zod-flattened field errors). May be absent.
details
array
Validation form only. Sibling of error (not nested). Items are { field, message } describing one validation failure.

HTTP status codes

StatusMeaning
400Bad request / validation error
401Missing or invalid API key
403Forbidden — insufficient scope, CSRF, or IP not allowed
404Resource not found
429Rate limit exceeded
500Internal server error
The request body or query failed validation (missing field, wrong type, out of range). Inspect the details array (string-form validation) or error.details (structured form) for the offending fields, fix the request, and resubmit. Retrying an unchanged request will fail again.
No API key was sent, or the key is malformed, unknown, inactive, or expired. Check the x-api-key header. See Authentication.
The key is valid but the request is not permitted: the key lacks the required scope, the write request is missing or has an invalid X-CSRF-Token, or the source IP is not on the key’s whitelist.
The referenced resource does not exist, or it exists but does not belong to your customer. Verify the ID.
You have exceeded the request limit. Back off and retry after the delay in the Retry-After header. See Rate limiting.
An unexpected error on our side. These are usually transient — retry with exponential backoff. If it persists, contact support with the timestamp.

Handling errors gracefully

1

Normalize, then read code / message

Coerce the string and structured forms into one shape (see the Node sample below). Branch on HTTP status for the broad category, then on the normalized code (when present) for the specific reason. Never depend on the wording of the message text.
2

Fix-and-retry vs. back-off-and-retry

400/403/404 are client errors — retrying an unchanged request will not help. Only retry 429 (after Retry-After) and 500 (with exponential backoff).
3

Log the full envelope

Persist the entire normalized error (code, message, details) plus the HTTP status. Never log the API key or request secrets.
Node
const body = await res.json();
if (body.error !== undefined) {
  const err = typeof body.error === 'string'
    ? { code: undefined, message: body.error, details: body.details }
    : body.error;
  switch (res.status) {
    case 400: return rejectValidation(err.details);
    case 401:
    case 403: return promptReauth(err.code);
    case 404: return notFound(err.message);
    case 429: return retryAfter(res.headers.get("Retry-After"));
    case 500: return retryWithBackoff();
    default:  return surfaceUnknown(err);
  }
}

Verifying external actions: reject-list over allow-list

Some responses reflect the state of an action handed off to the underlying banking infrastructure (for example a payment or transaction status). When you verify whether such an action succeeded, prefer a reject-list of known failure states over an allow-list of known success states. External systems may omit a status field, return a new value, or rename a field without notice. An allow-list treats every unrecognized value as a failure — so a benign new status silently breaks your integration. A reject-list treats only known-bad values as failures, so missing or unknown values are assumed successful.
const failed = ["failed", "rejected"].includes(status);
// missing / unknown status → treated as success
This applies only to verifying downstream action outcomes. Your own client-side validation should stay strict — reject anything you do not recognise before sending it.

Response format

The full success and error envelope contract.