# Errors

> Learn how Nexpay handles error codes, messages and more.

Nexpay uses conventional HTTP response codes to indicate the success or failure of an API request. In general: Codes in the `2xx` range indicate success. Codes in the `4xx` range indicate an error that failed given the information provided (e.g., a required parameter was omitted, a charge failed, etc.). Codes in the `5xx` range indicate an error with Nexpay’s servers.

---

## HTTP status code summary

| Number | Error | Description |
| --- | --- | --- |
| 200 | Ok | Everything worked as expected. |
| 400 | Bad Request | The request was unacceptable, often due to missing a required parameter. |
| 401 | Unauthorized | No valid API key provided. |
| 403 | Forbidden | The API key doesn’t have permissions to perform the request. |
| 404 | Not Found | The requested resource doesn’t exist. |
| 409 | Conflict | The request conflicts with another request. |
| 422 | Unprocessable Entity | The request was syntactically valid but could not be processed. Common causes include an expired quote (`[QOT0001]`), an invalid status transition (`[PAY0002]`), an unsupported currency pair (`[QOT0003]`), or reusing an `Idempotency-Key` with a different request body. Always check the response `code` to disambiguate. See [Idempotency](/docs/idempotency.md). |
| 429 | Too Many Requests | You have exceeded the rate limit. See [Rate limiting](/docs/rate-limiting.md). |
| 500, 502, 504 | Server Errors | Something went wrong on Nexpay's end. |
| 503 | Service Unavailable / Maintenance | Nexpay is undergoing scheduled maintenance or is temporarily unavailable. The response includes a `Retry-After` header. Check [status page](https://status.nexpay.com.au/) for updates. |

## Maintenance mode

In rare cases, Nexpay may enter maintenance mode for scheduled infrastructure work. When this happens, the API responds with HTTP status code `503 Service Unavailable` and includes a `Retry-After` header indicating the estimated time (in seconds) until the maintenance window ends. These windows are communicated in advance and you can monitor real-time status at [status.nexpay.com.au](https://status.nexpay.com.au/).

During maintenance:

* New payment attempts will fail.
* Dashboard login will be unavailable.
* Background operations (notifications, bank-related events) are queued and processed automatically once maintenance is over.
* Health checks and payment gateway callbacks continue to operate normally.

## Error codes

In addition to HTTP status codes, Nexpay returns application-level error codes to help you identify the specific issue. Each error code follows the format `[PREFIX####]`, where:

* **PREFIX** is a 3-letter module identifier (e.g., `PAY` for Payments, `AUT` for Authentication)
* **####** is a zero-padded numeric code unique within that module

For example, `[PAY0001]` means "Payment not found" and `[QOT0001]` means "Quote has expired". These codes remain stable and can be used to programmatically handle specific error scenarios, even if the human-readable message changes over time.

### Module prefixes

| Prefix | Module |
| --- | --- |
| `GEN` | General |
| `AUT` | Authentication |
| `PAY` | Payments |
| `QOT` | Quotes |
| `PYE` | Payees |
| `DOC` | Documents |
| `GWY` | Gateway |
| `TNT` | Tenant |
| `PRX` | Proxy (Legacy) |
| `COM` | Commissions |
| `ORG` | Organizations |
| `CNV` | Conversations |
| `DEV` | Developer (API Keys) |
| `CHT` | Chat |

### Error code reference

| Code | Message | HTTP Status |
| --- | --- | --- |
| `[GEN9001]` | Internal server error | 500 |
| `[GEN9002]` | Bad request — `details` pinpoints the cause: `details.missingRequirements` (connector-rule paths still unmet, e.g. `recipients.0.amount.portionBps`, `recipients.1.purposeProofDocumentId`, `documents.uniqueDocumentIds`) — these also surface on **dry-run**, so catch them there first; or `details.reason` for **live** checks that only run at submit (e.g. `'Legacy payee is not visible to the executing user'`, or `'Tenant recipient connectorPayeeId must match the payment-intent owner tenant'`). | 400 |
| `[GEN9003]` | Unauthorized | 401 |
| `[GEN9004]` | Forbidden — when a capability is missing, `details.failedRequirements` names it (e.g. `["User.AllowSplitPayment"]` for split / multi-payout quotes) and `details.legacyConstraint` the gate (`payment_access.multiPayoutQuote`). | 403 |
| `[GEN9013]` | Service is under maintenance. Please try again later. | 503 |
| `[AUT0001]` | Invalid credentials | 401 |
| `[AUT0002]` | Session expired | 401 |
| `[AUT0003]` | Invalid API key | 401 |
| `[AUT0004]` | Invalid Firebase token | 401 |
| `[AUT0006]` | An account with this email already exists | 409 |
| `[PAY0001]` | Payment not found | 404 |
| `[PAY0002]` | Invalid payment status transition | 422 |
| `[PAY0003]` | Duplicate payment detected | 409 |
| `[PAY0004]` | Payment type not supported | 400 |
| `[QOT0001]` | Quote has expired | 422 |
| `[QOT0002]` | Quote not found | 404 |
| `[QOT0003]` | Currency pair unavailable | 422 |
| `[QOT0004]` | Amount below minimum | 422 |
| `[PYE0001]` | Payee not found | 404 |
| `[PYE0002]` | Duplicate payee | 409 |
| `[PYE0003]` | Payee validation failed | 400 |
| `[DOC0001]` | Document not found | 404 |
| `[DOC0002]` | File exceeds maximum size | 413 |
| `[DOC0003]` | Invalid file format | 400 |
| `[GWY0001]` | Checkout session expired | 422 |
| `[GWY0002]` | Payment gateway unavailable | 503 |
| `[GWY0003]` | Invalid gateway callback | 400 |
| `[TNT0001]` | Tenant not found | 404 |
| `[TNT0002]` | Tenant branding not configured | 404 |
| `[COM0001]` | Commission request not found | 404 |
| `[COM0002]` | Minimum commission threshold not met | 422 |
| `[COM0003]` | Payment not eligible for commission | 422 |
| `[COM0004]` | Commission request already paid | 409 |
| `[COM0005]` | Commission request rejected | 422 |
| `[ORG0001]` | Organization not found | 404 |
| `[ORG0002]` | Organization update validation failed | 400 |
| `[ORG0003]` | Invalid document type | 400 |
| `[CNV0001]` | Conversation not found | 404 |
| `[CNV0002]` | Conversation is already closed | 422 |
| `[CNV0003]` | Conversation attachment not found | 404 |
| `[DEV0001]` | API key not found | 404 |
| `[DEV0002]` | Only authenticated users can perform this action | 403 |
| `[CHT0001]` | Chat conversation not found | 404 |
| `[CHT0002]` | AI is still processing. Please wait for the response. | 429 |
| `[CHT0003]` | This conversation has been closed | 422 |
| `[CHT0004]` | AI service temporarily unavailable | 503 |

## Error response

> **Warning — Error responses are NOT wrapped in `data`**
>
> Success responses are `{ "data": { ... } }`. Error responses are `{ statusCode, code, message, timestamp, details? }` at the top level. Destructure accordingly — `const { data } = await r.json()` on a non-2xx response will be `undefined` and your handler will swallow the real cause.

Error responses include the HTTP status code, the bracketed error `code`, a human-readable `message`, a `timestamp`, and optionally `details` with additional context:

```javascript
{
  "statusCode": 404,
  "code": "[PAY0001]",
  "message": "[PAY0001] Payment not found",
  "timestamp": "2026-01-15T10:30:00.000Z"
}
```

Some errors may include a `details` field with additional context about the issue:

```javascript
{
  "statusCode": 422,
  "code": "[QOT0003]",
  "message": "[QOT0003] Currency pair unavailable",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "fromCurrency": "BRL",
    "toCurrency": "USD"
  }
}
```

A payment-intent rejection carries the unmet connector requirements in `details.missingRequirements` — populate those paths on the intent and resubmit. **Dry-run reports the same paths**, so resolve them there before submitting:

```javascript
{
  "statusCode": 400,
  "code": "[GEN9002]",
  "message": "[GEN9002] Bad request",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "missingRequirements": [
      "recipients.0.amount.portionBps",
      "recipients.1.amount.portionBps",
      "recipients.1.purposeProofDocumentId"
    ]
  }
}
```

If the request failed due to the lack of permissions, Nexpay will return a `403 Forbidden` error. When a specific capability is missing, `details.failedRequirements` names it — for example, a split (multi-payout) quote without the `AllowSplitPayment` entitlement:

```javascript
{
  "statusCode": 403,
  "code": "[GEN9004]",
  "message": "[GEN9004] Forbidden",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "reason": "Payment access does not allow this quote creation",
    "legacyConstraint": "payment_access.multiPayoutQuote",
    "failedRequirements": ["User.AllowSplitPayment"]
  }
}
```
