> ## Documentation Index
> Fetch the complete documentation index at: https://docs.panoflow.io/llms.txt
> Use this file to discover all available pages before exploring further.

# PanoFlow API errors and response codes

> The PanoFlow error object, error types, HTTP statuses, every public error code and its ISO 8583 response_code, and which errors are safe to retry.

When a PanoFlow request fails, you get an HTTP status and one error object with a stable `code` you can branch on, a message you can show your user, and an ISO 8583 `response_code` for reconciliation. The same shape appears in webhook delivery logs and in MCP tool errors.

```json theme={null}
{
  "error": {
    "response_code": "51",
    "type": "invalid_request",
    "code": "insufficient_balance",
    "message": "Naira · Main has ₦120,000.00 available. This payment needs ₦850,000.00.",
    "param": "amount",
    "request_id": "req_01JC...",
    "doc_url": "https://docs.panoflow.io/errors/insufficient_balance",
    "details": { "available": { "amount": 12000000, "currency": "NGN" } }
  }
}
```

| Field | Meaning |
| - | - |
| `type` | The broad category; see below. |
| `code` | Stable, `snake_case` and never renamed. Branch on this. |
| `message` | Safe to show your customer. |
| `param` | For validation errors, the offending field as a dot path, for example `recipient.account_number`. |
| `response_code` | Two-character ISO 8583 code with NIBSS NIP meanings, mapped one-to-one from `code`. Never `00` on an error. |
| `request_id` | The request id (`req_...`). Quote it to support. |
| `details` | Extra data for some codes, such as `details.available` or `details.required_scope`. |

> [!NOTE]
>
> New error codes can be added at any time. Handle unknown codes by falling back on `type` and the HTTP status.

## Error types

| `type` | HTTP status |
| - | - |
| `invalid_request` | 400 or 422 |
| `authentication` | 401 |
| `permission` | 403 |
| `not_found` | 404 |
| `conflict` | 409 |
| `rate_limited` | 429 |
| `partner` | 502 or 503: a partner is down |
| `internal` | 500 |

## HTTP statuses

| Status | When |
| - | - |
| 200 | A read, or an action that completed synchronously |
| 201 | Created |
| 202 | Accepted and processing (payouts, batches, pay runs, statements). Poll the resource or wait for the webhook. |
| 400 | Malformed request |
| 401 | No or bad credentials |
| 403 | Authenticated but not allowed |
| 404 | Not found, or not visible to your business. You never get 403 for another business's resource. |
| 409 | State conflict |
| 422 | Valid JSON that failed a business rule |
| 429 | Rate limited. Wait for the number of seconds in `Retry-After`. |
| 502, 503 | A partner is unavailable. Safe to retry with the same [idempotency key](https://docs.panoflow.io/idempotency.md). |

## Error codes

| Code | HTTP status | `response_code` | Meaning |
| - | - | - | - |
| `invalid_json` | 400 | `30` | Body is not valid JSON. |
| `parameter_missing` | 400 | `30` | A required field is missing; see `param`. |
| `parameter_invalid` | 400 | `30` | A field has the wrong type or format; see `param`. |
| `parameter_unknown` | 400 | `30` | An unknown field was sent. |
| `invalid_api_key` | 401 | `63` | Key unknown, revoked, or its business inactive. Never treated as anonymous. |
| `token_expired` | 401 | `63` | OAuth access token expired; refresh it. |
| `role_not_permitted` | 403 | `57` | The caller's role can't do this. |
| `scope_missing` | 403 | `57` | The key or token lacks the scope in `details.required_scope`. |
| `step_up_required` | 403 | `63` | Send a fresh X-Step-Up-Token (2FA within 5 minutes). |
| `business_frozen` | 403 | `57` | The business is frozen; money can't move. |
| `business_not_approved` | 403 | `57` | Onboarding not approved yet; live money movement is closed. |
| `account_closed` | 403 | `57` | The account is closed. |
| `ip_not_allowed` | 403 | `63` | Request came from outside the allowed IP ranges. |
| `agent_paused` | 403 | `57` | The agent is paused by its owner; its tokens are revoked. |
| `test_key_on_live` | 403 | `58` | A pk\_test\_ key was used against a live-only operation. |
| `resource_not_found` | 404 | `25` | Not found or not visible to this tenant. |
| `idempotency_key_reused` | 409 | `94` | Same Idempotency-Key with a different body. |
| `invalid_state` | 409 | `12` | The resource's status doesn't allow this action; see `details.status`. |
| `self_approval_not_allowed` | 409 | `57` | The requester can't decide their own approval. |
| `already_decided` | 409 | `21` | The approval was already approved, rejected or expired. |
| `duplicate_resource` | 409 | `26` | An identical resource already exists (e.g. recipient). |
| `version_conflict` | 409 | `21` | The resource changed since it was read; re-read and retry. |
| `insufficient_balance` | 422 | `51` | Available balance can't cover amount + fee; `details.available`. |
| `limit_exceeded` | 422 | `61` | Over a per-payment limit; `details.limit`. |
| `daily_limit_exceeded` | 422 | `61` | Over the daily limit. |
| `approval_threshold_exceeded` | 422 | `09` | Over the caller's approval threshold; a pending approval was created instead (see `details.approval_id`). |
| `recipient_not_allowed` | 422 | `57` | Agent tried to pay a recipient outside its allowlist. |
| `recipient_name_mismatch` | 422 | `08` | Name on the account doesn't match the name given and confirmation wasn't sent. |
| `name_enquiry_failed` | 422 | `25` | The bank couldn't confirm the account name. |
| `account_number_invalid` | 422 | `07` | Not a valid NUBAN for that bank. |
| `currency_not_enabled` | 422 | `12` | Currency not live for this business yet. |
| `currency_mismatch` | 422 | `12` | Amount currency doesn't match the account's currency. |
| `amount_too_small` | 422 | `13` | Below the minimum for this payment type. |
| `amount_too_large` | 422 | `13` | Above the maximum for this payment type. |
| `schedule_in_past` | 422 | `12` | Scheduled date is not in the future. |
| `batch_row_invalid` | 422 | `30` | One or more batch rows failed validation; `details.rows`. |
| `batch_too_large` | 422 | `14` | Batch exceeds the row limit for this plan. |
| `document_rejected` | 422 | `30` | Upload is the wrong type, too large, or unreadable. |
| `payroll_short_funds` | 422 | `51` | Payroll account can't cover the run; `details.short_by`. |
| `webhook_url_invalid` | 422 | `30` | Webhook URL must be https and publicly reachable. |
| `scope_not_allowed` | 422 | `57` | None of the requested OAuth scopes can be granted to this client. |
| `account_balance_not_zero` | 422 | `12` | Move the balance out before closing the account. |
| `net_pay_negative` | 422 | `13` | Deductions exceed gross pay for this person; see `details.person_id`. |
| `rate_limited` | 429 | `65` | Too many requests; wait `Retry-After` seconds. |
| `internal_error` | 500 | `96` | Our fault. Safe to retry with the same Idempotency-Key. |
| `partner_timeout` | 502 | `97` | A partner didn't answer in time. The payment's status is checked with the bank before any retry. |
| `bank_unavailable` | 503 | `91` | Partner bank is down or degraded. Retry with the same Idempotency-Key. |

## Response codes

`response_code` uses ISO 8583 codes with their NIBSS NIP meanings. It appears on every error and on every money-movement object; reads and other successful responses don't carry one. The HTTP status stays the primary signal. For the code meanings and how they map to payment states, see [Payment statuses](https://docs.panoflow.io/payment-statuses.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.