How mobile checkout works
Your backend creates a checkout session with your secret key and returns its id and checkoutUrl to your app. Never put a secret key in an app: anyone can extract it from the app binary. Your app opens checkoutUrl in the system browser, SFSafariViewController on iOS or Chrome Custom Tabs on Android, and follows the checkout session status with your publishable key. Customers pay from their own wallet, and funds go directly to your settlement wallet.
Do not load checkout in an embedded WebView. An embedded WebView lets the host app read and change the checkout page, and wallet apps may not open from it. The system browser keeps checkout isolated from your app.
Fulfill orders from the signed invoice.paid webhook on your backend. The status your app reads is for your interface only and is not proof of payment.
Follow the checkout session status
Retrieve the checkout session with your publishable key every 3 seconds while your app is running, and once more when the customer returns to your app. A status of paid or overpaid means the invoice is paid. A status of expired, failed, or cancelled, or a collectionState of void or uncollectible, means checkout can no longer take a payment. A payment sent just before expiry can still confirm later, and the invoice.paid webhook reports it. A status of processing means a payment is confirming. A status of underpaid means part of the amount arrived; customerAmountRemaining shows what is left. Keep checking for any other status.
If the API returns HTTP 429, wait for the Retry-After interval before checking again. Treat network errors and 5xx responses as temporary and check again on the next interval.
curl https://api.payclave.com/v1/checkout-sessions/$CHECKOUT_SESSION_ID \
-H "Authorization: Bearer $PAYCLAVE_PUBLISHABLE_KEY"iOS with Swift
Present checkout in SFSafariViewController. Your app can close it as soon as the status is paid. The safariViewControllerDidFinish delegate method runs when the customer taps Done, so check the status once more there.
import SafariServices
let safari = SFSafariViewController(url: checkoutURL)
safari.delegate = self
present(safari, animated: true)
// Every 3 seconds while checkout is open:
var request = URLRequest(url: URL(string: "https://api.payclave.com/v1/checkout-sessions/\(sessionId)")!)
request.setValue("Bearer \(publishableKey)", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: request)
let session = try JSONDecoder().decode(CheckoutSessionResponse.self, from: data).data
if session.status == "paid" || session.status == "overpaid" {
safari.dismiss(animated: true)
}
struct CheckoutSessionResponse: Decodable {
struct Session: Decodable { let status: String; let collectionState: String }
let data: Session
}Android with Kotlin
Open checkout in a Chrome Custom Tab from the androidx.browser library. Your app cannot close a Custom Tab, so the tab stays open until the customer closes it. Check the status while the tab is open, and again in onResume when the customer returns to your activity.
import androidx.browser.customtabs.CustomTabsIntent
CustomTabsIntent.Builder().build()
.launchUrl(context, Uri.parse(checkoutUrl))
override fun onResume() {
super.onResume()
checkCheckoutStatus() // GET /v1/checkout-sessions/{id}
}Flutter
Open checkout with the url_launcher package in LaunchMode.inAppBrowserView, which uses the system browser view on each platform. Close it once the status is paid where the platform supports closing, and check the status again when the app lifecycle state returns to resumed.
import 'package:url_launcher/url_launcher.dart';
await launchUrl(Uri.parse(checkoutUrl), mode: LaunchMode.inAppBrowserView);
// When the status is paid or overpaid:
if (await supportsCloseForLaunchMode(LaunchMode.inAppBrowserView)) {
await closeInAppWebView();
}React Native
Install @payclave/sdk-react-native with expo-web-browser. openPayclaveCheckout looks the session up with your publishable key, opens checkout in SFSafariViewController on iOS or a Chrome Custom Tab on Android, and resolves paid, ended, or dismissed. On iOS it closes checkout as soon as the payment is confirmed. Android pauses JavaScript while the tab is in front, so the result comes from the check made when the customer returns. Bare React Native apps need Expo modules installed to use expo-web-browser.
npx expo install expo-web-browser
npm install @payclave/sdk-react-native@0.1.0
import { openPayclaveCheckout } from "@payclave/sdk-react-native"
const { outcome } = await openPayclaveCheckout({
publicKey: "pk_test_...",
checkoutSessionId,
})
// "paid", "ended", or "dismissed". Fulfill from the invoice.paid webhook.