Server integration
The server half of an SDK integration: create a checkout session for one payment, hand its id to your app, and read the session back whenever you need to. Two endpoints, one header, no SDK required on the server.
On this page
Hosts and authentication
| Live | https://yallapay.net — accepts sk_live_ / pk_live_ keys, mints cs_live_ sessions. |
| Sandbox | https://sandbox.yallapay.net — accepts sk_test_ / pk_test_ keys, mints cs_test_ sessions. |
Every request is authenticated with a bearer token in the Authorization header. Keys are never read from the query string.
Authorization: Bearer sk_live_YOUR_SECRET_KEY
A secret key is accepted everywhere. A publishable key is accepted only on the read endpoint, and it sees a reduced view of the session. A key of the wrong environment is refused with a message naming the correct host.
Create a session
POST Create a checkout session
https://yallapay.net/api/v1/sessions
Call this from your server when the customer starts a payment. Send the amount and currency, a short description the customer will see, and your own order id so you can match the payment later.
curl -X POST "https://yallapay.net/api/v1/sessions" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-881-attempt-1" \
-d '{
"amount": 250.00,
"currency": "AED",
"purpose": "Order #881",
"external_id": "881",
"return_url": "myapp://yallapay/return"
}'
<?php
$response = Http::withToken(env('YALLAPAY_SECRET_KEY'))
->withHeaders(['Idempotency-Key' => "order-{$order->id}-attempt-{$attempt}"])
->post('https://yallapay.net/api/v1/sessions', [
'amount' => 250.00,
'currency' => 'AED',
'purpose' => "Order #{$order->id}",
'external_id' => (string) $order->id,
'return_url' => 'myapp://yallapay/return',
]);
if ($response->failed()) {
$error = $response->json('error'); // ['code' => ..., 'message' => ..., 'param' => ...]
throw new RuntimeException($error['message']);
}
$sessionId = $response->json('session_id'); // send this to the app
const res = await fetch('https://yallapay.net/api/v1/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.YALLAPAY_SECRET_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `order-${order.id}-attempt-${attempt}`,
},
body: JSON.stringify({
amount: 250.0,
currency: 'AED',
purpose: `Order #${order.id}`,
external_id: String(order.id),
return_url: 'myapp://yallapay/return',
}),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
const sessionId = body.session_id; // send this to the app
import os, requests
res = requests.post(
"https://yallapay.net/api/v1/sessions",
headers={
"Authorization": f"Bearer {os.environ['YALLAPAY_SECRET_KEY']}",
"Idempotency-Key": f"order-{order.id}-attempt-{attempt}",
},
json={
"amount": 250.00,
"currency": "AED",
"purpose": f"Order #{order.id}",
"external_id": str(order.id),
"return_url": "myapp://yallapay/return",
},
timeout=10,
)
body = res.json()
if not res.ok:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
session_id = body["session_id"] # send this to the app
A successful call returns HTTP 201 and the full session object:
{
"session_id": "cs_live_9fK3rM2pQ7xTb4Lw8nHv1sZc6Yd0JeAq",
"status": "open",
"amount": 250,
"currency": "AED",
"transaction_id": null,
"payment_method": null,
"checkout_url": "https://yallapay.net/c/cs_live_9fK3rM2pQ7xTb4Lw8nHv1sZc6Yd0JeAq",
"expires_at": "2026-08-27T14:20:00+04:00",
"external_id": "881",
"purpose": "Order #881",
"fee_payer": "merchant",
"return_url": "myapp://yallapay/return",
"net_amount": null,
"created_at": "2026-08-27T13:20:00+04:00"
}
Send session_id to your app and store it against the order. The checkout_url is what the SDK opens; a website can open it directly.
Request fields
| amount required | Number, greater than 0, at most 1,000,000, in major units: 250.00 means two hundred and fifty. Two decimal places are kept. |
| currency required | Three-letter ISO code, such as AED. Upper-cased on arrival. |
| purpose required | Up to 500 characters. Shown to the customer on the checkout sheet, so write it for them: "Order #881", not an internal SKU. |
| external_id optional | Up to 191 characters. Your own order or invoice id. Echoed back on the session, on the return URL and on the webhook, so you can match a payment to an order without a lookup table. If omitted, YallaPay generates one. |
| return_url optional | Where to send the customer when checkout finishes, up to 2048 characters. For an app: myapp://yallapay/return. For a website: an https URL on a registered host. The scheme or host must be registered and active under Apps & Return URLs, plain http is refused, and javascript, data, file and similar schemes are always refused. Leave it out for a payment link the customer will close themselves. |
| fee_payer optional | merchant (default) or customer. With customer, the processing fee is added on top of the amount at checkout so that you receive the full amount; the customer sees the total before paying. |
| bill_to optional | Up to 191 characters. The customer's name or email, shown on the receipt. |
| store_id optional | The id of one of your webhook endpoints. Routes this payment's webhook to that endpoint. An id that does not belong to your account is refused rather than ignored. |
| expires_in optional | Seconds until the session can no longer be paid. Default 3600 (one hour); minimum 300; maximum 86400. Once expired, the checkout URL stops working and the session reports expired. |
| Authorization required | Bearer sk_live_… — the secret key. A publishable key is refused here. |
| Content-Type required | application/json |
| Idempotency-Key recommended | Up to 255 characters, unique per attempt. See Idempotency below. |
The session object
The same object comes back from both endpoints. With the secret key you see all of it; with the publishable key, only the first eight fields. The publishable view deliberately excludes merchant details, net amounts, fee splits and customer records, because it is read by code running on a customer's device.
| session_id | The id your app pays with. cs_live_ or cs_test_ followed by 32 random characters. |
| status | open, paid, failed or expired. See lifecycle below. |
| amount | Number. A whole amount is sent as 250, not 250.00; decode it as a numeric type that tolerates both. |
| currency | Three-letter code. |
| transaction_id | YallaPay's reference for the payment. Present only once paid. This is the value shown on your Payment Reports and carried on the webhook. |
| payment_method | card, apple_pay or google_pay; null until paid. Never names the processor. |
| checkout_url | The hosted checkout page. Present only while the status is open. |
| expires_at | ISO-8601 timestamp. |
| external_id | Your order id, as sent. |
| purpose | As sent. |
| fee_payer | merchant or customer. |
| return_url | As sent, or null. |
| net_amount | What you receive after fees. Present only once paid. |
| created_at | ISO-8601 timestamp. |
Session lifecycle
┌──────────── customer pays ────────────► paid (final)
│
open ─────────┼──────────── payment declined ────────► failed (customer may retry while open)
│
└──────────── expires_at passes ──────────► expired (final)
- open — created and payable. The checkout URL works and the SDK can present it.
- paid — money moved. This is final: no later failure or expiry can downgrade it, and re-opening the checkout URL shows the receipt instead of the form.
- failed — the most recent attempt was declined. The session is still payable until it expires, so the customer can try another card.
- expired — the expiry passed with no successful payment. Create a new session for a new attempt.
One session is one payment of one amount. Do not reuse a session across orders, and do not change the order's total after creating it; create a new session instead.
Idempotency
Mobile networks retry. If your app's request to your server times out on the way back and the app tries again, you would create a second session for the same order unless something stops you. The Idempotency-Key header is that something.
- Send a key that is unique per attempt, such as
order-881-attempt-1. Any string up to 255 characters. - Repeating a request with the same key and the same body returns the original response, with the same status code and an extra header
Idempotent-Replayed: true. - Repeating a key with a different body is refused with HTTP 409
idempotency_conflict, because the two requests cannot both be honoured. - Keys are remembered for 24 hours, per account and per endpoint. Only successful responses are stored, so a transient error does not pin itself to the key.
A random key is unique every time, which defeats the purpose. Use the order id plus an attempt counter that you increment only when you deliberately want a fresh session.
Read a session
GET Read a checkout session
https://yallapay.net/api/v1/sessions/cs_live_…
Returns the session object. With the secret key, the full view; with the publishable key, the public view. The SDKs call this endpoint with the publishable key to decide the result they return to your app, and you can call it from your server to reconcile an order whose webhook you have not seen.
curl "https://yallapay.net/api/v1/sessions/cs_live_9fK3rM2pQ7xTb4Lw8nHv1sZc6Yd0JeAq" \ -H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
$session = Http::withToken(env('YALLAPAY_SECRET_KEY'))
->get("https://yallapay.net/api/v1/sessions/{$sessionId}")
->throw()
->json();
if ($session['status'] === 'paid') {
$order->markPaid($session['transaction_id']);
}
const res = await fetch(`https://yallapay.net/api/v1/sessions/${sessionId}`, {
headers: { Authorization: `Bearer ${process.env.YALLAPAY_SECRET_KEY}` },
});
const session = await res.json();
if (session.status === 'paid') await orders.markPaid(session.transaction_id);
session = requests.get(
f"https://yallapay.net/api/v1/sessions/{session_id}",
headers={"Authorization": f"Bearer {os.environ['YALLAPAY_SECRET_KEY']}"},
timeout=10,
).json()
if session["status"] == "paid":
orders.mark_paid(session["transaction_id"])
Errors
Every error has the same shape. The param field is present only for validation errors, and names the offending field.
{
"error": {
"code": "validation_error",
"message": "The app scheme \"shop://\" is not registered for this account. Add it under Settings → Apps & return URLs.",
"param": "return_url"
}
}
| code | HTTP | Meaning and what to do |
|---|---|---|
| invalid_key | 401 | Missing, malformed, unknown, or issued for the other environment. The message says which. Check the header and the host. |
| insufficient_permissions | 401 | A publishable key was used to create a session. Create sessions from your server with the secret key. |
| validation_error | 422 | A field is missing or invalid; see param. Includes unregistered return URLs and store ids that are not yours. |
| merchant_not_approved | 403 | Your account cannot take payments yet. The message names the state. Nothing to fix in code. |
| session_not_found | 404 | No session with that id belongs to this account. Ids are case-sensitive. |
| idempotency_conflict | 409 | This Idempotency-Key was already used with a different body. Use a new key for a genuinely new request. |
| rate_limited | 429 | Too many requests. Back off and retry after a moment. |
| internal_error | 500 | Something failed on our side. Retry with the same Idempotency-Key; you will not create a duplicate. |
Rate limits
- Create a session: 60 requests per minute.
- Read a session: 120 requests per minute.
Both are generous for a checkout flow. If you poll a session from your server, poll at most every few seconds and stop once the status is final; better still, rely on the webhook and poll only as a fallback.
Recommended pattern
This is the shape of a server that works well with the SDKs. Each numbered piece is a small endpoint or job in your own codebase.
-
POST /orders/{id}/pay
Authenticated by your own app login. Looks up the order, creates a YallaPay session with the order total, stores the
session_idon the order, and returns it to the app. Usesorder-{id}-attempt-{n}as the Idempotency-Key. -
POST /webhooks/yallapay
Verifies
X-YallaPay-Signature, finds the order byexternal_id, and marks it paid if not already. Responds 200 quickly and does slow work (email, shipping) in a queue. See Webhooks & fulfilment. - GET /orders/{id} What the app polls after the SDK returns, to show the order as paid once your server has processed the webhook. If the webhook is late, this handler may read the session from YallaPay with the secret key and reconcile on the spot.
- A reconciliation job Every few minutes, for orders that have a session but are not yet paid or expired, read the session and update the order. This catches the rare webhook that never arrived.