Choose a checkout flow
Every flow creates the same kind of order and sends the customer through the same hosted checkout. The only difference is how the customer reaches that checkout. This page explains each order-creating endpoint, when to use it, and gives a full request and response for each.
| Flow | Endpoint | Use when | Customer reaches checkout by |
|---|---|---|---|
| Payment session | POST /api/v1/pay/session/create | You have a website or app | Your redirect to the returned paymentUrl |
| Checkout link | POST /api/v1/merchant/checkout-links with channel: "Link" | No website: in-store, WhatsApp, social | A link sent by SMS |
| Checkout QR | POST /api/v1/merchant/checkout-links with channel: "Qr" | No website, customer is in front of you | Scanning a QR code you display |
| In-store (POS) | POST /api/v1/pay/pos/create | Point-of-sale terminal | Opening or scanning the returned checkoutUrl |
| QR session | POST /api/v1/pay/qr/create | Scan-to-pay with full basket data | Scanning the returned qrCodeUrl |
Which one should I use?
- You have an online store or app: payment session. It carries the full basket, the customer and the addresses, and it is what the server SDKs and plugins use.
- You have no website, or you sell on chat or by phone: checkout link. You only need the amount and the customer's phone.
- The customer is at your counter: checkout QR if you only know the amount; QR session if your till has the basket lines; POS if your till also has the customer's name, phone and email.
What every flow has in common
Whichever integration path you choose, the rules below always apply.
Requirements
- Scope: every create call needs a secret key with the
orders:writescope. See Authentication. - Idempotency: send an
Idempotency-Keyheader with a fresh UUID v4 per order attempt, and reuse the same key when you retry. This guarantees a retried request never creates a duplicate order. - Your reference:
orderReferenceIdis your own order id, up to 255 characters. References are unique across all of Qasimha, not just within your merchant account, so namespace yours per store (for example,SHOP-ORDER-10001). - Status: the order is created as
AwaitingCheckout. No credit decision is made at this point, so no customer is ever refused by the create call. The decision happens entirely on the hosted page. - Validity: the hosted checkout link stays valid for two hours, after which the order becomes
Expired. The order'scheckoutExpiresAtfield tells you exactly when. - Return URLs: every create call accepts an optional
merchantUrlto send the customer back when checkout finishes. - Response:
201with the standard envelope. Save the Qasimha order id (installmentIdorid) alongside yourorderReferenceId. Every later call on this order uses the Qasimha id.
The hosted journey
From here, Qasimha runs the same flow for every customer: phone verification (OTP), identity verification through Nafath, the credit decision, instalment plan choice, contract acceptance, and the first instalment payment.
When the journey completes successfully, the order becomes Confirmed, the order.confirmed webhook fires, and the customer lands on your merchantUrl.success. If they're declined, the checkout expires, or they cancel, they're sent to failure or cancel instead.
All order-creating calls need the orders:write scope. Send an Idempotency-Key on every one.
Payment session
POST /api/v1/pay/session/create · SDK: checkout.createSession · API reference
Use this for e-commerce. You send the full basket, and Qasimha returns a hosted paymentUrl. Redirect the customer to it, or open it with a mobile SDK.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
orderReferenceId | string | Yes | Your order id, 1 to 255 characters, unique |
totalAmount | number | Yes | Greater than 0. Must equal the basket (see below) |
taxAmount | number | No (0) | Order-level tax. Not negative |
shippingAmount | number | No (0) | Not negative |
discountAmount | number | No (0) | Order-level discount. Not negative |
currencyCode | string | Yes | "SAR". Any other value is refused |
countryCode | string | Yes | Two letters, "SA" |
description | string | No | Shown with the order |
paymentType | string | No | Send "pay_by_instalments" |
instalments | integer | No | The plan length you suggest, usually 4. The customer confirms the plan on the hosted page |
items | array | Yes | At least one line (see below) |
consumer | object | Yes | firstName, lastName, phoneNumber; optional email; optional nationalId (10 digits starting with 1 or 2) |
shippingAddress | object | Yes | firstName, lastName, line1, city, countryCode; optional line2, region, postalCode, phoneNumber |
billingAddress | object | No | Same shape as shippingAddress |
riskAssessment | object | No | Optional context: customerDob, customerGender, isExistingCustomer, orderIsDigital, totalOrderCount, dateOfFirstTransaction |
merchantUrl | object | No | success, failure, cancel. See Return URLs |
Each line in items:
| Field | Type | Required | Meaning |
|---|---|---|---|
referenceId | string | Yes | Your line or product id, up to 255 characters |
name | string | Yes | Up to 500 characters |
type | string | No | "physical" or "digital", up to 50 characters |
sku | string | No | Up to 100 characters |
quantity | integer | Yes | 1 or more |
unitPrice | number | Yes | Not negative |
totalAmount | number | Yes | quantity × unitPrice − discountAmount |
taxAmount | number | No (0) | Not negative |
discountAmount | number | No (0) | Line discount. Not negative |
imageUrl | string | No | Up to 500 characters |
The hosted page opens in the language of your Accept-Language header: en… gives English, anything else Arabic. The server SDKs send their language option as this header.
Full example
curl -X POST https://api-uat.qasimha.com/api/v1/pay/session/create \
-H "X-Api-Key: $QASIMHA_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept-Language: en" \
-H "Idempotency-Key: 6f1c2a9e-3b7d-4e8a-9c41-2d5f8b0e7a13" \
-d '{
"orderReferenceId": "SHOP-ORDER-10001",
"totalAmount": 1302.10,
"taxAmount": 173.10,
"shippingAmount": 25.00,
"discountAmount": 50.00,
"currencyCode": "SAR",
"countryCode": "SA",
"description": "Order #10001",
"paymentType": "pay_by_instalments",
"instalments": 4,
"items": [
{ "referenceId": "SKU-1", "name": "Wireless Headphones", "type": "physical", "sku": "SKU-1",
"quantity": 1, "unitPrice": 899.00, "totalAmount": 899.00 },
{ "referenceId": "SKU-2", "name": "USB-C Cable", "type": "physical", "sku": "SKU-2",
"quantity": 3, "unitPrice": 45.00, "totalAmount": 135.00 },
{ "referenceId": "SKU-3", "name": "Extended Warranty", "type": "digital", "sku": "SKU-3",
"quantity": 1, "unitPrice": 120.00, "totalAmount": 120.00 }
],
"consumer": { "firstName": "Noura", "lastName": "Al-Shammari",
"phoneNumber": "+9665XXXXXXXX", "email": "[email protected]" },
"shippingAddress": { "firstName": "Noura", "lastName": "Al-Shammari",
"line1": "King Fahd Rd", "city": "Riyadh", "postalCode": "12271", "countryCode": "SA" },
"merchantUrl": {
"success": "https://shop.example/checkout/qasimha/return",
"failure": "https://shop.example/checkout/qasimha/return",
"cancel": "https://shop.example/cart"
}
}'Here the lines add up to 899.00 + 135.00 + 120.00 = 1154.00, and 1154.00 + 173.10 tax + 25.00 shipping − 50.00 discount = 1302.10.
Response 201:
{
"success": true,
"uuid": "6b1f0c3e-...",
"code": "SESSION_CREATED",
"message": "...",
"data": {
"installmentId": "9a2e6c4b-1111-4c2d-8a1f-000000000001",
"providerSessionId": "",
"paymentUrl": "https://.../pay/9a2e6c4b-1111-4c2d-8a1f-000000000001?lang=en",
"status": "AwaitingCheckout"
},
"errors": null
}Store installmentId. It is the Qasimha order id. A 201 always carries a paymentUrl.
The basket must add up
Qasimha stores every line as you send it and checks the totals before financing anything:
item.totalAmount=quantity × unitPrice − item.discountAmounttotalAmount= sum ofitem.totalAmount+taxAmount+shippingAmount−discountAmount
Both must match to the halala (0.01). A mismatch returns 400 VALIDATION_ERROR, and errors tells you which line failed. Price the basket on your server from your own catalogue; never trust amounts sent by a browser or an app.
Duplicate references
If you reuse an orderReferenceId, you get 409 DUPLICATE_ORDER_REFERENCE. When the existing order is yours, data includes its installmentId and paymentUrl:
{
"success": false,
"code": "DUPLICATE_ORDER_REFERENCE",
"message": "...",
"data": { "installmentId": "9a2e6c4b-1111-4c2d-8a1f-000000000001", "paymentUrl": "https://.../pay/9a2e6c4b-...?lang=en" },
"errors": []
}Send the customer back to that open checkout instead of creating a second order. If the reference belongs to another merchant, data is empty; choose a more specific reference. The server SDKs raise DuplicateOrderReferenceError with existingOrderId and existingPaymentUrl.
Errors
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | A field is missing or wrong, or the basket doesn't add up. errors names the fields |
| 400 | INVALID_REQUEST | No request body |
| 401 | (empty body) | Missing or invalid key |
| 403 | FORBIDDEN | The key lacks orders:write |
| 409 | DUPLICATE_ORDER_REFERENCE | orderReferenceId is already used |
| 409 | MERCHANT_SUSPENDED | Your account can't create orders right now. Contact support |
| 409 | REQUEST_IN_PROGRESS | The first request with this Idempotency-Key is still running |
| 422 | IDEMPOTENCY_KEY_REUSED | Same Idempotency-Key, different body |
| 429 | TOO_MANY_REQUESTS | Rate limited |
All codes: Errors and rate limits.
Checkout link and QR
POST /api/v1/merchant/checkout-links · SDK: checkoutLinks.create · API reference
Use this when you have no website. Qasimha creates the order first and returns the hosted URL /pay/{orderId}.
channel: "Link"(the default) needsphone. Qasimha texts the link to the customer, and also sends a push notification if they use the Qasimha app.channel: "Qr"needs no phone and sends nothing. You render the returnedurlas a QR code.
Both channels open the same page and run the same journey.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
amount | number | Yes, unless items are given | Order amount, greater than 0 and at most 9999999999.99. If you send only items, the amount is the sum of quantity × unitPrice. If you send both, they must be equal |
channel | string | No | "Link" (default) or "Qr" |
phone | string | For Link | The customer's mobile number, for example "+9665XXXXXXXX" |
language | string | No | "ar" (default) or "en". Sets the SMS, push and page language. If omitted, Accept-Language decides, then Arabic |
orderReferenceId | string | No | Up to 255 characters. If omitted, Qasimha generates one in the format YYDDDNNNNN (UTC year, day of the year, five digits), for example 2628041980 |
items | array | No | Lines of { name, sku, quantity, unitPrice, imageUrl }. quantity 1 or more, unitPrice not negative; name and imageUrl up to 500 characters, sku up to 100 |
ejarContractNumber | string | Real-estate merchants only | The Ejar platform contract the order finances, up to 50 characters. Required for a real-estate merchant, ignored for others |
merchantUrl | object | No | Return URLs |
Full example
curl -X POST https://api-uat.qasimha.com/api/v1/merchant/checkout-links \
-H "X-Api-Key: $QASIMHA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0b6c1f4e-8d2a-4c5b-9e1f-7a3d2c4b5e6f" \
-d '{
"amount": 250.00,
"channel": "Link",
"phone": "+9665XXXXXXXX",
"language": "ar",
"orderReferenceId": "SHOP-ORDER-10002",
"items": [ { "name": "Running Shoes", "sku": "SH-1", "quantity": 1, "unitPrice": 250.00 } ],
"merchantUrl": {
"success": "https://shop.example/qasimha/return",
"failure": "https://shop.example/qasimha/return"
}
}'Response 201:
{
"success": true,
"uuid": "1d2e3f40-...",
"code": "LINK_CREATED",
"message": "Checkout link created",
"data": {
"id": "c1e4f312-f61c-445f-bce4-8551984db9f3",
"url": "https://.../pay/c1e4f312-f61c-445f-bce4-8551984db9f3?lang=ar",
"channel": "Link",
"amount": 250.00,
"currency": "SAR",
"phone": "+9665XXXXXXXX",
"language": "ar",
"orderReferenceId": "SHOP-ORDER-10002",
"ejarContractNumber": null,
"items": [ { "name": "Running Shoes", "sku": "SH-1", "quantity": 1, "unitPrice": 250.00, "total": 250.00, "imageUrl": null } ],
"status": "AwaitingCheckout",
"expiresAt": "2026-10-20T11:30:00Z"
},
"errors": null
}id is the Qasimha order id. Use it with the order endpoints too.
Track, list and cancel links
| Call | Scope | What it does |
|---|---|---|
GET /api/v1/merchant/checkout-links/{id} | orders:read | One link: id, orderReferenceId, ejarContractNumber, amount, currency, phone (masked), channel, status, decisionStatus, expiresAt, createdAt, completed (the customer finished checkout), declined. A link past its expiry is moved to Expired when you read it |
GET /api/v1/merchant/checkout-links | orders:read | Your 50 most recent link and QR orders in data.items. Each row has a waiting flag for links still open, and shows Expired for links past their expiry |
POST /api/v1/merchant/checkout-links/{id}/cancel | orders:write | Cancels a link that is still open. Returns LINK_CANCELLED and the link; the order becomes Cancelled. A link that was completed or has expired returns 409 NOT_OPEN |
Rather than polling, subscribe to webhooks. A link or QR order that expires sends checkout_link.expired.
Errors
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_AMOUNT | No positive amount (and no items to compute one), or more than 9999999999.99 |
| 400 | AMOUNT_ITEMS_MISMATCH | amount doesn't equal the items total. Send one or the other, or make them agree |
| 400 | INVALID_ITEM | An item has quantity below 1, a negative unitPrice, or a field that is too long |
| 400 | PHONE_REQUIRED | channel is Link and phone is missing |
| 400 | INVALID_ORDER_REFERENCE | orderReferenceId is longer than 255 characters |
| 400 | EJAR_CONTRACT_NUMBER_REQUIRED | You are a real-estate merchant and sent no ejarContractNumber |
| 400 | INVALID_EJAR_CONTRACT_NUMBER | ejarContractNumber is longer than 50 characters |
| 400 | VALIDATION_ERROR | merchantUrl breaks a return URL rule |
| 403 | FORBIDDEN | The key lacks orders:write |
| 404 | MERCHANT_NOT_FOUND | Your merchant account wasn't found. Contact support |
| 409 | DUPLICATE_ORDER_REFERENCE | orderReferenceId is already used. On this endpoint data is empty |
| 409 | MERCHANT_SUSPENDED | Your account can't create orders right now |
In-store (POS)
POST /api/v1/pay/pos/create · SDK: checkout.createPosSession · API reference
Use this from a point-of-sale system that knows the basket and the customer. Qasimha creates the order and returns a hosted checkoutUrl (/pay/{orderId}) for the customer to open or scan on their own phone. Nothing is sent to the customer, so show the URL or a QR code of it.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
orderReferenceId | string | Yes | 1 to 255 characters, unique |
totalAmount | number | Yes | Greater than 0. Must equal the sum of items[].totalAmount |
currencyCode | string | Yes | "SAR" |
countryCode | string | Yes | "SA" |
items | array | Yes | At least one line, same fields and rules as the payment session |
consumer | object | Yes | firstName, lastName, phoneNumber, email (required here), optional nationalId |
merchantUrl | object | No | Return URLs |
Full example
curl -X POST https://api-uat.qasimha.com/api/v1/pay/pos/create \
-H "X-Api-Key: $QASIMHA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3c9e7a21-5b4d-4f60-8a2e-1d7c6b5a4f30" \
-d '{
"orderReferenceId": "STORE7-POS-55120",
"totalAmount": 2400.00,
"currencyCode": "SAR",
"countryCode": "SA",
"items": [
{ "referenceId": "TV-55", "name": "55-inch TV", "type": "physical", "sku": "TV-55",
"quantity": 1, "unitPrice": 2400.00, "totalAmount": 2400.00 }
],
"consumer": { "firstName": "Faisal", "lastName": "Al-Harbi",
"phoneNumber": "+9665XXXXXXXX", "email": "[email protected]" }
}'Response 201:
{
"success": true,
"uuid": "a7c3e9f1-...",
"code": "SESSION_CREATED",
"message": "...",
"data": {
"installmentId": "2f6d8b1a-3c4e-4d5f-9a7b-0c1d2e3f4a5b",
"providerSessionId": "",
"checkoutUrl": "https://.../pay/2f6d8b1a-3c4e-4d5f-9a7b-0c1d2e3f4a5b",
"status": "AwaitingCheckout"
},
"errors": null
}Errors are the same as for the payment session, except that 409 DUPLICATE_ORDER_REFERENCE is not returned here: always send a new reference.
QR session
POST /api/v1/pay/qr/create · SDK: checkout.createQrSession · API reference
Use this for scan-to-pay when you have the basket lines but no customer details. Encode the returned qrCodeUrl in a QR code. The customer scans it and identifies themselves on the hosted page.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
orderReferenceId | string | Yes | 1 to 255 characters, unique |
totalAmount | number | Yes | Greater than 0. Must equal the sum of items[].totalAmount |
currencyCode | string | Yes | "SAR" |
countryCode | string | Yes | "SA" |
items | array | Yes | At least one line, same fields and rules as the payment session |
merchantUrl | object | No | Return URLs |
The page language follows Accept-Language, as for the payment session.
Full example
curl -X POST https://api-uat.qasimha.com/api/v1/pay/qr/create \
-H "X-Api-Key: $QASIMHA_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept-Language: ar" \
-H "Idempotency-Key: 8e2d4c6a-1b3f-4a5c-9d7e-2f4a6c8e0b1d" \
-d '{
"orderReferenceId": "STORE7-QR-88014",
"totalAmount": 600.00,
"currencyCode": "SAR",
"countryCode": "SA",
"items": [
{ "referenceId": "CHAIR-2", "name": "Office Chair", "type": "physical",
"quantity": 2, "unitPrice": 300.00, "totalAmount": 600.00 }
]
}'Response 201:
{
"success": true,
"uuid": "f0e1d2c3-...",
"code": "SESSION_CREATED",
"message": "...",
"data": {
"installmentId": "5b7c9d1e-2f3a-4b5c-8d6e-7f8a9b0c1d2e",
"providerSessionId": "",
"qrCodeUrl": "https://.../pay/5b7c9d1e-2f3a-4b5c-8d6e-7f8a9b0c1d2e?lang=ar",
"status": "AwaitingCheckout"
},
"errors": null
}Errors are the same as for the POS endpoint. An expired QR session sends the checkout_link.expired webhook.
After you create the order
- Track it: subscribe to webhooks, or read
GET /api/v1/merchant/orders/{id}. - Confirm it: on the return and on the webhook, read the order and ship only when
statusisConfirmed. See Return URLs. - Cancel an open checkout:
POST /api/v1/merchant/orders/{id}/cancelworks for every flow while the order isAwaitingCheckout; the order becomesCancelled. - Manage it after checkout: fulfil, refund or cancel. See Order lifecycle.
Troubleshooting
400 VALIDATION_ERROR: totalAmount does not equal the basket
The message shows the computed figure, for example items 1154.00 + tax 173.10 + shipping 25.00 − discount 50.00 = 1302.10. Fix the line or the order-level amount so both sides agree to the halala. On POS and QR sessions, totalAmount must equal the sum of the lines' totalAmount.
400 VALIDATION_ERROR: Shipping address is required
The payment session needs shippingAddress with at least firstName, lastName, line1, city and countryCode. For digital goods, send the customer's own address.
The hosted page opens in the wrong language
Sessions follow your Accept-Language header; checkout links follow language, then Accept-Language. Anything that doesn't start with en gives Arabic. The server SDKs send their language option, which defaults to en.
The customer didn't receive the SMS
Only channel: "Link" sends an SMS, to the phone in the request. Check the number, then share the url from the response another way. Qr, POS and QR sessions never send messages.
Related
Next
After checkout, every flow follows the same order lifecycle.
Updated about 23 hours ago
