Guia para desenvolvedores de apps de pagamento do Android

Aprenda a adaptar seu app de pagamento Android para funcionar com pagamentos na Web e oferecer uma melhor experiência do usuário aos clientes.

Publicado em 5 de maio de 2020, última atualização: 27 de maio de 2025

A API Payment Request traz para a Web uma interface integrada baseada em navegador que permite aos usuários inserir as informações de pagamento necessárias com mais facilidade. A API também pode invocar apps de pagamento específicos da plataforma.

Browser Support

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

Source

Fluxo de finalização da compra com o app do Google Pay específico para a plataforma que usa pagamentos na Web.

Em comparação com o uso apenas de intents do Android, os pagamentos da Web permitem uma melhor integração com o navegador, a segurança e a experiência do usuário:

  • O app de pagamento é iniciado como um modal no contexto do site do comerciante.
  • A implementação é complementar ao seu app de pagamento atual, permitindo que você aproveite sua base de usuários.
  • A assinatura do app de pagamento é verificada para evitar sideloading.
  • Os apps de pagamento podem oferecer suporte a várias formas de pagamento.
  • Qualquer forma de pagamento, como criptomoedas, transferências bancárias e muito mais, pode ser integrada. Os apps de pagamento em dispositivos Android podem até integrar métodos que exigem acesso ao chip de hardware do dispositivo.

São necessárias quatro etapas para implementar os pagamentos da Web em um app de pagamento para Android:

  1. Permita que os comerciantes descubram seu app de pagamento.
  2. Informar ao comerciante se um cliente tem um instrumento registrado (como um cartão de crédito) que está pronto para pagar.
  3. Permitir que um cliente faça o pagamento.
  4. Verifique o certificado de assinatura do autor da chamada.

Para conferir os pagamentos da Web em ação, confira a demonstração android-web-payment.

Etapa 1: permitir que os comerciantes descubram seu app de pagamento

Defina a propriedade related_applications no manifesto do app da Web de acordo com as instruções em Como configurar uma forma de pagamento.

Para que um comerciante use seu app de pagamento, ele precisa usar a API Payment Request e especificar a forma de pagamento aceita usando o identificador da forma de pagamento.

Se você tiver um identificador de forma de pagamento exclusivo para seu app de pagamento, configure seu próprio manifesto de forma de pagamento para que os navegadores possam descobrir seu app.

Etapa 2: informar ao comerciante se um cliente tem um instrumento registrado pronto para pagamento

O comerciante pode chamar hasEnrolledInstrument() para consultar se o cliente pode fazer um pagamento. É possível implementar IS_READY_TO_PAY como um serviço do Android para responder a essa consulta.

AndroidManifest.xml

Declare seu serviço com um filtro de intent com a ação 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>

O serviço IS_READY_TO_PAY é opcional. Se não houver esse gerenciador de intents no app de pagamento, o navegador da Web vai presumir que o app sempre pode fazer pagamentos.

AIDL

A API para o serviço IS_READY_TO_PAY é definida no AIDL. Crie dois arquivos AIDL com o seguinte conteúdo:

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);
}

Implementação de IsReadyToPayService

A implementação mais simples de IsReadyToPayService é mostrada no exemplo a seguir:

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
    }
}
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;
    }
}

Resposta

O serviço pode enviar a resposta usando o método handleIsReadyToPay(Boolean).

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

Permissão

É possível usar Binder.getCallingUid() para verificar quem é o autor da chamada. É necessário fazer isso no método isReadyToPay, não no método onBind, porque o SO Android pode armazenar em cache e reutilizar a conexão do serviço, o que não aciona o método onBind().

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

Sempre verifique os parâmetros de entrada de null ao receber chamadas de comunicação interprocessos (IPC, na sigla em inglês). Isso é particularmente importante porque versões ou ramificações diferentes do SO Android podem se comportar de maneiras inesperadas e causar erros se não forem tratadas.

Embora packageManager.getPackagesForUid() normalmente retorne um único elemento, seu código precisa processar o cenário incomum em que um autor da chamada utiliza vários nomes de pacote. Isso garante que seu aplicativo permaneça robusto.

Consulte Verificar o certificado de assinatura do autor da chamada para saber como verificar se o pacote de chamada tem a assinatura correta.

Parâmetros

O pacote parameters foi adicionado no Chrome 139. Ele sempre precisa ser verificado contra null.

Os parâmetros a seguir são transmitidos para o serviço no pacote parameters:

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

O packageName foi adicionado no Chrome 138. Verifique esse parâmetro em relação a Binder.getCallingUid() antes de usar o valor dele. Essa verificação é essencial porque o pacote parameters está sob controle total do autor da chamada, enquanto Binder.getCallingUid() é controlado pelo SO Android.

O topLevelCertificateChain é null na WebView e em sites que não são https e geralmente são usados para testes locais, como http://localhost.

Etapa 3: permitir que um cliente faça o pagamento

O comerciante chama show() para iniciar o app de pagamento para que o cliente possa fazer um pagamento. O app de pagamento é invocado usando uma intent PAY do Android com informações de transação nos parâmetros da intent.

O app de pagamento responde com methodName e details, que são específicos do app de pagamento e são opacos para o navegador. O navegador converte a string details em um dicionário JavaScript para o comerciante usando a desserialização de string JSON, mas não impõe nenhuma validade além disso. O navegador não modifica o details. O valor desse parâmetro é transmitido diretamente ao merchant.

AndroidManifest.xml

A atividade com o filtro de intent PAY precisa ter uma tag <meta-data> que identifique o identificador da forma de pagamento padrão do app.

Para oferecer suporte a várias formas de pagamento, adicione uma tag <meta-data> com um 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>

O android:resource precisa ser uma lista de strings, cada uma precisa ser um URL absoluto válido com um esquema HTTPS, conforme mostrado aqui.

<?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

Os parâmetros a seguir são transmitidos para a atividade como extras Intent:

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

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

methodNames

Os nomes dos métodos usados. Os elementos são as chaves no dicionário methodData. Estes são os métodos aceitos pelo app de pagamento.

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

methodData

Um mapeamento de cada methodNames para o methodData.

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

merchantName

O conteúdo da tag HTML <title> da página de finalização da compra do comerciante (o contexto de navegação de nível superior do navegador).

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

topLevelOrigin

A origem do comerciante sem o esquema (a origem sem esquema do contexto de navegação de nível superior). Por exemplo, https://mystore.com/checkout é transmitido como mystore.com.

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

topLevelCertificateChain

A cadeia de certificados do comerciante (a cadeia de certificados do contexto de navegação de nível superior). O valor é null para WebView, localhost ou um arquivo no disco. Cada Parcelable é um pacote com uma chave certificate e um valor de matriz de bytes.

val topLevelCertificateChain: Array<Parcelable>? =
        extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
    (p as Bundle).getByteArray("certificate")
}
Parcelable[] topLevelCertificateChain =
        extras.getParcelableArray("topLevelCertificateChain");
if (topLevelCertificateChain != null) {
    for (Parcelable p : topLevelCertificateChain) {
        if (p != null && p instanceof Bundle) {
            ((Bundle) p).getByteArray("certificate");
        }
    }
}

paymentRequestOrigin

A origem sem esquema do contexto de navegação do iframe que invocou o construtor new PaymentRequest(methodData, details, options) em JavaScript. Se o construtor foi invocado no contexto de nível superior, o valor desse parâmetro será igual ao valor do parâmetro topLevelOrigin.

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

total

A string JSON que representa o valor total da transação.

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

Confira um exemplo de conteúdo da string:

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

modifiers

A saída de JSON.stringify(details.modifiers), em que details.modifiers contém apenas supportedMethods, data e total.

paymentRequestId

O campo PaymentRequest.id que os apps de "push-payment" precisam associar ao estado da transação. Os sites de comerciantes vão usar esse campo para consultar os apps de "push-payment" para o estado da transação fora da banda.

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

Resposta

A atividade pode enviar a resposta de volta por setResult com RESULT_OK.

setResult(Activity.RESULT_OK, Intent().apply {
    putExtra("methodName", "https://bobbucks.dev/pay")
    putExtra("details", "{\"token\": \"put-some-data-here\"}")
})
finish()
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();

É necessário especificar dois parâmetros como extras de intent:

  • methodName: o nome do método usado.
  • details: string JSON contendo informações necessárias para que o comerciante conclua a transação. Se o sucesso for true, o details precisará ser criado de modo que o JSON.parse(details) seja bem-sucedido. Se não houver nenhum dado que precise ser retornado, essa string poderá ser "{}", que o site do comerciante vai receber como um dicionário JavaScript vazio.

É possível transmitir RESULT_CANCELED se a transação não foi concluída no app de pagamento, por exemplo, se o usuário não digitou o código PIN correto da conta no app de pagamento. O navegador pode permitir que o usuário escolha um app de pagamento diferente.

setResult(Activity.RESULT_CANCELED)
finish()
setResult(Activity.RESULT_CANCELED);
finish();

Se o resultado da atividade de uma resposta de pagamento recebida do app de pagamento invocado estiver definido como RESULT_OK, o Chrome vai verificar se methodName e details não estão vazios nos extras. Se a validação falhar, o Chrome vai retornar uma promessa rejeitada de request.show() com uma das seguintes mensagens de erro do desenvolvedor:

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

Permissão

A atividade pode verificar o autor da chamada com o método getCallingPackage().

val caller: String? = callingPackage
String caller = getCallingPackage();

A etapa final é verificar o certificado de assinatura do autor da chamada para confirmar se o pacote de chamada tem a assinatura correta.

Etapa 4: verificar o certificado de assinatura do autor da chamada

É possível verificar o nome do pacote do autor da chamada com Binder.getCallingUid() em IS_READY_TO_PAY e com Activity.getCallingPackage() em PAY. Para verificar se o autor da chamada é o navegador que você tem em mente, verifique o certificado de assinatura dele e verifique se ele corresponde ao valor correto.

Se você estiver segmentando o nível 28 da API e versões mais recentes e estiver fazendo a integração com um navegador que tenha um único certificado de assinatura, use PackageManager.hasSigningCertificate().

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
)
String packageName =  // The caller's package name
byte[] certificate =  // The correct signing certificate
boolean verified = packageManager.hasSigningCertificate(
        callingPackage,
        certificate,
        PackageManager.CERT_INPUT_SHA256);

PackageManager.hasSigningCertificate() é a opção preferida para navegadores de certificado único, porque ele processa corretamente a rotação de certificados. O Chrome tem um certificado de assinatura único. Apps com vários certificados de assinatura não podem fazer a rotação deles.

Se você precisar oferecer suporte aos níveis 27 e anteriores da API ou se precisar processar navegadores com vários certificados de assinatura, use PackageManager.GET_SIGNATURES.

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)
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

Use o comando a seguir para observar erros ou mensagens informativas:

adb logcat | grep -i pay