Guia para desenvolvedores de apps de pagamento do Android

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

Publicado em 5 de maio de 2020, atualizado pela última vez em 27 de maio de 2025

A Payment Request API oferece à Web uma interface integrada baseada no navegador que permite aos usuários inserir as informações de pagamento necessárias com mais facilidade do que nunca. 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

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

Em comparação com o uso apenas de intents do Android, os pagamentos na 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 app de pagamento atual, permitindo que você aproveite sua base de usuários.
  • A assinatura do app de pagamento é verificada para evitar o sideload.
  • 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é mesmo integrar métodos que exigem acesso ao chip de hardware no dispositivo.

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

  1. Permitir que os comerciantes descubram seu app de pagamento.
  2. Informar a um 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 um pagamento.
  4. Verificar o certificado de assinatura do autor da chamada.

Para ver os pagamentos na 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 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 que você aceita usando o identificador da forma de pagamento.

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

Etapa 2: informar a um comerciante se um cliente tem um instrumento registrado que está pronto para pagar

O comerciante pode chamar hasEnrolledInstrument() para consultar se o cliente pode fazer um pagamento. Você pode 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 um gerenciador de intents desse tipo no app de pagamento, o navegador da Web vai presumir que o app sempre pode fazer pagamentos.

AIDL

A API do 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:

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

Resposta

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

Kotlin

callback?.handleIsReadyToPay(true)

Java

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

Permissão

Você pode usar Binder.getCallingUid() para verificar quem é o autor da chamada. Observe que você precisa 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 de serviço, o que não aciona o 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());
        // ...

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

Embora packageManager.getPackagesForUid() normalmente retorne um único elemento, seu código precisa processar o cenário incomum em que um autor da chamada usa vários nomes de pacotes. 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 em relação a null.

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

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

O packageName foi adicionado no Chrome 138. É necessário verificar 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 o controle total do autor da chamada, enquanto Binder.getCallingUid() é controlado pelo SO Android.

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

Etapa 3: permitir que um cliente faça um 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 do Android PAY 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 opacos ao 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 os details; o valor desse parâmetro é transmitido diretamente ao comerciante.

AndroidManifest.xml

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

Para oferecer suporte a várias formas de pagamento, adicione uma <meta-data> tag com um <string-array> recurso.

<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 delas 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 à atividade como extras 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

Os nomes dos métodos que estão sendo usados. Os elementos são as chaves no dicionário methodData. Esses são os métodos que o app de pagamento aceita.

Kotlin

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

Java

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

methodData

Um mapeamento de cada um dos methodNames para o methodData.

Kotlin

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

Java

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

Kotlin

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

Java

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.

Kotlin

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

Java

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.

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

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.

Kotlin

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

Java

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

total

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

Kotlin

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

Java

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 "pagamento por push" precisam associar ao estado da transação. Os sites de comerciantes vão usar esse campo para consultar os apps de "pagamento por push" sobre o estado da transação fora da banda.

Kotlin

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

Java

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

Resposta

A atividade pode enviar a resposta de volta por setResult com 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();

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

  • methodName: o nome do método que está sendo usado.
  • details: string JSON que contém as informações necessárias para que o comerciante conclua a transação. Se o sucesso for true, então details precisará ser construído de forma que JSON.parse(details) seja bem-sucedido. Se não houver dados que precisem ser retornados, essa string poderá ser "{}", que o site do comerciante vai receber como um dicionário JavaScript vazio.

Você pode transmitir RESULT_CANCELED se o usuário cancelar a transação no app de pagamento. Isso fará com que request.show() seja rejeitado com um AbortError no site do comerciante, indicando o cancelamento do usuário.

Kotlin

setResult(Activity.RESULT_CANCELED)
finish()

Java

setResult(Activity.RESULT_CANCELED);
finish();

A partir do Chrome 149, os seguintes valores de resultado são aceitos:

Kotlin

Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
const val INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER // 1 (0x00000001)

Java

Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
static final int INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER; // 1 (0x00000001)

Se o app de pagamento falhar devido a um erro interno, você poderá indicar isso transmitindo Activity.RESULT_FIRST_USER como código de resultado.

Se INTERNAL_PAYMENT_APP_ERROR for retornado, request.show() será rejeitado com um OperationError no site do comerciante, indicando um erro no app de pagamento.

Essa distinção entre RESULT_CANCELED (0) para cancelamento do usuário, que causa AbortError, e INTERNAL_PAYMENT_APP_ERROR (1) para um erro interno do app, que causa OperationError, permite que os comerciantes criem fluxos de usuários melhores.

Kotlin

setResult(Activity.RESULT_FIRST_USER)
finish()

Java

setResult(Activity.RESULT_FIRST_USER);
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 há methodName e details nã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 voltadas para o 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().

Kotlin

val caller: String? = callingPackage

Java

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

Você pode 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 e confira se ele corresponde ao valor correto.

Se você estiver segmentando o nível 28 da API e versões mais recentes e estiver se integrando a um navegador que tenha um único certificado de assinatura, poderá 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);

PackageManager.hasSigningCertificate() é preferível para navegadores de certificado único, porque processa corretamente a rotação de certificados. O Chrome tem um único certificado de assinatura. Os apps que têm vários certificados de assinatura não podem girá-los.

Se você precisar oferecer suporte aos níveis 27 e anteriores da API ou se precisar processar navegadores com vários certificados de assinatura, poderá 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

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

adb logcat | grep -i pay