Mobile
Checkout for apps
Open Vezmo's hosted checkout from your app in the phone's secure browser sheet. The buyer pays with any method you offer, and the checkout sends them straight back to your app with the result. No SDK to install, nothing to keep up to date, and card details never touch your app or your servers.
Use the secure browser sheet, not a WebView. On iOS that is ASWebAuthenticationSession; on Android, Chrome Custom Tabs. Banks, wallets and pay-later providers refuse to run inside an app's own WebView, and buyers can't tell a WebView from a fake page, so a WebView breaks payments and buyer trust.
How it works
- Your app asks your server for a checkout. Your server creates a session with
app.returnUrland returns the session'surlandclientToken. - Your app opens the
urlin the secure browser sheet. - The buyer pays. If they go to their bank app, a pay-later provider or 3D Secure, they come back to the same sheet.
- The checkout opens your return link:
acme://vezmo?paymentId=…&status=success. The sheet closes and your app shows the result. - Your server fulfils the order from the
payment.successwebhook.
Before you start
Add the app under Developers → Mobile apps with its return link (for example acme://vezmo), and make your app open that link: a URL scheme in your iOS target's Info.plist, and an intent filter on Android. See Take payments in your mobile app.
1. Create the session on your server
The same request as any API checkout (see Hosted Checkout), plus app.returnUrl. A return link that isn't registered for your business is refused with a 400.
// Your server, with your API key (never in the app).
const res = await fetch("https://api.vezmo.com/api/v1/merchant/secure-payments", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Idempotency-Key": order.id,
},
body: JSON.stringify({
title: `Order #${order.number}`,
amount: order.total, // 49.00
currency: "USD",
client: { name: buyer.name, email: buyer.email, country: "US", postalCode: "10001" },
// Registered under Developers → Mobile apps (a path or query under it is fine too).
app: { returnUrl: "acme://vezmo" },
}),
});
const { data } = await res.json();
// Send these two to your app:
// data.securePayment.url the checkout page
// data.securePayment.clientToken for GET …/status2. Open it from your app
React Native (Expo)
import * as WebBrowser from "expo-web-browser";
export async function payOrder(checkoutUrl: string) {
// The phone's secure browser sheet, closed for you when the checkout opens acme://vezmo.
const result = await WebBrowser.openAuthSessionAsync(checkoutUrl, "acme://vezmo");
if (result.type === "success") {
const url = new URL(result.url);
const status = url.searchParams.get("status"); // "success" | "pending" | "failed"
const paymentId = url.searchParams.get("paymentId");
return { status, paymentId };
}
// The buyer closed the sheet without finishing: ask the session status.
return { status: "closed" };
}In a bare React Native app, any library that opens ASWebAuthenticationSession and Custom Tabs works the same way.
iOS (Swift)
import AuthenticationServices
import UIKit
final class WebCheckout: NSObject, ASWebAuthenticationPresentationContextProviding {
private var session: ASWebAuthenticationSession?
func open(_ checkoutURL: URL, completion: @escaping (_ status: String?, _ paymentId: String?) -> Void) {
// The checkout ends on acme://vezmo?status=…&paymentId=…, which closes the sheet.
let session = ASWebAuthenticationSession(url: checkoutURL, callbackURLScheme: "acme") { url, _ in
let items = url.flatMap { URLComponents(url: $0, resolvingAgainstBaseURL: false)?.queryItems }
completion(
items?.first { $0.name == "status" }?.value, // "success" | "pending" | "failed"; nil = closed
items?.first { $0.name == "paymentId" }?.value
)
}
session.presentationContextProvider = self
// Keep the browser's cookies: some bank and pay-later pages need them.
session.prefersEphemeralWebBrowserSession = false
self.session = session
session.start()
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
UIApplication.shared.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.first ?? ASPresentationAnchor()
}
}Android (Kotlin)
// build.gradle.kts: implementation("androidx.browser:browser:1.8.0")
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
fun openCheckout(context: android.content.Context, checkoutUrl: String) {
CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(checkoutUrl))
}<!-- AndroidManifest.xml: the activity that receives acme://vezmo -->
<activity
android:name=".CheckoutReturnActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="acme" android:host="vezmo" />
</intent-filter>
</activity>class CheckoutReturnActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handle(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handle(intent)
}
private fun handle(intent: Intent) {
val link = intent.data ?: return
val status = link.getQueryParameter("status") // "success" | "pending" | "failed"
val paymentId = link.getQueryParameter("paymentId")
// Show the result, then refresh the order from your server (fulfilled by the webhook).
}
}Flutter
// pubspec.yaml: flutter_web_auth_2
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
Future<Map<String, String?>> payOrder(String checkoutUrl) async {
try {
final result = await FlutterWebAuth2.authenticate(
url: checkoutUrl,
callbackUrlScheme: 'acme', // the sheet closes when the checkout opens acme://vezmo
);
final link = Uri.parse(result);
return {
'status': link.queryParameters['status'], // success | pending | failed
'paymentId': link.queryParameters['paymentId'],
};
} catch (_) {
// The buyer closed the sheet without finishing: ask the session status.
return {'status': 'closed'};
}
}On Android, flutter_web_auth_2 needs its CallbackActivity with your scheme (acme) in AndroidManifest.xml; see the package's setup.
3. Show the result, fulfil from the webhook
status=success: paid. Show the confirmation.status=pending: paid, money on its way (a bank debit). Show “payment received”; the webhook tells your server when it settles.status=failed: not paid. Let the buyer try again: the same session works until it expires, or create a new one.- Sheet closed without a link: the buyer left. Ask
GET https://api.vezmo.com/api/v1/secure-payments/{clientToken}/status(see Know the result): they may have paid in their bank app before closing it.
Ship orders only from the webhook (payment.success), never from the return link alone.