Skip to content

Mobile SDKs (public keys only) ​

The mobile SDKs show the SOAMTAR hosted checkout inside your app and tell you how it ended. They never create payments and never hold a secret key: they accept public keys only (pk_test_… / pk_live_…) and refuse sk_… keys.

PlatformPackageMain entry points
iOS (Swift)SoamtarSDK (Swift Package) 1.0.1SoamtarCheckoutView, SoamtarCheckoutViewController
Android (Kotlin)com.soamtar:soamtar-android:1.1.0Soamtar.checkoutIntent(), SoamtarCheckoutActivity
Fluttersoamtar_sdk 1.0.1SoamtarCheckout widget
React Native@soamtar/react-native 1.1.0<Checkout /> component

Installation

None of these packages is published on a public registry yet: install them from the SOAMTAR repository until they are.

How it works ​

  1. The app sends the order to your backend.
  2. Your backend creates the payment with its secret key (POST /v1/payments), with your success_url and failed_url, and returns data.payment_url (https://app.so-amtar.com/pay/checkout/{id}) to the app.
  3. The app opens payment_url with the SDK. The payer enters their Orange Money number (+232 followed by 8 digits) and approves the request on their phone with their Orange Money PIN. The PIN never goes through your app.
  4. The hosted result page redirects to your success_url or failed_url. The SDK stops on that navigation and calls you back.
  5. Your backend confirms the final status (webhook payment.success, or GET /v1/payments/{id}) before you deliver the order.

The SDK callback is not a proof of payment

onSuccess / STATUS_SUCCESS only means the WebView reached your success_url. Only a verified webhook or an API read with your secret key settles an order.

Backend request (example) ​

bash
curl -X POST https://app.so-amtar.com/v1/payments \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0c6f3a52-6a0e-4f43-9a0e-2a7f4b1d9e11" \
  -d '{
    "merchant_ref": "ORDER-42",
    "amount": "150.00",
    "currency": "SLE",
    "provider": "orange_money_sl",
    "success_url": "https://shop.example/orders/42/paid",
    "failed_url": "https://shop.example/orders/42/failed"
  }'

Send payment_url to the app together with the same success_url / failed_url: the SDKs need them to detect the end of the checkout. With sk_test_ keys the payment goes through simulation on the same host.

Use two different paths for success_url and failed_url

All four SDKs match a navigation against your return URLs exactly on scheme, host and path. The query string and fragment are ignored, and it is never a prefix match. failed_url is checked first: if both URLs share the same path (for example https://shop.example/return?status=ok and https://shop.example/return?status=ko), every payment is reported as a failure. Use distinct paths, such as /orders/42/paid and /orders/42/failed.

iOS (Swift) ​

Requirements: iOS 14+, Swift 5.7+, Xcode 14+.

Installation (Swift Package Manager) ​

In Xcode: File → Add Package Dependencies…, URL https://github.com/soamtar/soamtar-ios-sdk, product SoamtarSDK. Or in Package.swift:

swift
.package(url: "https://github.com/soamtar/soamtar-ios-sdk", from: "1.0.1")

Configure (optional) ​

swift
import SoamtarSDK

Soamtar.shared.configure(publicKey: "pk_test_your_public_key")
// Anything other than pk_test_ / pk_live_ stops the app (fatalError).

SwiftUI ​

swift
import SwiftUI
import SoamtarSDK

struct PayButton: View {
    let paymentUrl: URL                 // from your backend
    @State private var showCheckout = false

    private let returnURLs = SoamtarReturnURLs(
        success: URL(string: "https://shop.example/orders/42/paid")!,
        failure: URL(string: "https://shop.example/orders/42/failed")!
    )

    var body: some View {
        Button("Pay \(SoamtarMoney.format("150.00") ?? "")") { showCheckout = true }  // "Pay Le 150.00"
            .sheet(isPresented: $showCheckout) {
                SoamtarCheckoutView(
                    paymentUrl: paymentUrl,
                    returnURLs: returnURLs,
                    onSuccess: { _ in showCheckout = false /* then confirm with your backend */ },
                    onFailure: { _ in showCheckout = false }
                )
            }
    }
}

UIKit ​

swift
let checkout = SoamtarCheckoutViewController(
    paymentUrl: paymentUrl,
    returnURLs: returnURLs,
    onSuccess: { [weak self] _ in self?.dismiss(animated: true) },
    onFailure: { [weak self] _ in self?.dismiss(animated: true) }
)
present(checkout, animated: true)

API ​

SymbolDescription
Soamtar.shared.configure(publicKey:baseURL:)Stores the public key. baseURL defaults to https://app.so-amtar.com.
Soamtar.validatePublicKey(_:) throwsThrows SoamtarError.invalidPublicKey unless the key starts with pk_test_ / pk_live_.
Soamtar.shared.isCheckoutURL(_:) -> Booltrue for an HTTPS …/pay/checkout/{id} page of the configured host.
SoamtarReturnURLs(success:failure:)Your success_url / failed_url; outcome(for:) matches scheme, host and path exactly (query ignored, failed_url checked first).
SoamtarCheckoutView(paymentUrl:returnURLs:onSuccess:onFailure:)SwiftUI checkout (WKWebView). Each callback receives the matched URL.
SoamtarCheckoutViewController(…)UIKit checkout, same parameters.
SoamtarMoney.format(_:currency:)Decimal or decimal string → "Le 1,250.00".

Android (Kotlin) ​

Requirements: Android API 21+, Kotlin 1.8+. The library manifest declares the INTERNET permission and the checkout activity.

Installation ​

groovy
dependencies {
    implementation 'com.soamtar:soamtar-android:1.1.0'
}

Initialize ​

kotlin
import com.soamtar.sdk.Soamtar

Soamtar.init("pk_test_your_public_key")
// Anything other than pk_test_… / pk_live_… throws IllegalArgumentException.

Open the checkout (Activity Result API) ​

kotlin
import com.soamtar.sdk.ui.SoamtarCheckoutActivity

private val checkoutLauncher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    when (result.data?.getStringExtra(SoamtarCheckoutActivity.RESULT_STATUS)) {
        SoamtarCheckoutActivity.STATUS_SUCCESS -> showPending("Confirming your payment…")
        SoamtarCheckoutActivity.STATUS_FAILED -> showError("Payment not completed.")
        else -> showPending("Checking your payment…") // closed by the payer
    }
    refreshOrderFromBackend() // the backend status is the source of truth
}

// paymentUrl, successUrl and failedUrl come from your backend
checkoutLauncher.launch(
    Soamtar.checkoutIntent(this, paymentUrl, successUrl = successUrl, failedUrl = failedUrl)
)

Legacy onActivityResult:

kotlin
Soamtar.checkout(this, paymentUrl, REQUEST_PAY, successUrl, failedUrl)

Result statuses ​

ConstantMeaning
SoamtarCheckoutActivity.STATUS_SUCCESSThe hosted page redirected to success_url
SoamtarCheckoutActivity.STATUS_FAILEDThe hosted page redirected to failed_url (failed, expired or cancelled)
SoamtarCheckoutActivity.STATUS_CANCELLEDThe payer closed the checkout before any redirect

payment_url must be HTTPS on the SOAMTAR host (app.so-amtar.com). The checkout stops when a navigation has the same scheme, host and path as failedUrl or successUrl (query and fragment ignored, never a prefix match); failedUrl is checked first. Without successUrl / failedUrl, the payer stays on the SOAMTAR result page and the activity returns STATUS_CANCELLED when they close it.

Display an amount ​

kotlin
SoamtarMoney.formatLe(BigDecimal("1250"))   // "Le 1,250.00"
SoamtarMoney.formatLe("150.5")              // "Le 150.50" (null if not a decimal)
Soamtar.formatLe("2000000.00")              // "Le 2,000,000.00"

Flutter ​

Requirements: Dart 3.0+, Flutter 3.10+, webview_flutter 4.0+.

Installation ​

yaml
dependencies:
  soamtar_sdk:
    git:
      url: https://github.com/soamtar/soamtar_flutter_sdk
      ref: v1.0.1
bash
flutter pub get

Android: add <uses-permission android:name="android.permission.INTERNET"/> to android/app/src/main/AndroidManifest.xml. iOS: nothing to configure.

Usage ​

dart
import 'package:flutter/material.dart';
import 'package:soamtar_sdk/soamtar_sdk.dart';

class PaymentPage extends StatelessWidget {
  const PaymentPage({super.key, required this.paymentUrl});

  /// `payment_url` received from your backend.
  final String paymentUrl;

  @override
  Widget build(BuildContext context) {
    final client = SoamtarClient('pk_test_your_public_key'); // ArgumentError on sk_
    if (!client.isCheckoutUrl(paymentUrl)) {
      return const Center(child: Text('Invalid payment link.'));
    }

    return Scaffold(
      appBar: AppBar(title: const Text('Pay with Orange Money')),
      body: SoamtarCheckout(
        paymentUrl: Uri.parse(paymentUrl),
        returnUrls: SoamtarReturnUrls(
          success: Uri.parse('https://shop.example/orders/42/paid'),
          failure: Uri.parse('https://shop.example/orders/42/failed'),
        ),
        onSuccess: (_) => Navigator.pop(context, true),  // then confirm with your backend
        onFailure: (_) => Navigator.pop(context, false),
      ),
    );
  }
}

Display an amount:

dart
SoamtarMoney.format('1250.00');                    // "Le 1,250.00"
SoamtarMoney.formatMinorUnits(BigInt.from(15000)); // "Le 150.00"
SymbolNotes
SoamtarClient(String publicKey, {Uri? baseUrl})pk_test_ / pk_live_ only. Default host https://app.so-amtar.com. isCheckoutUrl(url) checks HTTPS, host and /pay/checkout/….
SoamtarCheckout({paymentUrl, returnUrls, onSuccess, onFailure})WebView widget; fires one callback, once.
SoamtarReturnUrls({success, failure})resultFor(uri) → SoamtarCheckoutResult.success / .failure / null (exact scheme, host and path; failure checked first).
SoamtarMoney.format / formatMinorUnitsDecimal string or integer cents → "Le 1,250.00" (never double).

React Native ​

Installation ​

bash
npm install @soamtar/react-native react-native-webview

Usage ​

jsx
import { Client, Checkout } from '@soamtar/react-native';

const soamtar = new Client('pk_test_...'); // throws unless pk_test_… / pk_live_…

export function PayScreen({ navigation, route }) {
  // payment_url returned by your backend (must be https)
  const paymentUrl = soamtar.getCheckoutUrl(route.params.paymentUrl);

  return (
    <Checkout
      paymentUrl={paymentUrl}
      successUrl="https://shop.example/orders/42/paid"
      failureUrl="https://shop.example/orders/42/failed"
      onSuccess={() => navigation.replace('OrderStatus', { orderId: route.params.orderId })}
      onFailure={() => navigation.replace('PaymentFailed')}
      onError={(message) => console.warn(message)}
    />
  );
}
PropDescription
paymentUrl (required)payment_url from POST /v1/payments. Must be https.
successUrlThe success_url you sent. Reaching it (same scheme, host with port, and path; query ignored) calls onSuccess(url) once.
failureUrlThe failed_url you sent. Checked before successUrl. Reaching it calls onFailure(url) once.
onResult(url)The SOAMTAR result page (/pay/result/{id}) was reached: the payment was submitted and is being confirmed.
onError(message)Invalid URL, blocked navigation or WebView load error.
loaderColorSpinner colour (default #FF7900).

Client also exposes isTestMode (true for pk_test_ keys). Inside the checkout, https pages load, http:, javascript: and data: URLs are blocked, and other schemes (such as tel:) are handed to the operating system.

Mobile security ​

  • Public keys only: every SDK accepts only pk_test_… / pk_live_… keys and refuses secret keys (sk_…), which could be extracted from the APK/IPA.
  • Isolated web view: the payment page runs in a WKWebView / WebView over HTTPS only. The payer confirms with their Orange Money PIN on their own phone; the PIN never goes through your app.
  • Nothing stored: the SDKs keep no data on the device.
  • Server-side confirmation: the app is only a display layer for a transaction created and confirmed on your server.

SOAMTAR Payment API Documentation.