# Errors

The gateway returns errors as **[RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)
problem documents** with the `application/problem+json` content type. The `type` field
is a stable URI you can branch on.

## Shape

```json
{
  "type": "https://api.paymee.online/errors/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "One or more fields failed validation.",
  "errors": [
    { "field": "email", "code": "required", "message": "This field is required." }
  ]
}
```

| Field | Meaning |
| --- | --- |
| `type` | Stable URI identifying the error — branch on this. |
| `title` | Short, human-readable summary. |
| `status` | HTTP status code. |
| `detail` | Explanation specific to this occurrence. |
| `errors` | On `validation-error` only: one `{ field, code, message }` per invalid field. |
| `existing_session_id` | On `conflict` from session creation: the session that already uses this `merchant_reference`. |

Key your handling on `type` (and `errors[].code`), never on `title`, `detail` or
`message`, which may change.

## HTTP statuses

| Status | Means |
| --- | --- |
| `400` | The request is malformed (e.g. the body isn't valid JSON). |
| `401` | Missing, malformed, expired or revoked API key. |
| `404` | The object doesn't exist on this account. |
| `409` | The request conflicts with an earlier one. |
| `422` | The request is well-formed but was refused: a field failed validation, or a business rule refused it. |
| `429` | Rate limit exceeded. |
| `500` | Unexpected server error. Nothing was changed. |

## Error type catalog

All `type` URIs are under `https://api.paymee.online/errors/`.

| `type` suffix | HTTP | Meaning | Retry? |
| --- | --- | --- | --- |
| `bad-request` | 400 | Malformed request: the body isn't valid JSON, or a header is invalid. | No |
| `unauthorized` | 401 | Missing, malformed, expired, or revoked API key. | No |
| `forbidden` | 403 | Authenticated, but not allowed. | No |
| `not-found` | 404 | The session does not exist for this account. | No |
| `conflict` | 409 | Same `merchant_reference` as an existing session, with a different `amount` or `currency`. Includes `existing_session_id`. | No |
| `session-terminal` | 409 | The session is already final (cancel). | No |
| `idempotency-key-reuse` | 409 | The `Idempotency-Key` was already used for a different refund request. | No |
| `validation-error` | 422 | One or more fields failed validation (see `errors`). | No |
| `invalid-state` | 422 | Not allowed in the session's current state (e.g. canceling a `PROCESSING` session, refunding one that didn't succeed). | No |
| `no-payment` | 422 | The session has no successful payment to refund. | No |
| `partial-requires-settled` | 422 | Partial refund before the payment has settled. See [Refunds](/guides/refunds/#rules). | Later |
| `over-refund` | 422 | The refund exceeds the amount left to refund. | No |
| `insufficient-funds` | 422 | The refund can't be funded from your balance. | Later |
| `refund-failed` | 422 | The refund could not be recorded. | No |
| `rate-limit-exceeded` | 429 | Too many requests. Wait `Retry-After` seconds. | Yes |
| `error` | 405 / 415 | Wrong HTTP method or content type. | No |
| `internal-error` | 500 | Unexpected server error; nothing was changed. | Yes, with backoff |

:::tip
Only `429` and `500` are worth retrying automatically, with exponential backoff. When
you retry a session creation, reuse the same `merchant_reference`; when you retry a
refund, reuse the same `Idempotency-Key`. A request that did go through is then not
repeated.
:::

New `type`s may be added (see [Versioning](/guides/versioning/)): give your code a
default branch.

## Validation error codes

A `validation-error` lists each invalid field in `errors`. Nested fields are named with
dots, e.g. `items.1` or `metadata.cart_id`.

**Any field**

| `errors[].code` | Meaning |
| --- | --- |
| `required` | The field is missing. |
| `blank` | A required text field is empty. |
| `null` | The field is `null`. |
| `invalid` | The value is malformed (e.g. not an email address, wrong JSON type). |
| `max_length` | The text is too long. |
| `min_value` / `max_value` | The number is out of range (e.g. `max_attempts` outside 1–10). |
| `invalid_choice` | Not one of the allowed values (e.g. `refund_type`). |
| `not_a_list` / `not_a_dict` | `items` isn't a list, or one of its entries isn't an object. |

**`amount` and `currency`**

| `errors[].code` | Meaning |
| --- | --- |
| `invalid_format` | Thousands separators, leading zeros, or not a decimal number. |
| `invalid_precision` | The decimals don't match the currency: exactly 3 for `TND`, 2 for `EUR`. See [Amounts & currency](/guides/amounts-and-currency/). |
| `invalid_value` | Zero or negative, or a foreign amount that rounds to nothing in TND. |
| `amount_too_large` | Over `999999.999` TND, before or after conversion. |
| `currency_unsupported` | No exchange rate is configured for this currency. See [Amounts & currency](/guides/amounts-and-currency/#unsupported-currency). |

**`metadata`**

| `errors[].code` | Meaning |
| --- | --- |
| `too_many_keys` | More than 50 keys. |
| `key_too_long` | A key is longer than 40 characters. |
| `invalid_key_chars` | A key contains `[` or `]`. |
| `invalid_value_type` | A value is an object, a list or `null` (text, numbers and booleans are accepted). |
| `value_too_long` | A value is longer than 500 characters. |

**`return_url`, `cancel_url`, `webhook_url`**

| `errors[].code` | Meaning |
| --- | --- |
| `https_required` | The URL doesn't use `https://`. |
| `invalid_domain` | The host isn't a domain name with at least one dot (e.g. `localhost`). |
| `ip_not_allowed` | `webhook_url` uses an IP address instead of a domain name. |
| `invalid_scheme` | `javascript:` URLs are refused. |
| `url_too_long` | Over 2048 characters. |

## Rate limits

Limits apply **per merchant account**, separately for live (`sk_live_`) and test
(`sk_test_`) keys: test traffic never uses your live budget, and every key of the same
account shares that account's budget.

Every request counts against the account-wide limit. Creating sessions and refunds also
count against their own, lower limit.

| Budget | Endpoints | Live | Test |
| --- | --- | --- | --- |
| Account-wide | All gateway endpoints | 25 requests/second | 10 requests/second |
| Create | `POST /gateway/sessions/` | 10 requests/second | 5 requests/second |
| Refund | `POST /gateway/sessions/{id}/refund/` | 5 requests/second | 2 requests/second |

Over a limit, the API answers **`429 Too Many Requests`** with a **`Retry-After`**
header giving the number of seconds to wait. Spread bulk work (backfills,
reconciliation) over time rather than sending it in bursts.

Source: https://docs.paymee.online/guides/errors/
