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.

FlowEndpointUse whenCustomer reaches checkout by
Payment sessionPOST /api/v1/pay/session/createYou have a website or appYour redirect to the returned paymentUrl
Checkout linkPOST /api/v1/merchant/checkout-links with channel: "Link"No website: in-store, WhatsApp, socialA link sent by SMS
Checkout QRPOST /api/v1/merchant/checkout-links with channel: "Qr"No website, customer is in front of youScanning a QR code you display
In-store (POS)POST /api/v1/pay/pos/createPoint-of-sale terminalOpening or scanning the returned checkoutUrl
QR sessionPOST /api/v1/pay/qr/createScan-to-pay with full basket dataScanning 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:write scope. See Authentication.
  • Idempotency: send an Idempotency-Key header 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: orderReferenceId is 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's checkoutExpiresAt field tells you exactly when.
  • Return URLs: every create call accepts an optional merchantUrl to send the customer back when checkout finishes.
  • Response: 201 with the standard envelope. Save the Qasimha order id (installmentId or id) alongside your orderReferenceId. 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

FieldTypeRequiredMeaning
orderReferenceIdstringYesYour order id, 1 to 255 characters, unique
totalAmountnumberYesGreater than 0. Must equal the basket (see below)
taxAmountnumberNo (0)Order-level tax. Not negative
shippingAmountnumberNo (0)Not negative
discountAmountnumberNo (0)Order-level discount. Not negative
currencyCodestringYes"SAR". Any other value is refused
countryCodestringYesTwo letters, "SA"
descriptionstringNoShown with the order
paymentTypestringNoSend "pay_by_instalments"
instalmentsintegerNoThe plan length you suggest, usually 4. The customer confirms the plan on the hosted page
itemsarrayYesAt least one line (see below)
consumerobjectYesfirstName, lastName, phoneNumber; optional email; optional nationalId (10 digits starting with 1 or 2)
shippingAddressobjectYesfirstName, lastName, line1, city, countryCode; optional line2, region, postalCode, phoneNumber
billingAddressobjectNoSame shape as shippingAddress
riskAssessmentobjectNoOptional context: customerDob, customerGender, isExistingCustomer, orderIsDigital, totalOrderCount, dateOfFirstTransaction
merchantUrlobjectNosuccess, failure, cancel. See Return URLs

Each line in items:

FieldTypeRequiredMeaning
referenceIdstringYesYour line or product id, up to 255 characters
namestringYesUp to 500 characters
typestringNo"physical" or "digital", up to 50 characters
skustringNoUp to 100 characters
quantityintegerYes1 or more
unitPricenumberYesNot negative
totalAmountnumberYesquantity × unitPrice − discountAmount
taxAmountnumberNo (0)Not negative
discountAmountnumberNo (0)Line discount. Not negative
imageUrlstringNoUp 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.discountAmount
  • totalAmount = sum of item.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

HTTPCodeMeaning
400VALIDATION_ERRORA field is missing or wrong, or the basket doesn't add up. errors names the fields
400INVALID_REQUESTNo request body
401(empty body)Missing or invalid key
403FORBIDDENThe key lacks orders:write
409DUPLICATE_ORDER_REFERENCEorderReferenceId is already used
409MERCHANT_SUSPENDEDYour account can't create orders right now. Contact support
409REQUEST_IN_PROGRESSThe first request with this Idempotency-Key is still running
422IDEMPOTENCY_KEY_REUSEDSame Idempotency-Key, different body
429TOO_MANY_REQUESTSRate 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) needs phone. 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 returned url as a QR code.

Both channels open the same page and run the same journey.

Request fields

FieldTypeRequiredMeaning
amountnumberYes, unless items are givenOrder 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
channelstringNo"Link" (default) or "Qr"
phonestringFor LinkThe customer's mobile number, for example "+9665XXXXXXXX"
languagestringNo"ar" (default) or "en". Sets the SMS, push and page language. If omitted, Accept-Language decides, then Arabic
orderReferenceIdstringNoUp to 255 characters. If omitted, Qasimha generates one in the format YYDDDNNNNN (UTC year, day of the year, five digits), for example 2628041980
itemsarrayNoLines of { name, sku, quantity, unitPrice, imageUrl }. quantity 1 or more, unitPrice not negative; name and imageUrl up to 500 characters, sku up to 100
ejarContractNumberstringReal-estate merchants onlyThe Ejar platform contract the order finances, up to 50 characters. Required for a real-estate merchant, ignored for others
merchantUrlobjectNoReturn 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

CallScopeWhat it does
GET /api/v1/merchant/checkout-links/{id}orders:readOne 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-linksorders:readYour 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}/cancelorders:writeCancels 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

HTTPCodeMeaning
400INVALID_AMOUNTNo positive amount (and no items to compute one), or more than 9999999999.99
400AMOUNT_ITEMS_MISMATCHamount doesn't equal the items total. Send one or the other, or make them agree
400INVALID_ITEMAn item has quantity below 1, a negative unitPrice, or a field that is too long
400PHONE_REQUIREDchannel is Link and phone is missing
400INVALID_ORDER_REFERENCEorderReferenceId is longer than 255 characters
400EJAR_CONTRACT_NUMBER_REQUIREDYou are a real-estate merchant and sent no ejarContractNumber
400INVALID_EJAR_CONTRACT_NUMBERejarContractNumber is longer than 50 characters
400VALIDATION_ERRORmerchantUrl breaks a return URL rule
403FORBIDDENThe key lacks orders:write
404MERCHANT_NOT_FOUNDYour merchant account wasn't found. Contact support
409DUPLICATE_ORDER_REFERENCEorderReferenceId is already used. On this endpoint data is empty
409MERCHANT_SUSPENDEDYour 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

FieldTypeRequiredMeaning
orderReferenceIdstringYes1 to 255 characters, unique
totalAmountnumberYesGreater than 0. Must equal the sum of items[].totalAmount
currencyCodestringYes"SAR"
countryCodestringYes"SA"
itemsarrayYesAt least one line, same fields and rules as the payment session
consumerobjectYesfirstName, lastName, phoneNumber, email (required here), optional nationalId
merchantUrlobjectNoReturn 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

FieldTypeRequiredMeaning
orderReferenceIdstringYes1 to 255 characters, unique
totalAmountnumberYesGreater than 0. Must equal the sum of items[].totalAmount
currencyCodestringYes"SAR"
countryCodestringYes"SA"
itemsarrayYesAt least one line, same fields and rules as the payment session
merchantUrlobjectNoReturn 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 status is Confirmed. See Return URLs.
  • Cancel an open checkout: POST /api/v1/merchant/orders/{id}/cancel works for every flow while the order is AwaitingCheckout; the order becomes Cancelled.
  • 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.


Did this page help you?