Paytrim

Mobile apps

Accept payments in your Android or iOS app through your own backend and the Paytrim hosted payment page.

Your app never talks to the Paytrim API directly. It talks to your own backend, and your backend talks to the API. Your customer pays on the hosted payment page, which your app opens in a secure browser view.

Never put your API key in your app

Anyone can extract an API key from an app package. With your key, they can create orders, issue refunds and read your payment data. Keep the key on your backend.

How it works

Your app asks your backend for a payment

Your app sends what your customer buys, such as a cart ID, to your backend. Don't let your app send the amount: your backend determines it, so nobody can change the price.

Your backend creates an order

Your backend creates an order with the API and returns the order_url to your app. Set the return_url to a link that opens your app, such as https://www.example.com/app/order/EXAMPLE001.

Your app opens the payment page

Your app opens the order_url in a Custom Tab on Android or an ASWebAuthenticationSession on iOS. Don't use a plain WebView: banks, wallets and 3-D Secure may need to open their own app or page, which WebViews often block.

Your customer returns to your app

After the payment, the browser view goes to your return_url. An Android App Link or iOS universal link sends your customer back into your app.

Your backend confirms the status

Your app asks your backend for the result. Your backend retrieves the order from the API, or uses the status it stored from the webhook. Only show a confirmation when the order status is completed.

Android

Ask your backend for an order_url and open it in a Custom Tab. The example uses AndroidX Browser and assumes your backend has a POST /payments endpoint that returns {"orderId": "...", "orderUrl": "..."}.

import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL

// Calls YOUR backend, which creates the order with its API key.
suspend fun startPayment(context: Context, cartId: String) {
    val payment = withContext(Dispatchers.IO) {
        val connection = URL("https://www.example.com/payments").openConnection() as HttpURLConnection
        connection.requestMethod = "POST"
        connection.setRequestProperty("Content-Type", "application/json")
        connection.doOutput = true
        connection.outputStream.use { it.write(JSONObject().put("cartId", cartId).toString().toByteArray()) }
        check(connection.responseCode == 200) { "Payment request failed: ${connection.responseCode}" }
        JSONObject(connection.inputStream.bufferedReader().use { it.readText() })
    }

    CustomTabsIntent.Builder().build()
        .launchUrl(context, Uri.parse(payment.getString("orderUrl")))
}

Register your return_url as a verified App Link in your AndroidManifest.xml. In the activity that handles the link, ask your backend for the order status.

iOS and iPadOS

Ask your backend for an order_url and open it in an ASWebAuthenticationSession. The example assumes the same backend endpoint as the Android example and a return_url on the universal link https://www.example.com/app/....

import AuthenticationServices

struct Payment: Decodable {
    let orderId: String
    let orderUrl: URL
}

// Calls YOUR backend, which creates the order with its API key.
func createPayment(cartId: String) async throws -> Payment {
    var request = URLRequest(url: URL(string: "https://www.example.com/payments")!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(["cartId": cartId])

    let (data, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    return try JSONDecoder().decode(Payment.self, from: data)
}

@MainActor
func startPayment(cartId: String, presenter: ASWebAuthenticationPresentationContextProviding) async throws {
    let payment = try await createPayment(cartId: cartId)

    let session = ASWebAuthenticationSession(
        url: payment.orderUrl,
        callback: .https(host: "www.example.com", path: "/app")
    ) { _, _ in
        // The browser view is closed: ask your backend for the order status.
    }
    session.presentationContextProvider = presenter
    session.start()
}

ASWebAuthenticationSession with an HTTPS callback needs iOS 17.4 or newer and an associated domain for www.example.com. On older versions, open the order_url in an SFSafariViewController and handle the universal link in your app delegate.

Check the status

Whatever happens in the app, the result of the payment comes from your backend:

  • Your customer may close the browser view before they're redirected, or your app may be killed. When your app opens again, ask your backend for the status of open payments.
  • Show a "payment pending" screen while the order status is new or processing, and check again later.
  • Let your customer try again when the status is cancelled, expired or error.

See order status for all statuses.

Apple Pay and Google Pay

The hosted payment page shows Apple Pay and Google Pay when they're active in your project and the browser view supports them. To show the native Apple Pay or Google Pay sheet in your app instead, see Apple Pay and Google Pay.

On this page