tva
← Artigos

Integração do Stripe Checkout para comércio eletrônico internacional: casos excepcionais e lições

A documentação do Stripe é excelente, e a integração inicial geralmente funciona na primeira tentativa. Mas, na prática, o comércio eletrônico internacional expõe uma classe de problemas que o guia de início rápido não cobre: confiabilidade na entrega de webhooks durante reinicializações de infraestrutura, a mecânica sutil de reembolsos em moedas diferentes da moeda de liquidação e o custo operacional de gerenciar chaves de API em múltiplos ambientes. Este artigo documenta os casos excepcionais que encontramos e o que mudamos como resultado.

O modelo de sessão de checkout e suas premissas

O Stripe Checkout é uma página de pagamento hospedada. Você cria uma sessão no seu servidor, redireciona o cliente e o Stripe chama seu webhook quando o pagamento é concluído. O fluxo é simples até você operar em múltiplas moedas ou precisar inspecionar o objeto de pagamento completo no momento do processamento e envio do pedido.

Por padrão, uma resposta de sessão do Checkout contém uma representação mínima do pagamento. O campo payment_intent é um ID em formato de string, não o objeto expandido. Isso importa quando sua lógica de processamento e envio de pedidos precisa ler payment_intent.latest_charge.payment_method_details — por exemplo, para registrar o país do cartão na lógica de determinação do IVA. (Nas versões da API anteriores a 2022-11-15, o caminho era payment_intent.charges.data[0]; o Stripe removeu a propriedade charges dos PaymentIntents nessa versão.) Eventos de webhook nunca contêm objetos expandidos, portanto obter esses dados no momento do processamento e envio dos pedidos exige uma segunda chamada de API por evento.

O parâmetro expand aceita um array de caminhos separados por pontos. Ele se aplica apenas à resposta da chamada de API que o solicita: defini-lo na criação da sessão não altera o objeto de sessão que o Stripe envia depois no webhook, pois o Stripe sempre envia os objetos dos eventos em sua forma mínima. No manipulador do webhook, recupere a sessão expandindo ["payment_intent.latest_charge"]. A contrapartida é uma chamada de API adicional por evento e uma resposta maior, o que importa se você está gravando respostas brutas em um armazenamento de logs.

Confiabilidade de webhooks

A entrega de webhooks do Stripe é baseada em novas tentativas: se o seu endpoint não retornar um status 2xx ou houver um timeout, o Stripe tentará novamente com espera exponencial por até três dias no modo de produção; os eventos da sandbox são reenviados três vezes ao longo de algumas horas. Este é um bom design, mas cria um risco de processamento incorreto se seu manipulador de eventos não for idempotente. A criação de pedidos, a reserva de estoque e o envio de e-mails são todos efeitos colaterais que devem ocorrer exatamente uma vez.

A solução padrão é registrar IDs de eventos processados antes de executar efeitos colaterais. Usamos uma tabela de banco de dados simples com o ID do evento Stripe como chave primária. O manipulador de eventos lê o campo id de entrada, tenta uma inserção e prossegue apenas se a inserção for bem-sucedida. Um ID de evento duplicado causa um conflito e o manipulador de eventos retorna 200 imediatamente. Para que isso seja seguro, a inserção precisa ser confirmada na mesma transação de banco de dados que os efeitos colaterais (ou a linha deve ser marcada como processada somente depois que eles forem concluídos com sucesso). Caso contrário, uma falha após a inserção faz a nova tentativa do Stripe parecer uma duplicata, e o pedido nunca é processado e enviado.

O que a documentação subestima é a lacuna entre receber um evento checkout.session.completed e o pagamento ser realmente capturado no cronograma de liquidação do Stripe. Para certos métodos de pagamento — particularmente transferências bancárias e alguns instrumentos BNPL disponíveis na Alemanha e nos Países Baixos — a sessão de checkout é concluída antes que os fundos sejam confirmados. Nesses casos, a sessão é concluída com payment_status igual a unpaid, e os eventos corretos são checkout.session.async_payment_succeeded para acionar o processamento e envio dos pedidos e checkout.session.async_payment_failed para tratar a falha. Processar e enviar pedidos a cada checkout.session.completed sem verificar payment_status fará com que sejam enviados pedidos cujo pagamento falha posteriormente.

Também encontramos um problema com nosso ciclo de reinicialização de contêiner. Durante uma implantação, requisições de webhook em andamento podem chegar no momento em que o contêiner antigo está sendo desligado e o novo está iniciando. O Nginx mantém conexões durante a transição upstream, mas observamos eventos descartados durante uma breve janela. A correção foi adicionar configurações explícitas de keepalive_timeout e proxy_read_timeout à nossa configuração do Nginx e depender do mecanismo de novas tentativas do Stripe em vez de tentar tornar nossa própria janela de implantação sem indisponibilidade no nível do receptor de webhook.

Gerenciamento de chaves de produção e de teste

O erro mais comum que vemos em integrações Stripe é não manter as chaves de produção e de teste verdadeiramente separadas. A versão óbvia é usar acidentalmente uma chave de produção em um ambiente de teste e gerar cobranças reais. A versão menos óbvia é mais sutil: usar chaves de modo de teste em um ambiente de staging que compartilha um banco de dados com dados semelhantes aos de produção.

Nossa convenção é três ambientes, cada um com seu próprio conjunto completo de credenciais do Stripe: development, staging e production. O desenvolvimento usa o modo de teste do Stripe. O staging também usa o modo de teste, mas com um conjunto diferente de chaves restritas e um endpoint de webhook separado registrado no painel do Stripe. A produção usa o modo de produção. Todas as chaves são injetadas em tempo de execução como variáveis de ambiente — nunca incluídas em commits no controle de versão, nunca interpoladas em imagens Docker durante o build.

O segredo de assinatura de webhook é uma credencial separada da chave de API. Cada endpoint registrado no painel do Stripe tem seu próprio segredo de assinatura. Quando você tem três ambientes, tem três segredos de assinatura. É fácil copiar o errado, especialmente ao configurar uma nova implantação. Documentamos o mapeamento explicitamente em nosso manual operacional interno: qual conta Stripe, qual endpoint, qual segredo de assinatura, qual prefixo de chave. Isso parece trabalho adicional, mas a alternativa é uma sessão de depuração envolvendo eventos do modo de produção que falham silenciosamente na verificação de assinatura.

O recurso de chaves restritas do Stripe é subutilizado. Em vez de dar a cada serviço sua chave secreta completa, você pode criar chaves limitadas às operações de que esse serviço precisa. Um worker de processamento e envio de pedidos que apenas precisa ler intenções de pagamento e criar reembolsos não precisa de acesso de escrita a produtos ou assinaturas. Emitimos chaves restritas por serviço e as rotacionamos independentemente da chave secreta principal.

Tratamento de moeda e o problema de reembolso

O Stripe suporta mais de 135 moedas de apresentação, mas sua moeda de liquidação é determinada pelo país da sua conta Stripe. Se sua conta liquida em EUR e um cliente paga em SGD, o Stripe realiza a conversão no momento da cobrança. Isso é transparente até você emitir um reembolso.

Os reembolsos são processados na moeda de apresentação — a moeda em que o cliente pagou. Mas o valor deduzido do seu saldo Stripe é calculado à taxa de câmbio atual no momento do reembolso, não no momento da cobrança original. Se a taxa de câmbio se moveu desfavoravelmente entre a cobrança e o reembolso, você pode reembolsar mais do que recebeu em sua moeda de liquidação. Isso não é um bug; é o comportamento esperado. Isso está documentado no guia do Stripe sobre reembolsos e contestações, mas é fácil deixar passar, e tem implicações reais de custo se você opera com margens apertadas em múltiplas moedas. (Em outubro de 2026, o Adaptive Pricing do Stripe é uma exceção: os reembolsos usam a taxa de câmbio da transação original.)

A mitigação prática é restringir as moedas aceitas àquelas para as quais você entende a exposição, ou manter uma reserva em moeda que leva em conta a variação da taxa de câmbio. Para operações de alto volume, as contas de liquidação em múltiplas moedas do Stripe permitem que você mantenha saldos em moedas específicas, liquidando em cada moeda separadamente. Isso elimina a conversão no momento do reembolso para essas moedas. A contrapartida é um gerenciamento de tesouraria mais complexo.

Também encontramos um caso excepcional com reembolsos parciais em pedidos cobrados em uma moeda diferente da nossa moeda base interna. (Uma única sessão do Checkout usa uma só moeda, compartilhada por todos os itens do pedido.) Reembolsos parciais exigem que você especifique um valor na moeda da cobrança original. Se seu sistema de gerenciamento de pedidos armazena valores em sua moeda base interna, você precisa armazenar o valor e a moeda de apresentação originais junto ao registro do pedido, não apenas o valor convertido. Adicionamos ambos os campos à nossa tabela de pedidos após a primeira tentativa de reembolso parcial falhar com um erro de validação confuso.

Disponibilidade de métodos de pagamento por país

O recurso de métodos de pagamento dinâmicos do Stripe Checkout permite que o formulário de pagamento mostre métodos de pagamento relevantes com base na localização do cliente. Isso é conveniente, mas requer cuidado. Quando você deixa o Stripe escolher os métodos com base nas configurações do painel em vez de passar uma lista explícita, o conjunto de métodos disponíveis não é totalmente previsível no momento em que você cria a sessão — depende de fatores que o Stripe avalia no momento da renderização.

Isso importa para sua suíte de testes. Se você estiver testando fluxos de checkout cujos testes verificam se métodos de pagamento específicos aparecem, esses testes se comportarão de maneira diferente em diferentes regiões. Escrevemos nossos testes de integração com base no parâmetro explícito payment_method_types em vez de métodos de pagamento dinâmicos, o que nos dá um conjunto determinístico. O Checkout de produção usa métodos de pagamento dinâmicos. A divergência é intencional e documentada.

SEPA Direct Debit, iDEAL, Bancontact e Przelewy24 têm requisitos que não se aplicam aos pagamentos com cartão. O Checkout hospedado coleta por conta própria os dados específicos de cada método — o IBAN e a aceitação do mandato para SEPA, por exemplo —, mas há outras restrições: o SEPA Direct Debit no Checkout exige que todos os itens tenham preços em EUR e é um método de notificação diferida, portanto se aplicam os eventos assíncronos descritos acima. Testamos cada método de pagamento habilitado em relação ao fluxo de checkout completo, não apenas na etapa de confirmação de pagamento.

O que mudamos após os primeiros problemas de produção

Após executar a integração pelos primeiros meses de transações reais, fizemos três mudanças estruturais. Primeiro, movemos o armazenamento de chaves de idempotência da memória para o banco de dados, porque um armazenamento em memória não sobrevive a reinicializações de contêiner. Segundo, adicionamos registro explícito do ID do evento Stripe, o tipo de evento e o resultado de cada execução do manipulador de webhooks — não apenas erros. Isso tornou possível reconstruir a sequência de eventos ao diagnosticar discrepâncias de estado de pedido. Terceiro, paramos de depender do log de eventos do painel do Stripe como nossa ferramenta de depuração principal e começamos a tratar nossos próprios logs como fonte de referência.

O painel do Stripe é útil para explorar eventos interativamente, mas ele mostra a visão do Stripe sobre a entrega, não a visão do seu aplicativo sobre o recebimento. Uma entrega que o painel marca como bem-sucedida significa apenas que nosso endpoint retornou um status 2xx; ele não mostra se o trabalho por trás dessa resposta foi concluído. Nossos logs contaram a história completa; o painel contou uma história incompleta.


Artigos relacionados

Leitura complementar