Mobile apps
Accept payments in your Android or iOS app through your own backend and the Ginger Payments hosted payment page.
Your app never talks to the Ginger Payments 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
neworprocessing, and check again later. - Let your customer try again when the status is
cancelled,expiredorerror.
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.