Checking out without a web form
Typing shipping details into a web form is error-prone and often makes users abandon a purchase. The Payment Request API offers a better path: hand that collection job to an Android payment app. When the app supports delegation, the browser skips its own address and contact forms entirely, so the shopper enters everything once, inside the payment app's own UI.
That approach yields addresses in a standardized structure, fewer typos, and a faster tap-through experience. The merchant's page can still react to the chosen address and shipping option by updating totals and delivery choices in real time.
What the payment app must declare
The browser only delegates information your app explicitly supports. Add a <meta-data> element to AndroidManifest.xml, pointing android:resource at a <string-array> resource.
<activity
android:name=".PaymentActivity"
…
<meta-data
android:name="org.chromium.payment_supported_delegations"
android:resource="@array/chromium_payment_supported_delegations" />
</activity>
The resource array may hold any subset of these values, each matching a piece of data the app can collect:
payerNamepayerEmailpayerPhoneshippingAddress
For example, an app that only collects an email and a shipping destination would declare:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="chromium_payment_supported_delegations">
<item>payerEmail</item>
<item>shippingAddress</item>
</string-array>
</resources>
Reading what the merchant requests
Merchants state their data needs via the paymentOptions dictionary in the Payment Request. When your app declares support for any of those options, Chrome passes that subset along to your PAY activity as an Intent extra, so the app knows exactly what fields to show.
val paymentOptions: Bundle? = extras.getBundle("paymentOptions")
val requestPayerName: Boolean? = paymentOptions?.getBoolean("requestPayerName")
val requestPayerPhone: Boolean? = paymentOptions?.getBoolean("requestPayerPhone")
val requestPayerEmail: Boolean? = paymentOptions?.getBoolean("requestPayerEmail")
val requestShipping: Boolean? = paymentOptions?.getBoolean("requestShipping")
val shippingType: String? = paymentOptions?.getString("shippingType")
The paymentOptions extra can carry these booleans and a string:
requestPayerName— the payer's name is requiredrequestPayerPhone— the payer's phone is requiredrequestPayerEmail— the payer's email is requiredrequestShipping— shipping details are requiredshippingType— one of"shipping","delivery", or"pickup", useful as a UI hint inside the app
When shipping is required, Chrome also sends shippingOptions, a parcelable array of the merchant's available delivery methods.
val shippingOptions: List<ShippingOption>? =
extras.getParcelableArray("shippingOptions")?.mapNotNull {
p -> from(p as Bundle)
}
Each option in that array is a Bundle with these keys:
id— identifier stringlabel— display labelamount— a nested bundle holdingcurrency(ISO4217 code) andvalue(valid decimal monetary value)selected— boolean; true when this is the option the app should preselect
To preselect an option, iterate the array and compare that flag:
val id: String = bundle.getString("id")
val label: String = bundle.getString("label")
val amount: Bundle = bundle.getBundle("amount")
val selected: Boolean = bundle.getBoolean("selected", false)
Building the payment response
The PAY activity's result Intent must include all requested information as extras. Required fields and their formats:
payerName,payerPhone,payerEmail— non-empty strings, present only when the matching request boolean was trueshippingAddress— a non-empty bundle with the physical address keyscountryCode,postalCode,sortingCode,region,city,dependentLocality,organization,recipient,phone, andaddressLine. All are strings exceptaddressLine, which is an array of strings.shippingOptionId— the identifier of the user's chosen shipping method as a non-empty string
Chrome validates a RESULT_OK response against the requested extras. A missing or malformed field causes request.show() to reject with one of these errors:
'Payment app returned invalid response. Missing field "payerEmail".'
'Payment app returned invalid response. Missing field "payerName".'
'Payment app returned invalid response. Missing field "payerPhone".'
'Payment app returned invalid shipping address in response.'
'... is not a valid CLDR country code, should be 2 upper case letters [A-Z].'
'Payment app returned invalid response. Missing field "shipping option".'
The following response includes both contact fields and a full shipping block:
fun Intent.populateRequestedPaymentOptions() {
if (requestPayerName) {
putExtra("payerName", "John Smith")
}
if (requestPayerPhone) {
putExtra("payerPhone", "5555555555")
}
if (requestPayerEmail) {
putExtra("payerEmail", "[email protected]")
}
if (requestShipping) {
val address: Bundle = Bundle()
address.putString("countryCode", "CA")
val addressLines: Array<String> =
arrayOf<String>("111 Richmond st. West")
address.putStringArray("addressLines", addressLines)
address.putString("region", "Ontario")
address.putString("city", "Toronto")
address.putString("postalCode", "M5H2G4")
address.putString("recipient", "John Smith")
address.putString("phone", "5555555555")
putExtra("shippingAddress", address)
putExtra("shippingOptionId", "standard")
}
}
Handling live updates from the merchant
Delegation fits into the dynamic checkout flow. When the user changes the shipping address, shipping option, or payment method inside your app, the app can notify the merchant with a change request and receive a fresh payment details response from Chrome. That updated response can bundle information like the corrected total or a new set of shipping options, keeping the app's UI in sync with the merchant's backend without leaving the payment flow.
Handling dynamic transaction flows
Shipping and payment details aren't always static. Selecting express shipping or an international address can change the total cost, available shipping options, or their prices. Payment apps that support these dynamic flows need a way to notify the merchant about the user's selections and display the updated payment details returned by the merchant.
Setting up the update service
To support notifications about changes to the user's payment method, shipping address, or shipping option, implement the IPaymentDetailsUpdateServiceCallback interface and declare it in AndroidManifest.xml with an UPDATE_PAYMENT_DETAILS intent filter. When the payment app invokes the PAY intent, Chrome attempts to connect to this service within the same package and provides the endpoint for sending updates via setPaymentDetailsUpdateService(service).
When receiving inter-process communication (IPC), validate that the calling app's package matches the one that invoked the PAY intent using packageManager.getPackagesForUid(Binder.getCallingUid()).
Create the AIDL interface files:
org/chromium/components/payments/IPaymentDetailsUpdateServiceCallback.aidl
package org.chromium.components.payments;
import android.os.Bundle;
import org.chromium.components.payments.IPaymentDetailsUpdateService;
interface IPaymentDetailsUpdateServiceCallback {
oneway void updateWith(in Bundle updatedPaymentDetails);
oneway void paymentDetailsNotUpdated();
oneway void setPaymentDetailsUpdateService(IPaymentDetailsUpdateService service);
}
org/chromium/components/payments/IPaymentDetailsUpdateService.aidl
package org.chromium.components.payments;
import android.os.Bundle;
import org.chromium.components.payments.IPaymentDetailsUpdateServiceCallback;
interface IPaymentDetailsUpdateService {
oneway void changePaymentMethod(in Bundle paymentHandlerMethodData,
IPaymentDetailsUpdateServiceCallback callback);
oneway void changeShippingOption(in String shippingOptionId,
IPaymentDetailsUpdateServiceCallback callback);
oneway void changeShippingAddress(in Bundle shippingAddress,
IPaymentDetailsUpdateServiceCallback callback);
}
Implement the service in Kotlin or Java:
class SampleUpdatePaymentDetailsCallbackService : Service() {
private val binder = object : IPaymentDetailsUpdateServiceCallback.Stub() {
override fun updateWith(updatedPaymentDetails: Bundle) {}
override fun paymentDetailsNotUpdated() {}
override fun setPaymentDetailsUpdateService(service: IPaymentDetailsUpdateService) {}
}
override fun onBind(intent: Intent?): IBinder? {
return binder
}
}
In the manifest, expose the service for IPaymentDetailsUpdateServiceCallback. The intent filters from the Pay Activity that you are using are available in the reference below:
<service
android:name=".SampleUpdatePaymentDetailsCallbackService"
android:exported="true">
<intent-filter>
<action android:name="org.chromium.intent.action.UPDATE_PAYMENT_DETAILS" />
</intent-filter>
</service>
Notifying the merchant of changes
Once the service is set up, use the following code to notify the merchant about a user change. Connect to the service in the onReadyToPay callback:
try {
if (isOptionChange) {
service?.changeShippingOption(selectedOptionId, callback)
} else (isAddressChange) {
service?.changeShippingAddress(selectedAddress, callback)
} else {
service?.changePaymentMethod(methodData, callback)
}
} catch (e: RemoteException) {
// Handle the remote exception
}
Then notify the merchant:
Change in payment method — call changePaymentMethod with a paymentHandlerMethodData bundle containing methodName and optional details keys (both strings). Chrome rejects empty bundles or empty methodName values and sends an error via callback.updateWith:
'Method data required.'
'Method name required.'
Change in shipping option — call changeShippingOption with the shippingOptionId matching one of the merchant-specified options. Chrome sends an error if the identifier is empty:
'Shipping option identifier required.'
Change in shipping address — call changeShippingAddress with a non-empty shippingAddress bundle that includes a valid countryCode. Chrome sends an error if validation fails:
'Payment app returned invalid shipping address in response.'
If Chrome is in an invalid state—for example, still awaiting a merchant response to a prior change, or when the shipping option identifier doesn't correspond to any merchant-specified option—it invokes callback.updateWith with a redacted bundle containing only an error key set to "Invalid state".
Receiving updated payment details
The merchant's response arrives as an updatedPaymentDetails bundle that mirrors the PaymentRequestDetailsUpdate WebIDL dictionary. All keys are optional and absent keys indicate no change in that value:
total— a bundle with stringcurrencyandvaluekeys.shippingOptions— a parcelable array of shipping option bundles.error— a generic error string (e.g., from an invalid shipping option).stringifiedPaymentMethodErrors— a JSON string with payment method validation errors.addressErrors— a bundle where each key corresponds to a shipping address field and its value describes that field's error.modifiers— a parcelable array of bundles, each withtotalandmethodDatasub-bundles.



