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"
}
}
}| Field | Type | Description |
|---|---|---|
type | string (enum) | High-level category (see below) |
code | string | Machine-readable code, e.g. missing_field |
message | string | Human-readable description, safe to show to end-users when relevant |
field | string (optional) | When the error is about one specific input field |
detail | object (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
| Code | Cause | Fix |
|---|---|---|
recipient_unresolvable | Account number invalid at every provider in the cascade | Check account number + bank code |
name_mismatch | (FX rails only) Merchant-supplied name doesn’t match bank-of-record | Use the bank-of-record name; NUBAN auto-resolves on NGN |
beneficiary_cooldown | Same recipient paid in the last 5 minutes | Wait or pass allow_duplicate: true if intentional |
limit_violation | Per-tx / daily / per-beneficiary cap exceeded | Check MerchantLimits settings or split the payment |
fraud_rule_blocked | Internal fraud rule blocked | Contact support; check rule details in error.detail.hits |
merchant_blocked | Recipient on your merchant blacklist | Remove from blacklist or use different recipient |
globally_blocked | Recipient on platform-wide blacklist | Cannot override; contact support |
sender_info_required | Your account requires sender info (IMTO compliance) | Add sender block to request |
POST /v1/batches
| Code | Cause | Fix |
|---|---|---|
validation_failed | One or more rows failed pre-flight checks | Inspect error.detail.row_errors for per-row reasons |
bulk_disabled_for_key | API key not enabled for bulk | Use a key with payout_bulk_upload permission |
bulk_requires_ip_allowlist | Bulk needs a non-empty IP allowlist | Add at least one IP to the key |