Hosted checkout#
A checkout session is the unit of work: one basket, one shopper, one outcome. This page is the field-by-field guide to POST /v1/checkout/sessions and to what the page at checkout.povito.com/c/{code} does with each field. The quickstart has the end-to-end flow.
Session lifecycle#
open ──▶ processing ──▶ completed
│ │
│ └──▶ open (an attempt failed; the shopper picks another method)
├──▶ expired (expires_at passed, or POST /expire)
└──▶ canceled (the shopper backed out on the page)status tells you where the session is; payment_status tells you about money: unpaid → paid | requires_offline_collection | failed, and after refunds partially_refunded | refunded. One session can hold many payment attempts — a shopper who is declined on ZainCash can succeed on FIB without you creating anything new.
The create call#
| Field | Type | Required | Rules |
|---|---|---|---|
reference_id |
string, 1–128 | yes | Your own id (order, cart, invoice). While a session for it is open in this mode, a second create with the same amount, currency and line_items returns that session with 200; a create with a different price (the cart changed) expires the open session — checkout.session.expired fires — and creates a new one with 201. |
amount |
integer minor units | yes | Must be positive and must equal the net of line_items. |
currency |
ISO 4217 | yes | IQD (whole dinars) or USD (cents) today; other currencies with a recorded exponent are accepted at create but no method supports them. |
line_items[] |
see below | yes, 1–100 | charge, shipping and fee add; discount subtracts. amount is per unit; quantity (default 1) multiplies it. |
payment_method_types[] |
method codes, 1–20 | no | Default: every method enabled on your merchant for this mode. A code that is not enabled fails the whole create with 422 method_not_enabled. |
customer |
{phone?, name?, address?} |
no | Prefill. phone is stored as E.164 (+9647…); Iraqi local forms are normalised for you — 0780 123 4567, 7801234567, 009647801234567 and +964 780 123 4567 all become +9647801234567. The shopper can still change it; the phone they verify is the one you get back. |
collect |
{phone, name, shipping_address} |
no | Each required, optional or none. Defaults required, required, none. |
presentment |
{currency, fx_rate} |
no | Only for methods that cannot present the session currency — Stripe presents USD. See USD presentment. |
locale |
ar, ku, en |
no | Language of the page. Default: your merchant's default locale. |
success_url |
https URL | yes | Where the shopper lands after completion. Host must be on your allowed list. |
cancel_url |
https URL | no | Where the shopper lands after cancelling. Same https:// and host rules. |
expires_at |
ISO 8601 | no | Default now + 60 min. Must be between now + 5 min and now + 24 h, else 422 expiry_out_of_range. |
metadata |
object | no | Up to 20 keys of ≤ 64 chars, string values ≤ 500 chars. Returned verbatim on the session and in webhooks; never shown to the shopper; never used for money. |
Unknown fields are rejected (400 validation_error with the field named in details[]), so a typo cannot silently drop an option.
Line items#
"line_items": [
{ "label": "Blue kettle", "amount": 21000, "quantity": 2, "type": "charge" },
{ "label": "Delivery — Hi-Express", "amount": 5000, "type": "shipping" },
{ "label": "Coupon RAMADAN", "amount": 2000, "type": "discount" }
]Net: 21,000 × 2 + 5,000 − 2,000 = 45,000, so amount must be 45000. Anything else is 400 amount_mismatch:
{
"error": {
"code": "amount_mismatch",
"message": "Line items net to 45000 but amount is 46000.",
"details": [{ "field": "line_items", "issue": "sum_differs" }],
"request_id": "req_5c1b0a4e-2f6d-4a0b-9d3e-7b8c1f2a3d4e"
}
}The page shows label and the extended amount for every line, in the shopper's locale, with IQD formatted as whole dinars.
Payment methods on the session#
Pass payment_method_types to narrow the picker — for example ["zaincash"] when the shopper already chose ZainCash on your side, so the hosted page goes straight to the vendor after phone verification. Omit it to show every enabled method. The list you may pass is GET /v1/payment_methods filtered to enabled: true; each row also tells you the method's flow, currencies and capabilities — see payment methods.
A method that is enabled but not usable for this session (wrong currency, amount outside the vendor's limits, the vendor not configured in this mode) still lets the create succeed; the page shows it as unavailable with a reason, and an attempt on it fails with 422 method_not_available.
Prefill and what to collect#
customer prefills; collect decides what the page insists on.
collect.phone |
Behaviour |
|---|---|
required (default) |
The shopper must verify a phone by SMS code before any attempt. The verified phone is stored on the session. |
optional |
The page offers verification but an attempt may proceed without it. |
none |
No phone step. Use for cash on delivery with a phone you already verified. |
collect.name and collect.shipping_address work the same way; when required, an attempt without them fails with 400 validation_error naming name or address. Addresses use the platform shape: governorate (Iraqi governorate code), city, line, landmark, country (default IQ) and optional geo: {lat, lng}.
What the shopper enters comes back on the session:
"customer": {
"id": null,
"phone": "+9647800000000",
"name": "Ali M. Ismail",
"address": { "governorate": "BG", "city": "Karrada", "line": "Street 12, building 4", "landmark": "Next to the pharmacy", "country": "IQ" }
}customer.id is the shopper's Povito ID and is null until customer profiles ship in Phase 2.
Locale#
locale picks the page language: ar (Arabic), ku (Sorani Kurdish) or en. The shopper can switch on the page; the session keeps the locale you set. Method names are localised by Povito (display_name in GET /payment_methods has all three).
Expiry#
Sessions expire lazily: a GET after expires_at returns status: "expired" and a sweeper expires the ones nobody reads. Expiry emits checkout.session.expired. To close a session early — the shopper changed their basket — call POST /v1/checkout/sessions/{id}/expire; it is 409 session_not_open for anything but an open session.
curl -X POST https://api.checkout.povito.com/v1/checkout/sessions/cs_test_7KQ4M2XA9BC3DEFGH1JK/expire \
-H "Authorization: Bearer $POVITO_SECRET_KEY"A session in processing (an attempt is with a vendor) cannot be expired by you; it returns to open if the attempt fails, and completes if it succeeds.
Return URLs#
success_url and cancel_url must be https:// and their host must match, or be a subdomain of, a host on your merchant's allowed return hosts list — set by Povito when your account is created and changeable on request. Otherwise the create fails with 422 return_host_not_allowed.
After completion the page appends session_id and reference_id to success_url (using & if it already has a query string). Nothing else is appended, ever: no status, no amount, no method. Read session_id, GET the session, and act on that.
cancel_url is used verbatim.
Metadata#
metadata is for your correlation keys — cart id, order group, campaign. It is echoed on the session and in every event about it, and is never shown on the page. Do not put money, prices or anything a shopper must not see in it; the page never receives it, but your webhook logs will.
The session object#
Every read of a session returns the same shape (GET, the create response, data.object in events):
| Field | Meaning |
|---|---|
id |
cs_live_… or cs_test_… — 20 characters after the mode. |
object |
Always checkout.session. |
code |
The short unguessable code in url; 26 characters, ~130 bits of entropy. |
url |
Send the shopper here. |
livemode |
false for test keys. |
status, payment_status |
See lifecycle. |
reference_id, amount, currency, line_items, payment_method_types, collect, presentment, locale, success_url, cancel_url, expires_at, metadata |
As created (line_items come back with quantity filled in). |
customer |
{id, phone, name, address} — what the shopper verified and entered, prefill until then. |
payment |
null until an attempt succeeds, then {attempt_id, method, gateway, gateway_reference, captured_amount, captured_currency, presented_amount, presented_currency, fx_rate, captured_at, instrument}. For cash on delivery captured_amount and captured_currency are null — nothing was captured. |
refunded_amount |
Sum of succeeded refunds, in the captured currency. |
completed_at |
When status became completed. |
created_at, updated_at |
ISO 8601, UTC. |
Looking sessions up#
curl "https://api.checkout.povito.com/v1/checkout/sessions?reference_id=POV-1041" \
-H "Authorization: Bearer $POVITO_SECRET_KEY"Lists take reference_id, status, created_after, limit (1–100, default 20) and cursor; they return { data: [...], next_cursor }, newest first, next_cursor: null on the last page. A session created with a test key is invisible to a live key and vice versa — the lookup returns nothing rather than 403.
Branding#
The page shows your display_name, logo_url and an accent colour, set on your merchant record by Povito. The accent is contrast-checked at save time; buttons and focus rings pick it up, everything else stays on Povito's neutral palette so every merchant's page is recognisably the same secure surface. The page is Arabic, Kurdish and English, right-to-left where the language is, and meets WCAG 2.1 AA.
What the page does, in order#
- Phone — the shopper enters an Iraqi mobile number and a six-digit SMS code (5 codes per 10 minutes per phone; a code lives 5 minutes; 5 wrong tries lock it). In test mode
+9647500000000accepts000000. - Identity — name and address as
collectrequires, prefilled fromcustomer. - Method — one tile per method in
payment_method_types, in your merchant's display order, unavailable ones greyed with a reason. A wallet row (Apple Pay / Google Pay) appears above the tiles when a card rail that supports them is enabled — Phase 2. - Pay — a redirect to the vendor, a QR code and app links to scan, or an instant completion, per the method's flow.
- Return — vendors send the shopper back to
checkout.povito.com/r/{attempt_id}; the page ignores every vendor query parameter for money purposes, asks the API to verify with the vendor, and only then sends the shopper to yoursuccess_url, or shows the failure with "try another method".