Pagar.me (Stone) — Integração para o Frontend¶
Data: 2026-10-21 (revisto em 2026-10-31) | Status: no ar em produção desde 23/10/2026 (ADR-0169); cartão exige
billing_address desde 31/10/2026 (ADR-0192); webhook de produção sendo cadastrado (runbook abaixo); a conta não tem
ambiente de teste (DEBT-154) | Gateway: Pagar.me API v5
O que muda nos apps (cliente e painel), arquivo por arquivo: Especificação para os apps Flutter.
📋 Visão Geral¶
O FDPlay passa a ter três gateways em paralelo, escolhidos por GET /config/payment-gateway
(default_payment_gateway: asaas | stripe | pagarme):
| Gateway | Cartão | PIX | Dado de cartão na API |
|---|---|---|---|
| Asaas | Dados brutos (RSA) → API | Manual ou Pix Automático | Sim |
| Stripe | pm_xxx (SDK) |
PaymentIntent (período único) | Não |
| Pagar.me | token_xxx (checkout transparente) |
Pedido avulso por período | Não |
Diferenças que mudam a tela
- O cartão nunca passa pela API. O app tokeniza com a chave pública e envia só o
card_token. - Não existe PIX recorrente no Pagar.me. O PIX é um pedido do período (mesmo modelo do PIX da Stripe):
pagou, tem acesso até
expires_at; vencido, o cliente renova (PUT /customers/me/subscription/renew) e recebe um QR novo.pix_subscription_modenão se aplica ao Pagar.me. - Dinheiro em centavos na ponta do gateway (como a Stripe). A API continua falando reais onde sempre falou (
valuede estorno,info.pix.value). - O
card_tokené de uso único: errou, tokeniza de novo.
🔑 Endpoints¶
| Método | Endpoint | Auth | Descrição |
|---|---|---|---|
GET |
/api/v1/pagarme/config |
Nenhuma | public_key e account_id para tokenizar |
POST |
/api/v1/pagarme/subscribe |
JWT (Customer) | Assinar no cartão (plan_id, card_token, billing_address, promo_code?) |
POST |
/api/v1/pagarme/subscribe/pix |
JWT (Customer) | Assinar por PIX (pedido do período) |
GET |
/api/v1/pagarme/subscription |
JWT (Customer) | Status (confirma com o gateway se pending) |
PUT |
/api/v1/pagarme/subscription/credit-card |
JWT (Customer) | Trocar o cartão (card_token, billing_address) |
DELETE |
/api/v1/pagarme/subscription |
JWT (Customer) | Cancelar |
POST |
/api/v1/store/purchase/pagarme/card · /pix |
JWT (Customer) | Ingresso |
POST |
/api/v1/store/purchase/checkout/pagarme/card · /pix |
Nenhuma | Ingresso sem cadastro (buyer) |
POST |
/webhooks/pagarme |
Basic Auth + reconsulta | Webhook (server-to-server) |
GET/POST |
/api/v1/admin/pagarme/subscriptions… |
JWT (Admin) | Painel: listar, stats, cancel, payments, refund, refunds |
Já existentes e agora cientes do Pagar.me: GET /customers/me/subscription, GET /customers/me/pix-qr,
PUT /customers/me/subscription/renew (card_token, billing_address), DELETE /customers/me/subscription,
GET /me/ticket-orders/{id}/pix-qr, DELETE /me/ticket-orders/{id}, PUT /admin/config/payment-gateway
(gateway: "pagarme"; 409 se o ambiente não tem as chaves), POST /admin/refunds/by-charge (gateway: "pagarme").
📱 Fluxo no app¶
1. Chave pública¶
2. Tokenizar o cartão (direto no Pagar.me)¶
POST https://api.pagar.me/core/v5/tokens?appId={public_key}
{"type": "card", "card": {"number": "...", "holder_name": "...", "exp_month": 5, "exp_year": 2030, "cvv": "123"}}
→ {"id": "token_AbCdEfGhIjKlMnOp", ...}
Conferir no sandbox
O endpoint e o formato do token seguem a documentação do Pagar.me, mas ainda não foram exercitados
com chaves de teste (pk_test_…). Ver DEBT-154.
3. Assinar¶
POST /api/v1/pagarme/subscribe
{"plan_id": "plan-basic", "card_token": "token_AbCdEfGhIjKlMnOp", "promo_code": "BEMVINDO",
"billing_address": {"street": "Avenida Brigadeiro Faria Lima", "number": "2927", "complement": "8º andar",
"locality": "Jardim Paulistano", "city": "São Paulo", "region_code": "SP",
"postal_code": "01452000"}}
→ 201 {"docs": [{"status": "pending", "gateway": "pagarme", ...}], "info": {"pagarme_subscription_id": "sub_...", "payment_status": "future", "message": "..."}}
Endereço de cobrança é obrigatório (ADR-0192)
O Pagar.me recusa cartão salvo sem endereço (validation_error | billing | "value" is required): foi o
que derrubou o 1º checkout real, em 31/10/2026. Mande billing_address (PagarmeBillingAddress: postal_code
com 8 dígitos, street, city e region_code com a UF; number, locality e complement opcionais).
Só pode faltar se o cliente já tem address no cadastro; no checkout sem cadastro é sempre obrigatório. Sem endereço utilizável: 422 com
error.field = "billing_address", antes de qualquer cobrança. Vale também para trocar o cartão,
renovar e comprar ingresso no cartão.
A assinatura nasce pending; o webhook (ou a consulta de GET /customers/me/subscription, que o app já
faz em polling) a ativa. Cupom de primeiro mês vira desconto nativo de 1 ciclo no gateway.
Cartão recusado na 1ª cobrança. O Pagar.me cobra na criação. Se recusar ali, a resposta já é
402 PAYMENT_DECLINED, com error.reason (motivo do gateway, para log, não para a tela); a assinatura já
foi encerrada e o cliente pode tentar outro cartão (tokenizar de novo). Se a recusa só aparecer depois, o
polling devolve a assinatura com status: "cancelled" e pagarme_failure_reason: tratar igual, sair do
"Confirmando" e oferecer outro cartão. Nas consultas seguintes não há mais assinatura corrente (404).
Erros: 400 sem CPF no perfil · 400 assinatura viva · 402 cartão recusado · 422 sem endereço ·
429 outra operação em andamento ·
504 o gateway não respondeu (a assinatura pode ter sido criada: aguardar, não tentar de novo) ·
502 Pagar.me não configurado.
4. PIX¶
POST /api/v1/pagarme/subscribe/pix {"plan_id": "plan-basic"}
→ 201 {"info": {"pix": {"charge_id": "ch_...", "qr_code": "000201...", "qr_code_image_url": "https://...png",
"expiration_date": "<agora+10min>", "value": 19.9}}}
expiration_date é a nossa janela de 10 min (ADR-0095); o QR continua pagável no gateway por até 24 h e o
pagamento tardio é reconhecido pelo webhook. Reabrir o QR do mesmo pedido: GET /customers/me/pix-qr
(410 se a cobrança já não está aberta).
5. Ingresso¶
POST /store/purchase/pagarme/card com {event_id, quantity, card_token, billing_address, subscribe?}; …/pix com {event_id, quantity}.
A resposta do PIX traz pix.payload (copia-e-cola), pix.image_url e pix.expiration_date.
subscribe=true só vale no cartão e só arma a mensalidade se o cartão liquidar na hora (a cobrança começa no fim do período incluso).
Cartão recusado volta como erro (o gateway devolve 200 com failed; a API converte) e a capacidade é devolvida.
⚙️ Configuração (credentials.toml)¶
[production.pagarme]
account_id = "acc_..."
public_key = "pk_..."
secret_key = "sk_..."
# opcionais — Basic Auth que o Pagar.me envia ao webhook (painel → Webhooks)
webhook_user = ""
webhook_password = ""
# Trava (padrão false): enquanto false, `PUT /admin/config/payment-gateway` recusa `pagarme` (409).
# Ligue só quando os apps lerem `pagarme` e tokenizarem o cartão — app antigo cai em Stripe.
allow_as_default = false
Sem secret_key os pagamentos por esse gateway respondem 502 e PUT /admin/config/payment-gateway recusa pagarme (409).
Fora de produção (FDPLAY_ENV ≠ production) só vale chave de teste (sk_test_…): qualquer outra secret_key é
tratada como ausente (o mesmo 502) e a API avisa no log ao subir (Pagar.me desligado em development…). A conta não
tem ambiente de teste hoje, então a stack dev e a API local ficam com o Pagar.me desligado.
Webhook de produção — cadastro (runbook)¶
O Pagar.me não tem API para cadastrar o endpoint: é no painel (Configurações → Webhooks), por quem tem login na conta. Só existe a conta live, então o cadastro é direto em produção.
- Senha do Basic Auth. Gerar usuário e senha fortes e gravar em
[production.pagarme]docredentials.tomlde produção (webhook_user/webhook_password). Com os dois preenchidos, credencial errada ou ausente →401; vazios, o endpoint aceita sem conferir (fail-open declarado, a reconsulta continua protegendo) e loga umwarningpor entrega. Subir a config antes de cadastrar no painel — senão a 1ª entrega já volta 401. - URL:
https://fdplay-api.infraifd.com/webhooks/pagarme— sem/api/v1(o router não tem prefixo, como/webhooks/asaase/webhooks/stripe).POST, público (main.py), autenticado só pelo Basic Auth + reconsulta. - Eventos — marcar só estes sete, que são os que o handler trata (
HANDLED_EVENTSempagarme_webhooks.py):charge.paid,charge.payment_failed,charge.refunded,charge.partial_canceled,charge.chargedback,chargeback.received,subscription.canceled. Qualquer outro tipo (order.*,invoice.*,customer.*,card.*,charge.antifraud_*,subscription.created…) responde200 ignoredsem ler nem gravar nada — marcar a mais só gera tráfego. - Usuário e senha do passo 1 no campo de autenticação básica do webhook.
- Conferir a 1ª entrega. Pelo Pagar.me, sem painel:
GET /hooks(conta live, só leitura, 30 por página) lista cada entrega comurl,event,status(sent/failed) eattempts; um hook com a nossa URL emsentprova a entrega, efailedcom tentativas indica senha ou URL errada. Antes de cadastrar, todos iam parahubapi.mundipagg.com. Nos logs da app de produção (cd /root/fdplay-api && docker compose logs --since 1h, sem nome de serviço), procurarPagar.me webhook received: type=… id=…— é logado para toda entrega autenticada e bem-formada, nossa ou não, de qualquer tipo.Pagar.me webhook REJEITADO: Basic Auth inválido= senha do painel ≠credentials.toml. Emwebhook_logs({gateway: 'pagarme'}) só entram eventos nossos (assinatura, pedido de ingresso ou cliente compagarme_customer_id), comsignature_valid: truequando o Basic Auth está configurado eresult.statusprocessed,rejectedousubscription_not_found.not_ourseignorednunca são gravados:webhook_logsvazio não prova que o webhook não chega — quem prova é o log da app (ou o histórico de entregas no painel).
O que o webhook cobre e o polling não: renovação mensal do cartão (charge.paid de cada ciclo é o que avança
expires_at), recusa na renovação, estorno e chargeback feitos fora da API. A renovação paga sem um
next_billing_at futuro na assinatura do gateway (consulta falhou ou o ciclo ainda não virou) responde 503 sem
gravar nada, para o Pagar.me reentregar: aceitar deixaria o acesso preso no vencimento antigo e a reentrega viraria
duplicate. No log da app, renovação de … sem next_billing_at futuro; aguardando reentrega.
Conta do Instituto FD
A conta Pagar.me é do Instituto FD, a empresa dona do FD Play, e também atende outro produto do Instituto
(21/10/2026: ~530 assinaturas e ~29 mil entregas que não são do FD Play). Com o webhook cadastrado, esse tráfego chega ao
endpoint, e isso é esperado: o evento alheio responde 200 sem chamada nenhuma ao Pagar.me, sem gravar nada e com
poucas leituras indexadas no Mongo (webhook_logs.event_id, ticket_orders.gateway_payment_id, os ids Pagar.me de
subscriptions, users.pagarme_customer_id). A API só trata e só guarda o que resolve a uma assinatura, pedido ou
cliente nossos. Nunca procurar cadastro por CPF/e-mail no Pagar.me — o cliente do outro produto não é nosso.
🔒 Segurança do webhook¶
- Basic Auth (quando configurado) — camada redundante.
- Reconsulta obrigatória da cobrança no Pagar.me antes de ativar (
charge.paid): payload forjado não ativa nada (ADR-0079). - Indisponibilidade do gateway →
503, para o Pagar.me reentregar. - Deduplicação por
iddo evento emwebhook_logs.