Errors

Errors

The Swappr API uses a consistent error envelope. Every error response has the same shape, so you can write a single error handler that covers every endpoint.

Error envelope

{
  "error": {
    "type": "invalid_request_error",
    "code": "missing_field",
    "message": "amount_minor is required.",
    "field": "amount_minor",
    "detail": {
      "expected": "string",
      "received": "undefined"
    }
  }
}
FieldTypeDescription
typestring (enum)High-level category (see below)
codestringMachine-readable code, e.g. missing_field
messagestringHuman-readable description, safe to show to end-users when relevant
fieldstring (optional)When the error is about one specific input field
detailobject (optional)Additional structured context

Error types

invalid_request_error (400, 422)

The request was malformed, missing required fields, or violated a business rule. Fix the request body before retrying.

Common codes: missing_field, invalid_field, invalid_json, invalid_url, invalid_event_type, wallet_not_found, customer_not_found, customer_not_verified, env_mismatch, unsupported_currency, invalid_recipient, recipient_unresolvable, validation_failed, limit_violation, fraud_rule_blocked, merchant_blocked, globally_blocked, name_mismatch, beneficiary_cooldown, sender_info_required.

authentication_error (401)

The API key is missing, malformed, or revoked.

Codes: missing_api_key, invalid_api_key, grace_expired (rotated key past grace window).

permission_error (403)

The API key lacks the permission required for this endpoint, OR your merchant account is suspended/closed, OR the request IP isn’t in the key’s allowlist.

Codes: permission_denied, merchant_suspended, merchant_closed, ip_not_allowed, bulk_disabled_for_key, bulk_requires_ip_allowlist, remittances_not_enabled.

not_found_error (404)

Resource doesn’t exist or doesn’t belong to your merchant. We return 404 across env boundaries (e.g. looking up a sandbox payout with a live key) so we don’t leak the existence of resources you can’t access.

Codes: payout_not_found, batch_not_found, wallet_not_found, beneficiary_not_found, customer_not_found, webhook_endpoint_not_found.

rate_limit_error (429)

You exceeded the request budget. Wait until Retry-After seconds elapse + retry. See Rate limits.

Codes: rate_limit_exceeded.

provider_error (502, 503)

A downstream rail returned an error or was unreachable. The original request is not committed; retrying with the same idempotency key is safe.

Codes: provider_unreachable, delivery_failed, rail_not_configured, plus rail-specific codes surfaced from the underlying response.

api_error (500)

Unexpected internal error. Our on-call team is paged automatically. Retry with the same idempotency key — if the issue clears within 60 minutes you’ll get the cached successful response.

Handling errors safely

Always parse error.code, not error.message. The message is human-readable + may change over time. The code is stable and machine-readable.

async function handleSwapprResponse(res: Response) {
  if (res.ok) return await res.json();
 
  const body = await res.json();
  const code = body?.error?.code;
  const type = body?.error?.type;
 
  switch (code) {
    case 'recipient_unresolvable':
      // Account number couldn't be resolved at any provider — show user a
      // friendly "check the account details" prompt.
      throw new UserFacingError('Could not verify recipient account.');
 
    case 'beneficiary_cooldown':
      // Same recipient was paid recently. Possibly a duplicate retry.
      // Your UI may want to ask "are you sure?" before retrying.
      throw new DuplicateRetryError(body.error.detail);
 
    case 'limit_violation':
      // Per-tx, daily, or per-beneficiary cap exceeded.
      throw new LimitExceededError(body.error.detail);
 
    case 'fraud_rule_blocked':
      // Internal fraud rule blocked the payout. Escalate to support.
      throw new FraudRuleError(body.error);
 
    case 'rate_limit_exceeded':
      // Wait Retry-After then try again.
      const retryAfter = parseInt(res.headers.get('retry-after') ?? '60', 10);
      await sleep(retryAfter * 1000);
      // Retry — caller should re-invoke with same idempotency key.
      throw new RetryableError(body.error.message, { retryAfter });
 
    default:
      if (type === 'provider_error' || type === 'api_error') {
        // Transient — caller can retry with same idempotency key.
        throw new RetryableError(body.error.message);
      }
      throw new ApiError(body.error);
  }
}

Common errors by endpoint

POST /v1/payouts

CodeCauseFix
recipient_unresolvableAccount number invalid at every provider in the cascadeCheck account number + bank code
name_mismatch(FX rails only) Merchant-supplied name doesn’t match bank-of-recordUse the bank-of-record name; NUBAN auto-resolves on NGN
beneficiary_cooldownSame recipient paid in the last 5 minutesWait or pass allow_duplicate: true if intentional
limit_violationPer-tx / daily / per-beneficiary cap exceededCheck MerchantLimits settings or split the payment
fraud_rule_blockedInternal fraud rule blockedContact support; check rule details in error.detail.hits
merchant_blockedRecipient on your merchant blacklistRemove from blacklist or use different recipient
globally_blockedRecipient on platform-wide blacklistCannot override; contact support
sender_info_requiredYour account requires sender info (IMTO compliance)Add sender block to request

POST /v1/batches

CodeCauseFix
validation_failedOne or more rows failed pre-flight checksInspect error.detail.row_errors for per-row reasons
bulk_disabled_for_keyAPI key not enabled for bulkUse a key with payout_bulk_upload permission
bulk_requires_ip_allowlistBulk needs a non-empty IP allowlistAdd at least one IP to the key