Documentação oficial · v1

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).

Failover de adquirentes
Idempotência + external_id
Webhooks HMAC
Pronto p/ IA

Começar

Quickstart em 3 passos

  1. 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. 2

    Faça sua primeira cobrança

    cURL
    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"
      }'
  3. 3

    Receba a confirmação por webhook

    Cadastre um endpoint em API & Integrações, assine o evento charge.paid e valide a assinatura HMAC. Se preferir polling, use GET /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.

prompt.md
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/api

Base URL & ambientes

GEThttps://api.phanterpay.com.br/v1

Endpoint 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 Pix
  • bol_ — boleto bancário
  • evt_ — 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_.

header
Authorization: Bearer bp_XXXX_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Alternativas 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.

header
Idempotency-Key: 3d9d1f9e-9c9d-4d3e-b1a1-1234567890ab

external_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_id que já existe pra sua conta, devolvemos o registro anterior com 200.
  • 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-Afterheader

Segundos para aguardar antes de repetir a chamada — presente apenas em 429.

Exemplo de resposta 429

json
{
  "error": "Rate limit exceeded"
}
header
Retry-After: 10

Boas práticas

  1. Retry com backoff exponencial. Em 429, respeite Retry-After antes de retentar. Para 502/503, use backoff exponencial (1s → 2s → 4s → 8s).
    node.js
    async 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");
    }
  2. Webhook em vez de polling. Polling em GET /charges desperdiça quota. Cadastre um endpoint em API & Integrações e reaja aos eventos charge.paid / payout.completed. Reconcilie via listagem só periodicamente (1× por hora).
  3. Idempotência. Use o header Idempotency-Key em 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.
  4. 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

HTTPCenárioAção
429Limite de requisições atingidoEspere Retry-After segundos e retente
403IP bloqueado temporariamente (anti-abuso)Espere 5 minutos — não retente imediatamente
403IP em blacklist permanenteContate o suporte
423Conta sob revisão de segurançaContate 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, /boletos e 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.
json
{
  "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.

limitnumber

Tamanho da página (padrão 50, máximo 200).

starting_afterstring

ID do último item da página anterior (ex.: chg_... ou pyt_...). Aceita também uma data ISO 8601.

Resposta

json
{
  "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

POST/charges

Gera um QR Code Pix (copia-e-cola + PNG base64) com vencimento configurável. Roteia entre adquirentes com failover automático.

Body

amountnumberObrigatório

Valor em reais (ex.: 49.90).

descriptionstring

Descrição livre (até 140 caracteres).

payer_namestring

Nome do pagador. Aceita também aninhado: payer: { name }.

payer_documentstring

CPF/CNPJ do pagador (só números).

expires_innumber (segundos)

Validade do QR (60 a 86400, padrão 3600).

external_idstring

Sua chave de negócio (deduplica).

cURL
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

json
{
  "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

GET/charges/{id}

Aceita o chg_... retornado, o txid legado, ou ext:<external_id>.

cURL
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

json
{
  "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

GET/charges?limit=50
cURL
curl "https://api.phanterpay.com.br/v1/charges?limit=100" \
  -H "Authorization: Bearer bp_XXXX_xxx..."

Boleto

Emitir boleto bancário

POST/boletos

Emite 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ório

Valor em reais.

due_datestring (YYYY-MM-DD)Obrigatório

Data de vencimento.

descriptionstring

Descrição livre.

external_idstring

Sua chave de negócio.

payer.namestringObrigatório

Nome do pagador.

payer.documentstringObrigatório

CPF ou CNPJ.

payer.emailstring

E-mail (recomendado).

payer.addressobjectObrigatório

Endereço: zipcode, street, neighborhood, number, city, state (UF).

cURL
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

json
{
  "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

GET/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
curl https://api.phanterpay.com.br/v1/boletos/bol_01hxy8k4z9a0b1c2d3e4f5g6h7 \
  -H "Authorization: Bearer bp_XXXX_xxx..."
Notificação em tempo real: eventos dedicados 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

POST/payouts

Envia 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.

Atenção: a resposta síncrona do POST nunca devolve "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ório

Valor bruto em reais.

pix_keystringObrigatório

Chave Pix do destinatário. Aceita também aninhado: pix: { key }.

pix_key_type'cpf' | 'cnpj' | 'email' | 'phone' | 'random'

Opcional — inferido quando ausente.

recipient_namestring

Nome do destinatário.

recipient_documentstring

CPF/CNPJ do destinatário.

descriptionstring

Descrição (até 140 caracteres).

external_idstring

Sua chave de negócio (deduplica).

cURL
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

json
{
  "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

GET/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
curl https://api.phanterpay.com.br/v1/payouts/pyt_01hxy... \
  -H "Authorization: Bearer bp_XXXX_xxx..."

Resposta 200

json
{
  "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

GET/payouts?limit=50
cURL
curl "https://api.phanterpay.com.br/v1/payouts?limit=100" \
  -H "Authorization: Bearer bp_XXXX_xxx..."

Saldo

Consultar saldo

GET/balance
cURL
curl https://api.phanterpay.com.br/v1/balance \
  -H "Authorization: Bearer bp_XXXX_xxx..."

Resposta 200

json
{
  "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) ou fixed_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

GET/splits/rules
cURL
curl https://api.phanterpay.com.br/v1/splits/rules \
  -H "Authorization: Bearer bp_XXXX_xxx..."

Criar regra de split

POST/splits/rules
cURL
curl -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

json
{
  "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:

json
{
  "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 /chargespayer_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 pendente
  • under_review — defesa enviada, aguardando análise
  • accepted — devolução autorizada (valor debitado)
  • rejected — defesa aceita, valor liberado
  • cancelled — pagador desistiu

Você também pode gerenciar visualmente em MED / Infrações.

Listar infrações

GET/med

Filtros: status, limit (máx 100), starting_after.

cURL
curl "https://api.phanterpay.com.br/v1/med?status=open&limit=20" \
  -H "Authorization: Bearer bp_XXXX_xxx..."

Consultar infração

GET/med/{id}
cURL
curl https://api.phanterpay.com.br/v1/med/inf_abc123... \
  -H "Authorization: Bearer bp_XXXX_xxx..."

Enviar defesa

POST/med/{id}

Envia sua defesa e move a infração para under_review.

cURL
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-Signature no formato t=<unix>,v1=<hex>
  • Metadados: headers X-PhanterPay-Event-Id, X-PhanterPay-Event-Type, X-PhanterPay-Attempt
  • Timeout: 10s. Responda com 2xx rápido; processamento pesado deve ir para uma fila local.

Payload de exemplo

json
{
  "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

TipoQuando dispara
charge.createdCobrança criada
charge.paidPagamento confirmado pelo adquirente
charge.expiredQR Code passou do expires_at sem pagamento
charge.refundedEstorno aplicado
charge.failedCobrança rejeitada antes de gerar QR
payout.createdPayout aceito para processamento
payout.processingPayout enviado ao BACEN
payout.completedLiquidação Pix confirmada (end_to_end_id disponível)
payout.failedFalha antes da liquidação
payout.reversedDevolução do BACEN após liquidação
split.completedRateio de split executado com sucesso
split.failedRateio de split falhou (não afeta cobrança)
med.openedInfração MED aberta contra sua conta
med.updatedStatus da infração mudou (defesa/decisão)
med.resolvedMED 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.

Node.js (Express)
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:

TentativaEspera após falha
1 → 21 minuto
2 → 35 minutos
3 → 430 minutos
4 → 52 horas
5 → 612 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:

  1. Suba seu servidor local (ex.: http://localhost:3000/webhooks/phanterpay).
  2. Abra um túnel público: ngrok http 3000 (ou cloudflared tunnel --url http://localhost:3000). Copie a URL https://....
  3. Em API & Integrações → Webhooks, cadastre a URL do túnel + /webhooks/phanterpay e guarde o whsec_....
  4. Crie uma cobrança em modo de teste (valor ≤ R$ 5,00) e pague pelo QR — o evento charge.paid chega no seu endpoint em segundos.
  5. 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.

Atenção com o 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)

pendingpaid · expired · refunded

Payout (cash-out)

pendingprocessingcompleted · 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

pendingpaid · 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.

Resposta padrão
{
  "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):

Validação (400)
{
  "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.

HTTPerror.codeSignificado
400VALIDATION_ERRORBody inválido, JSON malformado ou campos faltando (ver issues)
401UNAUTHORIZEDAPI key ausente ou inválida
402INSUFFICIENT_BALANCESaldo insuficiente para cash-out
403FORBIDDENRecurso desabilitado no perfil ou escopo insuficiente
404NOT_FOUNDRecurso inexistente (id/external_id não encontrado)
409IDEMPOTENCY_CONFLICTIdempotency-Key reusada com body diferente
422AMOUNT_LIMIT_EXCEEDEDValor acima do limite por transação (api_tx_limit)
423ACCOUNT_LOCKEDConta sob revisão — contate o suporte
429RATE_LIMIT_EXCEEDEDRetry-After segundos e retente (retryable: true)
502PROVIDER_UNAVAILABLETodas 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: boolean para valores ≤ R$ 5,00.
    • starting_after em GET /charges e GET /payouts.
  • 2026-07-20v1

    Webhooks assinados + external_id

    • Assinatura HMAC-SHA256 no header X-PhanterPay-Signature.
    • Dedupe semântico via external_id em todos os recursos.
    • IDs prefixados chg_ / pyt_ / bol_ / evt_.