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.
| Platform | Package | Main entry points |
|---|---|---|
| iOS (Swift) | SoamtarSDK (Swift Package) 1.0.1 | SoamtarCheckoutView, SoamtarCheckoutViewController |
| Android (Kotlin) | com.soamtar:soamtar-android:1.1.0 | Soamtar.checkoutIntent(), SoamtarCheckoutActivity |
| Flutter | soamtar_sdk 1.0.1 | SoamtarCheckout 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
- The app sends the order to your backend.
- Your backend creates the payment with its secret key (
POST /v1/payments), with yoursuccess_urlandfailed_url, and returnsdata.payment_url(https://app.so-amtar.com/pay/checkout/{id}) to the app. - The app opens
payment_urlwith the SDK. The payer enters their Orange Money number (+232followed by 8 digits) and approves the request on their phone with their Orange Money PIN. The PIN never goes through your app. - The hosted result page redirects to your
success_urlorfailed_url. The SDK stops on that navigation and calls you back. - Your backend confirms the final status (webhook
payment.success, orGET /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)
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:
.package(url: "https://github.com/soamtar/soamtar-ios-sdk", from: "1.0.1")Configure (optional)
import SoamtarSDK
Soamtar.shared.configure(publicKey: "pk_test_your_public_key")
// Anything other than pk_test_ / pk_live_ stops the app (fatalError).SwiftUI
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
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
| Symbol | Description |
|---|---|
Soamtar.shared.configure(publicKey:baseURL:) | Stores the public key. baseURL defaults to https://app.so-amtar.com. |
Soamtar.validatePublicKey(_:) throws | Throws SoamtarError.invalidPublicKey unless the key starts with pk_test_ / pk_live_. |
Soamtar.shared.isCheckoutURL(_:) -> Bool | true 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
dependencies {
implementation 'com.soamtar:soamtar-android:1.1.0'
}Initialize
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)
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:
Soamtar.checkout(this, paymentUrl, REQUEST_PAY, successUrl, failedUrl)Result statuses
| Constant | Meaning |
|---|---|
SoamtarCheckoutActivity.STATUS_SUCCESS | The hosted page redirected to success_url |
SoamtarCheckoutActivity.STATUS_FAILED | The hosted page redirected to failed_url (failed, expired or cancelled) |
SoamtarCheckoutActivity.STATUS_CANCELLED | The 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
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
dependencies:
soamtar_sdk:
git:
url: https://github.com/soamtar/soamtar_flutter_sdk
ref: v1.0.1flutter pub getAndroid: add <uses-permission android:name="android.permission.INTERNET"/> to android/app/src/main/AndroidManifest.xml. iOS: nothing to configure.
Usage
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:
SoamtarMoney.format('1250.00'); // "Le 1,250.00"
SoamtarMoney.formatMinorUnits(BigInt.from(15000)); // "Le 150.00"| Symbol | Notes |
|---|---|
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 / formatMinorUnits | Decimal string or integer cents → "Le 1,250.00" (never double). |
React Native
Installation
npm install @soamtar/react-native react-native-webviewUsage
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)}
/>
);
}| Prop | Description |
|---|---|
paymentUrl (required) | payment_url from POST /v1/payments. Must be https. |
successUrl | The success_url you sent. Reaching it (same scheme, host with port, and path; query ignored) calls onSuccess(url) once. |
failureUrl | The 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. |
loaderColor | Spinner 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/WebViewover 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.