Guía para desarrolladores de aplicaciones de pago Android

Descubre cómo adaptar tu app de pagos para Android para que funcione con pagos web y brindar una mejor experiencia del usuario a los clientes.

Fecha de publicación: 5 de mayo de 2020; Última actualización: 27 de mayo de 2025

La API de Payment Request lleva a la Web una interfaz integrada basada en el navegador que permite a los usuarios ingresar la información de pago requerida más fácil que nunca. La API también puede invocar apps de pago específicas de la plataforma.

Browser Support

  • Chrome: 60.
  • Edge: 15.
  • Firefox: behind a flag.
  • Safari: 11.1.

Source

Flujo de confirmación de la compra con la app de Google Pay específica de la plataforma que usa pagos web.

En comparación con el uso solo de intents de Android, los pagos web permiten una mejor integración con el navegador, la seguridad y la experiencia del usuario:

  • La app de pagos se inicia como un diálogo modal en el contexto del sitio web del comercio.
  • La implementación es complementaria a tu app de pagos existente, lo que te permite aprovechar tu base de usuarios.
  • Se verifica la firma de la app de pago para evitar el sideloading.
  • Las aplicaciones de pago pueden admitir varias formas de pago.
  • Se puede integrar cualquier forma de pago, como criptomonedas, transferencias bancarias y mucho más. Las apps de pago en dispositivos Android incluso pueden integrar métodos que requieren acceso al chip de hardware del dispositivo.

Para implementar los pagos web en una app de pagos para Android, se deben seguir cuatro pasos:

  1. Permite que los comercios descubran tu app de pagos.
  2. Informar a un comercio si un cliente tiene un instrumento inscrito (como una tarjeta de crédito) listo para pagar
  3. Permite que un cliente realice un pago.
  4. Verifica el certificado de firma del emisor.

Para ver los pagos web en acción, consulta la demo de android-web-payment.

Paso 1: Permite que los comercios descubran tu app de pagos

Configura la propiedad related_applications en el manifiesto de la app web según las instrucciones de Cómo configurar una forma de pago.

Para que un comercio use tu app de pagos, debe usar la API de Payment Request y especificar la forma de pago que admites con el identificador de forma de pago.

Si tienes un identificador de forma de pago único para tu app de pagos, puedes configurar tu propio manifiesto de forma de pago para que los navegadores puedan descubrir tu app.

Paso 2: Informa a un comercio si un cliente tiene un instrumento inscrito listo para pagar

El comercio puede llamar a hasEnrolledInstrument() para consultar si el cliente puede realizar un pago. Puedes implementar IS_READY_TO_PAY como un servicio de Android para responder esta consulta.

AndroidManifest.xml

Declara tu servicio con un filtro de intents que tenga la acción org.chromium.intent.action.IS_READY_TO_PAY.

<service
  android:name=".SampleIsReadyToPayService"
  android:exported="true">
  <intent-filter>
    <action android:name="org.chromium.intent.action.IS_READY_TO_PAY" />
  </intent-filter>
</service>

El servicio IS_READY_TO_PAY es opcional. Si no hay un controlador de intents de este tipo en la app de pagos, el navegador web supone que la app siempre puede realizar pagos.

AIDL

La API del servicio IS_READY_TO_PAY se define en AIDL. Crea dos archivos AIDL con el siguiente contenido:

org/chromium/IsReadyToPayServiceCallback.aidl

package org.chromium;

interface IsReadyToPayServiceCallback {
    oneway void handleIsReadyToPay(boolean isReadyToPay);
}

org/chromium/IsReadyToPayService.aidl

package org.chromium;

import org.chromium.IsReadyToPayServiceCallback;

interface IsReadyToPayService {
    oneway void isReadyToPay(IsReadyToPayServiceCallback callback, in Bundle parameters);
}

Cómo implementar IsReadyToPayService

La implementación más simple de IsReadyToPayService se muestra en el siguiente ejemplo:

Kotlin

class SampleIsReadyToPayService : Service() {
    private val binder = object : IsReadyToPayService.Stub() {
        override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
            callback?.handleIsReadyToPay(true)
        }
    }

    override fun onBind(intent: Intent?): IBinder? {
        return binder
    }
}

Java

import org.chromium.IsReadyToPayService;

public class SampleIsReadyToPayService extends Service {
    private final IsReadyToPayService.Stub mBinder =
        new IsReadyToPayService.Stub() {
            @Override
            public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
                if (callback != null) {
                    callback.handleIsReadyToPay(true);
                }
            }
        };

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

Respuesta

El servicio puede enviar su respuesta con el método handleIsReadyToPay(Boolean).

Kotlin

callback?.handleIsReadyToPay(true)

Java

if (callback != null) {
    callback.handleIsReadyToPay(true);
}

Permiso

Puedes usar Binder.getCallingUid() para verificar quién es el llamador. Ten en cuenta que debes hacerlo en el método isReadyToPay, no en el método onBind, ya que el SO Android puede almacenar en caché y reutilizar la conexión del servicio, lo que no activa el método onBind().

Kotlin

override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
    try {
        val untrustedPackageName = parameters?.getString("packageName")
        val actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid())
        // ...

Java

@Override
public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
    try {
        String untrustedPackageName = parameters != null
                ? parameters.getString("packageName")
                : null;
        String[] actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid());
        // ...

Siempre verifica los parámetros de entrada de null cuando recibas llamadas de comunicación entre procesos (IPC). Esto es muy importante porque las diferentes versiones o bifurcaciones del SO Android pueden comportarse de formas inesperadas y generar errores si no se controlan.

Si bien packageManager.getPackagesForUid() suele mostrar un solo elemento, tu código debe controlar la situación poco común en la que un llamador usa varios nombres de paquetes. Esto garantiza que tu aplicación siga siendo sólida.

Consulta Cómo verificar el certificado de firma del llamador para obtener información sobre cómo verificar que el paquete de llamada tenga la firma correcta.

Parámetros

El paquete parameters se agregó en Chrome 139. Siempre se debe verificar con null.

Los siguientes parámetros se pasan al servicio en el paquete parameters:

  • packageName
  • methodNames
  • methodData
  • topLevelOrigin
  • paymentRequestOrigin
  • topLevelCertificateChain

Se agregó packageName en Chrome 138. Debes verificar este parámetro en Binder.getCallingUid() antes de usar su valor. Esta verificación es esencial porque el paquete parameters está bajo el control total del llamador, mientras que el SO Android controla Binder.getCallingUid().

El topLevelCertificateChain es null en WebView y en sitios web que no son HTTPS que suelen usarse para pruebas locales, como http://localhost.

Paso 3: Permite que un cliente realice el pago

El comercio llama a show() para iniciar la app de pago, de modo que el cliente pueda realizar un pago. La app de pagos se invoca con un intent PAY de Android con información de transacción en los parámetros del intent.

La app de pagos responde con methodName y details, que son específicos de la app de pagos y son opacos para el navegador. El navegador convierte la cadena details en un diccionario de JavaScript para el comercio mediante la desserialización de cadenas JSON, pero no aplica ninguna validez más allá de eso. El navegador no modifica details; el valor de ese parámetro se pasa directamente al comercio.

AndroidManifest.xml

La actividad con el filtro de intents PAY debe tener una etiqueta <meta-data> que identifique el identificador de forma de pago predeterminada de la app.

Para admitir varias formas de pago, agrega una etiqueta <meta-data> con un recurso <string-array>.

<activity
  android:name=".PaymentActivity"
  android:theme="@style/Theme.SamplePay.Dialog">
  <intent-filter>
    <action android:name="org.chromium.intent.action.PAY" />
  </intent-filter>

  <meta-data
    android:name="org.chromium.default_payment_method_name"
    android:value="https://bobbucks.dev/pay" />
  <meta-data
    android:name="org.chromium.payment_method_names"
    android:resource="@array/chromium_payment_method_names" />
</activity>

android:resource debe ser una lista de cadenas, cada una de las cuales debe ser una URL absoluta válida con un esquema HTTPS, como se muestra aquí.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="chromium_payment_method_names">
        <item>https://alicepay.com/put/optional/path/here</item>
        <item>https://charliepay.com/put/optional/path/here</item>
    </string-array>
</resources>

Parámetros

Los siguientes parámetros se pasan a la actividad como extras de Intent:

  • methodNames
  • methodData
  • merchantName
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
  • paymentOptions
  • shippingOptions

Kotlin

val extras: Bundle? = getIntent()?.extras

Java

Bundle extras = getIntent() != null ? getIntent().getExtras() : null;

methodNames

Los nombres de los métodos que se usan. Los elementos son las claves del diccionario methodData. Estos son los métodos que admite la app de pagos.

Kotlin

val methodNames: List<String>? = extras.getStringArrayList("methodNames")

Java

List<String> methodNames = extras.getStringArrayList("methodNames");

methodData

Una asignación de cada uno de los methodNames a methodData

Kotlin

val methodData: Bundle? = extras.getBundle("methodData")

Java

Bundle methodData = extras.getBundle("methodData");

merchantName

Es el contenido de la etiqueta HTML <title> de la página de confirmación de la compra del comercio (el contexto de navegación de nivel superior del navegador).

Kotlin

val merchantName: String? = extras.getString("merchantName")

Java

String merchantName = extras.getString("merchantName");

topLevelOrigin

El origen del comercio sin el esquema (el origen sin esquema del contexto de navegación de nivel superior) Por ejemplo, https://mystore.com/checkout se pasa como mystore.com.

Kotlin

val topLevelOrigin: String? = extras.getString("topLevelOrigin")

Java

String topLevelOrigin = extras.getString("topLevelOrigin");

topLevelCertificateChain

La cadena de certificados del comercio (la cadena de certificados del contexto de navegación de nivel superior) El valor es null para WebView, localhost o un archivo en el disco. Cada Parcelable es un paquete con una clave certificate y un valor de array de bytes.

Kotlin

val topLevelCertificateChain: Array<Parcelable>? =
        extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
    (p as Bundle).getByteArray("certificate")
}

Java

Parcelable[] topLevelCertificateChain =
        extras.getParcelableArray("topLevelCertificateChain");
if (topLevelCertificateChain != null) {
    for (Parcelable p : topLevelCertificateChain) {
        if (p != null && p instanceof Bundle) {
            ((Bundle) p).getByteArray("certificate");
        }
    }
}

paymentRequestOrigin

El origen sin esquema del contexto de navegación del iframe que invocó el constructor new PaymentRequest(methodData, details, options) en JavaScript. Si el constructor se invocó desde el contexto de nivel superior, el valor de este parámetro es igual al valor del parámetro topLevelOrigin.

Kotlin

val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")

Java

String paymentRequestOrigin = extras.getString("paymentRequestOrigin");

total

Es la cadena JSON que representa el importe total de la transacción.

Kotlin

val total: String? = extras.getString("total")

Java

String total = extras.getString("total");

Este es un ejemplo de contenido de la cadena:

{"currency":"USD","value":"25.00"}

modifiers

El resultado de JSON.stringify(details.modifiers), en el que details.modifiers solo contiene supportedMethods, data y total

paymentRequestId

Es el campo PaymentRequest.id que las apps de "pago push" deben asociar con el estado de la transacción. Los sitios web de los comercios usarán este campo para consultar el estado de la transacción fuera del canal a las apps de "pago directo".

Kotlin

val paymentRequestId: String? = extras.getString("paymentRequestId")

Java

String paymentRequestId = extras.getString("paymentRequestId");

Respuesta

La actividad puede enviar su respuesta a través de setResult con RESULT_OK.

Kotlin

setResult(Activity.RESULT_OK, Intent().apply {
    putExtra("methodName", "https://bobbucks.dev/pay")
    putExtra("details", "{\"token\": \"put-some-data-here\"}")
})
finish()

Java

Intent result = new Intent();
Bundle extras = new Bundle();
extras.putString("methodName", "https://bobbucks.dev/pay");
extras.putString("details", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();

Debes especificar dos parámetros como extras de Intent:

  • methodName: Es el nombre del método que se usa.
  • details: Es una cadena JSON que contiene la información necesaria para que el comercio complete la transacción. Si el éxito es true, details se debe construir de manera que JSON.parse(details) tenga éxito. Si no hay datos que se deban mostrar, esta cadena puede ser "{}", que el sitio web del comercio recibirá como un diccionario de JavaScript vacío.

Puedes pasar RESULT_CANCELED si la transacción no se completó en la app de pago, por ejemplo, si el usuario no ingresó el código PIN correcto de su cuenta en la app de pago. Es posible que el navegador permita que el usuario elija una app de pago diferente.

Kotlin

setResult(Activity.RESULT_CANCELED)
finish()

Java

setResult(Activity.RESULT_CANCELED);
finish();

Si el resultado de la actividad de una respuesta de pago recibida de la app de pago invocada está configurado como RESULT_OK, Chrome buscará methodName y details no vacíos en sus elementos adicionales. Si la validación falla, Chrome mostrará una promesa rechazada de request.show() con uno de los siguientes mensajes de error que el desarrollador puede ver:

'Payment app returned invalid response. Missing field "details".'
'Payment app returned invalid response. Missing field "methodName".'

Permiso

La actividad puede verificar al llamador con su método getCallingPackage().

Kotlin

val caller: String? = callingPackage

Java

String caller = getCallingPackage();

El último paso es verificar el certificado de firma del llamador para confirmar que el paquete de llamada tenga la firma correcta.

Paso 4: Verifica el certificado de firma del llamador

Puedes verificar el nombre del paquete del llamador con Binder.getCallingUid() en IS_READY_TO_PAY y con Activity.getCallingPackage() en PAY. Para verificar que el llamador es el navegador que tienes en mente, debes verificar su certificado de firma y asegurarte de que coincida con el valor correcto.

Si te orientas al nivel de API 28 y versiones posteriores, y realizas la integración con un navegador que tiene un solo certificado de firma, puedes usar PackageManager.hasSigningCertificate().

Kotlin

val packageName: String =  // The caller's package name
val certificate: ByteArray =  // The correct signing certificate
val verified = packageManager.hasSigningCertificate(
        callingPackage,
        certificate,
        PackageManager.CERT_INPUT_SHA256
)

Java

String packageName =  // The caller's package name
byte[] certificate =  // The correct signing certificate
boolean verified = packageManager.hasSigningCertificate(
        callingPackage,
        certificate,
        PackageManager.CERT_INPUT_SHA256);

Se prefiere PackageManager.hasSigningCertificate() para los navegadores de un solo certificado, ya que controla correctamente la rotación de certificados. (Chrome tiene un certificado de firma único). Las apps que tienen varios certificados de firma no pueden rotarlos.

Si necesitas admitir niveles de API 27 y anteriores, o si necesitas controlar navegadores con varios certificados de firma, puedes usar PackageManager.GET_SIGNATURES.

Kotlin

val packageName: String =  // The caller's package name
val expected: Set<String> =  // The correct set of signing certificates

val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val actual = packageInfo.signatures.map {
    SerializeByteArrayToString(sha256.digest(it.toByteArray()))
}
val verified = actual.equals(expected)

Java

String packageName  =  // The caller's package name
Set<String> expected =  // The correct set of signing certificates

PackageInfo packageInfo =
        packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
Set<String> actual = new HashSet<>();
for (Signature it : packageInfo.signatures) {
    actual.add(SerializeByteArrayToString(sha256.digest(it.toByteArray())));
}
boolean verified = actual.equals(expected);

Depurar

Usa el siguiente comando para observar errores o mensajes informativos:

adb logcat | grep -i pay