PovitoDevelopers

Errors, idempotency, rate limits, versioning#

This is the Checkout API's complete error and retry reference. How it differs from the Partner API's errors — same field names, different codes, no partial success — is on the shared Errors page.

The error envelope#

Every non-2xx response has the same body:

json
{
  "error": {
    "code": "amount_mismatch",
    "message": "Line items net to 44000 but amount is 45000.",
    "details": [{ "field": "line_items", "issue": "sum_differs" }],
    "request_id": "req_5c1b0a4e-2f6d-4a0b-9d3e-7b8c1f2a3d4e"
  }
}

Branch on code, show message to a developer (never to a shopper), and quote request_id when you write to support — it is also echoed in the x-request-id response header on every call, and you may send your own x-request-id (8–64 characters of A-Za-z0-9_-) to have it used instead.

details[] names the offending field (dotted path, (body) for the whole body) and an issue — for validation errors it is the validator's message; for business rules it is a stable token such as sum_differs, host_not_allowed, not_enabled, private_target.

Error codes#

HTTP Code When
400 validation_error The body or query failed schema validation; details[] lists each field. Unknown fields are errors.
400 amount_mismatch line_items do not net to amount.
400 unsupported_currency currency or presentment.currency has no recorded minor-unit exponent.
400 unsupported_payment_method Reserved.
400 idempotency_key_required A creating POST without an Idempotency-Key, or one outside 8–128 characters of A-Za-z0-9_-.
400 otp_invalid, otp_expired Public surface — a wrong or stale SMS code.
401 invalid_api_key Missing, malformed, revoked or wrong-type key on a merchant route (a publishable key here is 401, not 403).
401 invalid_publishable_key Same, on /public/*.
403 insufficient_scope The key lacks a scope the route needs; the message names it.
403 mode_mismatch A live key acting on a test object or vice versa — e.g. registering a mode: "test" endpoint with a live key.
403 merchant_suspended Povito suspended the merchant.
403 phone_not_verified Public surface — an attempt before the required phone verification.
403 forbidden Anything else the caller may not do (e.g. decide on a non-test attempt).
404 session_not_found, refund_not_found, endpoint_not_found, event_not_found, attempt_not_found, customer_not_found Not found — also for another merchant's or another mode's objects. Never 403, so existence does not leak.
409 idempotency_key_conflict Same Idempotency-Key, different body (or different route) within 24 h.
409 session_not_open The transition needs an open session (expire, cancel, OTP), or a refund on a session with nothing captured.
409 attempt_in_progress Public surface — a new attempt while one is with a vendor.
409 refund_exceeds_captured Refund ceiling; the message states what remains.
410 session_expired, session_canceled Public surface — the page loaded a session that has expired or been cancelled.
422 method_not_enabled A payment_method_types entry is not enabled for this merchant and mode.
422 method_not_available Public surface — the method is enabled but unusable on this session (currency, limits, vendor not configured).
422 method_not_refundable The vendor cannot refund through the API, or needs a full-amount refund.
422 return_host_not_allowed success_url/cancel_url host is not on the allowed list.
422 expiry_out_of_range expires_at is under 5 minutes or over 24 hours away.
429 rate_limited Over a limit; Retry-After is set.
429 otp_locked Five wrong codes; Retry-After: 600.
502 gateway_error The vendor declined at initiation or could not start the payment; the attempt is failed and the session is open again.
503 gateway_not_configured The vendor has no credentials for this mode.
500 internal_error Povito's fault. Retry with backoff — see below.

On any 5xx or a network failure never assume the operation did not happen: re-send with the same Idempotency-Key, or look the session up by reference_id.

Idempotency#

POST /v1/checkout/sessions and POST /v1/refunds require an Idempotency-Key header — any 8–128 characters of A-Za-z0-9_-; a UUID v4 is the convention. Povito stores the key with a hash of the body and the first response for 24 hours, scoped to your merchant, mode and route:

Repeat request Result
same key, same body the stored response, same status code, plus Idempotent-Replayed: true
same key, different body 409 idempotency_key_conflict
same key, other route 409 idempotency_key_conflict
same key, first attempt still in flight or crashed before storing processed again (the stored response is then the newer one)

Independently of the header, session creation is reconciled on (merchant, mode, reference_id) while the previous session is open: the same amount, currency and line_items get that session back with 200 instead of 201; a different price expires the open session (checkout.session.expired) and creates a new one with 201, so a page still showing the old amount can never be paid. Once a session has completed, expired or been cancelled, the same reference_id simply creates a new session — keep reference_id unique per order attempt if that is not what you want.

GET, PATCH, DELETE, /expire, /redeliver, /rotate_secret and /test take no key; they are safe to retry by nature or produce distinct results on purpose.

Rate limits#

Where Limit Window Response
Public surface, per client IP 300 requests 1 min 429 rate_limited
OTP requests, per phone 5 10 min 429 rate_limited
OTP requests, per IP 20 10 min 429 rate_limited
Wrong OTP codes, per challenge 5 until a new code is requested 429 otp_locked, Retry-After: 600
Payment attempts, per session 10 10 min 429 rate_limited
Merchant API, per key generous; published before live traffic 429 rate_limited

Honour Retry-After (seconds). The public limits exist to protect shoppers from OTP abuse, not to shape merchant traffic; a merchant integration that only creates sessions and reads them back will not meet them.

Versioning#

The API is versioned in the path (/v1). Inside v1, changes are additive only: new fields, new enum values, new event types, new endpoints. Your integration must:

  • ignore fields it does not know;
  • treat unknown enum values as "other" rather than failing — new status values will not appear without a version bump, but new payment_method_types, flows and event types will.

Renames and removals need /v2. Retiring a field inside v1 is announced with Deprecation and Sunset headers on the affected routes at least 6 months before the date.

The executable contract is checkout-v1.yaml; the reference is generated from it on every build, and CI fails on a breaking diff that does not bump the version.

Changelog#

1.0.0-draft.3 — 2026-09-10#

  • Re-creating an open reference_id with a different price now expires the old session and creates a new one (201) instead of returning the stale one.
  • Iraqi local phone formats are normalised to E.164 on input.
  • cancel_url must be https:// like success_url.
  • Public surface: GET /public/sessions/{code} carries livemode and, with a valid customer token, profile; POST …/otp answers { sent, retry_after }; the attempt poll returns code, method, present and cancel_url as well; new GET /public/attempts/{attempt_id} (attempt lookup without the code, for the return page) and GET /public/test-gateway/{attempt_id} (what the simulated vendor page shows); session_canceled (410) split from session_expired.

1.0.0-draft.2 — 2026-09-10#

First public draft of v1 (Phase 0–1 of ADR-CHK-001). Sessions, payment methods, refunds, webhook endpoints, events, the public surface for the hosted page, the simulated test gateway, cash on delivery as an offline method. Customers and instruments are in the contract but not yet served.