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.

Sahel Sharify

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:

  • payerName
  • payerEmail
  • payerPhone
  • shippingAddress

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 required
  • requestPayerPhone — the payer's phone is required
  • requestPayerEmail — the payer's email is required
  • requestShipping — shipping details are required
  • shippingType — 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 string
  • label — display label
  • amount — a nested bundle holding currency (ISO4217 code) and value (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 true
  • shippingAddress — a non-empty bundle with the physical address keys countryCode, postalCode, sortingCode, region, city, dependentLocality, organization, recipient, phone, and addressLine. All are strings except addressLine, 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 string currency and value keys.

  • 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 with total and methodData sub-bundles.