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.
- 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ó
- 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
- 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.
| Atributo | Valor |
|---|---|
| Identificador | payments |
| Categoria | Financeiro |
| Escopo | Tenant (exige organizationId no token) |
| Porta (standalone) | 3027 |
| Path alias | @payments |
| Prefixo HTTP | /payments |
| Schema do banco | payments |
| Status | Beta desde 2026-03 |
| Depende de | PostgreSQL, 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_captureno Stripe,WAITINGno PagSeguro,pendingem outro. O seu código acaba com umswitchgigante 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
| Antes | Depois |
|---|---|
| Um SDK de gateway espalhado pelo código do produto | Uma interface PaymentProvider — o produto nunca importa o SDK |
| Trocar de adquirente é reescrever a integração | Trocar de adquirente é POST /provider-configs + set-default |
switch de status por gateway em cada serviço | Status canônico único, mapeado uma vez dentro do provider |
| Webhook com assinatura, dedup e log reimplementados a cada integração | Um endpoint por provedor, com verificação, deduplicação e payload cru guardado |
| Relatório de aprovação em planilha, por gateway | GET /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ério | Catalisa Payments | Stripe | Adyen | Pagar.me | Malga |
|---|---|---|---|---|---|
| O que é | Camada de orquestração | Gateway + adquirente | Adquirente global | Gateway (Stone) | Orquestrador |
| Processa e liquida | Não | Sim | Sim | Sim | Não |
| Mais de um adquirente | Sim, por organização | Não | Não | Não | Sim |
| Retentativa automática entre adquirentes | Não (ver §15) | N/A | N/A | N/A | Sim |
| Modelo de dados | Payment intent | Payment intent | Payment session | Transação/pedido | Payment intent |
| Vem com identidade, billing e pedido | Sim, mesmo catálogo | Não | Não | Não | Não |
| Dado de cartão na nossa infraestrutura | Não, só token | N/A | N/A | N/A | Não, tokeniza |
| Custo | Precificação em definição | Percentual por transação | Negociado por volume | Percentual por transação | Por 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
- 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.
- 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. - O intent já nasce ligado ao resto da operação.
billingInvoiceIdecommerceOrderIdsã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. - 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.
| Driver | Por 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 criados | Cada intent é uma chamada ao gateway e um registro com ciclo de vida próprio |
| Número de provedores configurados por organização | Cada 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.
| Fornecedor | Crédito à vista | Débito | Pix | Fonte |
|---|---|---|---|---|
| Stripe Brasil | 3,99% + R$ 0,39 | 3,99% + R$ 0,39 | 1,19% | stripe.com/br/pricing |
| PagBank (repasse em 30 dias) | 3,19% | 1,99% | 0% nos 30 primeiros dias | pagbank.com.br |
| PagBank (repasse na hora) | 4,99% | 1,99% | idem | idem |
| Cielo (plano aluguel) | 3,45% | 1,19% | 0,99% após 30 dias | cielo.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, comclientSecret,captureModeautomático ou manual eidempotencyKey, é 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 oPagSeguroProvidermapear 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 formatoiv: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 issowebhook.router.tslêc.req.text()primeiro e passarawBodyaté 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_ENFORCEMENTcontrola o que acontece quando um provider não consegue verificar. Emoff— o padrão — a requisição é aceita e registra aviso; emon, é 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 seron— 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
PaymentWebhookEventguardado 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 devolve409 CONFLICTem 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
| Termo | Significa |
|---|---|
| Provider config | A 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 intent | A intenção de cobrar um valor. É o registro que vive do início ao fim e responde "em que pé está essa cobrança". |
| Transaction | Uma tentativa concreta de cobrança dentro de um intent, numerada por attemptNumber. Um intent pode ter várias. |
| Refund | Um estorno, total ou parcial, sobre um intent já capturado ou concluído. |
| Payment link | Uma URL de cobrança autocontida, com valor, título, métodos permitidos e limite de usos. |
| Webhook event | O evento cru recebido do gateway, guardado com payload completo e marca de processamento. |
| Analytics snapshot | Métricas congeladas de um período, para não recalcular agregação a cada consulta. |
| Capture mode | AUTOMATIC cobra e finaliza numa etapa; MANUAL autoriza agora e captura depois. |
| Checkout type | Onde o cliente digita os dados: TRANSPARENT (na sua tela), EMBEDDED (componente do provedor), HOSTED (página do provedor), PAYMENT_LINK. |
| Canonical | O 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 Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
PaymentProviderConfig | payments.payment_provider_configs | Gateway configurado para uma organização | credentials (cifrado), webhookSecret, isDefault, isActive, isLiveMode, único (organizationId, name) |
PaymentIntent | payments.payment_intents | Ciclo de vida de uma cobrança | status, amount, capturedAmount, refundedAmount, externalId, idempotencyKey (único), billingInvoiceId, commerceOrderId |
PaymentTransaction | payments.payment_transactions | Cada tentativa de cobrança | attemptNumber, status, cardBrand, cardLast4, pixQrCode, boletoBarcode, failureReason |
PaymentRefund | payments.payment_refunds | Estorno de um intent | amount, status, reason, isDispute |
PaymentLink | payments.payment_links | Link de cobrança | url, shortUrl, status, maxUsages, usageCount, allowedMethods |
PaymentWebhookEvent | payments.payment_webhook_events | Evento cru do gateway | payload, processed, error, único (configId, externalEventId) |
PaymentAnalyticsSnapshot | payments.payment_analytics_snapshots | Métricas congeladas por período | periodType, 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
| Enum | Valores |
|---|---|
PaymentProviderType | STRIPE · PAGSEGURO · MOCK_PAYMENT · LINK |
PaymentIntentStatus | PENDING · PROCESSING · AUTHORIZED · CAPTURED · COMPLETED · FAILED · CANCELLED · EXPIRED |
PaymentMethodType | CREDIT_CARD · DEBIT_CARD · PIX · BOLETO · PAYMENT_LINK · WALLET_APPLE_PAY · WALLET_GOOGLE_PAY |
CheckoutType | TRANSPARENT · EMBEDDED · HOSTED · PAYMENT_LINK |
PaymentCaptureMode | AUTOMATIC · MANUAL |
PaymentTransactionStatus | PENDING · PROCESSING · AUTHORIZED · CAPTURED · FAILED · CANCELLED |
PaymentRefundStatus | PENDING · PROCESSING · COMPLETED · FAILED |
PaymentLinkStatus | ACTIVE · EXPIRED · DEACTIVATED · COMPLETED |
AnalyticsPeriodType | DAILY · 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ção | Só a partir de | Erro se fora disso |
|---|---|---|
POST /:id/charge | PENDING | 400 — Cannot charge intent in status: X |
POST /:id/capture | AUTHORIZED | 400 — Cannot capture intent in status: X. Must be AUTHORIZED. |
POST /:id/cancel | PENDING ou AUTHORIZED | 400 — Cannot cancel intent in status: X |
POST /refunds | intent em CAPTURED ou COMPLETED | 400 — Must be CAPTURED or COMPLETED. |
POST /links/:id/deactivate | link ACTIVE | 400 — Cannot 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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /payments/api/v1/provider-configs | Cadastra um gateway (testa a conexão antes de salvar) | PAYMENTS_ADMIN |
GET | /payments/api/v1/provider-configs | Lista configurações, paginado | PAYMENTS_READ |
GET | /payments/api/v1/provider-configs/:id | Busca uma configuração | PAYMENTS_READ |
PATCH | /payments/api/v1/provider-configs/:id | Atualiza (revalida conexão se trocar credencial) | PAYMENTS_ADMIN |
DELETE | /payments/api/v1/provider-configs/:id | Exclusão lógica. Responde 204 | PAYMENTS_ADMIN |
POST | /payments/api/v1/provider-configs/:id/set-default | Marca como padrão da organização | PAYMENTS_ADMIN |
POST | /payments/api/v1/provider-configs/:id/test | Testa a conexão com as credenciais salvas | PAYMENTS_ADMIN |
Filtros da listagem: filter[providerType], filter[isActive].
Payment intents — /payments/api/v1/intents
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /payments/api/v1/intents | Cria a intenção de cobrança. Responde 201 | PAYMENTS_WRITE |
GET | /payments/api/v1/intents | Lista intents, paginado | PAYMENTS_READ |
GET | /payments/api/v1/intents/:id | Busca um intent | PAYMENTS_READ |
POST | /payments/api/v1/intents/:id/charge | Executa a cobrança no provedor | PAYMENTS_WRITE |
POST | /payments/api/v1/intents/:id/capture | Captura um valor autorizado | PAYMENTS_WRITE |
POST | /payments/api/v1/intents/:id/cancel | Cancela o intent no provedor e localmente | PAYMENTS_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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /payments/api/v1/refunds | Cria estorno total ou parcial. Responde 201 | PAYMENTS_REFUND |
GET | /payments/api/v1/refunds | Lista estornos, paginado | PAYMENTS_READ |
GET | /payments/api/v1/refunds/:id | Busca um estorno | PAYMENTS_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.
Links de pagamento — /payments/api/v1/links
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /payments/api/v1/links | Cria link de cobrança. Responde 201 | PAYMENTS_WRITE |
GET | /payments/api/v1/links | Lista links, paginado | PAYMENTS_READ |
GET | /payments/api/v1/links/:id | Busca um link | PAYMENTS_READ |
POST | /payments/api/v1/links/:id/sync | Consulta o provedor e atualiza o status local | PAYMENTS_READ |
POST | /payments/api/v1/links/:id/deactivate | Desativa o link no provedor e localmente | PAYMENTS_WRITE |
Filtros da listagem: filter[configId], filter[status].
syncaltera dados mas exige apenasPAYMENTS_READno 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, tratesynccomo exceção conhecida.
Webhooks — /payments/api/v1/webhooks
| Método | Rota | Descrição | Autenticação |
|---|---|---|---|
POST | /payments/api/v1/webhooks/:providerType | Recebe evento do gateway | Sem JWT — assinatura do provedor |
GET | /payments/api/v1/webhooks/health | Sonda simples do receptor | Pú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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /payments/api/v1/analytics/metrics | Métricas agregadas do período | PAYMENTS_READ |
GET | /payments/api/v1/analytics/time-series | Série histórica a partir dos snapshots | PAYMENTS_READ |
POST | /payments/api/v1/analytics/compute | Calcula e grava um snapshot. Responde 201 | PAYMENTS_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étodo | Rota | Descrição |
|---|---|---|
GET | /payments/health | Nome 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 }
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–100) | Sim | Nome interno. Único dentro da organização |
providerType | STRIPE | PAGSEGURO | MOCK_PAYMENT | LINK | Sim | Qual driver usar |
credentials | object<string,string> | Sim | Chaves do gateway. Cifradas na gravação, nunca devolvidas |
isDefault | boolean | Não | Marca como padrão da organização |
isActive | boolean | Não | Padrão true |
isLiveMode | boolean | Não | Padrão false. É rótulo informativo, não altera endpoint |
webhookSecret | string | Não | Segredo usado para verificar a assinatura do webhook |
settings | object | Não | Ajustes do driver, ex.: {"sandbox": true} no PagSeguro |
Chaves esperadas em credentials por driver:
| Driver | Chaves | Observação |
|---|---|---|
STRIPE | secretKey | Chave secreta da API |
PAGSEGURO | token, opcionalmente sandbox: "true" | Token da API v4 |
LINK | baseUrl (opcional) | Gera links próprios, sem gateway externo |
MOCK_PAYMENT | nenhuma | Objeto 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
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod, ou Failed to connect to provider: ... |
403 | Sem PAYMENTS_ADMIN, ou token sem organizationId |
409 | Já 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"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
amount | number > 0 | Sim | Valor em unidade monetária, não em centavos |
currency | string (3) | Não | Padrão BRL |
configId | uuid | Não | Provedor a usar. Omitido, usa o padrão da organização |
paymentMethod | enum | Não | CREDIT_CARD, DEBIT_CARD, PIX, BOLETO, PAYMENT_LINK, WALLET_APPLE_PAY, WALLET_GOOGLE_PAY |
captureMode | AUTOMATIC | MANUAL | Não | Padrão AUTOMATIC |
checkoutType | enum | Não | Padrão TRANSPARENT |
customerEmail | string (e-mail) | Não | Repassado ao provedor |
customerName | string | Não | Repassado ao provedor |
description | string | Não | Aparece na fatura do cliente em vários gateways |
metadata | object | Não | Livre, guardado como JSONB |
idempotencyKey | string | Não | Único no banco. Repetir devolve 409 |
billingInvoiceId | uuid | Não | Fatura do Billing que originou a cobrança |
commerceOrderId | uuid | Não | Pedido do Commerce que originou a cobrança |
successUrl / cancelUrl | string (URL) | Não | Usados no checkout hospedado |
paymentToken | string | Não | Token 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
| Status | Quando |
|---|---|
400 | Corpo inválido, ou o provedor recusou (Stripe error: ..., PagSeguro error: ...) |
403 | Sem PAYMENTS_WRITE, ou token sem organizationId |
404 | configId não existe na organização, ou não há provedor padrão configurado |
409 | idempotencyKey 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 devolveu | captureMode | Status do intent | Evento publicado |
|---|---|---|---|
CAPTURED | AUTOMATIC | COMPLETED, com capturedAmount = amount | payments.intent.completed |
CAPTURED | MANUAL | AUTHORIZED | payments.intent.authorized |
AUTHORIZED | qualquer | AUTHORIZED | payments.intent.authorized |
FAILED ou erro | qualquer | FAILED | payments.intent.failed |
Erros
| Status | Quando |
|---|---|
400 | Cannot charge intent in status: X — só PENDING cobra |
400 | Provedor recusou. A transação fica gravada como FAILED com o motivo |
404 | Intent 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"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
intentId | uuid | Sim | Intent a estornar. Precisa estar CAPTURED ou COMPLETED |
amount | number > 0 | Não | Omitido, estorna o valor cheio do intent |
reason | string | Não | Motivo, 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
| Status | Quando |
|---|---|
400 | Intent em status não estornável, ou amount acima do saldo estornável |
403 | Sem PAYMENTS_REFUND |
404 | Intent 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:
- Valida se
:providerTypeé conhecido. - Lê
X-Organization-Id(header) ouorganizationId(query). Sem isso,400. - Preserva o corpo cru antes de qualquer parse — é sobre esses bytes que o HMAC é calculado.
- Localiza a configuração ativa daquele tipo de provedor na organização.
- Verifica a assinatura com o
webhookSecretda configuração. - Deduplica por
(configId, externalEventId)— evento repetido responde200sem reprocessar. - Guarda o evento cru em
payment_webhook_events. - Aplica o efeito: atualiza status do intent, do estorno ou o uso do link.
- 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
| Status | Quando |
|---|---|
400 | Unknown provider type: X, corpo não é JSON, ou falta a organização |
400 | Invalid webhook signature |
404 | Nã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
AUTHORIZEDno 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
CAPTUREDnão há novocapture. 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-defaultafeta intents novos. Os que já existem continuam amarrados aoconfigIdde 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/testantes. É 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
webhookSecretna configuração do provedor, o Stripe não tem como ser verificado. ComPROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT=onisso 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 responde200e não reprocessa. - Falha ao aplicar o efeito não vira erro HTTP: a resposta é
200com o erro gravado nopayment_webhook_events. Isso evita tempestade de retentativa do gateway, mas significa que200nã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.
syncexiste só para link de pagamento. Para intent não há endpoint equivalente hoje (seção 15); o caminho é o webhook.- Quando o
syncdetecta a conclusão, ele incrementausageCounte publicapayments.link.completeduma única vez, na virada deACTIVEparaCOMPLETED. Chamar de novo não republica.
Gerar um link de cobrança com limite de usos
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, chamedeactivate.maxUsagessó é contado quando chega confirmação — webhook ousync. Sem uma das duas, o contador não anda.allowedMethodsaceitaCREDIT_CARD,DEBIT_CARD,PIXeBOLETO. 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.
computefaz 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-serieslê snapshots. Semcomputeexecutado antes, a série volta vazia — não é bug.metricscalcula na hora sobre os intents e não depende de snapshot. Usemetricspara o número de agora,time-seriespara a história.
12Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token com organizationId e as permissões PAYMENTS_*. Sem ele nenhuma rota responde | Sim |
| Billing | O intent carrega billingInvoiceId; os eventos de conclusão permitem dar a fatura por paga | Não |
| Commerce | O intent carrega commerceOrderId, ligando o pagamento ao pedido | Não |
| Webhooks Engine | Repassa os eventos payments.* para os sistemas do cliente | Não |
| BaaS | Complementar, não sobreposto — Payments recebe dinheiro, BaaS movimenta conta | Não |
| Audit Trail | Registra quem cadastrou provedor, quem estornou, quem trocou o padrão | Não |
| Customers | Origem dos dados de pagador que vão em customerEmail e customerName | Não |
Payments não é BaaS, e a diferença importa na venda.
| Payments | BaaS | |
|---|---|---|
| Direção do dinheiro | Entra (você recebe) | Sai e entra em conta própria |
| O que é | Aceitação: checkout, gateway, adquirente | Conta bancária, transferência, chave Pix |
| Contraparte | Adquirente / gateway (Stripe, PagSeguro) | Instituição de pagamento (Celcoin, Fitbank, QI Tech) |
| Objeto central | Payment intent | Conta 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:
| Evento | Quando |
|---|---|
payments.intent.created | Intent criado |
payments.intent.processing | Cobrança em andamento no provedor |
payments.intent.authorized | Autorizado, aguardando captura |
payments.intent.captured | Captura manual concluída |
payments.intent.completed | Pagamento concluído |
payments.intent.failed | Cobrança recusada ou com erro |
payments.intent.cancelled | Intent cancelado |
payments.refund.created | Estorno criado, ainda não concluído |
payments.refund.completed | Estorno concluído |
payments.refund.failed | Estorno falhou |
payments.link.created | Link gerado |
payments.link.completed | Link atingiu o limite de usos ou foi pago |
payments.link.deactivated | Link 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ável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
PAYMENTS_CREDENTIAL_MASTER_KEY | Chave mestra AES-256-GCM das credenciais de gateway. 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32 | Sim, para usar o módulo | — |
MODULE_PAYMENTS_PORT | Porta no modo standalone | Não | 3027 |
MODULE_PAYMENTS_URL | URL do módulo, usada por outros BBs em standalone | Não | '' |
PAYMENTS_WEBHOOK_BASE_URL | Base pública para montar URLs de webhook | Não | — |
PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT | on rejeita webhook não verificável; off aceita e registra aviso. Use on em produção | Não | off |
DATABASE_URL | PostgreSQL | Sim | — |
REDIS_URL | Redis — stream de eventos e rate limit | Sim | — |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
A chave mestra é lida direto de
process.envno 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.envversionado.
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema payments — sete tabelas |
| Redis | Publicação dos eventos payments.* e contadores de rate limit |
| Gateway do cliente | Stripe (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
| Driver | Fala com gateway externo | Estado |
|---|---|---|
STRIPE | Sim | Cobertura completa da interface, com verificação de assinatura de webhook implementada |
PAGSEGURO | Sim | Pedido, Pix, boleto, captura e estorno via API v4. Link de pagamento e assinatura de webhook incompletos — ver §15 |
LINK | Não | Gera links próprios sem gateway. Não movimenta dinheiro |
MOCK_PAYMENT | Não | Simulador para desenvolvimento e teste. Sempre aprova |
Limites e quotas
| Limite | Valor |
|---|---|
| Tamanho do corpo da requisição | 1 MB (bodyLimit) |
| Rate limit global | RATE_LIMIT_GLOBAL_MAX — padrão 10.000 req por janela de 60s |
| Página padrão / tamanho | page[number]=1, page[size]=20 |
| Precisão monetária | Decimal(15,2) |
| Nome da configuração de provedor | 1 a 100 caracteres, único por organização |
| Título do link de pagamento | 1 a 200 caracteres |
| Janela padrão do analytics | Últimos 30 dias |
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | — | Corpo reprovado no Zod. O corpo da resposta traz o formato do Zod, não o envelope padrão | Confira campos e tipos contra §9 |
400 | VALIDATION | Transição de status proibida (Cannot charge intent in status: X) | Confira o estado atual na máquina de estados (§8) |
400 | VALIDATION | Refund amount X exceeds maximum refundable amount Y | Consulte refundedAmount do intent |
400 | VALIDATION | Stripe error: ... / PagSeguro error: ... — o gateway recusou | A mensagem do provedor vem junto. Para cobrança, veja failureReason na transação |
400 | VALIDATION | Invalid webhook signature | Confira o webhookSecret da configuração contra o do painel do gateway |
400 | VALIDATION | Payment provider config is not active | Reative a configuração ou informe outro configId |
401 | UNAUTHORIZED | Token ausente, inválido ou expirado | Renove pelo IAM |
403 | FORBIDDEN | Falta PAYMENTS_READ, PAYMENTS_WRITE, PAYMENTS_REFUND ou PAYMENTS_ADMIN | Confira as permissões do token |
403 | — | Organization context required | Autentique informando a organização |
404 | NOT_FOUND | Intent, estorno, link ou configuração inexistente ou de outra organização | O 404 é proposital: recurso de outro tenant não existe para você |
404 | NOT_FOUND | Default payment provider config | Nenhum provedor marcado como padrão. Use set-default ou passe configId |
409 | CONFLICT | idempotencyKey repetida, ou nome de configuração já usado | Use outra chave ou outro nome |
503 | SERVICE_UNAVAILABLE | ... connection failed no teste de conexão | Gateway fora do ar ou credencial inválida |
Observabilidade.
GET /payments/healthdevolve nome e versão do serviço. É sonda de liveness, não verifica banco nem gateway.GET /payments/api/v1/webhooks/healthconfirma 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
AppErrorgenérico. O motivo real da recusa está no log e empayment_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
PaymentTransactiondeclara:cardBrandecardLast4(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
providerResponseguardado é 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 webhookSecret — nenhum 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ão | Concede |
|---|---|
PAYMENTS_READ | Leitura de intents, estornos, links, configurações e analytics; sync de link |
PAYMENTS_WRITE | Criar intent, cobrar, capturar, cancelar; criar e desativar link |
PAYMENTS_REFUND | Criar estorno — separada da escrita de propósito |
PAYMENTS_ADMIN | Gerir 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ção | Impacto | Situação |
|---|---|---|
| Sem retentativa automática entre adquirentes | Vários provedores podem conviver e a troca é por API, mas nada retenta sozinho no segundo quando o primeiro recusa. O roteamento é decisão do chamador | Roadmap — é o que separa o BB de um orquestrador dedicado. Não anuncie failover automático |
| Sem regra de roteamento | Nã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ícito | Roadmap |
| PagSeguro parcialmente implementado | createPaymentLink 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 implementada | Parcial — para link de pagamento real, use Stripe |
| Assinatura de webhook só no Stripe | PAGSEGURO 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, rejeitados | Parcial — ligue on em produção e use webhook só com provedor que verifica |
Sem sync para payment intent | POST /:id/sync existe só para link. Para intent, a atualização depende de webhook | Roadmap |
EXPIRED nunca é atingido | Existe no enum de intent e de link, e expiresAt é gravado, mas nenhuma rotina expira nada. Intent abandonado fica PENDING para sempre | Especificado, não implementado |
| Sem estado de estorno no intent | Não existe REFUNDED nem PARTIALLY_REFUNDED. Um intent totalmente estornado continua COMPLETED; a evidência é refundedAmount | Por 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 disputa | Especificado, não implementado |
| Sem split de pagamento | Não há divisão de valor entre recebedores. Marketplace que precisa de split usa o recurso nativo do adquirente | Não implementado |
| Sem cobrança recorrente nativa | Não há assinatura nem tokenização de cartão para recorrência. Recorrência se monta com o Billing gerando intents | Por design — recorrência é do Billing |
| Sem antecipação de recebível e sem conciliação de liquidação | O BB sabe que a cobrança foi aprovada; não sabe quando o dinheiro cai nem quanto de taxa foi retido | Fora de escopo — o extrato de liquidação é do adquirente |
| Analytics conta estorno de forma imprecisa | refundCount em GET /analytics/metrics conta os intents do período, não os estornos. Para contagem exata, use GET /refunds com filtro de data | Conhecido — o refundVolume é confiável, o refundCount não |
| Múltiplas configurações do mesmo provedor e webhook | Com 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 corretamente | Conhecido — use uma configuração ativa por tipo de provedor por organização |
| Idempotência falha alto | Repetir criação com a mesma idempotencyKey devolve 409, e não o intent original como faz o Stripe | Por design — mas trate 409 como "já criei", não como erro |
| Erro de validação com formato próprio | O 400 de schema devolve o formato do Zod, diferente do envelope {error, message, details} do resto do catálogo | Conhecido — trate os dois formatos no cliente |
sync de link exige só PAYMENTS_READ | É uma operação que altera dados sob permissão de leitura | Conhecido — 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
Assinaturas, medição de uso e faturas sem pagar percentual sobre o que você fatura
CommerceCatálogo, estoque, preço e pedido multi-loja na mesma API
BaaSContas, Pix e TED por API, com o banco parceiro trocável sem reescrever a integração
IAMIdentidade, organizações e permissões para todo o catálogo de building blocks