# Statuses and errors

## Payment statuses { #statuses }

<div class="pp-statusmap" data-lang="en"></div>

| Status | Final | Meaning |
| --- | --- | --- |
| <span class="pp-status">created</span> | no | Accepted, no requisites yet |
| <span class="pp-status pp-status--wait">processing</span> | no | Requisites issued, waiting for the payer's transfer |
| <span class="pp-status pp-status--ok">completed</span> | yes | Payment confirmed |
| <span class="pp-status">canceled</span> | yes | Not paid: the payment lifetime ran out, it was canceled, or the payment could not be confirmed after requisites were issued |
| <span class="pp-status pp-status--bad">error</span> | yes | No requisites were issued, reason in `error_code` |
| <span class="pp-status pp-status--warn">appeal</span> | no | A closed payment is being re-checked |

A create response may already carry any status except `appeal`. A final status changes
only after a payment re-check or a support decision — see
[Status changes after closing](callbacks.md#status-changes).

**`error` happens only before requisites are issued**: from `created`, usually right in the
create response. Once requisites are issued (`processing`), a payment ends only as
`completed` or `canceled`. If the payment cannot be confirmed after that, it also becomes
`canceled`, with no `error_code`.

The payment lifetime is a project setting: 15 minutes from `created_at` by default. A
payment not paid within it becomes `canceled`.

**Callbacks are sent only for `completed` and `canceled`** — when the payment is confirmed
or canceled, when the lifetime runs out, after a payment re-check and after a support
decision. There is no callback for `error`: you see it in the create response
and when you read the payment. More in
[Callbacks](callbacks.md#when).

## `error_code` values { #error-codes }

`error_code` and `error_comment` are present only for a payment in `error` — in the create
response and when you read it. The code explains why no requisites were issued. A
`canceled` payment has no code, including when the payment could not be confirmed. For the payer the
action is always the same: pay again with a new payment and a new `merchant_payment_id`.

| Code | What happened |
| --- | --- |
| `provider_unavailable` | The payment cannot be accepted now: the payment method is temporarily unavailable |
| `provider_no_requisites` | No free requisites were found |
| `rate_unavailable` | No exchange rate available |
| `provider_result_ambiguous` | Issuing requisites was not confirmed, no requisites issued |
| `provider_result_unknown` | The outcome of issuing requisites is unknown, no requisites issued |
| `stale_dispatch` | Requisites could not be obtained in time |
| `invalid_payment` | The payment failed field validation |
| `json_invalid` | The request body could not be parsed |

The list may grow: treat an unknown code as a generic failure.

## HTTP errors { #http-errors }

The error body is always JSON:

```json
{"error": "signature_invalid"}
```

`Content-Type: application/json`; the `X-Request-ID` header is echoed from the request.

| HTTP | `error` | When | What to do |
| --- | --- | --- | --- |
| 400 | `bad_request` | Not JSON, unknown field, trailing data after the object | Fix the request |
| 400 | `invalid_payment` | A field failed validation: amount, order id, geo, method, bank, `callback_url`, … | Fix the request |
| 401 | `token_invalid` | No token, unknown, revoked or expired token, or the project is not active | Check the token in the cabinet |
| 401 | `signature_invalid` | The signature did not verify | [Signing checklist](signing.md#debug-checklist) |
| 403 | `insufficient_scope` | The token lacks the scope | Issue a token with `payments:create` / `payments:read` |
| 403 | `ip_not_allowed` | The address is not in the project allowlist | Add it in the cabinet |
| 403 | `project_blocked` | Project API closed by staff | Contact your manager |
| 403 | `merchant_blocked` | Merchant is blocked | Contact your manager |
| 403 | `merchant_archived` | Merchant is deleted | Contact your manager |
| 404 | `payment_not_found` | No such payment, or it belongs to someone else | Check the `id` |
| 409 | `request_replayed` | This `X-Request-Nonce` was already used | Retry with a new nonce and signature |
| 409 | `payment_idempotency_conflict` | `merchant_payment_id` is taken by a payment with other fields | New id, or the same fields, see [Idempotency](idempotency.md) |
| 413 | `request_too_large` | Body over the project limit (64 KB by default) | Shrink the body |
| 422 | `payment_not_covered` | Method, currency or amount outside your contract | Check terms with your manager |
| 422 | `payment_rate_unavailable` | No exchange rate | Retry later with a new payment |
| 422 | `payment_not_routable` | The payment cannot be accepted with this method right now | Retry later with a new payment |
| 429 | `rate_limited` | Rate limit exceeded | Wait `Retry-After` seconds |
| 429 | `too_many_concurrent_requests` | More than 20 concurrent project requests | Wait `Retry-After` (1 s) and resend the same request |
| 500 | `internal_error` | Server-side failure | Retry with backoff |
| 503 | `rate_limit_unavailable` | Rate accounting temporarily unavailable | Wait `Retry-After` and retry |
| 503 | `replay_protection_unavailable` | Nonce check temporarily unavailable | Wait `Retry-After` and retry |

If the payment was saved (in `error`) before a `422`, the body carries its id:

```json
{"error": "payment_not_routable", "payment_id": "3f1c2b9e-6a8d-4c4b-9a71-2c7f5d1e0a42"}
```

## When to retry { #retries }

| Answer | Retry? | How |
| --- | --- | --- |
| Timeout, dropped connection | yes | Same request and `merchant_payment_id`, **new** nonce and signature |
| `429`, `503` | yes | After `Retry-After` seconds, then with growing delays |
| `500` | yes | With growing delays: 1, 2, 4, 8 seconds… |
| `409 request_replayed` | yes | With a new nonce and signature |
| `401`, `403`, `400`, `404`, `413` | no | Fix the cause first |
| `422`, status `error` | no | A new attempt is a new payment with a new `merchant_payment_id` |

Retrying with the same `merchant_payment_id` is safe: no second payment is created. See
[Idempotency and limits](idempotency.md).

```python
# Retry with backoff: same body and merchant_payment_id, fresh signature each time.
for attempt in range(5):
    headers = sign_request("POST", url, body, key, key_id)  # new nonce every time
    response = send(url, body, headers)
    if response.status in (429, 503):
        time.sleep(int(response.headers.get("Retry-After", "1")))
        continue
    if response.status >= 500 or response.status == 0:  # 0: timeout or dropped connection
        time.sleep(2 ** attempt)
        continue
    break
```