# Error reference

Every error type the API can return, with meaning and how to react.

# Error reference

All errors are JSON with the same envelope:

```json
{
  "error": {
    "type": "invalid_request",
    "code": "customer_email_invalid",
    "message": "customer_email must be a valid email address",
    "param": "customer_email",
    "request_id": "req_01H…"
  }
}
```

- `type` — high-level family (see table below).
- `code` — machine-readable specific error. Always populated for validation and idempotency errors.
- `message` — human-readable. Safe to log, **do not** show verbatim to end customers.
- `param` — the offending body field, when applicable.
- `request_id` — always log it. Support can look it up instantly.

## HTTP status codes

| HTTP | Meaning | Retry? |
|---|---|---|
| `200` / `201` | Success | — |
| `400` | Client error (bad input) | No — fix and resend |
| `401` | Auth failed (missing / bad / revoked key) | No |
| `402` | Payment declined at the network | Depends (see `decline_code`) |
| `403` | Key valid but not allowed for this action | No |
| `404` | Resource not found | No |
| `409` | Conflict (idempotency reuse, merchant not ready) | No — see `code` |
| `422` | Semantically invalid (e.g. session already paid) | No |
| `429` | Rate limited | Yes — respect `Retry-After` |
| `5xx` | Southbill server issue | Yes — with the same `Idempotency-Key` |

## `error.type` values

| Type | HTTP | When |
|---|---|---|
| `invalid_request` | 400 | Missing / malformed field. `param` tells you which. |
| `amount_too_small` | 400 | Below the 2.50 EUR equivalent minimum. |
| `amount_too_large` | 400 | Above the per-charge maximum. |
| `currency_unsupported` | 400 | Currency not in the supported list. |
| `authentication_error` | 401 | Key missing, malformed, revoked or wrong mode (test vs live). |
| `permission_error` | 403 | Key valid but scope/role forbids this action. |
| `card_error` | 402 | Buyer's card was declined. See `code` / `decline_code`. |
| `idempotency_error` | 409 | Same `Idempotency-Key` reused with a **different** request body. `code = idempotency_key_reused`. |
| `resource_conflict` | 409 | e.g. `merchant_not_ready`, `subscription_already_canceled`. |
| `not_found` | 404 | The id does not exist under this merchant. |
| `invalid_state` | 422 | `session_expired`, `session_already_completed`, `charge_disputed`. |
| `rate_limit_error` | 429 | Too many requests. Back off. |
| `api_error` | 5xx | Transient Southbill error. Retry with the same `Idempotency-Key`. |

## Idempotency conflict — exact shape

Reusing an `Idempotency-Key` with a body that differs from the original request always returns:

```json
{
  "error": {
    "type": "idempotency_error",
    "code": "idempotency_key_reused",
    "message": "Idempotency-Key reused with a different request body"
  }
}
```

- HTTP status: **409 Conflict**
- Uniqueness scope: `(merchant_id, method, path, Idempotency-Key)`.
- Same key + **same** body → replays the original response (status + body) verbatim.
- Same key + **different** body → the 409 shown above.
- Keys expire after **24 hours**.

## Recommended handling

- **Never** retry `4xx` errors except `409 idempotency_error` after you've fixed the body and rotated the key.
- **Always** retry `5xx` with the same idempotency key, using exponential backoff (1s → 2s → 4s, max 5 attempts).
- On `429`, sleep for the number of seconds in `Retry-After` (default 1) before the next attempt.
- Show `error.message` to internal operators, not to end customers — use a friendly wrapper.

