Statuses and errors¶
Payment statuses¶
| Status | Final | Meaning |
|---|---|---|
| created | no | Accepted, no requisites yet |
| processing | no | Requisites issued, waiting for the payer's transfer |
| completed | yes | Payment confirmed |
| canceled | yes | Not paid: the payment lifetime ran out, it was canceled, or the payment could not be confirmed after requisites were issued |
| error | yes | No requisites were issued, reason in error_code |
| appeal | 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.
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.
error_code values¶
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¶
The error body is always JSON:
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 |
| 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 |
| 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:
When to retry¶
| 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.
# 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