Vezmo App

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

  1. Your app asks your server for a checkout. Your server creates a session with app.returnUrl and returns the session's url and clientToken.
  2. Your app opens the url in the secure browser sheet.
  3. The buyer pays. If they go to their bank app, a pay-later provider or 3D Secure, they come back to the same sheet.
  4. The checkout opens your return link: acme://vezmo?paymentId=…&status=success. The sheet closes and your app shows the result.
  5. Your server fulfils the order from the payment.success webhook.

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.

Node.js
// 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 …/status

2. Open it from your app

React Native (Expo)

React Native · expo-web-browser
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)

Swift · AuthenticationServices
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)

Kotlin · Custom Tabs
// 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
<!-- 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>
Kotlin · the return
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

Dart · flutter_web_auth_2
// 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.