Skip to content

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_mode não se aplica ao Pagar.me.
  • Dinheiro em centavos na ponta do gateway (como a Stripe). A API continua falando reais onde sempre falou (value de 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

GET /api/v1/pagarme/config
→ {"public_key": "pk_...", "account_id": "acc_..."}

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.

  1. Senha do Basic Auth. Gerar usuário e senha fortes e gravar em [production.pagarme] do credentials.toml de 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 um warning por entrega. Subir a config antes de cadastrar no painel — senão a 1ª entrega já volta 401.
  2. URL: https://fdplay-api.infraifd.com/webhooks/pagarme — sem /api/v1 (o router não tem prefixo, como /webhooks/asaas e /webhooks/stripe). POST, público (main.py), autenticado só pelo Basic Auth + reconsulta.
  3. Eventos — marcar só estes sete, que são os que o handler trata (HANDLED_EVENTS em pagarme_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…) responde 200 ignored sem ler nem gravar nada — marcar a mais só gera tráfego.
  4. Usuário e senha do passo 1 no campo de autenticação básica do webhook.
  5. Conferir a 1ª entrega. Pelo Pagar.me, sem painel: GET /hooks (conta live, só leitura, 30 por página) lista cada entrega com url, event, status (sent/failed) e attempts; um hook com a nossa URL em sent prova a entrega, e failed com tentativas indica senha ou URL errada. Antes de cadastrar, todos iam para hubapi.mundipagg.com. Nos logs da app de produção (cd /root/fdplay-api && docker compose logs --since 1h, sem nome de serviço), procurar Pagar.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. Em webhook_logs ({gateway: 'pagarme'}) só entram eventos nossos (assinatura, pedido de ingresso ou cliente com pagarme_customer_id), com signature_valid: true quando o Basic Auth está configurado e result.status processed, rejected ou subscription_not_found. not_ours e ignored nunca são gravados: webhook_logs vazio 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

  1. Basic Auth (quando configurado) — camada redundante.
  2. Reconsulta obrigatória da cobrança no Pagar.me antes de ativar (charge.paid): payload forjado não ativa nada (ADR-0079).
  3. Indisponibilidade do gateway → 503, para o Pagar.me reentregar.
  4. Deduplicação por id do evento em webhook_logs.