Quick start
This guide walks you through the entire Qasimha integration - from your very first UAT key to a confirmed, fulfilled test order. Along the way you'll create a payment session, send the customer to the hosted checkout, handle their return, receive the webhook, and verify the order on your server. Every step includes ready-to-run code in both cURL and Node.js, so you can copy, paste, and see results in minutes.
Before you start
You need:
- A user on the UAT Merchant Portal,
https://portal-uat.qasimha.com, with the owner or admin role (only they can create API keys and webhooks). - A server that can make HTTPS calls. The examples use cURL and Node.js 18 or later.
- For webhooks, a public HTTPS URL that reaches your server. Private and local addresses such as
localhostare refused, so use a tunnel or a test deployment.
This guide uses the UAT (test) environment, https://api-uat.qasimha.com. When you go live, switch to https://api.qasimha.com and an sk_live_ key. See Environments.1. Create a UAT secret key
1. Create a UAT secret key
- In the UAT Merchant Portal, go to Settings → API Keys and create a secret key.
- Give it the scopes
orders:write(create and manage orders; it includesorders:read) andwebhooks:manage(for step 5). A key created with no scope getsorders:readonly. - Copy the key. It starts with
sk_test_and is shown once. - Store it in a secrets manager and load it from an environment variable:
export QASIMHA_API_KEY="sk_test_..."
Keep secret keys on your serverNever put an
sk_key in a web page, mobile app, source repository, or AI chat assistant. If a key is exposed, revoke it in the portal right away.
To use the SDK, install it:
npm install @qasimha/sdk2. Create an order
Your server sends the basket and gets back a hosted paymentUrl. Send a fresh Idempotency-Key so a retry can never create a second order.
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: ar" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"orderReferenceId": "SHOP-ORDER-10001",
"totalAmount": 1225.00,
"shippingAmount": 25.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": 1200.00, "totalAmount": 1200.00 }
],
"consumer": { "firstName": "Noura", "lastName": "Al-Shammari",
"phoneNumber": "+9665XXXXXXXX", "email": "[email protected]" },
"shippingAddress": { "firstName": "Noura", "lastName": "Al-Shammari",
"line1": "King Fahd Rd", "city": "Riyadh", "countryCode": "SA" },
"merchantUrl": {
"success": "https://shop.example/checkout/qasimha/return",
"failure": "https://shop.example/checkout/qasimha/return",
"cancel": "https://shop.example/cart"
}
}'import Qasimha from '@qasimha/sdk';
// The SDK picks UAT from the sk_test_ prefix and sends an Idempotency-Key on every POST.
const qasimha = new Qasimha({
apiKey: process.env.QASIMHA_API_KEY!,
webhookSecret: process.env.QASIMHA_WEBHOOK_SECRET, // set after step 5
language: 'ar', // sent as Accept-Language: the page opens in Arabic
});
const session = await qasimha.checkout.createSession({
orderReferenceId: 'SHOP-ORDER-10001',
totalAmount: 1225.00,
shippingAmount: 25.00,
description: 'Order #10001',
instalments: 4,
items: [{ referenceId: 'SKU-1', name: 'Wireless Headphones', type: 'physical', sku: 'SKU-1',
quantity: 1, unitPrice: 1200.00, totalAmount: 1200.00 }],
consumer: { firstName: 'Noura', lastName: 'Al-Shammari', phoneNumber: '+9665XXXXXXXX', email: '[email protected]' },
shippingAddress: { firstName: 'Noura', lastName: 'Al-Shammari', line1: 'King Fahd Rd', city: 'Riyadh', countryCode: 'SA' },
merchantUrl: {
success: 'https://shop.example/checkout/qasimha/return',
failure: 'https://shop.example/checkout/qasimha/return',
cancel: 'https://shop.example/cart',
},
});
await saveQasimhaOrderId('SHOP-ORDER-10001', session.orderId); // your own persistence
// send session.paymentUrl to the browserA successful call returns 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=ar",
"status": "AwaitingCheckout"
},
"errors": null
}installmentIdis the Qasimha order id (the SDK calls itorderId). Save it next to yourorderReferenceId. Every later call uses it.- The basket must add up:
item.totalAmount = quantity × unitPrice − item.discountAmount, andtotalAmount = Σ item.totalAmount + taxAmount + shippingAmount − discountAmount, to the halala. Here 1200.00 + 25.00 = 1225.00. shippingAddressis required. Leaving it out returns400 VALIDATION_ERROR.- No credit decision is made here. The order is
AwaitingCheckout, and the hosted page is valid for two hours.
Full field reference: Choose a checkout flow.
The response data contains orderId (the Qasimha order id), url (the hosted checkout page), and qr. Save orderId next to your own orderReferenceId. Later calls (capture, fulfil, refund) use the Qasimha id.
Have a website or app? Use a payment session instead and redirect the customer to the returned paymentUrl. Prefer a library? See the server SDKs or the e-commerce plugins.
3. Send the customer to the hosted checkout
Redirect the browser to paymentUrl using an HTTP 302 redirect or window.location.assign(paymentUrl). On mobile, open it with the mobile SDK instead. The hosted checkout must open in a full browser window and cannot be displayed inside an iframe.
From there, Qasimha handles everything. The customer verifies their phone with an OTP, confirms their identity through Nafath, receives a credit decision, chooses an instalment plan, accepts the contract, and pays the first instalment. Once complete, Qasimha redirects them back to your merchantUrl.
4. Handle the return
Qasimha appends orderId, orderReferenceId and status to your return URL, for example:
https://shop.example/checkout/qasimha/return?orderId=9a2e6c4b-1111-4c2d-8a1f-000000000001&orderReferenceId=SHOP-ORDER-10001&status=confirmedAnyone can type that URL. Read the order from your server before you show "paid":
curl https://api-uat.qasimha.com/api/v1/merchant/orders/9a2e6c4b-1111-4c2d-8a1f-000000000001 \
-H "X-Api-Key: $QASIMHA_API_KEY"app.get('/checkout/qasimha/return', async (req, res) => {
const orderId = String(req.query.orderId ?? '');
const stored = await findOrderByQasimhaId(orderId); // your own lookup
if (!stored) return res.redirect('/cart');
const order = await qasimha.orders.get(orderId);
const paid = order.status === 'Confirmed' && order.orderReferenceId === stored.reference;
if (paid) {
await markPaid(stored.reference); // idempotent on your side
return res.redirect(`/orders/${stored.reference}/thank-you`);
}
return res.redirect(`/checkout?payment=${encodeURIComponent(order.status ?? "")}`); // Declined, Expired, Cancelled, AwaitingCheckout…
});Treat the order as paid only when data.status is Confirmed and data.orderReferenceId is yours. If you only kept your own reference, use GET /api/v1/merchant/orders/by-reference/{orderReferenceId} (orders.getByReference in the SDK). See Return URLs.
5. Register a webhook endpoint
The customer may close their browser before being redirected back to your site. The webhook is your durable record of what actually happened, delivered even when the return never arrives.
Register your endpoint to start receiving events. You can do this through the API (requires the webhooks:manage scope) or in a few clicks via Settings → Webhooks in the merchant portal.
curl -X POST https://api-uat.qasimha.com/api/v1/merchant/webhooks \
-H "X-Api-Key: $QASIMHA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://shop.example/webhooks/qasimha",
"description": "UAT test store",
"events": ["order.confirmed", "order.declined", "order.expired", "order.cancelled"]
}'const endpoint = await qasimha.webhookEndpoints.create({
url: 'https://shop.example/webhooks/qasimha',
description: 'UAT test store',
events: ['order.confirmed', 'order.declined', 'order.expired', 'order.cancelled'],
});
// endpoint.secret is the whsec_... signing secret. It is returned once: store it now.The registration response (201 WEBHOOK_CREATED) returns secret, the endpoint's whsec_… signing key. Qasimha shows it only once, so copy it now and store it somewhere secure, such as an environment variable: QASIMHA_WEBHOOK_SECRET. If you lose it, you'll need to register a new endpoint rather than retrieve it again. By default the endpoint receives every event. To subscribe to a subset, pass an events array in the registration call; leave events out to keep the full firehose.
Verify and handle deliveries. Read the raw request body, verify the signature, answer 2xx quickly, then re-read the order:
import express from 'express';
import { WebhookSignatureError } from '@qasimha/sdk';
app.post('/webhooks/qasimha', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = qasimha.webhooks.constructEvent(req.body, req.headers); // raw Buffer, not parsed JSON
} catch (error) {
return res.sendStatus(error instanceof WebhookSignatureError ? 400 : 500);
}
res.sendStatus(200);
if (await alreadyProcessed(event.id)) return; // retries repeat the same id
if (event.type === 'order.confirmed' && event.orderId) {
const order = await qasimha.orders.get(event.orderId);
if (order.status === 'Confirmed') await markPaid(order.orderReferenceId);
}
});Send a test event with POST /api/v1/merchant/webhooks/{id}/ping (webhookEndpoints.ping(id)) and check that your route answers 200. Writing your own verifier instead of using the SDK? See Verifying webhook signatures.
6. Confirm and fulfil
When the order is Confirmed, ship the goods and record the fulfilment. Leave out shippedAmount to fulfil everything still outstanding.
curl -X POST https://api-uat.qasimha.com/api/v1/merchant/orders/9a2e6c4b-1111-4c2d-8a1f-000000000001/fulfill \
-H "X-Api-Key: $QASIMHA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "shippingCompany": "Aramex", "trackingNumber": "ARX123", "trackingUrl": "https://track.example/ARX123" }'await qasimha.orders.fulfill(orderId, {
shippingCompany: 'Aramex',
trackingNumber: 'ARX123',
trackingUrl: 'https://track.example/ARX123',
});The response data is { id, status, fulfilledAmount }. Here status is the number 6, which means FullyFulfilled (action responses return the status as a number; the SDK gives you the name). Refunds and cancellations are in Order lifecycle.
No website? Use a checkout link
If you sell in store, by WhatsApp or on social media, let Qasimha create the order and text the link to the customer:
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: $(uuidgen)" \
-d '{ "amount": 1000.00, "channel": "Link", "phone": "+9665XXXXXXXX", "language": "ar",
"orderReferenceId": "SHOP-ORDER-10002" }'The response (201 LINK_CREATED) data contains id (the Qasimha order id), url (the hosted checkout page), status (AwaitingCheckout) and expiresAt. Steps 4 to 6 are the same. See Choose a checkout flow.
Test in UAT
Run each of these before you go live. Use test data only; never send real customer data to UAT.
- Happy path: complete a checkout, check the return route shows "paid", the
order.confirmedwebhook arrives, and the order readsConfirmed. - Cancel: press "Cancel and return to store" on the hosted page. You land on your
cancelURL withstatus=cancelled; the order staysAwaitingCheckoutuntil it expires or you cancel it withPOST /api/v1/merchant/orders/{id}/cancel. - Double submit: send the same
orderReferenceIdtwice. The second call returns409 DUPLICATE_ORDER_REFERENCEwith the existing order'sinstallmentIdandpaymentUrl. - Retry: send the same request twice with the same
Idempotency-Key. The second answer carriesIdempotent-Replay: true. - Webhook signature: change one byte of a stored delivery body and check that your route answers
400. - Fulfil and refund: fulfil a confirmed order, then refund part of what the customer paid.
Go-live checklist
- Create production keys in the production Merchant Portal,
https://portal.qasimha.com:sk_live_…, andpk_live_…if you show plans. - Put them in your production secret store, replacing the UAT keys. With an SDK, no code change is needed; it picks
https://api.qasimha.comfrom thesk_live_prefix. With plain HTTP, switch the base URL in the same change. - Check that your production return URLs point at your live site, never at a test host.
- Register your production webhook URL in production and set its
whsec_…secret. UAT endpoints and secrets don't carry over. - Remove any base URL override you used for testing.
- Give each production key only the scopes it needs. See Scopes.
- Place one small real order, fulfil it, and refund it, to check the return page, the webhook and your records end to end.
Troubleshooting
401 with an empty body
The key is missing, wrong, revoked or expired, or it belongs to the other environment (an sk_live_ key sent to UAT, or the reverse). A publishable pk_ key in X-Api-Key is also refused. The response has no JSON body, only a WWW-Authenticate: ApiKey realm="merchant-api" header. See Authentication.
403 FORBIDDEN
The key is valid but lacks the scope. Creating orders needs orders:write; registering webhooks needs webhooks:manage. Create a key with the right scopes.
400 VALIDATION_ERROR on session create
errors names each failing field, for example ShippingAddress or Items[0].TotalAmount. The usual causes are a missing shippingAddress, a basket that doesn't add up, a currencyCode other than SAR, or an invalid merchantUrl.
400 URL_PRIVATE when registering a webhook
The URL points at a private or local address. Use a public HTTPS URL, for example a tunnel to your machine.
Generate a typed client
Prefer the official server SDKs. For other languages you can generate a client from the spec:
npx @openapitools/openapi-generator-cli generate \
-i https://api.qasimha.com/swagger/merchant-v1/swagger.json \
-g typescript-fetch \
-o ./qasimha-clientReplace typescript-fetch with python, php, java, csharp, go, and so on. Point the generated client at the UAT base URL while you test.
Next
Updated 1 day ago
