Errors

Every error is a structured JSON object with a stable type and code. Handle them by code, show the message, and log the request_id.

The API uses conventional HTTP status codes and returns a consistent error object on every failure. Branch your logic on type and code (stable, machine-readable), surface message to your logs, and record request_id so support can trace the exact request.

Error object

json
{
  "error": {
    "type": "compliance_error",
    "code": "screening_blocked",
    "message": "Payout blocked by sanctions/blacklist screening",
    "param": "destination.address",
    "request_id": "req_2b8f1c9d3e4a"
  }
}
FieldDescription
typeHigh-level category (see below). Use it for coarse handling.
codeSpecific, stable machine-readable code. Use it for precise handling.
messageHuman-readable description. For logs and dashboards — do not parse it.
paramPresent when the error is tied to a specific request field.
request_idIdentifier for this exact request. Include it in support requests.

Error types

TypeTypical statusMeaning
invalid_request_error400 / 404 / 409 / 422The request was malformed, missing a field, or referenced something that doesn't exist.
authentication_error401The API key is missing or invalid.
permission_error403The key is valid but lacks the required scope, or the org is suspended.
rate_limit_error429Too many requests. Back off and retry.
compliance_error403 / 503Blocked by screening, limits, or a KYB requirement.
api_error500 / 503Something went wrong on our side. Safe to retry with backoff.

Common codes

CodeStatusWhat it means / what to do
api_key_missing401No key was sent. Add the X-API-Key header.
api_key_invalid401Unknown or revoked key. Check the key and its mode.
scope_insufficient403The key lacks the scope this endpoint needs.
org_suspended403The organization is suspended. Contact support.
kyb_required403Live mode needs a verified business. Submit KYB.
screening_blocked403The destination was blocked by screening. See Compliance & screening.
limit_exceeded403The payout would breach a per-payout, daily, or monthly limit.
insufficient_balance402Your organization's balance is below the payout amount.
unsupported_destination422The network/asset combination isn't supported.
idempotency_conflict409The idempotency key was reused with a different request.
payout_not_cancelable409The payout has advanced past the point where it can be cancelled.
resource_missing404The resource doesn't exist (or isn't yours).
sanctions_unavailable503Screening is temporarily unavailable. In live mode the payout is not sent; retry later.

Handling errors

javascript
const res = await fetch(url, options);
if (!res.ok) {
  const { error } = await res.json();
  switch (error.code) {
    case "idempotency_conflict":
      // You reused a key for a different payout — this is a bug in your code.
      throw new Error("idempotency key reused: " + error.message);
    case "insufficient_balance":
      return notifyOps("Top up the org balance");
    case "screening_blocked":
    case "limit_exceeded":
    case "kyb_required":
      return handleCompliance(error);           // don't retry — a human must act
    case "sanctions_unavailable":
      return retryLater();                       // transient
    default:
      if (res.status >= 500) return retryWithBackoff(); // our side, safe to retry
      throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
  }
}

Which errors to retry

  • Retry with backoff: 429 (rate limit) and 5xx (api_error, sanctions_unavailable). Always reuse the same Idempotency-Key when retrying a create.
  • Do not retry: 4xx other than 429 — the request itself needs to change. A blind retry will fail identically.
  • Never retry idempotency_conflict — it means your key generation is wrong.
Log request_id on every failure. It is the fastest way for support to find exactly what happened to a given request.