# Errors

> The error envelope, every error code, request ids and what to do about each kind of error.

Source: https://rais.app/docs/errors

Every response that is not a success has the **same envelope**, whatever went wrong. The HTTP status tells you the class of problem, `type` how to react, and `code` exactly what happened.

## The error envelope

~~~json
{
  "error": {
    "type": "invalid_request_error",
    "code": "amount_too_small",
    "message": "The minimum amount is 50.00 DA (5000 centimes).",
    "param": "amount",
    "request_id": "req_8f3a1c9d2b7e4a60"
  }
}
~~~


- `type` (string): the category of the error. It decides how you react.
- `code` (string): a stable, machine-readable identifier. Branch on this, never on the message.
- `message` (string): an explanation for developers. Do not show it to payers, and do not parse it.
- `param` (string, nullable): the request parameter the error is about, such as `amount` or `customer.email`. Nested names use dots and brackets: `items[0].quantity`.
- `request_id` (string): the id of the request, also in the `Rais-Request-Id` response header.


## Request ids

Every response, errors included, carries a `Rais-Request-Id` header such as `req_8f3a1c9d2b7e4a60c5d1`, and the `Rais-Version` header. Log the request id of every call that fails, and quote it when you contact support: it finds the exact request on our side. Raïs logs each request with its id but never its headers, query string or body. The id is the same in the header and in `error.request_id`.

## Types

| Type | Status | What to do |
| --- | --- | --- |
| `authentication_error` | 401 | Fix the key. See [Authentication](https://rais.app/docs/authentication.md). |
| `permission_error` | 403 | The key is valid but not allowed: a missing scope. (A suspended store, or live mode before verification, answer `401` instead: see [Authentication](https://rais.app/docs/authentication.md#authentication-errors).) |
| `invalid_request_error` | 400, 405, 409 | Fix the request. The `param` names the field. Do not retry as it is. |
| `not_found_error` | 404 | The object does not exist for this key. See [Authentication](https://rais.app/docs/authentication.md). |
| `idempotency_error` | 409, 422 | The `Idempotency-Key` conflicts with another request. See [Idempotency and retries](https://rais.app/docs/idempotency.md). |
| `rate_limit_error` | 429 | Wait for the `Retry-After` header, then retry. See [Rate limits](https://rais.app/docs/rate-limits.md). |
| `payment_error` | 402, 502 | The payment gateway or the bank refused. Nothing was changed on your side of the line. |
| `api_error` | 500, 503 | A problem on our side or the gateway's. Retry later with the same `Idempotency-Key`. |

## Error codes


| Code | HTTP status | Type | Meaning |
| --- | --- | --- | --- |
| `amount_mismatch` | 400 | `invalid_request_error` | The amount is not an integer, is outside 5000...999999999999 centimes, or disagrees with `items`. |
| `amount_too_large` | 400 | `invalid_request_error` | The amount is not an integer, is outside 5000...999999999999 centimes, or disagrees with `items`. |
| `amount_too_small` | 400 | `invalid_request_error` | The amount is not an integer, is outside 5000...999999999999 centimes, or disagrees with `items`. |
| `currency_not_supported` | 400 | `invalid_request_error` | Only `DZD`. |
| `customer_not_found` | 400 | `invalid_request_error` | A field is invalid (see `param`). |
| `insufficient_balance` | 400 | `invalid_request_error` | The refund is more than what is left on the payment, or than your balance can cover (the refund plus its fee). |
| `invalid_amount` | 400 | `invalid_request_error` | The amount is not an integer, is outside 5000...999999999999 centimes, or disagrees with `items`. |
| `invalid_cursor` | 400 | `invalid_request_error` | The header or the `starting_after` value is invalid. |
| `invalid_email` | 400 | `invalid_request_error` | A field is invalid (see `param`). |
| `invalid_idempotency_key` | 400 | `invalid_request_error` | The header or the `starting_after` value is invalid. |
| `invalid_json` | 400 | `invalid_request_error` | The request body or query is malformed. |
| `invalid_metadata` | 400 | `invalid_request_error` | A field is invalid (see `param`). |
| `invalid_parameter` | 400 | `invalid_request_error` | The request body or query is malformed. |
| `invalid_url` | 400 | `invalid_request_error` | A field is invalid (see `param`). |
| `parameter_missing` | 400 | `invalid_request_error` | The request body or query is malformed. |
| `parameter_unknown` | 400 | `invalid_request_error` | The request body or query is malformed. |
| `refund_exceeds_balance` | 400 | `invalid_request_error` | The refund is more than what is left on the payment, or than your balance can cover (the refund plus its fee). |
| `invalid_api_key` | 401 | `authentication_error` | The key is missing, unknown, revoked, expired, not allowed from this IP address, or its store cannot use it. |
| `refund_declined` | 402 | `payment_error` | The bank refused the refund. |
| `account_disabled` | 403 | `permission_error` | The store is suspended or disabled. |
| `insufficient_scope` | 403 | `permission_error` | The key lacks the scope the endpoint needs. |
| `live_mode_not_enabled` | 403 | `permission_error` | Complete business verification to take live payments. |
| `event_not_found` | 404 | `not_found_error` | Nothing there (or it belongs to another store or environment). |
| `payment_not_found` | 404 | `not_found_error` | Nothing there (or it belongs to another store or environment). |
| `refund_not_found` | 404 | `not_found_error` | Nothing there (or it belongs to another store or environment). |
| `route_not_found` | 404 | `not_found_error` | Nothing there (or it belongs to another store or environment). |
| `method_not_allowed` | 405 | `invalid_request_error` | Wrong HTTP method. |
| `idempotency_in_progress` | 409 | `idempotency_error` | The first request with this key is still running. |
| `payment_not_cancellable` | 409 | `invalid_request_error` | The payment is not in a state that allows it. |
| `payment_not_refundable` | 409 | `invalid_request_error` | The payment is not in a state that allows it. |
| `idempotency_key_reused` | 422 | `idempotency_error` | The key was used with different parameters. |
| `rate_limit_exceeded` | 429 | `rate_limit_error` | Too many requests (60 per second, bursts of 120, per key) or too many failed authentications. |
| `internal_error` | 500 | `api_error` | Our fault. Quote the `request_id`. |
| `gateway_rejected` | 502 | `payment_error` | The payment gateway refused to create the payment. |
| `gateway_unavailable` | 503 | `api_error` | The gateway cannot be reached; retry later. Nothing was created or refunded. |


More codes can appear while a request is being read. They are produced by the validation of the request itself, so they are not tied to one endpoint:

| Code | HTTP status | Meaning |
| --- | --- | --- |
| `invalid_request_body` | 400 | The body is missing or is not a JSON object, for example an array. |
| `parameter_out_of_range` | 400 | A number is below or above what the parameter accepts, for example `limit=0` or `expires_in: 299`. `param` names it. |
| `parameter_too_long` | 400 | A text or a list is longer than allowed, for example a `reference` of 65 characters or 51 `items`. `param` names it. |
| `parameter_too_short` | 400 | A text or a list is shorter than required. |
| `invalid_request` | 400 | A request that is invalid in a way no other code describes. Read `message`. |

## Handling errors

~~~javascript
async function rais(path, init) {
  const response = await fetch(`https://api.rais.app/v1${path}`, init);
  const body = await response.json();
  if (response.ok) return body;

  const { error } = body;
  console.error("Raïs error", error.code, error.request_id); // always log the request id

  if (error.type === "rate_limit_error" || error.type === "api_error") {
    // transient: retry with backoff, reusing the same Idempotency-Key
    throw Object.assign(new Error(error.code), { retryable: true, retryAfter: response.headers.get("Retry-After") });
  }
  throw Object.assign(new Error(error.code), { retryable: false, param: error.param });
}
~~~

## Retry guidance

| Situation | Retry? |
| --- | --- |
| Network error or timeout | Yes, with the **same** `Idempotency-Key`. The first request may have succeeded. |
| `429` `rate_limit_exceeded` | Yes, after `Retry-After` seconds. |
| `500` `internal_error` | Yes, with backoff, and the same `Idempotency-Key`. |
| `503` `gateway_unavailable` | Yes. Nothing was created or refunded. Wait the `Retry-After` seconds. |
| `409` `idempotency_in_progress` | Yes, after `Retry-After` seconds: the first request is still running. |
| `502` `gateway_rejected` | Not as it is: the gateway refused the payment. Read `message`. |
| `402` `refund_declined` | No. The bank refused. Do not send it again unchanged. |
| Other `4xx` | No. Fix the request. |

Back off exponentially (1 s, 2 s, 4 s ...) with some random jitter, and give up after a few attempts. Never retry a `POST` without an `Idempotency-Key`.
