Hosted checkout flow
The hosted checkout is the standard way to accept a card payment. Paymee hosts the payment page and handles 3-D Secure, so card data never reaches your servers.
Lifecycle
Section titled “Lifecycle”- Create a session —
POST /gateway/sessions/with the amount, the buyer, and your URLs. The response includes acheckout_url. - Redirect the buyer to
checkout_url. - The buyer pays on the Paymee-hosted page (with 3-D Secure). After a decline they
can try again on the same page, up to
max_attemptstimes. See The payment page. - Paymee sends a webhook to your
webhook_urlwith the authoritative result. Fulfil the order here, not on the browser redirect. - After a successful payment, the buyer is redirected to your
return_url.
sequenceDiagram
participant S as Your server
participant P as Paymee
participant B as Buyer
S->>P: POST /gateway/sessions/
P-->>S: { checkout_url }
S->>B: redirect to checkout_url
B->>P: pays on hosted page (3-D Secure)
P->>S: webhook session.succeeded (authoritative) → fulfil
P->>B: redirect to return_url
Create a session
Section titled “Create a session”POST https://api.paymee.online/api/v1/gateway/sessions/
Request fields
Section titled “Request fields”| Field | Type | Required | Notes |
|---|---|---|---|
amount |
string | ✅ | Decimal string with the currency’s exact number of decimals: 3 for TND, e.g. "10.500". See Amounts & currency. |
currency |
string | – | ISO 4217 code. Defaults to your account’s currency (TND). |
merchant_reference |
string (≤ 200) | ✅ | Your unique reference for this payment, e.g. your order id. See Idempotency. |
first_name |
string (≤ 100) | ✅ | Buyer’s given name. |
last_name |
string (≤ 100) | ✅ | Buyer’s surname. |
email |
string (≤ 255) | ✅ | Buyer’s email. |
phone |
string (≤ 20) | – | Buyer’s phone. |
description |
string (≤ 500) | – | What the buyer is paying for. |
order_id |
string (≤ 100) | – | Your display-friendly order id. |
items |
array of objects | – | Line items, e.g. [{ "name": "T-shirt", "quantity": 2, "unit_price": "25.000" }]. Returned as you sent them. |
metadata |
object | – | Your own key/values, returned on the session and every webhook. Up to 50 keys; keys up to 40 characters, without [ or ]; values up to 500 characters. Numbers and booleans are stored as text (42 → "42"); objects and lists are refused. |
return_url |
string (≤ 2048) | – | Where the buyer is sent after a successful payment. See Returning the buyer. |
cancel_url |
string (≤ 2048) | – | Where the buyer goes back to your store without paying. |
webhook_url |
string (≤ 2048) | – | Where Paymee sends this session’s events. Defaults to your account’s endpoint. See Webhooks. |
max_attempts |
integer | – | Payment attempts allowed on the session, 1–10 (default 5). |
locale |
string | – | Language of the payment page: auto (default), fr or en. See Language. |
All three URLs must use https:// on a domain name; webhook_url must not be an IP
address. An optional text field sent as "" counts as not given.
See the full schema in the API Reference → Create a payment session.
Response
Section titled “Response”201 Created, with the session:
{ "id": "ps_0b7c3a1e-5d2f-4e8a-9c6b-1f2e3d4c5b6a", "merchant_reference": "order-0001", "order_number": "PM-014A2C", "order_id": "", "amount": "10.500", "currency": "TND", "original": { "amount": "10.500", "currency": "TND", "rate": "1.000000" }, "description": "", "items": [], "status": "CREATED", "livemode": true, "buyer": { "first_name": "Aymen", "last_name": "Ben Salah", "email": "aymen@example.com", "phone": "" }, "metadata": { "cart_id": "8812" }, "return_url": "https://your-site.tn/orders/order-0001/paid", "cancel_url": "https://your-site.tn/cart", "webhook_url": "https://your-site.tn/webhooks/paymee", "max_attempts": 5, "checkout_url": "https://pay.paymee.online/checkout/kq2V8Zr3xN0bT6mW1yPdF4sHjL9cA7uE", "locale": "auto", "expires_at": "2026-11-21T10:24:00.000000Z", "completed_at": null, "created_at": "2026-11-20T10:24:00.000000Z", "attempts": [], "refunds": []}buyer is the same object as in webhooks. Each entry of attempts has
failure_code and failure_message — the acquirer’s decline code and reason, as in
webhooks — when that attempt was declined.
Redirect the buyer to checkout_url exactly as returned. Sending the same
merchant_reference again returns this session with 200 OK instead of creating a
second one — see Idempotency.
The payment page
Section titled “The payment page”checkout_url opens Paymee’s payment page, where the buyer pays by card (Visa or
Mastercard).
- Two pages, one flow. Paymee’s page shows the amount, a language switcher, the result and the buttons back to your store. The card is entered on the page of ClicToPay, the acquiring bank, so card details never reach your servers.
- 3-D Secure on every payment. The buyer’s bank checks that the card is theirs: it may ask for a code by SMS or an approval in its app, or approve without asking anything.
- A decline is not the end. The buyer stays on the page and can try again, with the
same card or another, up to
max_attemptstimes (default 5). See Session status.
Language
Section titled “Language”The payment page is in French or English. Choose its language with locale:
locale |
The page opens in |
|---|---|
auto (default) |
The buyer’s browser language: French for a French browser, English for any other. |
fr |
French. |
en |
English. |
The buyer can switch language on the page; their choice wins over locale and is kept on
that browser for later payments. The card-entry page opens in the language the buyer is
reading when they click Pay.
Websites and mobile apps
Section titled “Websites and mobile apps”On a website, send the buyer to checkout_url in the same tab or a new one. The payment
page can’t be shown in an iframe: browsers refuse to frame it.
In a mobile app, open checkout_url in an in-app browser tab:
SFSafariViewController on iOS, Custom Tabs on Android. The buyer stays in your app, the
page shows its real address, and banks’ 3-D Secure steps work as in a browser, including
a hand-off to the bank’s own app.
To bring the buyer back to your app, make return_url and cancel_url links your app
opens: a universal link on iOS or an App Link on Android, on your own domain (for example
https://your-site.tn/app/orders/order-0001/paid). When the app opens, read the order’s
state from your server, which the webhook keeps up to date.
Returning the buyer
Section titled “Returning the buyer”Paymee doesn’t add anything to your URLs: the buyer lands on exactly the return_url or
cancel_url you gave. Put what you need to show the right page in the URL itself, for
example https://your-site.tn/orders/order-0001/paid.
return_url— after a successful payment, the hosted page shows a short confirmation, then redirects the buyer here. Without one, the buyer stays on the confirmation.cancel_url— the hosted page shows a Return to store button linking here: while paying, after a decline, and when the link or the session has expired. Without one, the page tells the buyer they can close the window.
A declined buyer is not redirected: they stay on the hosted page and can try again while attempts remain.
Session status
Section titled “Session status”A session moves through these statuses (status field):
| Status | Meaning |
|---|---|
CREATED |
Created; no payment attempt yet. |
PROCESSING |
A payment attempt is in progress (the buyer is on the card or 3-D Secure page). |
PENDING |
An attempt was declined; the buyer can try again. |
SUCCEEDED |
Paid. Final. |
FAILED |
Every allowed attempt was declined. Final. |
EXPIRED |
Not paid within the session’s lifetime (about 24 hours). Final. |
CANCELED |
Canceled by your cancel call. Final. |
stateDiagram-v2
direction LR
[*] --> CREATED
CREATED --> PROCESSING: payment attempt
PROCESSING --> SUCCEEDED: approved
PROCESSING --> PENDING: declined, attempts left
PENDING --> PROCESSING: buyer tries again
PROCESSING --> FAILED: declined, no attempts left
PENDING --> FAILED: no attempts left
CREATED --> CANCELED: your cancel call
PENDING --> CANCELED: your cancel call
CREATED --> EXPIRED: lifetime over
PENDING --> EXPIRED: lifetime over
PROCESSING --> EXPIRED: lifetime over
SUCCEEDED --> [*]
FAILED --> [*]
CANCELED --> [*]
EXPIRED --> [*]
The buyer can’t cancel a session: one they abandon stays open until it expires. Each
final status has its webhook (session.succeeded, session.failed, session.expired)
except CANCELED, which only your own call can cause.
Retrieving sessions
Section titled “Retrieving sessions”Fetch a session at any time with
GET /gateway/sessions/{id}/. It includes its
payment attempts and its refunds. Webhooks are the primary mechanism; fetching
is the backup and the source of truth for reconciliation.
List your sessions, newest first, with
GET /gateway/sessions/list/:
curl "https://api.paymee.online/api/v1/gateway/sessions/list/?limit=50" \ -H "Authorization: Bearer sk_live_…"The response is { "data": [ …sessions… ], "has_more": true }. limit is 1–100
(default 20). To get the next page, pass the last session’s id as starting_after;
ending_before pages backwards.
Canceling
Section titled “Canceling”Cancel a session that hasn’t been paid with
POST /gateway/sessions/{id}/cancel/. The
response is the session, now CANCELED, and its checkout link stops working.
- Only
CREATEDandPENDINGsessions can be canceled. While a payment attempt isPROCESSING, the call is refused with422invalid-state: wait for the attempt to finish. - A session that is already final answers
409session-terminal.

