Mobile SDKs
Mobile SDKs for Android, iOS, Flutter, and React Native
The Qasimha mobile SDKs open the hosted checkout inside your app's flow and tell you how it ended. Your server creates the checkout session; the app only presents the paymentUrl it receives. The SDKs hold no secret key and make no API call with one. Each SDK also has an optional promo view that shows the instalment plan for a price, using your publishable key.
How it works
- Your server creates the checkout session (
POST /api/v1/pay/session/create) with a Qasimha server SDK and your secret key. It sendsmerchantUrl.success,merchantUrl.failureandmerchantUrl.cancel, and returns thepaymentUrland those three URLs to the app. - Your app starts the SDK with that
paymentUrland the same three return URLs. - The shopper completes the journey: phone verification, Nafath, credit decision, plan choice, contract and first instalment. The hosted page then navigates to one of your return URLs with
orderId,orderReferenceIdandstatusappended. - The SDK detects that navigation (prefix match on scheme, host and path), closes the checkout and reports
Success,FailureorCancelled. - Your app asks your server, and your server asks Qasimha (
GET /api/v1/merchant/orders/{orderId},orders.getin the server SDKs), before anything is shown as paid or shipped. Your server also receives the webhook, which is the durable record.
sequenceDiagram
participant App
participant Server as Merchant server
participant SDK as Qasimha mobile SDK
participant Q as Qasimha
App->>Server: Start payment
Server->>Q: Create session (secret key, merchantUrl)
Q-->>Server: paymentUrl
Server-->>App: paymentUrl
App->>SDK: start(paymentUrl, merchantUrl)
SDK->>Q: Hosted checkout
Q-->>SDK: Navigate to return URL (orderId, status)
SDK-->>App: Success / Failure / Cancelled
App->>Server: Confirm orderId
Server->>Q: Get order (secret key)
Q-->>Server: Order status
Server-->>App: Paid or not
The return URL is navigation, not proofAnyone can type a return URL, and a custom scheme can be claimed by another app. Treat
Successas "the shopper came back on the success URL". Fulfil only after your server re-reads the order. See Return URLs and Order lifecycle.
Hosted checkouts expire after 2 hours, so create the session when the shopper is ready to pay.
Choose your SDK
| SDK | Package | Install | Minimum platform | Presentation modes |
|---|---|---|---|---|
| Android | com.qasimha:checkout-android | implementation("com.qasimha:checkout-android:0.1.0") | minSdk 24, compileSdk 35, Kotlin, AndroidX | Custom Tab (default), hardened WebView |
| iOS | QasimhaCheckout | Swift Package Manager or pod 'QasimhaCheckout' | iOS 14, Swift 5.9 | SFSafariViewController (default), ASWebAuthenticationSession, hardened WKWebView |
| Flutter | qasimha_checkout | flutter pub add qasimha_checkout | Flutter 3.38 (Dart 3.10), Android API 24, iOS 15.0 | System browser (default), ASWebAuthenticationSession on iOS |
| React Native | @qasimha/react-native-sdk | npm install @qasimha/react-native-sdk | React Native 0.72, React 18 or 19, Android minSdk 24, iOS 13.4 | System browser (default), ASWebAuthenticationSession on iOS |
The same calls on every platform:
| Task | Android | iOS | Flutter | React Native |
|---|---|---|---|---|
| Start a checkout | QasimhaCheckoutContract or QasimhaCheckout.start(activity, options, callback) | QasimhaCheckout.start(from:options:completion:) | await QasimhaCheckout.start(options) | await startCheckout(options) |
| Hand a return link over | QasimhaCheckout.handleReturn(uri) | QasimhaCheckout.handleReturn(url:) | Automatic; QasimhaCheckout.handleReturn(uri) for your own router | handleReturn(url) from Linking |
| Options | CheckoutOptions, MerchantUrl | CheckoutOptions, MerchantURL | CheckoutOptions, MerchantUrl | { paymentUrl, merchantUrl, presentation?, language? } |
| Promo | QasimhaPromoView | QasimhaPromoView | QasimhaPromo | <QasimhaPromo> |
Result types
All four SDKs read the same three query parameters from the return URL. The common cases:
| Return URL | Result |
|---|---|
status=confirmed with an orderId | Success(orderId, orderReferenceId). Never produced without an orderId. |
status=declined, expired or failed | Failure(status, orderId, orderReferenceId) |
status=cancelled, or the cancel URL with no status | Cancelled |
| The shopper closed the sheet without returning | Cancelled |
The edge cases differ slightly by platform. Each SDK page lists its own statuses:
| Edge case | Android | iOS | Flutter | React Native |
|---|---|---|---|---|
confirmed without an orderId | Failure("malformed") | .failure("unverified") | CheckoutFailure("malformed") | failure, status unknown |
Success or failure URL with no status | Failure("unknown") | .failure("unverified") | CheckoutFailure("unknown") | failure, status unknown |
| Any other status value | Failure(status), lower-cased | .failure(status), lower-cased | CheckoutFailure(status) | failure with that status |
| Could not present | Failure("no_browser"), Failure("ssl_error"), Failure("network_error") | .failure("invalid_options"), .failure("already_presenting") | throws ArgumentError, StateError or CheckoutException | rejects with QasimhaCheckoutError |
A cancelled order stays AwaitingCheckout on the Qasimha side and expires on its own. Your server may cancel it through the API.
Presentation modes
| Mode | Platforms | When to use |
|---|---|---|
| System browser sheet (default) | All four. Android Custom Tabs, iOS SFSafariViewController. | Almost always. |
ASWebAuthenticationSession (on request) | iOS, Flutter (iOS side), React Native (iOS side) | You use a custom scheme and want iOS to hand back the return URL without app-delegate wiring. Flutter and React Native also accept https return URLs here on iOS 17.4+. |
| Hardened WebView (opt-in) | Android and iOS native SDKs only | Only when the checkout must not leave your app and you know every host the journey visits. |
Why the browser sheet is the default:
- The hosted checkout redirects to third-party pages: the payment provider's hosted payment page, the Nafath app switch, and App Links into the Qasimha app. A WebView would need an allow-list that tracks all of them.
- The system browser gives the shopper the platform's TLS, autofill and cookie isolation.
- Payment providers and Nafath expect a real browser.
The WebView mode is locked down: JavaScript on, file and content access off, mixed content blocked, no JavaScript bridges, navigation limited to https hosts on an allow-list (checkout host, your return hosts, plus hosts you add), TLS errors fail closed, HTTP auth prompts cancelled, third-party cookies off. Because the SDK does not know your payment provider and identity hosts, a WebView journey dead-ends unless you add them.
Returning into your app
With the browser sheet, the browser must bring the shopper back to your app. Pick one of two options when you choose your merchantUrl values.
| Option | Example | Security |
|---|---|---|
| Universal Links / App Links (recommended) | https://shop.example/qasimha/return | Verified against your domain (apple-app-site-association, assetlinks.json). No other app can claim them. |
| Custom scheme | myshop://qasimha/return | Simpler, but not verified by the OS. Another app can register the same scheme and receive the return URL. |
What you set up on each platform:
| Platform | Custom scheme | App Links / Universal Links |
|---|---|---|
| Android | manifestPlaceholders["qasimhaReturnScheme"] = "myshop" (required by every SDK's manifest, even with App Links only) | <intent-filter android:autoVerify="true"> for your host and path, plus https://<host>/.well-known/assetlinks.json |
| iOS | CFBundleURLTypes in Info.plist | Associated Domains applinks:<host>, plus https://<host>/.well-known/apple-app-site-association |
On iOS, SFSafariViewController opens a Universal Link into your app only from a user tap. The hosted checkout shows a "Return to store" button on every final screen, so the shopper's tap brings the result back. Custom-scheme URLs and ASWebAuthenticationSession return without a tap.
The return URL only carries the order id, your reference and the status. It never carries a secret, but you still must not trust it without the server check.
Return URL rules (checked by the API and by each SDK before presenting):
- Absolute, at most 1024 characters, no fragment.
https;httponly onlocalhost/127.0.0.1for UAT development; or a custom app scheme of at least 3 characters. The Android SDK refuseshttpentirely.javascript,data,file,blobandvbscriptare refused. The Android SDK also refusesintent,content,android-appandabout.successandfailurego together.canceldefaults tofailureon the API, iOS and Flutter; the Android and React Native SDKs need all three.- Give the cancel URL its own path. Matching is a prefix on the path, so a return URL with an empty path matches every link on that host.
- Pass the app exactly the values your server sent when creating the session. The simplest way is to return them from your server with
paymentUrl.
Your server endpoint
The app never creates the session. Give it one endpoint that prices the basket, creates the session with the app's return URLs and returns everything the SDK needs, and a second that confirms the order. A Node.js version with @qasimha/sdk; every server SDK has the same calls:
import express from 'express';
import Qasimha from '@qasimha/sdk';
const app = express();
app.use(express.json({ limit: '64kb' }));
const qasimha = new Qasimha({ apiKey: process.env.QASIMHA_API_KEY }); // sk_test_... or sk_live_...
// The return URLs of the app build. Use https App Links / Universal Links in production.
const appMerchantUrl = {
success: 'https://shop.example/qasimha/return',
failure: 'https://shop.example/qasimha/return',
cancel: 'https://shop.example/qasimha/cancel',
};
app.post('/api/checkout', async (req, res) => {
const basket = priceBasket(req.body.items); // your prices, never the app's
if (!basket) return res.status(400).json({ error: 'INVALID_BASKET' });
const customer = await currentCustomer(req); // from your app's session
const orderReferenceId = await nextOrderReference(); // your own unique id
const address = { firstName: customer.firstName, lastName: customer.lastName, line1: customer.line1,
city: customer.city, countryCode: 'SA', phoneNumber: customer.phoneNumber };
const session = await qasimha.checkout.createSession({
orderReferenceId,
totalAmount: basket.total,
shippingAmount: basket.shipping,
items: basket.items, // { referenceId, name, type, sku, quantity, unitPrice, totalAmount }
consumer: { firstName: customer.firstName, lastName: customer.lastName,
phoneNumber: customer.phoneNumber, email: customer.email },
shippingAddress: address,
billingAddress: address,
merchantUrl: appMerchantUrl,
});
await saveOrder(orderReferenceId, session.orderId, customer.id);
res.status(201).json({ orderId: session.orderId, orderReferenceId,
paymentUrl: session.paymentUrl, merchantUrl: appMerchantUrl });
});
app.get('/api/orders/:id', async (req, res) => {
if (!(await orderBelongsTo(req.params.id, req))) return res.sendStatus(404);
const order = await qasimha.orders.get(req.params.id);
const paid = ['Confirmed', 'FullyFulfilled', 'PartiallyFulfilled'].includes(order.status);
res.json({ id: order.id, status: order.status, paid });
});priceBasket, currentCustomer, nextOrderReference, saveOrder and orderBelongsTo are your own code. Handle the webhook on the same server. The Web SDKs page has a version with error handling and the webhook route. If you serve web and app from one server, give the app its own return URLs: the demo merchant backend does this when the request carries "client": "app".
What the SDK never holds
- No secret key.
sk_test_.../sk_live_...keys stay on your server. The session is always created server-side. - Only a publishable key, and only for the optional promo view (
pk_test_.../pk_live_...), which can only read plan quotes fromGET /api/v1/widget/plans.pk_test_keys callhttps://api-uat.qasimha.com,pk_live_keys callhttps://api.qasimha.com. - No storage, no analytics, no device fingerprinting.
- No certificate pinning. TLS validation is the platform's, and there is no option to weaken it.
- No card data. Card entry happens on the payment provider's hosted page. Merchants using the SDKs stay in PCI DSS SAQ-A scope.
Application shielding (root/jailbreak detection, anti-tamper, runtime protection) is recommended for merchant apps that take payments. It is your app's decision; the SDKs do not perform it.
Promo view
Every SDK shows "Split into 4 monthly payments of [symbol] 300" (Arabic or English) from the first plan the API returns. The iOS, Flutter and React Native views add an "Interest-free · Sharia-compliant" line for Sharia-compliant plans. Amounts use the currency symbol to the left of the number. The request is GET /api/v1/widget/plans?amount=1200.00¤cy=SAR&lang=ar with the X-Public-Key header and a 5-second timeout.
The view fails closed: it is hidden, never showing a computed number, when the key is not pk_test_ / pk_live_, the amount is not positive with at most two decimals, the request fails, or the amount is outside your account's minimum and maximum. Place it on product, cart and checkout screens, not in every list row: each instance makes its own request.
Test in UAT
- Create
sk_test_andpk_test_keys and a webhook endpoint in the UAT Merchant Portal (https://portal-uat.qasimha.com). - Run your server against UAT with the
sk_test_key. ThepaymentUrlit receives already points at the UAT checkout; the app has no environment switch. - Build a UAT flavour of the app with its own return scheme or link host, so it never collides with your production app on a test device.
- On a real device, run each path: finish, decline, cancel on the hosted page, and close the sheet. Check the result and that the app waits for your server.
- Kill the app while the checkout is open, finish the payment, and check your app (or your server, through the webhook) still finds the order.
Go-live checklist
sk_live_...and the live webhook secret on the production server;pk_live_...in the production app build.- Production return URLs are
httpson your domain, withassetlinks.jsonandapple-app-site-associationserved from it. - The server sends the production app's return URLs, and the app passes them to the SDK unchanged.
- Every "paid" screen and every fulfilment waits for
orders.getor the webhook. - One real order placed end to end on each platform.
Pick your platform
Kotlin library with Custom Tabs, an opt-in WebView and App Links.
Swift package with SFSafariViewController, ASWebAuthenticationSession and Universal Links.
qasimha_checkout plugin for Android and iOS.
@qasimha/react-native-sdk module with Linking integration.
Related guides
- Checkout flows: creating the session on your server.
- Return URLs: the full return-URL contract.
- Webhooks and Webhook signatures: the server-side record of the outcome.
- Plans widget: the web version of the promo view.
- Authentication: UAT and production keys.
Updated about 20 hours ago
