Accept payments
Mobile and app integrations
iOS, Android, Flutter and React Native: open the checkout in a secure in-app browser and return with a deep link.
A mobile app takes payments the same way a website does: your server creates the payment, and the app opens the checkout_url in a secure browser and comes back through a deep link. The app never talks to the Raïs API and never holds a key.
The flow
- The app asks your server to start a payment for an order or a cart.
- Your server computes the amount, creates the payment with Raïs (see Payments) using your secret key, stores the payment id with the order, and returns the
checkout_urlto the app. - The app opens
checkout_urlin a system browser session. The payer enters their card and confirms with 3-D Secure. - Raïs sends the payer to your
success_urlorcancel_url, which is a deep link into your app, withpayment_idandstatusadded. - The app asks your server for the real status of the order. Your server knows the answer from the signed webhook or by retrieving the payment.
Return with a deep link
Set success_url and cancel_url to an address on your own app scheme. Raïs accepts any custom scheme in both environments and keeps it exactly as you wrote it, adding the result parameters:
myapp://pay/result?payment_id=5b1c2f4e-9d3a-4c61-8a7e-0f2d6b9e1a33&status=paid&reference=order-1042&order_number=RS12AB34CDcurl https://api.rais.app/v1/payments \
-H "Authorization: Bearer rais_sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 500000,
"reference": "order-1042",
"success_url": "myapp://pay/result",
"cancel_url": "myapp://pay/cancelled",
"language": "fr"
}'After a successful payment the result page stays on screen, so the payer can read the details, and its Return to button opens the deep link. The page does not redirect by itself. If you prefer https links that open your app (universal links on iOS, app links on Android), use an https address that your site redirects to the app.
Open the checkout
Use the secure browser of the platform, not a plain WebView. It shares the system's security features, shows the address to the payer and handles 3-D Secure pages the way banks expect.
Use ASWebAuthenticationSession. It opens a secure sheet and returns to your app when the browser navigates to your scheme.
import AuthenticationServices
final class CheckoutPresenter: NSObject, ASWebAuthenticationPresentationContextProviding {
private var session: ASWebAuthenticationSession?
/// checkoutURL comes from YOUR server, which created the payment.
func pay(checkoutURL: URL, completion: @escaping (_ paymentId: String?) -> Void) {
let session = ASWebAuthenticationSession(url: checkoutURL, callbackURLScheme: "myapp") { callbackURL, _ in
guard let callbackURL,
let items = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems else {
completion(nil) // the payer closed the sheet
return
}
completion(items.first { $0.name == "payment_id" }?.value)
}
session.presentationContextProvider = self
session.start()
self.session = session
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
UIApplication.shared.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.first ?? ASPresentationAnchor()
}
}The server side of the app
Your server owns the amount, the keys and the confirmation. A minimal version has two endpoints: one that starts the payment and one that tells the app the status of an order.
// POST /api/checkout (authenticated by your own login)
const order = await orders.createFor(req.user, req.body.cartId); // the amount is computed here
const response = await fetch("https://api.rais.app/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RAIS_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `order-${order.id}`,
},
body: JSON.stringify({
amount: order.amount,
reference: `order-${order.id}`,
success_url: "myapp://pay/result",
cancel_url: "myapp://pay/cancelled",
}),
});
const payment = await response.json();
await orders.attachPayment(order.id, payment.id);
res.json({ checkoutUrl: payment.checkout_url });
// GET /api/orders/:id/status (what the app asks after the deep link or when it comes back)
// Answer from your database, which the signed webhook updates. If the order is still pending, retrieve the payment.Handle every ending
- Success deep link. Treat
status=paidas a hint to refresh. Ask your server; show the order as paid only when it says so. - Cancel deep link. The payer pressed Back to, or the attempt did not go through. The payment may still be
pending: let them try again by reopening the samecheckout_urluntilexpires_at, and create a new payment when it is no longerpending. - The payer closes the browser. You get no deep link. When the app comes back to the foreground, ask your server for the status of the order: the payment may have succeeded.
- The app is killed during payment. The webhook still reaches your server. When the app restarts, show the current status of the order.
Checklist
- No Raïs key, and no key-like string, anywhere in the app.
- The amount is computed on your server, never sent by the app.
- The deep link carries no authority: the order is confirmed by your server, not by
?status=paid. - The app handles a closed browser and a killed process.
- You tested with sandbox keys and the test cards. The sandbox accepts
http://return addresses for local servers; live payments needhttps://or your app scheme.