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

  1. Your server creates the checkout session (POST /api/v1/pay/session/create) with a Qasimha server SDK and your secret key. It sends merchantUrl.success, merchantUrl.failure and merchantUrl.cancel, and returns the paymentUrl and those three URLs to the app.
  2. Your app starts the SDK with that paymentUrl and the same three return URLs.
  3. 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, orderReferenceId and status appended.
  4. The SDK detects that navigation (prefix match on scheme, host and path), closes the checkout and reports Success, Failure or Cancelled.
  5. Your app asks your server, and your server asks Qasimha (GET /api/v1/merchant/orders/{orderId}, orders.get in 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 proof

Anyone can type a return URL, and a custom scheme can be claimed by another app. Treat Success as "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

SDKPackageInstallMinimum platformPresentation modes
Androidcom.qasimha:checkout-androidimplementation("com.qasimha:checkout-android:0.1.0")minSdk 24, compileSdk 35, Kotlin, AndroidXCustom Tab (default), hardened WebView
iOSQasimhaCheckoutSwift Package Manager or pod 'QasimhaCheckout'iOS 14, Swift 5.9SFSafariViewController (default), ASWebAuthenticationSession, hardened WKWebView
Flutterqasimha_checkoutflutter pub add qasimha_checkoutFlutter 3.38 (Dart 3.10), Android API 24, iOS 15.0System browser (default), ASWebAuthenticationSession on iOS
React Native@qasimha/react-native-sdknpm install @qasimha/react-native-sdkReact Native 0.72, React 18 or 19, Android minSdk 24, iOS 13.4System browser (default), ASWebAuthenticationSession on iOS

The same calls on every platform:

TaskAndroidiOSFlutterReact Native
Start a checkoutQasimhaCheckoutContract or QasimhaCheckout.start(activity, options, callback)QasimhaCheckout.start(from:options:completion:)await QasimhaCheckout.start(options)await startCheckout(options)
Hand a return link overQasimhaCheckout.handleReturn(uri)QasimhaCheckout.handleReturn(url:)Automatic; QasimhaCheckout.handleReturn(uri) for your own routerhandleReturn(url) from Linking
OptionsCheckoutOptions, MerchantUrlCheckoutOptions, MerchantURLCheckoutOptions, MerchantUrl{ paymentUrl, merchantUrl, presentation?, language? }
PromoQasimhaPromoViewQasimhaPromoViewQasimhaPromo<QasimhaPromo>

Result types

All four SDKs read the same three query parameters from the return URL. The common cases:

Return URLResult
status=confirmed with an orderIdSuccess(orderId, orderReferenceId). Never produced without an orderId.
status=declined, expired or failedFailure(status, orderId, orderReferenceId)
status=cancelled, or the cancel URL with no statusCancelled
The shopper closed the sheet without returningCancelled

The edge cases differ slightly by platform. Each SDK page lists its own statuses:

Edge caseAndroidiOSFlutterReact Native
confirmed without an orderIdFailure("malformed").failure("unverified")CheckoutFailure("malformed")failure, status unknown
Success or failure URL with no statusFailure("unknown").failure("unverified")CheckoutFailure("unknown")failure, status unknown
Any other status valueFailure(status), lower-cased.failure(status), lower-casedCheckoutFailure(status)failure with that status
Could not presentFailure("no_browser"), Failure("ssl_error"), Failure("network_error").failure("invalid_options"), .failure("already_presenting")throws ArgumentError, StateError or CheckoutExceptionrejects 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

ModePlatformsWhen 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 onlyOnly 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.

OptionExampleSecurity
Universal Links / App Links (recommended)https://shop.example/qasimha/returnVerified against your domain (apple-app-site-association, assetlinks.json). No other app can claim them.
Custom schememyshop://qasimha/returnSimpler, 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:

PlatformCustom schemeApp Links / Universal Links
AndroidmanifestPlaceholders["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
iOSCFBundleURLTypes in Info.plistAssociated 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; http only on localhost / 127.0.0.1 for UAT development; or a custom app scheme of at least 3 characters. The Android SDK refuses http entirely.
  • javascript, data, file, blob and vbscript are refused. The Android SDK also refuses intent, content, android-app and about.
  • success and failure go together. cancel defaults to failure on 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 from GET /api/v1/widget/plans. pk_test_ keys call https://api-uat.qasimha.com, pk_live_ keys call https://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&currency=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

  1. Create sk_test_ and pk_test_ keys and a webhook endpoint in the UAT Merchant Portal (https://portal-uat.qasimha.com).
  2. Run your server against UAT with the sk_test_ key. The paymentUrl it receives already points at the UAT checkout; the app has no environment switch.
  3. 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.
  4. 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.
  5. 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 https on your domain, with assetlinks.json and apple-app-site-association served 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.get or the webhook.
  • One real order placed end to end on each platform.

Pick your platform


Related guides


Did this page help you?