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 localhost are 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

  1. In the UAT Merchant Portal, go to Settings → API Keys and create a secret key.
  2. Give it the scopes orders:write (create and manage orders; it includes orders:read) and webhooks:manage (for step 5). A key created with no scope gets orders:read only.
  3. Copy the key. It starts with sk_test_ and is shown once.
  4. Store it in a secrets manager and load it from an environment variable:
    export QASIMHA_API_KEY="sk_test_..."

Keep secret keys on your server

Never 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/sdk

2. 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 browser

A 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
}
  • installmentId is the Qasimha order id (the SDK calls it orderId). Save it next to your orderReferenceId. Every later call uses it.
  • The basket must add up: item.totalAmount = quantity × unitPrice − item.discountAmount, and totalAmount = Σ item.totalAmount + taxAmount + shippingAmount − discountAmount, to the halala. Here 1200.00 + 25.00 = 1225.00.
  • shippingAddress is required. Leaving it out returns 400 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=confirmed

Anyone 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"

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"]
  }'

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" }'

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.confirmed webhook arrives, and the order reads Confirmed.
  • Cancel: press "Cancel and return to store" on the hosted page. You land on your cancel URL with status=cancelled; the order stays AwaitingCheckout until it expires or you cancel it with POST /api/v1/merchant/orders/{id}/cancel.
  • Double submit: send the same orderReferenceId twice. The second call returns 409 DUPLICATE_ORDER_REFERENCE with the existing order's installmentId and paymentUrl.
  • Retry: send the same request twice with the same Idempotency-Key. The second answer carries Idempotent-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

  1. Create production keys in the production Merchant Portal, https://portal.qasimha.com: sk_live_…, and pk_live_… if you show plans.
  2. Put them in your production secret store, replacing the UAT keys. With an SDK, no code change is needed; it picks https://api.qasimha.com from the sk_live_ prefix. With plain HTTP, switch the base URL in the same change.
  3. Check that your production return URLs point at your live site, never at a test host.
  4. Register your production webhook URL in production and set its whsec_… secret. UAT endpoints and secrets don't carry over.
  5. Remove any base URL override you used for testing.
  6. Give each production key only the scopes it needs. See Scopes.
  7. 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-client

Replace typescript-fetch with python, php, java, csharp, go, and so on. Point the generated client at the UAT base URL while you test.


Next


Did this page help you?