Pix e Boleto em produção em minutos
REST + JSON, snake_case, webhooks assinados por HMAC-SHA256 com retentativas automáticas e IDs prefixados (chg_, pyt_, bol_). Feito para plugar em qualquer stack, incluindo agentes de IA (ChatGPT, Claude, Cursor, n8n, Zapier).
Começar
Quickstart em 3 passos
- 1
Gere sua API key
Vá em API Keys e clique em Criar chave. Ela já vem com acesso completo — Pix in/out, Boleto e Saldo. Sem seleção de escopo, sem consent screen. - 2
Faça sua primeira cobrança
cURLcurl -X POST https://api.phanterpay.com.br/v1/charges \ -H "Authorization: Bearer bp_XXXX_xxx..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 3d9d1f9e-9c9d-4d3e-b1a1-1234" \ -d '{ "amount": 49.90, "description": "Pedido #1042", "payer_name": "João da Silva", "payer_document": "12345678909", "expires_in": 3600, "external_id": "pedido_1042" }' - 3
Receba a confirmação por webhook
Cadastre um endpoint em API & Integrações, assine o eventocharge.paide valide a assinatura HMAC. Se preferir polling, useGET /charges/:id.
Para IAs
Integrar com agentes (ChatGPT, Claude, Cursor…)
Cole o bloco abaixo no seu agente. Ele tem tudo o que a IA precisa para gerar código sem alucinar endpoint ou payload.
Você é um agente que integra pagamentos via API PhanterPay v1.
Base URL: https://api.phanterpay.com.br/v1
Autenticação: header "Authorization: Bearer <API_KEY>" (também aceita "x-api-key: <API_KEY>").
Formato da chave: começa com "bp_" (ex.: bp_a1b2_c3d4...48hex). NÃO existem chaves "pk_live_" ou "sk_live_".
Padrão: REST + JSON, snake_case em todos os campos (request e response). Valores em BRL com decimais.
IDs de resposta são prefixados: chg_ (cobrança), pyt_ (payout), bol_ (boleto), evt_ (webhook event).
Envie sempre "Idempotency-Key: <uuid-v4>" em requisições POST.
Envie "external_id" (string opcional até 120 chars) para deduplicar por sua chave de negócio.
Rate limit: aplicado por endpoint e por credencial. Se estourar, HTTP 429 com header Retry-After (segundos). Respeite Retry-After e use backoff exponencial. Prefira webhooks a polling.
Modo de teste: valores <= R$ 5,00 marcam "test_mode": true no response e no payload do webhook. É apenas um sinal — o dinheiro se move normalmente.
Paginação: GET /charges e GET /payouts aceitam ?limit=N (max 200) e ?starting_after=<id>. Response inclui "has_more" e "next_starting_after".
Webhooks: entregues a partir da borda global (Cloudflare Workers) — os IPs de origem NÃO são fixos. A validação canônica é a assinatura HMAC, nunca allowlist de IP.
Endpoints:
POST /charges -> cria cobrança Pix. Body: { amount, description?, payer_name?, payer_document?, expires_in?, external_id? }
GET /charges/:id -> consulta cobrança (aceita chg_..., txid legado, ou "ext:<external_id>")
GET /charges -> lista cobranças (query: limit, starting_after)
POST /boletos -> emite boleto. Body: { amount, due_date (YYYY-MM-DD), description?, external_id?, payer:{name,document,email?,address:{zipcode,street,neighborhood,number,city,state}} }
GET /boletos/:id -> consulta boleto (aceita bol_...). Fonte da verdade para confirmar pagamento — use polling.
POST /payouts -> envia Pix. Body: { amount, pix_key, pix_key_type?, recipient_name?, recipient_document?, description?, external_id? }
GET /payouts/:id -> consulta payout (aceita pyt_..., uuid, ou "ext:<external_id>")
GET /payouts -> lista saques (query: limit, starting_after)
GET /balance -> saldos { available_balance, pending_balance, blocked_balance, total_balance, currency }
GET /splits/rules -> lista regras de split reutilizáveis
POST /splits/rules -> cria regra. Body: { name, recipient:{pix_key,name?}, percentage? OR fixed_amount? }
GET /med -> lista infrações PIX (MED). Query: status?, limit?, starting_after?
GET /med/:id -> consulta infração
POST /med/:id -> envia defesa. Body: { defense_text, defense_evidence_url? }
Regras:
- Nunca invente uma API key. Use variável de ambiente PHANTERPAY_KEY.
- Idempotency-Key (UUID v4) é obrigatória apenas em POST /charges, POST /payouts e POST /boletos. Endpoints de configuração (splits/rules, med/:id) NÃO usam Idempotency-Key.
- Erros: sempre { "success": false, "error": { "code", "message", "retryable", "issues"? }, "request_id" }.
Trate pelo STATUS HTTP + error.code:
400 VALIDATION_ERROR · 401 UNAUTHORIZED · 402 INSUFFICIENT_BALANCE · 403 FORBIDDEN
· 404 NOT_FOUND · 409 IDEMPOTENCY_CONFLICT · 422 AMOUNT_LIMIT_EXCEEDED · 423 ACCOUNT_LOCKED
· 429 RATE_LIMIT_EXCEEDED (respeite Retry-After) · 502 PROVIDER_UNAVAILABLE
Toda resposta (sucesso ou erro) inclui header X-Request-Id — use no suporte para rastrear a chamada.
- Status charge: pending → paid | expired | refunded
- Status payout: pending → processing → completed | failed | reversed
IMPORTANTE: payout NUNCA volta "completed" na resposta síncrona do POST — sempre "processing" ou "failed".
Confirmação final chega apenas via webhook payout.completed OU polling em GET /payouts/:id.
- Boleto: NÃO possui webhook dedicado. Confirme pagamento via polling em GET /boletos/:id a cada 5–10 minutos até o vencimento.
- Para receber notificações, cadastre um webhook em /api-keys (assinado com HMAC-SHA256 do material "<t>.<raw_body>", header X-PhanterPay-Signature: t=<unix>,v1=<hex>).
Tipos: charge.created, charge.paid, charge.expired, charge.refunded, charge.failed,
payout.created, payout.processing, payout.completed, payout.failed, payout.reversed,
split.completed, split.failed,
med.opened, med.updated, med.resolved
Boleto: eventos dedicados (boleto.*) ainda não são emitidos — confirme pagamento via polling em GET /boletos/:id.
- Referência oficial: https://phanterpay.com.br/docs/apiBase URL & ambientes
https://api.phanterpay.com.br/v1Endpoint canônico da API v1. Todos os endpoints são HTTPS-only. Não há sandbox separado — use valores baixos (ex.: R$ 1,00) na sua chave para testar em produção.
Convenções da API
snake_case em todos os campos
Requests e responses usam snake_case. Se você tem código legado em camelCase(payerName, pixKeyDest) ele continua sendo aceito na entrada — a documentação e as respostas usam apenas a forma nova.
IDs prefixados
chg_— cobrança Pix (charges)pyt_— payout / cash-out Pixbol_— boleto bancárioevt_— evento de webhook
Você pode consultar qualquer recurso por: o id novo (chg_…, pyt_…), o id interno legado (txid ou uuid) ou o prefixo ext: seguido do seu external_id. Exemplo: GET /charges/ext:pedido_1042.
Datas em ISO-8601 (UTC)
Todos os timestamps são strings ISO-8601 com sufixo Z. Ex.: 2026-07-27T20:32:14Z.
Autenticação
API Key
Envie a chave em todas as requisições no header. Formato aceito: bp_XXXX_... (48 hex após o segundo underscore). Não existem chaves pk_live_ ou sk_live_.
Authorization: Bearer bp_XXXX_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxAlternativas aceitas: Authorization: bp_XXXX_... (sem "Bearer") ou x-api-key: bp_XXXX_....
O que a chave libera
Chave única, todos os recursos liberados — sem seleção de escopo pelo cliente:
- POST /charges · GET /charges/:id · GET /charges
- POST /boletos · GET /boletos/:id
- POST /payouts · GET /payouts/:id · GET /payouts
- GET /balance
Idempotency-Key
Envie um UUID único em Idempotency-Key em requisições POST de criação financeira — /charges, /boletos e /payouts. Requisições repetidas com a mesma chave e o mesmo body retornam a resposta original — mesmo em caso de retry por timeout. Se você reutilizar a chave com um body diferente, recebe 409.
Endpoints de configuração (split rules, defesa MED) não usam Idempotency-Key — são operações naturalmente idempotentes por recurso.
Idempotency-Key: 3d9d1f9e-9c9d-4d3e-b1a1-1234567890abexternal_id (dedupe semântico)
Diferente da Idempotency-Key (que dedupe por HTTP retry), o external_iddedupe pela sua chave de negócio: o número do pedido, id da fatura, hash do carrinho, o que fizer sentido.
- String de até 120 caracteres.
- Se você tentar criar uma cobrança/payout/boleto com um
external_idque já existe pra sua conta, devolvemos o registro anterior com200. - Aparece em toda resposta, em todo webhook e você pode consultar por ele (
GET /charges/ext:pedido_1042). - É a forma recomendada de casar eventos com sua base de dados.
Segurança
Rate limiting
A PhanterPay aplica limites de requisição por endpoint e credencial para garantir estabilidade, segurança e disponibilidade da plataforma. Os limites podem variar de acordo com o endpoint, perfil da operação e capacidade contratada.
Operações com alto volume ou necessidades específicas de throughput podem solicitar capacidade dedicada junto ao time PhanterPay.
Resposta ao atingir o limite
Quando um limite é atingido, a API responde com HTTP 429 Too Many Requests e inclui o header Retry-After, informando quantos segundos aguardar antes de nova tentativa.
Retry-AfterheaderSegundos para aguardar antes de repetir a chamada — presente apenas em 429.
Exemplo de resposta 429
{
"error": "Rate limit exceeded"
}Retry-After: 10Boas práticas
- Retry com backoff exponencial. Em
429, respeiteRetry-Afterantes de retentar. Para502/503, use backoff exponencial (1s → 2s → 4s → 8s).node.jsasync function withRetry(fn, maxRetries = 4) { for (let attempt = 0; attempt < maxRetries; attempt++) { const res = await fn(); if (res.status === 429) { const wait = Number(res.headers.get("retry-after") ?? 2 ** attempt); await new Promise(r => setTimeout(r, wait * 1000)); continue; } if (res.status === 502 || res.status === 503) { await new Promise(r => setTimeout(r, (2 ** attempt) * 1000)); continue; } return res; } throw new Error("max_retries exceeded"); } - Webhook em vez de polling. Polling em
GET /chargesdesperdiça quota. Cadastre um endpoint em API & Integrações e reaja aos eventoscharge.paid/payout.completed. Reconcilie via listagem só periodicamente (1× por hora). - Idempotência. Use o header
Idempotency-Keyem toda criação de cobrança ou saque. Retentativas com a mesma chave devolvem a transação original — sem cobrar quota extra nem duplicar operação. - Cache de leituras estáveis. Saldo, taxas e limites mudam com baixa frequência — cacheie por 5–30s no seu lado para não consumir quota à toa.
Respostas relacionadas
| HTTP | Cenário | Ação |
|---|---|---|
| 429 | Limite de requisições atingido | Espere Retry-After segundos e retente |
| 403 | IP bloqueado temporariamente (anti-abuso) | Espere 5 minutos — não retente imediatamente |
| 403 | IP em blacklist permanente | Contate o suporte |
| 423 | Conta sob revisão de segurança | Contate o suporte para liberação |
Modo de teste (≤ R$ 5,00)
Não temos ambiente de sandbox separado — sua API key é sempre real. Para você distinguir integração de produção, marcamos automaticamente toda transação com valor menor ou igual a R$ 5,00 com o campo test_mode: true.
- Aparece no response de
POST /charges,/payouts,/boletose em todo webhook. - O dinheiro se move normalmente — taxa, saldo e liquidação são reais.
- Serve para você separar logs de testes internos da sua contabilidade.
{
"id": "chg_...",
"amount": 1.00,
"test_mode": true,
"status": "pending"
}Paginação por cursor
As listagens GET /charges e GET /payouts usam paginação por cursor — estável mesmo quando novos registros chegam em tempo real.
limitnumberTamanho da página (padrão 50, máximo 200).
starting_afterstringID do último item da página anterior (ex.: chg_... ou pyt_...). Aceita também uma data ISO 8601.
Resposta
{
"data": [ { "id": "chg_...", "amount": 49.90, "status": "paid" } ],
"has_more": true,
"next_starting_after": "chg_9c4e2f8a..."
}Quando has_more é false, você chegou ao fim. Ordenação padrão: mais recente primeiro.
Cash-in
Criar cobrança Pix
/chargesGera um QR Code Pix (copia-e-cola + PNG base64) com vencimento configurável. Roteia entre adquirentes com failover automático.
Body
amountnumberObrigatórioValor em reais (ex.: 49.90).
descriptionstringDescrição livre (até 140 caracteres).
payer_namestringNome do pagador. Aceita também aninhado: payer: { name }.
payer_documentstringCPF/CNPJ do pagador (só números).
expires_innumber (segundos)Validade do QR (60 a 86400, padrão 3600).
external_idstringSua chave de negócio (deduplica).
curl -X POST https://api.phanterpay.com.br/v1/charges \
-H "Authorization: Bearer bp_XXXX_xxx..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3d9d1f9e-9c9d-4d3e-b1a1-1234" \
-d '{
"amount": 49.90,
"description": "Pedido #1042",
"payer_name": "João da Silva",
"payer_document": "12345678909",
"expires_in": 3600,
"external_id": "pedido_1042"
}'Resposta 201
{
"id": "chg_9c4e2f8a1b3d4c5e6f7a8b9c0d1e2f3a",
"txid": "9c4e2f8a1b3d4c5e6f7a8b9c0d1e2f3a",
"external_id": "pedido_1042",
"amount": 49.90,
"test_mode": false,
"status": "pending",
"description": "Pedido #1042",
"pix": {
"copy_paste": "00020126580014BR.GOV.BCB.PIX0136...6304ABCD",
"qr_code_image": "data:image/png;base64,iVBORw0K..."
},
"end_to_end_id": null,
"paid_at": null,
"expires_at": "2026-07-13T18:30:00Z",
"created_at": "2026-07-13T17:30:00Z"
}Consultar cobrança
/charges/{id}Aceita o chg_... retornado, o txid legado, ou ext:<external_id>.
curl https://api.phanterpay.com.br/v1/charges/chg_9c4e2f... \
-H "Authorization: Bearer bp_XXXX_xxx..."
# ou por external_id:
curl https://api.phanterpay.com.br/v1/charges/ext:pedido_1042 \
-H "Authorization: Bearer bp_XXXX_xxx..."Resposta 200
{
"id": "chg_9c4e2f8a1b3d4c5e6f7a8b9c0d1e2f3a",
"external_id": "pedido_1042",
"amount": 49.90,
"test_mode": false,
"status": "paid",
"description": "Pedido #1042",
"pix": {
"copy_paste": "00020126580014BR.GOV.BCB.PIX...",
"qr_code_image": "data:image/png;base64,iVBORw0K..."
},
"end_to_end_id": "E12345678202607272032abcdef123",
"paid_at": "2026-07-27T20:32:14Z",
"expires_at": "2026-07-27T21:30:00Z",
"created_at": "2026-07-27T20:30:00Z"
}Listar cobranças
/charges?limit=50curl "https://api.phanterpay.com.br/v1/charges?limit=100" \
-H "Authorization: Bearer bp_XXXX_xxx..."Boleto
Emitir boleto bancário
/boletosEmite um boleto registrado com linha digitável, código de barras e PDF pronto para envio. Ativo por padrão em toda chave PhanterPay.
Body
amountnumberObrigatórioValor em reais.
due_datestring (YYYY-MM-DD)ObrigatórioData de vencimento.
descriptionstringDescrição livre.
external_idstringSua chave de negócio.
payer.namestringObrigatórioNome do pagador.
payer.documentstringObrigatórioCPF ou CNPJ.
payer.emailstringE-mail (recomendado).
payer.addressobjectObrigatórioEndereço: zipcode, street, neighborhood, number, city, state (UF).
curl -X POST https://api.phanterpay.com.br/v1/boletos \
-H "Authorization: Bearer bp_XXXX_xxx..." \
-H "Idempotency-Key: 7f4b1a2e-9c9d-4d3e-b1a1-1234567890ab" \
-H "Content-Type: application/json" \
-d '{
"amount": 149.90,
"description": "Fatura #2025",
"due_date": "2026-08-15",
"external_id": "fatura_2025_08",
"payer": {
"name": "João da Silva",
"document": "12345678909",
"email": "joao@email.com",
"address": {
"zipcode": "38010000",
"street": "Av. Leopoldino",
"neighborhood": "Centro",
"number": "100",
"city": "Uberaba",
"state": "MG"
}
}
}'Resposta 201
{
"id": "bol_01hxy8k4z9a0b1c2d3e4f5g6h7",
"external_id": "fatura_2025_08",
"amount": 149.90,
"test_mode": false,
"status": "pending",
"digitable_line": "23793.38128 60007.827136 95000.063305 8 96860000014990",
"bar_code": "23798968600000149902338126000782713950000633058",
"pdf_url": "https://.../boleto.pdf",
"view_url": "https://.../boleto",
"due_date": "2026-08-15"
}Consultar boleto
/boletos/{id}Fonte da verdade para confirmar pagamento de boleto. Aceita bol_... retornado no POST. Devolve o mesmo status (pending, paid, refunded) — faça polling a cada 5–10 minutos até o vencimento.
curl https://api.phanterpay.com.br/v1/boletos/bol_01hxy8k4z9a0b1c2d3e4f5g6h7 \
-H "Authorization: Bearer bp_XXXX_xxx..."boleto.paid, boleto.expired e boleto.refunded estão no roadmap. Até lá, use polling neste endpoint como método oficial de confirmação de pagamento de boleto.Cash-out
Enviar Pix
/payoutsEnvia Pix para qualquer chave (CPF, CNPJ, e-mail, telefone ou aleatória). A taxa configurada no seu perfil é descontada do valor bruto. Saques via API não passam por aprovação manual — o limite máximo por transação é o campo api_tx_limitdo seu perfil. Saques manuais pelo painel acima de R$ 5.000 exigem aprovação da equipe PhanterPay.
"status": "completed". O status inicial é sempre processing(ou failed). A confirmação final chega apenas via webhook payout.completedou polling em GET /payouts/:id.Body
amountnumberObrigatórioValor bruto em reais.
pix_keystringObrigatórioChave Pix do destinatário. Aceita também aninhado: pix: { key }.
pix_key_type'cpf' | 'cnpj' | 'email' | 'phone' | 'random'Opcional — inferido quando ausente.
recipient_namestringNome do destinatário.
recipient_documentstringCPF/CNPJ do destinatário.
descriptionstringDescrição (até 140 caracteres).
external_idstringSua chave de negócio (deduplica).
curl -X POST https://api.phanterpay.com.br/v1/payouts \
-H "Authorization: Bearer bp_XXXX_xxx..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <uuid>" \
-d '{
"amount": 250.00,
"pix_key": "cliente@email.com",
"pix_key_type": "email",
"recipient_name": "Maria Souza",
"recipient_document": "98765432100",
"description": "Pagamento fornecedor",
"external_id": "fatura_2025_07"
}'Resposta 201
{
"id": "pyt_01hxy8k4z9a0b1c2d3e4f5g6h7",
"external_id": "fatura_2025_07",
"amount": 248.00,
"fee": 2.00,
"test_mode": false,
"gross_amount": 250.00,
"net_amount": 248.00,
"status": "processing",
"pix": {
"key": "cliente@email.com",
"recipient_name": "Maria Souza",
"recipient_document": "98765432100"
},
"description": "Pagamento fornecedor",
"end_to_end_id": null,
"error_message": null,
"created_at": "2026-07-27T20:15:00Z",
"completed_at": null
}Consultar saque
/payouts/{id}Aceita pyt_..., o uuid legado, ou ext:<external_id>. Devolve o mesmo shape do POST — incluindo end_to_end_id quando o pagamento é liquidado.
curl https://api.phanterpay.com.br/v1/payouts/pyt_01hxy... \
-H "Authorization: Bearer bp_XXXX_xxx..."Resposta 200
{
"id": "pyt_01hxy8k4z9a0b1c2d3e4f5g6h7",
"external_id": "fatura_2025_07",
"amount": 248.00,
"fee": 2.00,
"test_mode": false,
"status": "completed",
"pix": {
"key": "cliente@email.com",
"recipient_name": "Maria Souza",
"recipient_document": "98765432100"
},
"description": "Pagamento fornecedor",
"end_to_end_id": "E60701190202607272045abcdef123",
"error_message": null,
"created_at": "2026-07-27T20:15:00Z",
"completed_at": "2026-07-27T20:15:47Z"
}Listar saques
/payouts?limit=50curl "https://api.phanterpay.com.br/v1/payouts?limit=100" \
-H "Authorization: Bearer bp_XXXX_xxx..."Saldo
Consultar saldo
/balancecurl https://api.phanterpay.com.br/v1/balance \
-H "Authorization: Bearer bp_XXXX_xxx..."Resposta 200
{
"available_balance": 12847.55,
"pending_balance": 1840.20,
"blocked_balance": 500.00,
"total_balance": 15187.75,
"currency": "BRL"
}Split
Como funciona o split
O split rateia automaticamente cada cobrança paga entre múltiplos beneficiários — parceiros, marketplace, comissão de vendedor. Você pode enviar a regra inline na criação da cobrança ou cadastrar regras reutilizáveis em Split e referenciá-las por id.
- Cada item aceita
percentage(0-100) oufixed_amount(BRL). - A soma dos percentuais não pode ultrapassar 100. O restante fica para o merchant.
- Splits são executados como Pix cash-out para as chaves informadas quando a cobrança confirma pagamento (evento
charge.paid). - Falhas individuais em um split não desfazem os outros — cada execução tem seu próprio status.
E em caso de estorno (charge.refunded)?
Splits Pix são liquidações finais: uma vez que o valor sai para a chave do beneficiário, não temos como estorná-lo automaticamente. Se a cobrança pai for estornada depois do split executar, o valor do estorno é debitado integralmente do saldo do merchant e cabe a você reaver o repasse com cada beneficiário fora da PhanterPay (contrato, acordo comercial, etc.). Para reduzir o risco em cobranças com alto índice de contestação, cadastre o split como regra reutilizável e desative-a nesses fluxos, ou aguarde a janela de MED (7 dias corridos) antes de fazer o repasse.
Listar regras de split
/splits/rulescurl https://api.phanterpay.com.br/v1/splits/rules \
-H "Authorization: Bearer bp_XXXX_xxx..."Criar regra de split
/splits/rulescurl -X POST https://api.phanterpay.com.br/v1/splits/rules \
-H "Authorization: Bearer bp_XXXX_xxx..." \
-H "Content-Type: application/json" \
-d '{
"name": "Comissão parceiro",
"recipient": { "pix_key": "parceiro@exemplo.com", "name": "Parceiro Ltda" },
"percentage": 10
}'Resposta 201
{
"id": "spl_a1b2c3d4e5...",
"name": "Comissão parceiro",
"recipient": { "pix_key": "parceiro@exemplo.com", "name": "Parceiro Ltda" },
"percentage": 10,
"fixed_amount": null,
"active": true,
"created_at": "2026-07-27T10:00:00Z"
}Split inline na cobrança
Envie o campo split junto ao body da cobrança para aplicar o rateio apenas nessa transação:
{
"amount": 100.00,
"payer_name": "Cliente",
"payer_document": "12345678900",
"external_id": "pedido_123",
"split": [
{ "pix_key": "parceiro@exemplo.com", "percentage": 10 },
{ "pix_key": "afiliado@exemplo.com", "fixed_amount": 5.00 }
]
}Use exatamente os mesmos campos do POST /charges — payer_name / payer_document (ou payer:{name,document}). Não existe campo customer.
MED
MED — Mecanismo Especial de Devolução
O MED é o processo do BACEN em que um pagador contesta uma transação Pix suspeita e solicita devolução. A PhanterPay recebe a infração do adquirente, congela o valor no seu saldo bloqueado e você tem até 7 dias corridos para apresentar defesa.
open— infração aberta, defesa pendenteunder_review— defesa enviada, aguardando análiseaccepted— devolução autorizada (valor debitado)rejected— defesa aceita, valor liberadocancelled— pagador desistiu
Você também pode gerenciar visualmente em MED / Infrações.
Listar infrações
/medFiltros: status, limit (máx 100), starting_after.
curl "https://api.phanterpay.com.br/v1/med?status=open&limit=20" \
-H "Authorization: Bearer bp_XXXX_xxx..."Consultar infração
/med/{id}curl https://api.phanterpay.com.br/v1/med/inf_abc123... \
-H "Authorization: Bearer bp_XXXX_xxx..."Enviar defesa
/med/{id}Envia sua defesa e move a infração para under_review.
curl -X POST https://api.phanterpay.com.br/v1/med/inf_abc123... \
-H "Authorization: Bearer bp_XXXX_xxx..." \
-H "Content-Type: application/json" \
-d '{
"defense_text": "Transação legítima. Cliente adquiriu o produto...",
"defense_evidence_url": "https://drive.exemplo.com/nota-fiscal.pdf"
}'Webhooks
Como funciona
Cadastre endpoints em API & Integrações, escolha os eventos que quer receber e guarde o whsec_... retornado (ele é exibido uma única vez). A PhanterPay envia POST assinado em JSON toda vez que um evento assinado ocorre.
- Content-Type:
application/json - Assinatura: header
X-PhanterPay-Signatureno formatot=<unix>,v1=<hex> - Metadados: headers
X-PhanterPay-Event-Id,X-PhanterPay-Event-Type,X-PhanterPay-Attempt - Timeout: 10s. Responda com
2xxrápido; processamento pesado deve ir para uma fila local.
Payload de exemplo
{
"id": "evt_9c4e2f8a1b3d4c5e6f7a8b9c",
"type": "charge.paid",
"created_at": "2026-07-27T20:32:15Z",
"data": {
"id": "chg_9c4e2f8a1b3d4c5e6f7a8b9c0d1e2f3a",
"external_id": "pedido_1042",
"amount": 49.90,
"status": "paid",
"pix": { "copy_paste": "...", "qr_code_image": "..." },
"end_to_end_id": "E12345678202607272032abcdef123",
"paid_at": "2026-07-27T20:32:14Z",
"created_at": "2026-07-27T20:30:00Z"
}
}Eventos disponíveis
| Tipo | Quando dispara |
|---|---|
| charge.created | Cobrança criada |
| charge.paid | Pagamento confirmado pelo adquirente |
| charge.expired | QR Code passou do expires_at sem pagamento |
| charge.refunded | Estorno aplicado |
| charge.failed | Cobrança rejeitada antes de gerar QR |
| payout.created | Payout aceito para processamento |
| payout.processing | Payout enviado ao BACEN |
| payout.completed | Liquidação Pix confirmada (end_to_end_id disponível) |
| payout.failed | Falha antes da liquidação |
| payout.reversed | Devolução do BACEN após liquidação |
| split.completed | Rateio de split executado com sucesso |
| split.failed | Rateio de split falhou (não afeta cobrança) |
| med.opened | Infração MED aberta contra sua conta |
| med.updated | Status da infração mudou (defesa/decisão) |
| med.resolved | MED finalizado (accepted/rejected/cancelled) |
Assinatura & verificação
A assinatura é HMAC-SHA256 do material <t>.<raw_body> — concatenamos o timestamp t do header, um ponto, e o body cru (bytes exatos que chegam na requisição), assinados com o secret do endpoint. Reformatar o JSON quebra a assinatura — sempre calcule sobre o buffer original. Incluir o t no HMAC bloqueia ataques de replay onde só o timestamp do header é trocado.
import { createHmac, timingSafeEqual } from "crypto";
import express from "express";
const app = express();
// IMPORTANTE: precisamos do body cru (Buffer), NÃO do JSON parseado —
// qualquer reformatação quebra a assinatura HMAC.
app.post("/webhooks/phanterpay", express.raw({ type: "application/json" }), (req, res) => {
const secret = process.env.PHANTERPAY_WEBHOOK_SECRET; // whsec_...
const header = req.header("x-phanterpay-signature") ?? "";
// Formato: "t=<unix>,v1=<hex>"
const parts = Object.fromEntries(
header.split(",").map(kv => kv.split("=") as [string, string]),
);
const t = parts.t;
const v1 = parts.v1;
if (!t || !v1) return res.status(400).send("missing signature");
// Defesa anti-replay: rejeita eventos com mais de 5 minutos
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) {
return res.status(400).send("timestamp too old");
}
// O material assinado é "<t>.<raw_body>" — o timestamp faz parte do HMAC,
// então trocar apenas o `t` do header invalida a assinatura.
const signedPayload = Buffer.concat([Buffer.from(`${t}.`), req.body]);
const expected = createHmac("sha256", secret).update(signedPayload).digest("hex");
const a = Buffer.from(v1, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString());
switch (event.type) {
case "charge.paid": await creditOrder(event.data.external_id); break;
case "payout.completed": await markPayoutDone(event.data.external_id); break;
// ... demais tipos
}
res.status(200).send("ok"); // devolva 2xx rápido; processamento pesado em fila
});Retentativas
Se o seu endpoint não responder com 2xx em até 10 segundos, agendamos até 5 retentativas com backoff exponencial:
| Tentativa | Espera após falha |
|---|---|
| 1 → 2 | 1 minuto |
| 2 → 3 | 5 minutos |
| 3 → 4 | 30 minutos |
| 4 → 5 | 2 horas |
| 5 → 6 | 12 horas |
Todas as tentativas (bem-sucedidas ou não) ficam auditadas em API & Integrações → Webhooks.
Testando webhooks localmente
Como o PhanterPay precisa alcançar sua URL pela internet pública, o jeito mais rápido de testar em localhost é expor sua porta com um túnel HTTP. Fluxo recomendado:
- Suba seu servidor local (ex.:
http://localhost:3000/webhooks/phanterpay). - Abra um túnel público:
ngrok http 3000(oucloudflared tunnel --url http://localhost:3000). Copie a URLhttps://.... - Em API & Integrações → Webhooks, cadastre a URL do túnel +
/webhooks/phanterpaye guarde owhsec_.... - Crie uma cobrança em modo de teste (valor ≤ R$ 5,00) e pague pelo QR — o evento
charge.paidchega no seu endpoint em segundos. - Para reprocessar sem gerar transação nova, use o botão "Reenviar" na tela do webhook — ele dispara o payload original com um novo
X-PhanterPay-Attempt.
Dica: valide a assinatura já no ambiente local — assim você não descobre um bug de HMAC só em produção. O secret é o mesmo em teste e produção porque o webhook é cadastrado por endpoint, não por ambiente.
whsec_ do túnel: como o secret vale tanto em teste quanto em produção, quem capturar essa string em um túnel público, log de terminal, print de tela ou histórico de shell consegue forjar webhooks reais para o seu endpoint. Não deixe o túnel exposto após o teste, não commit o secret e prefira cadastrar um endpoint de webhook dedicado só para o ambiente de desenvolvimento — assim você pode rotacionar o whsec_ sem afetar produção.Referência
Status de cada recurso
Cobrança (charge)
pending → paid · expired · refunded
Payout (cash-out)
pending → processing → completed · failed · reversed
A resposta síncrona do POST /payouts nunca devolve completed — esse status só aparece no webhook payout.completed após confirmação do BACEN.
Boleto
pending → paid · expired · refunded
Códigos de erro HTTP
Todos os erros retornam o mesmo envelope estruturado — nunca uma string simples. O envelope combina success: false, um objeto errorcom code / message / retryable, e um request_id para rastreio no suporte.
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Saldo insuficiente para completar o saque.",
"retryable": false
},
"request_id": "req_9c4e2f8a1b3d4c5e6f7a8b9c0d1e2f3a"
}Em erros de validação (400) vem também issues com o path normalizado (não é mais um array Zod):
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation error",
"retryable": false,
"issues": [
{ "path": "amount", "message": "Expected number, received string" }
]
},
"request_id": "req_..."
}Trate erros pelo status HTTP + error.code (fonte de verdade). O texto de error.message pode mudar para melhorar clareza — não faça parsing por string. Toda resposta inclui o header X-Request-Id com o mesmo valor de request_id — cite-o em qualquer contato com o suporte.
| HTTP | error.code | Significado |
|---|---|---|
| 400 | VALIDATION_ERROR | Body inválido, JSON malformado ou campos faltando (ver issues) |
| 401 | UNAUTHORIZED | API key ausente ou inválida |
| 402 | INSUFFICIENT_BALANCE | Saldo insuficiente para cash-out |
| 403 | FORBIDDEN | Recurso desabilitado no perfil ou escopo insuficiente |
| 404 | NOT_FOUND | Recurso inexistente (id/external_id não encontrado) |
| 409 | IDEMPOTENCY_CONFLICT | Idempotency-Key reusada com body diferente |
| 422 | AMOUNT_LIMIT_EXCEEDED | Valor acima do limite por transação (api_tx_limit) |
| 423 | ACCOUNT_LOCKED | Conta sob revisão — contate o suporte |
| 429 | RATE_LIMIT_EXCEEDED | Retry-After segundos e retente (retryable: true) |
| 502 | PROVIDER_UNAVAILABLE | Todas as adquirentes falharam — repita a requisição |
SDKs & ferramentas
Não publicamos SDK oficial ainda — mas a API é 100% REST + JSON, então qualquer cliente HTTP funciona.
- Postman / Insomnia / Bruno — cole qualquer cURL desta doc.
- n8n / Make / Zapier — nó HTTP Request com Bearer token, sem plugin proprietário.
- Agentes de IA (ChatGPT, Claude, Cursor) — cole o prompt da seção Integrar com IA.
Dúvidas de integração? Fale com nosso suporte.
Referência
Changelog
- 2026-07-27v1
Rate limiting por endpoint, modo de teste e paginação por cursor
- Limites de requisição aplicados por endpoint e credencial; capacidade dedicada sob solicitação. Em 429, a API retorna o header
Retry-After. - Campo
test_mode: booleanpara valores ≤ R$ 5,00. starting_afteremGET /chargeseGET /payouts.
- Limites de requisição aplicados por endpoint e credencial; capacidade dedicada sob solicitação. Em 429, a API retorna o header
- 2026-07-20v1
Webhooks assinados + external_id
- Assinatura HMAC-SHA256 no header
X-PhanterPay-Signature. - Dedupe semântico via
external_idem todos os recursos. - IDs prefixados
chg_/pyt_/bol_/evt_.
- Assinatura HMAC-SHA256 no header