Catalisa.
Building blocks/FinanceiroBeta

Payments

Aceite cartão, Pix e boleto por mais de um adquirente com uma API só

26
Endpoints
7
Entidades
2
Provedores
Tenant
Escopo
3027
Porta

Você conecta um adquirente hoje e outro no mês que vem sem reescrever o checkout. O Payments fala com todos eles pelo mesmo contrato, e trocar de provedor vira configuração — não projeto.

Para quem é
  • Fintechs e financeiras que cobram tarifa, parcela ou prêmio e hoje dependem de um único adquirente
  • Plataformas B2B e marketplaces que precisam cobrar do cliente final sem virar processadora de pagamento
  • Operações de varejo e serviços que querem Pix e cartão no mesmo fluxo, com conciliação em um lugar só
Substitui
  • Uma integração ponto a ponto por adquirente, mantida à mão dentro do seu produto
  • Assinatura de um orquestrador de pagamentos externo (Malga, Yuno, Spreedly)
  • Painel caseiro de conciliação e taxa de aprovação montado em planilha
O que não é
  • Uma instituição de pagamento — a Catalisa não é adquirente, não liquida e não guarda saldo
  • Conta bancária, transferência ou Pix de saída — isso é o building block BaaS
  • Um antifraude ou motor de decisão de risco transacional
  • Uma página de checkout pronta — o BB devolve o intent e a URL, a interface é sua

01Resumo executivo

O Payments é a camada que recebe dinheiro do seu cliente. Ele conversa com os gateways de pagamento — hoje Stripe e PagSeguro — por trás de um contrato único, guarda o histórico de cada cobrança e devolve para o seu produto uma resposta que não muda quando o gateway muda.

Na prática, isso significa que a decisão "vamos trocar de adquirente porque a taxa ficou melhor" deixa de ser um projeto de dois meses e vira uma chamada de configuração. Você cadastra o provedor novo, marca como padrão, e os pagamentos seguintes saem por ele. Os antigos continuam consultáveis, com o provedor de origem gravado em cada registro.

Está em beta desde março de 2026. O que já roda ponta a ponta é o ciclo de cobrança com Stripe, o link de pagamento, o estorno e a recepção de webhook com verificação de assinatura. O que ainda não está completo está listado, sem rodeio, na seção 15.

AtributoValor
Identificadorpayments
CategoriaFinanceiro
EscopoTenant (exige organizationId no token)
Porta (standalone)3027
Path alias@payments
Prefixo HTTP/payments
Schema do bancopayments
StatusBeta desde 2026-03
Depende dePostgreSQL, Redis, gateway de pagamento do cliente

02O problemanegócio

O cenário. Uma empresa precisa receber dinheiro pela internet. Cartão de crédito, Pix, às vezes boleto. Ela escolhe um gateway, integra, e por seis meses está tudo bem. Depois começam as perguntas que o gateway não responde: por que essa fatia de transações foi recusada? dá para tentar de novo em outro adquirente? quanto essa taxa está custando por ano? o que acontece se o gateway cair na Black Friday?

O que trava hoje.

  • A integração é ponto a ponto e não se desfaz. O SDK do gateway espalha tipos, status e nomes de campo pelo código inteiro. Trocar de provedor significa achar todos eles. É por isso que empresas continuam pagando taxa ruim: sair custa mais que ficar.
  • Cada gateway inventou o próprio vocabulário. requires_capture no Stripe, WAITING no PagSeguro, pending em outro. O seu código acaba com um switch gigante que ninguém quer tocar, e cada provedor novo acrescenta um ramo.
  • Um adquirente é um ponto único de falha e um único poder de negociação. Se ele recusa a transação, a venda acabou — não há segunda tentativa. E na hora de renegociar taxa, quem só tem um fornecedor não tem argumento.
  • Webhook é sempre a parte que quebra. Verificar assinatura, tratar evento duplicado, guardar o payload cru para investigar depois: cada integração reimplementa isso, e quase sempre reimplementa errado na primeira vez.
  • Nenhum gateway te mostra o que você precisa ver. O painel dele mostra o que passou por ele. A pergunta que importa — qual provedor está aprovando mais, com que ticket, com quanto de estorno — só existe se você juntar os dados fora.

O custo de não resolver. Ele aparece em três linhas, e duas delas têm ordem de grandeza pública.

A primeira é a venda que morre no primeiro "não" do adquirente. Um levantamento PYMNTS/Spreedly estimou US$ 157 bilhões em vendas de e-commerce nos Estados Unidos em risco por declínios indevidos em 2023, dos quais US$ 81 bilhões viraram perda permanente — e apontou que 82% dos varejistas não conseguem identificar a causa da falha (PYMNTS, 14/01/2025). A Stripe reporta que 33% dos consumidores não voltam depois de um declínio indevido (Stripe, State of online fraud). Com um adquirente só, não existe segunda tentativa.

A segunda é a taxa que você não renegocia porque não tem alternativa pronta. Nas tabelas públicas brasileiras, um ponto percentual de MDR é dinheiro visível: o PagBank publica crédito à vista de 3,19% a 4,99% conforme o prazo de repasse, e a Cielo publica 3,45% no crédito à vista do plano de aluguel — enquanto o Pix aparece a 0,99% na Cielo e 1,19% na Stripe (tabelas consultadas em 2026-08-16). Sobre volume anual, essa diferença é a linha inteira de um orçamento.

A terceira não tem estatística e todo diretor de engenharia reconhece: as semanas gastas integrando cada gateway, e as seguintes mantendo, em vez de trabalhar no produto que o cliente compra.


03Proposta de valornegócio

AntesDepois
Um SDK de gateway espalhado pelo código do produtoUma interface PaymentProvider — o produto nunca importa o SDK
Trocar de adquirente é reescrever a integraçãoTrocar de adquirente é POST /provider-configs + set-default
switch de status por gateway em cada serviçoStatus canônico único, mapeado uma vez dentro do provider
Webhook com assinatura, dedup e log reimplementados a cada integraçãoUm endpoint por provedor, com verificação, deduplicação e payload cru guardado
Relatório de aprovação em planilha, por gatewayGET /analytics/metrics sobre todos os provedores da organização

O contrato é nosso, não do gateway. Todo provedor implementa a mesma interface — criar intent, cobrar, capturar, estornar, gerar link, ler webhook. O código do seu produto fala com essa interface. O gateway é detalhe de configuração, e detalhe de configuração se troca.

Mais de um adquirente na mesma organização. PaymentProviderConfig é por organização e não é único: você pode ter Stripe e PagSeguro ativos ao mesmo tempo, um marcado como padrão, e escolher o outro por transação passando configId. Isso é o que abre a porta para redundância e para negociação com dois fornecedores.

Pix, cartão e boleto pelo mesmo fluxo. O método é um campo do intent (PIX, CREDIT_CARD, BOLETO, ...), não uma integração separada. Quando o provedor devolve QR code ou linha digitável, isso volta nos mesmos campos canônicos.

O pagamento sabe de onde veio. O intent carrega billingInvoiceId e commerceOrderId. Conciliar "esse dinheiro é de qual fatura" deixa de ser um join que alguém faz na mão.

Dado de cartão não entra. A API não tem campo para número de cartão nem CVV. Ela aceita um token gerado pelo provedor. O que fica guardado é bandeira e os quatro últimos dígitos — ver seção 14.


04Casos de uso reaisnegócio

Caso 1 — Uma financeira passa a cobrar a parcela por Pix sem trocar de gateway Cenário ilustrativo

Contexto. Financeira de crédito pessoal com carteira de 40 mil contratos ativos, cobrança mensal via boleto emitido pelo banco liquidante.

A dor. O boleto tem custo por emissão, compensa em D+1 ou pior, e uma fatia relevante dos clientes simplesmente perde o prazo porque só lembra da parcela quando o boleto vence. O time queria oferecer Pix, mas a integração de cobrança estava amarrada ao banco, e abrir uma segunda integração significava duplicar o código de conciliação.

A solução com o BB. A financeira cadastra um PaymentProviderConfig com o gateway que já usa e cria um intent por parcela com paymentMethod: "PIX". O provider devolve pixQrCode no campo canônico, que o app mostra direto na tela da parcela. Quando o pagamento liquida, o gateway chama POST /payments/api/v1/webhooks/{provider}, o BB muda o intent para COMPLETED e publica payments.intent.completed — que o Billing consome para dar a parcela por quitada. O boleto continua existindo como método alternativo no mesmo intent, sem código novo.

O resultado. Um único caminho de conciliação para dois meios de pagamento, e a baixa da parcela deixa de depender de arquivo de retorno.

Caso 2 — Um marketplace mantém dois adquirentes para não parar quando um cai Cenário ilustrativo

Contexto. Marketplace de serviços com pico forte em datas comerciais, processando cartão de crédito.

A dor. Numa indisponibilidade do adquirente único, o checkout inteiro morre. Não é degradação, é zero venda. E como só existia uma integração, não havia botão para virar a chave — a alternativa era subir um deploy no meio do incidente.

A solução com o BB. O marketplace cadastra dois PaymentProviderConfig ativos na mesma organização, um marcado como padrão. O checkout cria o intent sem configId no caminho normal e usa o padrão. Durante um incidente, a operação chama POST /provider-configs/{id}/set-default no provedor secundário, e os intents seguintes saem por ele — sem deploy. Cada intent guarda o configId de origem, então a conciliação depois do incidente sabe exatamente o que passou por onde.

O resultado. O plano B existe antes do incidente e é acionado por chamada de API. Vale notar o limite: a troca é uma decisão humana ou do seu orquestrador — o BB não retenta automaticamente em outro adquirente (seção 15).

Caso 3 — Uma operação de cobrança recorrente descobre quanto está perdendo em recusa Cenário ilustrativo

Contexto. Empresa de software por assinatura, cerca de 6 mil cobranças de cartão por mês.

A dor. O painel do gateway mostrava transações aprovadas e recusadas, mas não respondia à pergunta que o financeiro fazia: qual o efeito disso no caixa, mês a mês, por método. Todo fechamento consumia um dia de exportação de CSV e tabela dinâmica.

A solução com o BB. GET /payments/api/v1/analytics/metrics devolve volume, contagem, aprovadas, falhas, taxa de sucesso, ticket médio e volume estornado no período, filtrável por provedor. POST /analytics/compute congela o número do período em um snapshot, e GET /analytics/time-series devolve a série para o gráfico. Tudo escopado na organização, sem exportação.

O resultado. A conversa de fechamento passa a ser sobre a recusa, não sobre montar a planilha que mostra a recusa.

Caso 4 — Por que empresas grandes usam mais de um adquirente Referência de mercado

Contexto. A existência de uma categoria inteira de produto — os orquestradores de pagamento, como Malga, Yuno, Spreedly e Primer — é a evidência mais direta de que operar com um adquirente só é um problema que o mercado paga para resolver. Nenhuma dessas empresas processa pagamento: elas vendem a camada que fala com vários processadores.

A dor do mercado. Os três argumentos que essas empresas usam publicamente são sempre os mesmos: recuperar venda recusada tentando em outro adquirente, não parar quando um provedor fica indisponível, e ter poder de negociação por poder migrar volume. Nenhum deles é possível com integração única.

A aritmética que a Yuno publica ilustra bem por que a categoria existe: um lojista de US$ 100 milhões por ano com 90% de aprovação deixa US$ 10 milhões na mesa; subir para 93% recupera US$ 3 milhões sem nenhum tráfego adicional (Yuno, 04/08/2025). A Gr4vy afirma ganho de "3%+ na taxa de autorização" ao adotar estratégia multi-PSP (Gr4vy). Trate os dois como número de fornecedor, não como estudo independente — nenhuma das duas publica metodologia, e nesta redação não localizamos estudo primário de analista quantificando o ganho de retentativa em segundo adquirente. O que é verificável é o tamanho do problema que elas atacam: os US$ 157 bilhões de declínio indevido do levantamento PYMNTS/Spreedly citado na seção 2.

Como a Catalisa endereça. O PaymentProviderConfig por organização e a interface PaymentProvider entregam a fundação: vários adquirentes vivos ao mesmo tempo, escolha por transação, mesmo contrato de código. O que ainda não entregamos é o roteamento automático — regra de negócio que decide sozinha para onde mandar e retenta no segundo. Isso está no roadmap e está declarado na seção 15, não vendido como pronto.

O resultado. Você não precisa contratar um orquestrador separado para ter mais de um adquirente. Precisa contratar um se o que você quer é a regra automática de retentativa hoje.

Caso 5 — O Pix virou o meio de pagamento de varejo, e quem não o oferece paga por isso Referência de mercado

Contexto. Em 2025 o Pix movimentou R$ 29,6 trilhões em 71,3 bilhões de transações no Brasil. O recorte que interessa a quem vende — o Pix entre pessoa e empresa — foram 30,6 bilhões de transações e R$ 3,5 trilhões, crescimento de 38,4% em quantidade sobre o ano anterior. Em julho de 2026 o Pix de varejo passou o Pix entre pessoas em quantidade de transações pela primeira vez (Banco Central, Estatísticas do Pix, consulta em 2026-08-16).

A dor do mercado. O Pix é bem mais barato para o lojista do que o cartão — nas tabelas públicas, entre 0,99% e 1,19% contra 3,19% a 4,99% no crédito à vista (§6) — e liquida na hora, sem os 30 dias do crédito. Mesmo assim, muita operação continua oferecendo só cartão, porque adicionar Pix significava abrir uma segunda integração, com segunda conciliação e segundo tratamento de confirmação.

Como a Catalisa endereça. Pix não é integração separada aqui: é o valor PIX no campo paymentMethod do mesmo intent. O QR code volta em providerData.pixQrCode, a confirmação chega pelo mesmo webhook, e a métrica sai no mesmo GET /analytics/metrics que o cartão. O código do checkout que já cria intent de cartão passa a criar de Pix mudando um campo.

O resultado. A decisão de oferecer o meio mais barato deixa de ter custo de engenharia associado. Vale a ressalva honesta: quem processa Pix é o provedor que você configurar — o Payments não é participante do arranjo Pix e não tem conta no Banco Central.


05Mercado e diferenciaisnegócio

O tamanho da mesa. Os cartões movimentaram R$ 4,5 trilhões no Brasil em 2025, alta de 10,1% sobre 2024, em 48,1 bilhões de transações — cerca de 132 milhões de pagamentos por dia. Compras não presenciais somaram R$ 1,1 trilhão (+18,3%), e a ABECS projeta que 2026 ultrapasse R$ 5 trilhões pela primeira vez (ABECS, Balanço do setor 2025, publicado em 11/02/2026).

Ao lado disso, o Pix parou de ser tendência. Em 2025 foram 71,3 bilhões de transações movimentando R$ 29,6 trilhões; só o Pix entre pessoa e empresa — o que interessa a quem vende — foram 30,6 bilhões de transações e R$ 3,5 trilhões, com alta de 38,4% em quantidade. Em julho de 2026, pela primeira vez, o Pix de varejo (46,4% das transações) passou o Pix entre pessoas (39,5%) (Banco Central, Estatísticas do Pix, consulta em 2026-08-16).

A consequência comercial é direta: o meio de pagamento que mais cresce é também o mais barato para o lojista, e quem não consegue oferecer os dois pelo mesmo fluxo está escolhendo entre conversão e margem sem precisar.

Panorama. O mercado brasileiro de aceitação de pagamento tem três camadas que costumam ser confundidas. Na base estão os adquirentes e subadquirentes, que efetivamente processam e liquidam — Cielo, Rede, Getnet, Stone, PagBank, Mercado Pago. Acima deles, os gateways e plataformas que embrulham essa capacidade numa API decente — Stripe, Pagar.me, Adyen, Asaas, Iugu. E numa terceira camada, mais recente, os orquestradores — Malga, Yuno, Spreedly, Primer — que não processam nada e vendem exatamente a capacidade de falar com vários dos de baixo.

O Payments da Catalisa mora na terceira camada, com uma diferença de origem: ele não é um produto avulso de orquestração, é o pedaço de pagamento de um catálogo que já tem identidade, cobrança recorrente, pedido e entrega de evento. Quem compra orquestrador puro está resolvendo pagamento. Quem usa o Payments normalmente já está usando o Billing ou o Commerce, e o pagamento é a etapa que faltava.

CritérioCatalisa PaymentsStripeAdyenPagar.meMalga
O que éCamada de orquestraçãoGateway + adquirenteAdquirente globalGateway (Stone)Orquestrador
Processa e liquidaNãoSimSimSimNão
Mais de um adquirenteSim, por organizaçãoNãoNãoNãoSim
Retentativa automática entre adquirentesNão (ver §15)N/AN/AN/ASim
Modelo de dadosPayment intentPayment intentPayment sessionTransação/pedidoPayment intent
Vem com identidade, billing e pedidoSim, mesmo catálogoNãoNãoNãoNão
Dado de cartão na nossa infraestruturaNão, só tokenN/AN/AN/ANão, tokeniza
CustoPrecificação em definiçãoPercentual por transaçãoNegociado por volumePercentual por transaçãoPor transação orquestrada

Sobre meios de pagamento. A tabela não compara cobertura de Pix, boleto e carteira por fornecedor de propósito: essa matriz muda com frequência e por país, e um dado desatualizado numa proposta comercial custa caro. Confirme na documentação pública de cada um na data da sua análise. O que vale para nós é estrutural — o Payments não implementa Pix nem boleto por conta própria, ele expõe o que o provedor configurado oferece. Hoje, na prática: Pix e boleto chegam pelo PagSeguro, e Pix e cartão pelo Stripe.

Nossos diferenciais

  1. Vários adquirentes sem contratar um orquestrador. A configuração de provedor é por organização e permite mais de uma ativa, com escolha por transação. Isso não é difícil de copiar tecnicamente — é difícil de copiar para quem já vendeu integração única, porque significa admitir que o cliente vai embora mais fácil.
  2. O contrato canônico é o produto. CanonicalPaymentIntent, CanonicalTransaction, CanonicalRefund. Cada gateway novo é uma classe que implementa dez métodos; nada acima dela muda. O custo de adicionar o terceiro provedor é o mesmo do segundo, o que não é verdade em integração ponto a ponto.
  3. O intent já nasce ligado ao resto da operação. billingInvoiceId e commerceOrderId são campos indexados do próprio modelo. Um orquestrador externo não tem como ter isso, porque ele não sabe o que é a sua fatura.
  4. Credencial de gateway criptografada e nunca devolvida. As chaves ficam em AES-256-GCM com chave mestra fora do banco, e a serialização de resposta do provider config simplesmente não tem o campo. Não existe endpoint que devolva a chave do seu adquirente.

Quando escolher o concorrente. Se você processa cartão em vários países e o problema é taxa de aprovação em cross-border, a Adyen tem adquirência própria em mercados onde nós dependemos de quem você contratar — e isso não se compensa com arquitetura. Se você precisa de regra automática de retentativa entre adquirentes hoje, um orquestrador dedicado como Malga ou Yuno entrega isso agora e nós não (§15). Se o seu produto é global, o time é pequeno e a prioridade é velocidade de integração com a melhor documentação disponível, o Stripe direto continua sendo a escolha mais rápida — e nós, aliás, falamos com ele. E se você quer uma solução brasileira única com antecipação, split e conta tudo no mesmo lugar, Pagar.me, Asaas ou Iugu resolvem sem camada extra. O Payments ganha quando o problema é não ficar preso a um adquirente e quando pagamento é uma peça de uma operação maior que já roda na Catalisa.


06Modelo de cobrança e ROInegócio

Unidade de cobrança. Precificação em definição. O Payments não substitui a taxa do adquirente: você continua pagando o MDR do gateway que contratou, direto com ele. O que se discute aqui é o custo da camada de orquestração e do histórico.

O que dispara custo.

DriverPor que ele importa
Volume transacionadoÉ o que dimensiona o valor entregue e o custo de armazenar histórico e webhooks
Número de payment intents criadosCada intent é uma chamada ao gateway e um registro com ciclo de vida próprio
Número de provedores configurados por organizaçãoCada provedor ativo é uma superfície a mais de webhook e de conciliação

O que a conta precisa comparar. Antes de qualquer número, o desenho da comparação é este:

  Cenário A — integração direta       Cenário B — com o Payments
  ────────────────────────────        ──────────────────────────
  MDR do adquirente                   MDR do(s) adquirente(s)
  + engenharia da integração          + camada Payments
  + manutenção por gateway            + engenharia da 1ª integração (uma vez)
  + custo de migrar (alto, adiado)    + custo de migrar ≈ configuração
  + venda perdida sem 2ª tentativa

O ganho não está na taxa por transação — essa é do adquirente nos dois cenários. Está em duas coisas mensuráveis dentro da sua operação: o custo de engenharia que não se repete a cada gateway, e a taxa efetiva que você consegue quando pode de fato migrar volume para outro fornecedor.

A referência de mercado que ancora a conta. Estes são preços de tabela, publicados pelos próprios fornecedores e consultados em 2026-08-16. Servem para dimensionar ordem de grandeza, não para fechar proposta: Cielo e PagBank dizem explicitamente que a taxa é negociável, e operação com volume paga menos que a tabela.

FornecedorCrédito à vistaDébitoPixFonte
Stripe Brasil3,99% + R$ 0,393,99% + R$ 0,391,19%stripe.com/br/pricing
PagBank (repasse em 30 dias)3,19%1,99%0% nos 30 primeiros diaspagbank.com.br
PagBank (repasse na hora)4,99%1,99%idemidem
Cielo (plano aluguel)3,45%1,19%0,99% após 30 diascielo.com.br/planos

Duas leituras saltam dessa tabela, e as duas são argumento comercial.

Primeira: receber antes tem preço, e ele é grande. Na curva do PagBank, o mesmo crédito à vista custa 3,19% com repasse em 30 dias e 4,99% recebendo na hora — 1,8 ponto percentual pela antecipação. Isso importa porque o parcelado sem juros respondeu por 42,6% do valor transacionado no crédito em 2025, R$ 1,8 trilhão (ABECS): o descasamento de caixa é estrutural no varejo brasileiro, e ele é pago em MDR.

Segunda: Pix e cartão não custam a mesma coisa, nem de longe. Entre 0,99% e 1,19% de tabela no Pix contra 3,19% a 4,99% no crédito à vista, a diferença é de 2 a 4 pontos percentuais — sobre R$ 10 milhões transacionados, algo entre R$ 200 mil e R$ 400 mil por ano. O Payments não reduz nenhuma dessas taxas, elas são do adquirente. O que ele faz é remover o motivo técnico para você não oferecer o meio mais barato ao lado do mais conveniente, no mesmo intent e com a mesma conciliação.

Como fazer a sua conta. Pegue o volume transacionado do último trimestre e a diferença entre a taxa que você paga e a melhor proposta que recebeu. Multiplique. Compare com o custo de migrar hoje, que na integração direta é um projeto de engenharia e no Payments é cadastro de provedor. Essa subtração é o ROI, e ela é específica da sua operação — por isso não damos um número genérico aqui.

Sobre os números acima. São taxas de tabela na data indicada, e mudam. Nenhuma delas é preço da Catalisa. Não conseguimos consultar publicamente as tabelas de Mercado Pago, Pagar.me, Asaas, Iugu, Getnet e Rede nesta redação — a ausência delas aqui não é julgamento sobre esses fornecedores. Antes de usar qualquer cifra desta seção numa proposta, reconfira na página do fornecedor na data da apresentação.


07Arquitetura

                            HTTP
                              │
  ┌───────────────────────────┴──────────────────────────────────────┐
  │ Hono app  basePath('/payments')  + applyCommonMiddleware          │
  │   bodyLimit 1MB · CORS · security headers · rate limit global     │
  │                                                                   │
  │  /api/v1/provider-configs   providerConfigRouter    (7 rotas)     │
  │  /api/v1/intents            paymentIntentRouter     (6 rotas)     │
  │  /api/v1/refunds            refundRouter            (3 rotas)     │
  │  /api/v1/links              paymentLinkRouter       (5 rotas)     │
  │  /api/v1/webhooks           webhookRouter           (2 rotas)     │
  │  /api/v1/analytics          analyticsRouter         (3 rotas)     │
  │  /health                    versão do serviço                     │
  └───────────────────────────┬──────────────────────────────────────┘
                              │ authMiddleware → requirePermission
                              │ → requireOrganization → Zod
  ┌───────────────────────────┴──────────────────────────────────────┐
  │ services/                                                         │
  │   PaymentProviderConfigService  credenciais, default, fábrica     │
  │   PaymentIntentService          criar, listar, cancelar           │
  │   PaymentProcessingService      charge, capture, tentativas       │
  │   RefundService                 estorno total e parcial           │
  │   PaymentLinkService            link, sync, desativação           │
  │   WebhookHandlerService         assinatura, dedup, efeito         │
  │   AnalyticsService              métricas e snapshots              │
  └────────────┬────────────────────────────────┬────────────────────┘
               │                                │
  ┌────────────┴──────────────┐   ┌─────────────┴────────────────────┐
  │ repositories/ (Prisma)    │   │ providers/  PaymentProvider       │
  │ schema "payments"         │   │  ┌──────────┬──────────┐          │
  │  7 modelos                │   │  │  Stripe  │PagSeguro │ externos │
  └───────────────────────────┘   │  ├──────────┼──────────┤          │
                                  │  │   Link   │   Mock   │ internos │
  ┌───────────────────────────┐   │  └──────────┴──────────┘          │
  │ Redis — stream de eventos │   └───────────────┬───────────────────┘
  │ payments.intent.*         │                   │ HTTPS
  └───────────────────────────┘                   ▼
                                          gateway do cliente

Decisões não óbvias.

  • O modelo de dados é o Payment Intent, copiado do Stripe de propósito. PaymentIntent + PaymentTransaction (tentativas) + PaymentRefund, com clientSecret, captureMode automático ou manual e idempotencyKey, é literalmente o desenho do Stripe. Isso é escolha, não coincidência: quando o integrador já sabe o que é um intent, o que é capturar depois de autorizar e por que existe tentativa numerada, o custo de integração cai para perto de zero. Adotar um modelo mental que o mercado já conhece vale mais que inventar um melhor. O preço é que ficamos parecidos com um provedor específico — mitigado pelo fato de o PagSeguroProvider mapear o mundo dele (WAITING, IN_ANALYSIS, PAID) para o mesmo vocabulário canônico.

  • Provider é objeto criado por requisição, não singleton injetado. createPaymentProvider(tipo, credenciais, settings) monta a instância na hora, a partir da configuração da organização daquela chamada. Não existe cliente de gateway compartilhado entre tenants, então não existe o bug clássico de a credencial de um cliente vazar para a requisição de outro por cache mal escopado. O custo é instanciar um objeto por chamada, o que é irrelevante perto de uma ida ao gateway.

  • Credenciais criptografadas com AES-256-GCM, não com hash. Diferente de senha, a chave do gateway precisa ser recuperada em texto claro para assinar a chamada. Então é cifra autenticada com PAYMENTS_CREDENTIAL_MASTER_KEY (32 bytes em hex), guardada no formato iv:authTag:ciphertext. O GCM detecta adulteração do registro no banco; um ciphertext editado falha na decifragem em vez de virar credencial estranha.

  • O corpo cru do webhook é preservado antes de qualquer JSON.parse. O HMAC do Stripe é calculado sobre os bytes exatos recebidos. Reserializar o objeto muda espaçamento e ordem de chave, e a assinatura não bate. Por isso webhook.router.tsc.req.text() primeiro e passa rawBody até o provider. É um detalhe pequeno que quebra a integração inteira quando esquecido.

  • A verificação de assinatura tem um interruptor de rollout. PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT controla o que acontece quando um provider não consegue verificar. Em off — o padrão — a requisição é aceita e registra aviso; em on, é rejeitada. Isso existe para permitir ligar verificação em produção sem derrubar integração de um dia para o outro. Em produção o valor deve ser on — ver seção 13.

  • Evento é publicado depois da escrita no banco, sem bloquear a resposta. A mudança de estado é gravada primeiro; a publicação em Redis vem em seguida. Pagamento confirmado que não conseguiu emitir evento continua confirmado. A recuperação de evento perdido é o PaymentWebhookEvent guardado cru e o próprio estado do intent, consultável a qualquer momento.

  • Idempotência é do lado de cá, por chave única no banco. PaymentIntent.idempotencyKey é @unique. Repetir a criação com a mesma chave devolve 409 CONFLICT em vez de criar um segundo intent. Note que isso é diferente do comportamento do Stripe, que devolve o recurso original — a escolha aqui foi falhar alto para o chamador perceber a repetição.

Monolito vs. standalone. Em monolito, src/app.ts monta o app do Payments no mesmo processo; em standalone, main.ts sobe na porta MODULE_PAYMENTS_PORT (padrão 3027). O comportamento das rotas é idêntico: as proteções globais são aplicadas pelo próprio app.ts do módulo via applyCommonMiddleware, então não dependem do monolito estar na frente.


08Conceitos e modelo de dados

Glossário

TermoSignifica
Provider configA configuração de um gateway para uma organização: tipo, credenciais cifradas, segredo de webhook, modo teste ou produção. Uma organização pode ter várias.
Payment intentA intenção de cobrar um valor. É o registro que vive do início ao fim e responde "em que pé está essa cobrança".
TransactionUma tentativa concreta de cobrança dentro de um intent, numerada por attemptNumber. Um intent pode ter várias.
RefundUm estorno, total ou parcial, sobre um intent já capturado ou concluído.
Payment linkUma URL de cobrança autocontida, com valor, título, métodos permitidos e limite de usos.
Webhook eventO evento cru recebido do gateway, guardado com payload completo e marca de processamento.
Analytics snapshotMétricas congeladas de um período, para não recalcular agregação a cada consulta.
Capture modeAUTOMATIC cobra e finaliza numa etapa; MANUAL autoriza agora e captura depois.
Checkout typeOnde o cliente digita os dados: TRANSPARENT (na sua tela), EMBEDDED (componente do provedor), HOSTED (página do provedor), PAYMENT_LINK.
CanonicalO formato neutro de resposta (CanonicalPaymentIntent e afins). É o que sai do provider e o que o resto do BB entende.

Modelo de dados — schema payments no PostgreSQL.

Modelo PrismaTabelaPropósitoCampos-chave
PaymentProviderConfigpayments.payment_provider_configsGateway configurado para uma organizaçãocredentials (cifrado), webhookSecret, isDefault, isActive, isLiveMode, único (organizationId, name)
PaymentIntentpayments.payment_intentsCiclo de vida de uma cobrançastatus, amount, capturedAmount, refundedAmount, externalId, idempotencyKey (único), billingInvoiceId, commerceOrderId
PaymentTransactionpayments.payment_transactionsCada tentativa de cobrançaattemptNumber, status, cardBrand, cardLast4, pixQrCode, boletoBarcode, failureReason
PaymentRefundpayments.payment_refundsEstorno de um intentamount, status, reason, isDispute
PaymentLinkpayments.payment_linksLink de cobrançaurl, shortUrl, status, maxUsages, usageCount, allowedMethods
PaymentWebhookEventpayments.payment_webhook_eventsEvento cru do gatewaypayload, processed, error, único (configId, externalEventId)
PaymentAnalyticsSnapshotpayments.payment_analytics_snapshotsMétricas congeladas por períodoperiodType, totalVolume, successCount, avgTicket, refundVolume

Valores monetários são Decimal(15,2) no banco — nunca ponto flutuante. Na fronteira com provedores que trabalham em centavos, utils/money.ts converte com toCents/fromCents.

Enumerações

EnumValores
PaymentProviderTypeSTRIPE · PAGSEGURO · MOCK_PAYMENT · LINK
PaymentIntentStatusPENDING · PROCESSING · AUTHORIZED · CAPTURED · COMPLETED · FAILED · CANCELLED · EXPIRED
PaymentMethodTypeCREDIT_CARD · DEBIT_CARD · PIX · BOLETO · PAYMENT_LINK · WALLET_APPLE_PAY · WALLET_GOOGLE_PAY
CheckoutTypeTRANSPARENT · EMBEDDED · HOSTED · PAYMENT_LINK
PaymentCaptureModeAUTOMATIC · MANUAL
PaymentTransactionStatusPENDING · PROCESSING · AUTHORIZED · CAPTURED · FAILED · CANCELLED
PaymentRefundStatusPENDING · PROCESSING · COMPLETED · FAILED
PaymentLinkStatusACTIVE · EXPIRED · DEACTIVATED · COMPLETED
AnalyticsPeriodTypeDAILY · WEEKLY · MONTHLY

Máquina de estados do payment intent

Este é o diagrama que um integrador precisa ter na mesa. As transições abaixo são as que existem no código.

                        POST /intents
                             │
                             ▼
                       ┌───────────┐
              ┌────────│  PENDING  │────────┐
              │        └─────┬─────┘        │
              │              │              │  POST /:id/cancel
   POST /:id/charge          │              ▼
              │              │        ┌───────────┐
              ▼              │        │ CANCELLED │  (terminal)
       ┌────────────┐        │        └───────────┘
       │ PROCESSING │        │              ▲
       └──────┬─────┘        │              │ POST /:id/cancel
              │              │              │
    resposta do provedor     │              │
              │              │              │
    ┌─────────┼─────────┬────┴──────────────┤
    │         │         │                   │
    ▼         ▼         ▼                   │
 CAPTURED  CAPTURED  FAILED/erro            │
 & modo    & modo    ┌──────────┐           │
 AUTOMATIC MANUAL    │  FAILED  │(terminal) │
    │         │      └──────────┘           │
    ▼         ▼                             │
┌───────────┐ ┌────────────┐                │
│ COMPLETED │ │ AUTHORIZED │────────────────┘
└───────────┘ └──────┬─────┘
 (terminal)          │ POST /:id/capture
                     ▼
               ┌──────────┐
               │ CAPTURED │  (terminal)
               └──────────┘

  Estorno NÃO é um estado. POST /refunds sobre um intent CAPTURED ou
  COMPLETED cria um PaymentRefund e soma em intent.refundedAmount.
  O status do intent não muda — não existe REFUNDED no enum.

  Webhook pode mover o intent para COMPLETED, PROCESSING, AUTHORIZED,
  FAILED ou CANCELLED, conforme o status que o provedor enviar.

  EXPIRED existe no enum mas nenhuma transição do código chega nele
  hoje — ver §15.

Regras de transição que o código impõe

OperaçãoSó a partir deErro se fora disso
POST /:id/chargePENDING400Cannot charge intent in status: X
POST /:id/captureAUTHORIZED400Cannot capture intent in status: X. Must be AUTHORIZED.
POST /:id/cancelPENDING ou AUTHORIZED400Cannot cancel intent in status: X
POST /refundsintent em CAPTURED ou COMPLETED400Must be CAPTURED or COMPLETED.
POST /links/:id/deactivatelink ACTIVE400Cannot deactivate link in status: X

Estorno também valida o teto: amount não pode passar de intent.amount - intent.refundedAmount. Omitir amount estorna o valor cheio do intent.

Ciclo de vida do payment link

   POST /links
        │
        ▼
   ┌────────┐  POST /:id/deactivate   ┌─────────────┐
   │ ACTIVE │ ──────────────────────▶ │ DEACTIVATED │
   └───┬────┘                         └─────────────┘
       │
       │  usageCount >= maxUsages (via webhook)
       │  ou POST /:id/sync vê pagamento no provedor
       ▼
  ┌───────────┐
  │ COMPLETED │
  └───────────┘

  EXPIRED existe no enum; a expiração por `expiresAt` não é aplicada
  por rotina automática hoje — ver §15.

09Referência da API

Prefixo: /payments. Todas as rotas abaixo, exceto as duas de webhook, exigem authMiddleware (JWT), a permissão indicada e requireOrganization — token sem organizationId recebe 403 antes da regra de negócio.

Paginação segue o padrão do catálogo: page[number] e page[size] (padrão 1 e 20). Filtros vão em filter[campo]. O corpo aceita tanto o formato JSON:API ({"data":{"attributes":{...}}}) quanto o objeto plano.

Configurações de provedor — /payments/api/v1/provider-configs

MétodoRotaDescriçãoPermissão
POST/payments/api/v1/provider-configsCadastra um gateway (testa a conexão antes de salvar)PAYMENTS_ADMIN
GET/payments/api/v1/provider-configsLista configurações, paginadoPAYMENTS_READ
GET/payments/api/v1/provider-configs/:idBusca uma configuraçãoPAYMENTS_READ
PATCH/payments/api/v1/provider-configs/:idAtualiza (revalida conexão se trocar credencial)PAYMENTS_ADMIN
DELETE/payments/api/v1/provider-configs/:idExclusão lógica. Responde 204PAYMENTS_ADMIN
POST/payments/api/v1/provider-configs/:id/set-defaultMarca como padrão da organizaçãoPAYMENTS_ADMIN
POST/payments/api/v1/provider-configs/:id/testTesta a conexão com as credenciais salvasPAYMENTS_ADMIN

Filtros da listagem: filter[providerType], filter[isActive].

Payment intents — /payments/api/v1/intents

MétodoRotaDescriçãoPermissão
POST/payments/api/v1/intentsCria a intenção de cobrança. Responde 201PAYMENTS_WRITE
GET/payments/api/v1/intentsLista intents, paginadoPAYMENTS_READ
GET/payments/api/v1/intents/:idBusca um intentPAYMENTS_READ
POST/payments/api/v1/intents/:id/chargeExecuta a cobrança no provedorPAYMENTS_WRITE
POST/payments/api/v1/intents/:id/captureCaptura um valor autorizadoPAYMENTS_WRITE
POST/payments/api/v1/intents/:id/cancelCancela o intent no provedor e localmentePAYMENTS_WRITE

Filtros da listagem: filter[configId], filter[status], filter[paymentMethod], filter[search] (procura em e-mail, nome e descrição), filter[startDate], filter[endDate], filter[minAmount], filter[maxAmount].

Estornos — /payments/api/v1/refunds

MétodoRotaDescriçãoPermissão
POST/payments/api/v1/refundsCria estorno total ou parcial. Responde 201PAYMENTS_REFUND
GET/payments/api/v1/refundsLista estornos, paginadoPAYMENTS_READ
GET/payments/api/v1/refunds/:idBusca um estornoPAYMENTS_READ

Filtros da listagem: filter[status], filter[startDate], filter[endDate].

PAYMENTS_REFUND é uma permissão separada de propósito: quem pode cobrar não devolve dinheiro por padrão.

MétodoRotaDescriçãoPermissão
POST/payments/api/v1/linksCria link de cobrança. Responde 201PAYMENTS_WRITE
GET/payments/api/v1/linksLista links, paginadoPAYMENTS_READ
GET/payments/api/v1/links/:idBusca um linkPAYMENTS_READ
POST/payments/api/v1/links/:id/syncConsulta o provedor e atualiza o status localPAYMENTS_READ
POST/payments/api/v1/links/:id/deactivateDesativa o link no provedor e localmentePAYMENTS_WRITE

Filtros da listagem: filter[configId], filter[status].

sync altera dados mas exige apenas PAYMENTS_READ no código atual. É intencional no sentido de que a origem da verdade é o provedor e a operação é uma leitura ativa, mas se a sua política exige permissão de escrita para qualquer mutação, trate sync como exceção conhecida.

Webhooks — /payments/api/v1/webhooks

MétodoRotaDescriçãoAutenticação
POST/payments/api/v1/webhooks/:providerTypeRecebe evento do gatewaySem JWT — assinatura do provedor
GET/payments/api/v1/webhooks/healthSonda simples do receptorPública

:providerType aceita stripe, pagseguro, mock_payment ou link (o valor é normalizado para maiúsculas). A organização é identificada pelo header X-Organization-Id ou pelo query param organizationId — configure isso na URL de webhook cadastrada no painel do gateway.

Analytics — /payments/api/v1/analytics

MétodoRotaDescriçãoPermissão
GET/payments/api/v1/analytics/metricsMétricas agregadas do períodoPAYMENTS_READ
GET/payments/api/v1/analytics/time-seriesSérie histórica a partir dos snapshotsPAYMENTS_READ
POST/payments/api/v1/analytics/computeCalcula e grava um snapshot. Responde 201PAYMENTS_ADMIN

Query de metrics e time-series: configId, startDate, endDate, e periodType (só em time-series). Sem datas, a janela padrão é os últimos 30 dias.

Saúde

MétodoRotaDescrição
GET/payments/healthNome e versão do serviço

POST /payments/api/v1/provider-configs

Cadastra um gateway. A conexão é testada antes de gravar — credencial errada falha aqui, não na primeira cobrança de um cliente.

Request

{
  "name": "stripe-producao",
  "providerType": "STRIPE",
  "credentials": { "secretKey": "SUBSTITUA_PELA_SUA_CHAVE" },
  "isDefault": true,
  "isActive": true,
  "isLiveMode": true,
  "webhookSecret": "SUBSTITUA_PELO_SEGREDO_DO_WEBHOOK",
  "settings": { "sandbox": false }
}
CampoTipoObrigatórioDescrição
namestring (1–100)SimNome interno. Único dentro da organização
providerTypeSTRIPE | PAGSEGURO | MOCK_PAYMENT | LINKSimQual driver usar
credentialsobject<string,string>SimChaves do gateway. Cifradas na gravação, nunca devolvidas
isDefaultbooleanNãoMarca como padrão da organização
isActivebooleanNãoPadrão true
isLiveModebooleanNãoPadrão false. É rótulo informativo, não altera endpoint
webhookSecretstringNãoSegredo usado para verificar a assinatura do webhook
settingsobjectNãoAjustes do driver, ex.: {"sandbox": true} no PagSeguro

Chaves esperadas em credentials por driver:

DriverChavesObservação
STRIPEsecretKeyChave secreta da API
PAGSEGUROtoken, opcionalmente sandbox: "true"Token da API v4
LINKbaseUrl (opcional)Gera links próprios, sem gateway externo
MOCK_PAYMENTnenhumaObjeto vazio {} serve

Resposta 201

{
  "data": {
    "type": "payment-provider-config",
    "id": "0f2c9c1e-6d4f-4a3a-9c6c-2f0f2e0a1b33",
    "links": { "self": "/api/v1/payments/provider-configs/0f2c9c1e-..." },
    "attributes": {
      "name": "stripe-producao",
      "providerType": "STRIPE",
      "isDefault": true,
      "isActive": true,
      "isLiveMode": true,
      "settings": { "sandbox": false },
      "createdAt": "2026-08-16T12:00:00.000Z",
      "updatedAt": "2026-08-16T12:00:00.000Z"
    }
  }
}

Repare no que não volta: credentials e webhookSecret. Não existe endpoint que devolva esses valores.

Erros

StatusQuando
400Corpo reprovado no Zod, ou Failed to connect to provider: ...
403Sem PAYMENTS_ADMIN, ou token sem organizationId
409Já existe configuração com esse name na organização

POST /payments/api/v1/intents

Cria a intenção de cobrança e já registra o intent correspondente no gateway.

Request

{
  "amount": 149.90,
  "currency": "BRL",
  "paymentMethod": "PIX",
  "captureMode": "AUTOMATIC",
  "checkoutType": "TRANSPARENT",
  "customerEmail": "cliente@exemplo.com.br",
  "customerName": "Maria Souza",
  "description": "Parcela 3/12 — contrato 88213",
  "idempotencyKey": "contrato-88213-parcela-3",
  "billingInvoiceId": "8f6a2b40-1f2c-4c7e-9a01-3b6d2e5f7a10"
}
CampoTipoObrigatórioDescrição
amountnumber > 0SimValor em unidade monetária, não em centavos
currencystring (3)NãoPadrão BRL
configIduuidNãoProvedor a usar. Omitido, usa o padrão da organização
paymentMethodenumNãoCREDIT_CARD, DEBIT_CARD, PIX, BOLETO, PAYMENT_LINK, WALLET_APPLE_PAY, WALLET_GOOGLE_PAY
captureModeAUTOMATIC | MANUALNãoPadrão AUTOMATIC
checkoutTypeenumNãoPadrão TRANSPARENT
customerEmailstring (e-mail)NãoRepassado ao provedor
customerNamestringNãoRepassado ao provedor
descriptionstringNãoAparece na fatura do cliente em vários gateways
metadataobjectNãoLivre, guardado como JSONB
idempotencyKeystringNãoÚnico no banco. Repetir devolve 409
billingInvoiceIduuidNãoFatura do Billing que originou a cobrança
commerceOrderIduuidNãoPedido do Commerce que originou a cobrança
successUrl / cancelUrlstring (URL)NãoUsados no checkout hospedado
paymentTokenstringNãoToken do meio de pagamento, gerado no cliente pelo SDK do provedor

Resposta 201 — o objeto do intent, com providerData carregando o que o gateway devolveu:

{
  "data": {
    "type": "payment-intent",
    "id": "7c3f8a5b-2e1d-4c9a-8f70-11a2b3c4d5e6",
    "attributes": {
      "status": "PENDING",
      "amount": 149.9,
      "currency": "BRL",
      "capturedAmount": 0,
      "refundedAmount": 0,
      "paymentMethod": "PIX",
      "captureMode": "AUTOMATIC",
      "externalId": "pi_3Q...",
      "providerData": {
        "clientSecret": "pi_3Q..._secret_...",
        "checkoutUrl": null,
        "pixQrCode": "00020126580014br.gov.bcb.pix...",
        "boletoUrl": null
      },
      "billingInvoiceId": "8f6a2b40-1f2c-4c7e-9a01-3b6d2e5f7a10",
      "createdAt": "2026-08-16T12:00:00.000Z"
    }
  }
}

Erros

StatusQuando
400Corpo inválido, ou o provedor recusou (Stripe error: ..., PagSeguro error: ...)
403Sem PAYMENTS_WRITE, ou token sem organizationId
404configId não existe na organização, ou não há provedor padrão configurado
409idempotencyKey já usada

POST /payments/api/v1/intents/:id/charge

Executa a cobrança. Só aceita intent em PENDING. Cada chamada cria uma PaymentTransaction com attemptNumber incremental — inclusive quando falha, com failureReason preenchido.

Request (corpo opcional)

{ "paymentToken": "pm_1Q...", "paymentMethod": "CREDIT_CARD" }

O paymentToken é gerado no navegador ou no app, pelo SDK do provedor, a partir dos dados do cartão. Ele nunca passa pelo seu servidor nem pelo nosso — ver seção 14.

Resposta 200 — o intent atualizado. O status final depende do captureMode:

Provedor devolveucaptureModeStatus do intentEvento publicado
CAPTUREDAUTOMATICCOMPLETED, com capturedAmount = amountpayments.intent.completed
CAPTUREDMANUALAUTHORIZEDpayments.intent.authorized
AUTHORIZEDqualquerAUTHORIZEDpayments.intent.authorized
FAILED ou erroqualquerFAILEDpayments.intent.failed

Erros

StatusQuando
400Cannot charge intent in status: X — só PENDING cobra
400Provedor recusou. A transação fica gravada como FAILED com o motivo
404Intent não existe nesta organização

POST /payments/api/v1/refunds

Estorna total ou parcialmente.

Request

{
  "intentId": "7c3f8a5b-2e1d-4c9a-8f70-11a2b3c4d5e6",
  "amount": 50.00,
  "reason": "Cancelamento parcial solicitado pelo cliente"
}
CampoTipoObrigatórioDescrição
intentIduuidSimIntent a estornar. Precisa estar CAPTURED ou COMPLETED
amountnumber > 0NãoOmitido, estorna o valor cheio do intent
reasonstringNãoMotivo, guardado e repassado ao provedor quando ele aceita

Resposta 201 — o estorno criado. O refundedAmount do intent é somado no mesmo fluxo. O status do intent não muda — não existe REFUNDED no enum; a evidência do estorno é o refundedAmount e o registro em payment_refunds.

Erros

StatusQuando
400Intent em status não estornável, ou amount acima do saldo estornável
403Sem PAYMENTS_REFUND
404Intent não existe nesta organização

POST /payments/api/v1/webhooks/:providerType

Recebe evento do gateway. Não usa JWT. A confiança vem da assinatura do provedor.

O que acontece, em ordem:

  1. Valida se :providerType é conhecido.
  2. X-Organization-Id (header) ou organizationId (query). Sem isso, 400.
  3. Preserva o corpo cru antes de qualquer parse — é sobre esses bytes que o HMAC é calculado.
  4. Localiza a configuração ativa daquele tipo de provedor na organização.
  5. Verifica a assinatura com o webhookSecret da configuração.
  6. Deduplica por (configId, externalEventId) — evento repetido responde 200 sem reprocessar.
  7. Guarda o evento cru em payment_webhook_events.
  8. Aplica o efeito: atualiza status do intent, do estorno ou o uso do link.
  9. Marca o evento como processado. Se o passo 8 falhar, grava o erro no evento e ainda responde 200, para o gateway não entrar em retentativa infinita — o evento fica lá para reprocessamento.

Resposta 200

{ "received": true }

Erros

StatusQuando
400Unknown provider type: X, corpo não é JSON, ou falta a organização
400Invalid webhook signature
404Não há configuração ativa desse provedor nessa organização

A URL a cadastrar no painel do gateway fica assim:

https://SEU-HOST/payments/api/v1/webhooks/stripe?organizationId=SUA-ORGANIZACAO

10Início rápido

Do zero a um pagamento concluído, sem gateway real e sem cartão — usando o driver MOCK_PAYMENT, que existe exatamente para isso.

Os comandos abaixo não foram executados nesta redação. Eles usam o ambiente local (bun run dev, monolito na porta 3000). Em staging, troque a base pela URL do serviço e as credenciais pelas de AMBIENTES.md. Nunca use credencial de produção em script de exemplo.

0. Preparar o ambiente

docker-compose up -d postgres redis minio
export PAYMENTS_CREDENTIAL_MASTER_KEY=$(openssl rand -hex 32)
bun run dev

1. Autenticar

BASE=http://localhost:3000

TOKEN=$(curl -s -X POST $BASE/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@test.com",
    "password": "password123",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

2. Cadastrar o provedor de teste

CONFIG=$(curl -s -X POST $BASE/payments/api/v1/provider-configs \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "sandbox-local",
    "providerType": "MOCK_PAYMENT",
    "credentials": {},
    "isDefault": true
  }')

CONFIG_ID=$(echo "$CONFIG" | jq -r '.data.id')
echo "config: $CONFIG_ID"
{ "data": { "type": "payment-provider-config", "id": "…",
  "attributes": { "name": "sandbox-local", "providerType": "MOCK_PAYMENT",
                  "isDefault": true, "isActive": true, "isLiveMode": false } } }

3. Criar o payment intent

INTENT=$(curl -s -X POST $BASE/payments/api/v1/intents \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "amount": 149.90,
    "currency": "BRL",
    "paymentMethod": "PIX",
    "description": "Primeiro pagamento",
    "idempotencyKey": "primeiro-teste-001"
  }')

INTENT_ID=$(echo "$INTENT" | jq -r '.data.id')
echo "$INTENT" | jq '.data.attributes | {status, amount, externalId, providerData}'
{
  "status": "PENDING",
  "amount": 149.9,
  "externalId": "mock_pi_…",
  "providerData": { "pixQrCode": "00020126580014br.gov.bcb.pix…" }
}

4. Cobrar

curl -s -X POST $BASE/payments/api/v1/intents/$INTENT_ID/charge \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{}' | jq '.data.attributes | {status, capturedAmount}'
{ "status": "COMPLETED", "capturedAmount": 149.9 }

O provedor de teste sempre aprova, e como o captureMode é AUTOMATIC, o intent vai direto de PENDING para COMPLETED.

5. Estornar metade

curl -s -X POST $BASE/payments/api/v1/refunds \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"intentId\":\"$INTENT_ID\",\"amount\":74.95,\"reason\":\"teste\"}" \
  | jq '.data.attributes | {status, amount}'

curl -s $BASE/payments/api/v1/intents/$INTENT_ID \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.data.attributes | {status, amount, refundedAmount}'
{ "status": "COMPLETED", "amount": 149.9, "refundedAmount": 74.95 }

O intent continua COMPLETED — o estorno aparece em refundedAmount, como descrito na seção 8.

6. Ver o resultado no analytics

curl -s "$BASE/payments/api/v1/analytics/metrics" \
  -H "Authorization: Bearer $TOKEN" | jq .data

Quando for para o gateway de verdade, o único passo que muda é o 2: troque MOCK_PAYMENT por STRIPE com a sua chave. Os passos 3 a 6 são idênticos — que é justamente o ponto do building block.


11Receitas

Autorizar agora e capturar depois

Útil quando você só cobra ao despachar: reserva o limite do cliente na hora do pedido e captura na expedição.

# 1. Intent com captura manual
INTENT_ID=$(curl -s -X POST $BASE/payments/api/v1/intents \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"amount": 320.00, "paymentMethod": "CREDIT_CARD", "captureMode": "MANUAL"}' \
  | jq -r '.data.id')

# 2. Cobrar com o token gerado no cliente → intent fica AUTHORIZED
curl -s -X POST $BASE/payments/api/v1/intents/$INTENT_ID/charge \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"paymentToken": "TOKEN_GERADO_NO_FRONTEND"}' | jq '.data.attributes.status'

# 3. Dias depois, capturar (valor total ou parcial)
curl -s -X POST $BASE/payments/api/v1/intents/$INTENT_ID/capture \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"amount": 320.00}' | jq '.data.attributes | {status, capturedAmount}'

Armadilhas.

  • Autorização tem prazo de validade no adquirente, não aqui. Passou do prazo, a captura falha no provedor mesmo com o intent ainda AUTHORIZED no nosso banco. Capture dentro da janela do seu adquirente.
  • Capturar valor menor que o autorizado funciona, mas o restante não é liberado por chamada nossa — o desbloqueio é regra do adquirente.
  • Depois de CAPTURED não há novo capture. Devolver dinheiro é POST /refunds.

Trocar o adquirente padrão sem deploy

# Ver quem está ativo
curl -s "$BASE/payments/api/v1/provider-configs?filter[isActive]=true" \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {id, name: .attributes.name, default: .attributes.isDefault}'

# Testar o secundário antes de promover
curl -s -X POST $BASE/payments/api/v1/provider-configs/$SECUNDARIO/test \
  -H "Authorization: Bearer $TOKEN" | jq

# Promover
curl -s -X POST $BASE/payments/api/v1/provider-configs/$SECUNDARIO/set-default \
  -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.isDefault'

Armadilhas.

  • set-default afeta intents novos. Os que já existem continuam amarrados ao configId de origem — e precisam continuar, porque só o provedor que autorizou consegue capturar ou estornar.
  • Um provedor inativo não pode virar padrão: a chamada volta 400.
  • Cadastre o webhook do provedor secundário antes de promover. Sem isso, os pagamentos saem por ele e nenhuma confirmação volta.
  • Teste com POST /:id/test antes. É a diferença entre descobrir a credencial errada agora ou no primeiro cliente.

Receber e reprocessar um webhook

Configure no painel do gateway:

https://SEU-HOST/payments/api/v1/webhooks/stripe?organizationId=SUA-ORGANIZACAO

Para conferir se o evento chegou e o que ele fez:

# O efeito é visível no próprio intent
curl -s $BASE/payments/api/v1/intents/$INTENT_ID \
  -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'

Armadilhas.

  • Sem webhookSecret na configuração do provedor, o Stripe não tem como ser verificado. Com PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT=on isso vira rejeição — que é o comportamento correto e o motivo de cadastrar o segredo junto da credencial.
  • O evento é deduplicado por (configId, externalEventId). Reenviar o mesmo evento pelo painel do gateway responde 200 e não reprocessa.
  • Falha ao aplicar o efeito não vira erro HTTP: a resposta é 200 com o erro gravado no payment_webhook_events. Isso evita tempestade de retentativa do gateway, mas significa que 200 não é prova de que o efeito aconteceu. Confirme pelo status do intent.
  • Em ambiente local o gateway não alcança seu localhost. Use túnel, ou use a receita seguinte.

Confirmar pagamento sem webhook

Quando o webhook não é possível — desenvolvimento local, firewall, evento perdido — o link de pagamento tem consulta ativa ao provedor:

curl -s -X POST $BASE/payments/api/v1/links/$LINK_ID/sync \
  -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'

Armadilhas.

  • sync existe só para link de pagamento. Para intent não há endpoint equivalente hoje (seção 15); o caminho é o webhook.
  • Quando o sync detecta a conclusão, ele incrementa usageCount e publica payments.link.completed uma única vez, na virada de ACTIVE para COMPLETED. Chamar de novo não republica.
curl -s -X POST $BASE/payments/api/v1/links \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "amount": 89.90,
    "title": "Mensalidade — turma de agosto",
    "description": "Acesso até 30/09",
    "maxUsages": 30,
    "allowedMethods": ["PIX", "CREDIT_CARD"],
    "expiresAt": "2026-09-30T23:59:59.000Z"
  }' | jq '.data.attributes | {url, shortUrl, status}'

Armadilhas.

  • expiresAt é gravado, mas não há rotina que expire o link automaticamente hoje. Para encerrar de fato, chame deactivate.
  • maxUsages só é contado quando chega confirmação — webhook ou sync. Sem uma das duas, o contador não anda.
  • allowedMethods aceita CREDIT_CARD, DEBIT_CARD, PIX e BOLETO. Carteira digital não entra aqui, embora exista no enum de método do intent.

Fechar o mês com número congelado

# Congela o snapshot do mês
curl -s -X POST $BASE/payments/api/v1/analytics/compute \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"periodType":"MONTHLY",
       "startDate":"2026-08-01T00:00:00.000Z",
       "endDate":"2026-08-31T23:59:59.000Z"}' \
  | jq '.data.attributes | {totalVolume, successCount, failedCount, avgTicket}'

# Série para o gráfico
curl -s "$BASE/payments/api/v1/analytics/time-series?periodType=MONTHLY" \
  -H "Authorization: Bearer $TOKEN" | jq '.data | length'

Armadilhas.

  • compute faz upsert por (organização, provedor, tipo de período, início, fim). Rodar de novo com os mesmos limites atualiza o snapshot, não duplica.
  • time-seriessnapshots. Sem compute executado antes, a série volta vazia — não é bug.
  • metrics calcula na hora sobre os intents e não depende de snapshot. Use metrics para o número de agora, time-series para a história.

12Integração com outros building blocks

Building blockComo se relacionaObrigatório
IAMEmite o token com organizationId e as permissões PAYMENTS_*. Sem ele nenhuma rota respondeSim
BillingO intent carrega billingInvoiceId; os eventos de conclusão permitem dar a fatura por pagaNão
CommerceO intent carrega commerceOrderId, ligando o pagamento ao pedidoNão
Webhooks EngineRepassa os eventos payments.* para os sistemas do clienteNão
BaaSComplementar, não sobreposto — Payments recebe dinheiro, BaaS movimenta contaNão
Audit TrailRegistra quem cadastrou provedor, quem estornou, quem trocou o padrãoNão
CustomersOrigem dos dados de pagador que vão em customerEmail e customerNameNão

Payments não é BaaS, e a diferença importa na venda.

PaymentsBaaS
Direção do dinheiroEntra (você recebe)Sai e entra em conta própria
O que éAceitação: checkout, gateway, adquirenteConta bancária, transferência, chave Pix
ContraparteAdquirente / gateway (Stripe, PagSeguro)Instituição de pagamento (Celcoin, Fitbank, QI Tech)
Objeto centralPayment intentConta e transação
Pergunta que responde"O cliente pagou?""Quanto tenho e para quem transferi?"

Uma operação completa costuma usar os dois: o Payments recebe do cliente final, o BaaS movimenta o saldo depois. Vender um no lugar do outro gera frustração no primeiro mês.

A cadeia completa de uma cobrança

  ┌──────────┐   fatura vence     ┌──────────┐
  │ Billing  │───────────────────▶│          │
  └──────────┘  billingInvoiceId  │          │
                                  │ Payments │──▶ gateway ──▶ adquirente
  ┌──────────┐   pedido fechado   │          │
  │ Commerce │───────────────────▶│          │◀── webhook ◀───────┘
  └──────────┘  commerceOrderId   └────┬─────┘
                                       │ payments.intent.completed
                                       │ payments.refund.completed
                                       ▼
                              ┌──────────────────┐
                              │ Webhooks Engine  │──▶ sistemas do cliente
                              └──────────────────┘
                                       │
                              ┌────────┴─────────┐
                              │   Audit Trail    │
                              └──────────────────┘

  Em todo o caminho, o organizationId vem do token emitido pelo IAM —
  nunca do corpo da requisição.

Este diagrama é o argumento comercial: um orquestrador de pagamento avulso resolve a caixa do meio. A fatura que originou a cobrança, o pedido, a entrega do evento e o rastro de auditoria são peças que ele não tem — e que aqui já estão no mesmo catálogo, falando a mesma linguagem de organização e permissão.

Eventos publicados — todos no stream Redis, consumíveis pelo Webhooks Engine:

EventoQuando
payments.intent.createdIntent criado
payments.intent.processingCobrança em andamento no provedor
payments.intent.authorizedAutorizado, aguardando captura
payments.intent.capturedCaptura manual concluída
payments.intent.completedPagamento concluído
payments.intent.failedCobrança recusada ou com erro
payments.intent.cancelledIntent cancelado
payments.refund.createdEstorno criado, ainda não concluído
payments.refund.completedEstorno concluído
payments.refund.failedEstorno falhou
payments.link.createdLink gerado
payments.link.completedLink atingiu o limite de usos ou foi pago
payments.link.deactivatedLink desativado

O payload de intent carrega intentId, organizationId, status, amount e, quando existem, billingInvoiceId e commerceOrderId — o suficiente para o consumidor conciliar sem consultar de volta.


13Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
PAYMENTS_CREDENTIAL_MASTER_KEYChave mestra AES-256-GCM das credenciais de gateway. 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32Sim, para usar o módulo
MODULE_PAYMENTS_PORTPorta no modo standaloneNão3027
MODULE_PAYMENTS_URLURL do módulo, usada por outros BBs em standaloneNão''
PAYMENTS_WEBHOOK_BASE_URLBase pública para montar URLs de webhookNão
PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENTon rejeita webhook não verificável; off aceita e registra aviso. Use on em produçãoNãooff
DATABASE_URLPostgreSQLSim
REDIS_URLRedis — stream de eventos e rate limitSim
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith

A chave mestra é lida direto de process.env no momento do uso. Trocá-la torna ilegíveis todas as credenciais já gravadas — não há rotação automática. Guarde com SOPS, nunca em .env versionado.

Dependências de infraestrutura

DependênciaPara quê
PostgreSQLSchema payments — sete tabelas
RedisPublicação dos eventos payments.* e contadores de rate limit
Gateway do clienteStripe (api.stripe.com) ou PagSeguro (api.pagseguro.com, ou sandbox.api.pagseguro.com com settings.sandbox = true)

Saída HTTPS para o gateway precisa estar liberada, e a URL de webhook precisa ser alcançável pela internet.

Drivers disponíveis

DriverFala com gateway externoEstado
STRIPESimCobertura completa da interface, com verificação de assinatura de webhook implementada
PAGSEGUROSimPedido, Pix, boleto, captura e estorno via API v4. Link de pagamento e assinatura de webhook incompletos — ver §15
LINKNãoGera links próprios sem gateway. Não movimenta dinheiro
MOCK_PAYMENTNãoSimulador para desenvolvimento e teste. Sempre aprova

Limites e quotas

LimiteValor
Tamanho do corpo da requisição1 MB (bodyLimit)
Rate limit globalRATE_LIMIT_GLOBAL_MAX — padrão 10.000 req por janela de 60s
Página padrão / tamanhopage[number]=1, page[size]=20
Precisão monetáriaDecimal(15,2)
Nome da configuração de provedor1 a 100 caracteres, único por organização
Título do link de pagamento1 a 200 caracteres
Janela padrão do analyticsÚltimos 30 dias

Catálogo de erros

StatusCódigoSignificaO que fazer
400Corpo reprovado no Zod. O corpo da resposta traz o formato do Zod, não o envelope padrãoConfira campos e tipos contra §9
400VALIDATIONTransição de status proibida (Cannot charge intent in status: X)Confira o estado atual na máquina de estados (§8)
400VALIDATIONRefund amount X exceeds maximum refundable amount YConsulte refundedAmount do intent
400VALIDATIONStripe error: ... / PagSeguro error: ... — o gateway recusouA mensagem do provedor vem junto. Para cobrança, veja failureReason na transação
400VALIDATIONInvalid webhook signatureConfira o webhookSecret da configuração contra o do painel do gateway
400VALIDATIONPayment provider config is not activeReative a configuração ou informe outro configId
401UNAUTHORIZEDToken ausente, inválido ou expiradoRenove pelo IAM
403FORBIDDENFalta PAYMENTS_READ, PAYMENTS_WRITE, PAYMENTS_REFUND ou PAYMENTS_ADMINConfira as permissões do token
403Organization context requiredAutentique informando a organização
404NOT_FOUNDIntent, estorno, link ou configuração inexistente ou de outra organizaçãoO 404 é proposital: recurso de outro tenant não existe para você
404NOT_FOUNDDefault payment provider configNenhum provedor marcado como padrão. Use set-default ou passe configId
409CONFLICTidempotencyKey repetida, ou nome de configuração já usadoUse outra chave ou outro nome
503SERVICE_UNAVAILABLE... connection failed no teste de conexãoGateway fora do ar ou credencial inválida

Observabilidade.

  • GET /payments/health devolve nome e versão do serviço. É sonda de liveness, não verifica banco nem gateway.
  • GET /payments/api/v1/webhooks/health confirma que o receptor de webhook está de pé.
  • POST /provider-configs/:id/test é a sonda que realmente importa em operação: ela bate no gateway com a credencial salva. Vale monitorar periodicamente — credencial revogada aparece aqui antes de aparecer no cliente.
  • Toda falha de provedor é logada com o erro completo antes de virar AppError genérico. O motivo real da recusa está no log e em payment_transactions.failure_reason.
  • Webhook aceito sem verificação (enforcement=off) gera aviso no log com o provedor e o motivo. Esse aviso é o que você monitora durante o rollout da verificação.
  • Todo evento payments.* publicado é contabilizado na métrica de eventos por building block.

14Segurança e compliance

Isolamento entre tenants. Toda rota autenticada aplica requireOrganization, que devolve 403 quando o token não traz organizationId. O organizationId usado nas consultas vem sempre do token (user.organizationId) e nunca do corpo ou da query. Cada getById recarrega o registro e compara a organização antes de devolver — e quando não bate, a resposta é 404, não 403, para não confirmar a existência do recurso alheio. Estorno tem um passo a mais: como PaymentRefund não guarda organização, o serviço sobe até o intent e verifica a organização lá.

A exceção é o endpoint de webhook, que por natureza não tem token. Nele a organização vem do header ou da query, e a confiança está na assinatura do provedor — motivo pelo qual PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT=on e um webhookSecret cadastrado são requisitos de produção, não opcionais.

Dados de cartão: eles não chegam aqui. Isso merece ser dito com precisão, porque é o ponto que mais pesa numa avaliação técnica.

  • Nenhum schema Zod da API tem campo para número de cartão, CVV ou validade. Não há como enviar PAN para este building block, mesmo querendo.
  • O que a API aceita é paymentToken — um identificador opaco gerado no navegador ou no aplicativo pelo SDK do próprio gateway (Stripe.js e equivalentes), a partir dos dados que o cliente digita. O dado do cartão vai do dispositivo do portador direto para o gateway.
  • O que fica no nosso banco é o que PaymentTransaction declara: cardBrand e cardLast4 (VarChar(4)). Bandeira e quatro últimos dígitos, que são dados permitidos para exibição e conciliação.
  • Nenhuma tabela guarda PAN, CVV ou trilha magnética, e o providerResponse guardado é a resposta do gateway — que também não devolve esses dados.

Sobre PCI-DSS, com honestidade. A Catalisa não declara certificação PCI-DSS para este building block, e este documento não deve ser lido como atestado de conformidade. O que está descrito acima é a arquitetura: com tokenização no provedor, os dados de portador de cartão não transitam nem repousam na nossa infraestrutura, o que é exatamente o desenho que reduz o escopo de PCI de quem integra. O enquadramento formal do seu escopo — qual SAQ se aplica, o que precisa ser evidenciado — é determinado pelo seu adquirente e pelo seu avaliador (QSA), a partir da sua implementação de frontend. Se o seu frontend capturar dados de cartão e enviá-los para qualquer servidor seu, o escopo muda, e essa decisão está fora deste building block.

Credenciais de gateway. Cifradas com AES-256-GCM, chave mestra de 32 bytes vinda de PAYMENTS_CREDENTIAL_MASTER_KEY, formato iv:authTag:ciphertext. A cifra é autenticada: adulteração do registro no banco falha na decifragem em vez de produzir credencial silenciosamente errada. A serialização de resposta (toResponseData) não inclui credentials nem webhookSecretnenhum endpoint devolve a chave do seu gateway, nem para quem tem PAYMENTS_ADMIN. Perdeu, cadastra de novo.

Assinatura de webhook. O corpo cru é preservado antes de qualquer parse, porque o HMAC é calculado sobre os bytes exatos. Para Stripe, a verificação implementa o esquema oficial (t=<timestamp>,v1=<hmac>, HMAC-SHA256 sobre ${t}.${rawBody}), com comparação em tempo constante. Provedores cuja verificação ainda não está implementada seguem a política de PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT — ver §13 e §15.

Autenticação e permissões. Quatro permissões, com separação de dever embutida:

PermissãoConcede
PAYMENTS_READLeitura de intents, estornos, links, configurações e analytics; sync de link
PAYMENTS_WRITECriar intent, cobrar, capturar, cancelar; criar e desativar link
PAYMENTS_REFUNDCriar estorno — separada da escrita de propósito
PAYMENTS_ADMINGerir credenciais de gateway, definir padrão, testar conexão, calcular snapshot

Quem opera cobrança não devolve dinheiro, e quem opera cobrança não vê nem troca credencial de gateway. Essa divisão é o controle que auditoria financeira procura primeiro.

Retenção e exclusão. A configuração de provedor usa exclusão lógica (deletedAt) — o histórico de pagamento continua apontando para o provedor que o originou depois da remoção. Intents, transações, estornos e eventos de webhook não têm expurgo automático: são registro financeiro e ficam.

LGPD. O BB guarda customerEmail e customerName — dados pessoais — além do payload cru do webhook, que pode conter dados do pagador enviados pelo gateway. Trate payment_webhook_events como base com dado pessoal no seu inventário. Não há rotina automatizada de eliminação; atender a pedido de exclusão exige processo explícito, ponderado contra a obrigação de retenção de registro financeiro.


15Limitações conhecidas

LimitaçãoImpactoSituação
Sem retentativa automática entre adquirentesVários provedores podem conviver e a troca é por API, mas nada retenta sozinho no segundo quando o primeiro recusa. O roteamento é decisão do chamadorRoadmap — é o que separa o BB de um orquestrador dedicado. Não anuncie failover automático
Sem regra de roteamentoNão há política do tipo "Pix vai no provedor A, cartão no B" nem roteamento por taxa ou por bandeira. Só existe padrão e configId explícitoRoadmap
PagSeguro parcialmente implementadocreatePaymentLink e getPaymentLink devolvem URL construída localmente, sem chamar a API — não é link real do PagSeguro. getRefund devolve valor fixo sem consultar. Verificação de assinatura de webhook não implementadaParcial — para link de pagamento real, use Stripe
Assinatura de webhook só no StripePAGSEGURO e LINK não verificam assinatura. Com PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT=off (padrão) esses webhooks são aceitos com aviso no log; com on, rejeitadosParcial — ligue on em produção e use webhook só com provedor que verifica
Sem sync para payment intentPOST /:id/sync existe só para link. Para intent, a atualização depende de webhookRoadmap
EXPIRED nunca é atingidoExiste no enum de intent e de link, e expiresAt é gravado, mas nenhuma rotina expira nada. Intent abandonado fica PENDING para sempreEspecificado, não implementado
Sem estado de estorno no intentNão existe REFUNDED nem PARTIALLY_REFUNDED. Um intent totalmente estornado continua COMPLETED; a evidência é refundedAmountPor design — mas quebra a expectativa de quem vem do Stripe. Filtre por refundedAmount, não por status
Sem contestação (chargeback)PaymentRefund.isDispute existe no schema e nenhum caminho de código o preenche. Não há fluxo de disputaEspecificado, não implementado
Sem split de pagamentoNão há divisão de valor entre recebedores. Marketplace que precisa de split usa o recurso nativo do adquirenteNão implementado
Sem cobrança recorrente nativaNão há assinatura nem tokenização de cartão para recorrência. Recorrência se monta com o Billing gerando intentsPor design — recorrência é do Billing
Sem antecipação de recebível e sem conciliação de liquidaçãoO BB sabe que a cobrança foi aprovada; não sabe quando o dinheiro cai nem quanto de taxa foi retidoFora de escopo — o extrato de liquidação é do adquirente
Analytics conta estorno de forma imprecisarefundCount em GET /analytics/metrics conta os intents do período, não os estornos. Para contagem exata, use GET /refunds com filtro de dataConhecido — o refundVolume é confiável, o refundCount não
Múltiplas configurações do mesmo provedor e webhookCom duas configurações STRIPE ativas na mesma organização, o webhook é associado à primeira encontrada. Duas contas do mesmo gateway na mesma organização não são atendidas corretamenteConhecido — use uma configuração ativa por tipo de provedor por organização
Idempotência falha altoRepetir criação com a mesma idempotencyKey devolve 409, e não o intent original como faz o StripePor design — mas trate 409 como "já criei", não como erro
Erro de validação com formato próprioO 400 de schema devolve o formato do Zod, diferente do envelope {error, message, details} do resto do catálogoConhecido — trate os dois formatos no cliente
sync de link exige só PAYMENTS_READÉ uma operação que altera dados sob permissão de leituraConhecido — ver §9

16Perguntas frequentes

Quais gateways funcionam de verdade hoje?

Dois falam com o mundo externo: Stripe, com cobertura completa da interface e verificação de assinatura de webhook, e PagSeguro, com pedido, Pix, boleto, captura e estorno pela API v4 — mas com link de pagamento e verificação de assinatura incompletos (§15). Os outros dois drivers não são gateways: MOCK_PAYMENT é simulador de desenvolvimento e LINK gera links internos sem movimentar dinheiro. Adicionar um adquirente novo é implementar dez métodos de uma interface; nada acima dela muda.

Vocês são PCI-DSS certificados?

Não declaramos certificação para este building block, e desconfie de quem responde essa pergunta rápido demais. O que afirmamos, e está no código: a API não tem campo para número de cartão nem CVV, o que trafega é um token gerado pelo SDK do gateway no dispositivo do cliente, e o banco guarda no máximo bandeira e quatro últimos dígitos. Dado de cartão não entra na nossa infraestrutura. O enquadramento formal do seu escopo de PCI é definido pelo seu adquirente e pelo seu avaliador, e depende de como o seu frontend captura os dados. Detalhes na seção 14.

Se o meu adquirente cair, o pagamento vai automaticamente para o outro?

Não. Isso é a pergunta mais importante desta lista, e a resposta honesta é não. O que existe é a fundação: mais de um provedor ativo na mesma organização, escolha por transação via configId, e troca do padrão por chamada de API sem deploy. A decisão de virar a chave é sua ou do seu orquestrador. Retentativa automática está no roadmap (seção 15). Se você precisa dela hoje, um orquestrador dedicado entrega e nós não.

Qual a diferença entre Payments e BaaS?

Direção do dinheiro. Payments é aceitação: o cliente final paga você, através de um adquirente. BaaS é conta: você tem saldo, transfere por Pix ou TED, gerencia chaves. Um recebe, o outro movimenta. A maioria das operações completas usa os dois, e usar um esperando o comportamento do outro é a confusão mais cara que a gente vê. A tabela comparativa está na seção 12.

Por que meu payment intent continua PENDING depois que o cliente pagou?

Quase sempre porque o webhook não chegou. Confira, nesta ordem: a URL cadastrada no painel do gateway inclui ?organizationId=... ou o header equivalente; o webhookSecret da configuração bate com o do painel; e o host é alcançável pela internet — em localhost o gateway não chega. Para link de pagamento existe POST /links/:id/sync, que consulta o provedor ativamente. Para intent não há equivalente hoje.

Posso estornar mais de uma vez o mesmo pagamento?

Pode, desde que a soma não passe do valor. Cada chamada cria um PaymentRefund e soma em intent.refundedAmount; passar do teto volta 400 com o saldo estornável na mensagem. Repare que o status do intent não muda para nada parecido com "estornado" — quem vem do Stripe estranha. Para saber se um pagamento foi devolvido por inteiro, compare refundedAmount com amount.

Como faço cobrança recorrente de assinatura?

Não por aqui. O Payments cobra uma vez por intent e não guarda cartão para uso futuro. Recorrência é do Billing: ele gera a fatura no ciclo, cria o intent com billingInvoiceId e reage ao evento payments.intent.completed. Para pedido de loja, o equivalente é o Commerce com commerceOrderId. Cada uma das duas peças faz uma coisa, e é isso que permite trocar o adquirente sem mexer no ciclo de faturamento.

Trocar de gateway quebra o histórico?

Não. Cada intent guarda o configId que o originou, e a configuração antiga é excluída logicamente — a linha continua no banco. Intents antigos permanecem consultáveis, e capturar ou estornar um deles continua indo para o provedor de origem, que é o único que consegue fazer isso. O que muda de padrão são apenas os intents novos.

O que acontece se eu perder a PAYMENTS_CREDENTIAL_MASTER_KEY?

Todas as credenciais de gateway gravadas ficam ilegíveis, e não há recuperação — é cifra autenticada, não codificação. O caminho é cadastrar as credenciais de novo em cada configuração de provedor. Guarde essa chave com SOPS, junto dos outros segredos de produção, e trate a rotação como migração planejada, não como troca de variável.

Consigo usar dois adquirentes ao mesmo tempo, um para Pix e outro para cartão?

Consegue, mas a decisão é do seu código. Crie o intent de Pix passando o configId de um provedor e o de cartão passando o do outro. O que não existe é regra automática que faça esse roteamento por você (seção 15).


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md

Building blocks relacionados