Documentação oficial · v1

Pix e Cripto 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, Cripto 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), 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 /payouts            -> envia Pix. Body: { amount, pix_key, pix_key_type?, recipient_name?, recipient_document?, description?, external_id? }
                              IMPORTANTE: "amount" = valor LÍQUIDO que o favorecido recebe. A taxa é somada por cima
                              e o total (gross_amount = amount + fee_amount) é debitado do seu saldo. Ex.: amount=100,
                              fee=R$ 3,50 → precisa ter R$ 103,50 de saldo, favorecido recebe R$ 100,00.
                              Response inclui gross_amount, net_amount e fee_amount para auditoria.
  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  /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 e POST /payouts. Endpoints de configuração (med/:id) NÃO usam Idempotency-Key.
- Split de pagamentos: recurso previsto para versões futuras. Não existe endpoint disponível hoje.
- 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.
- 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,
         med.opened, med.updated, med.resolved
- Métodos suportados hoje: Pix e cripto. CARTÃO NÃO EXISTE na API — não gere código para /card_payments.
- Compatibilidade (v1 estável): campos e endpoints nunca são removidos/renomeados; novos campos são opcionais;
  novos métodos de pagamento entram como endpoints novos (nunca alteram /charges); ignore chaves e tipos de
  evento desconhecidos com um default neutro para nunca quebrar quando algo novo for lançado.
- 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
  • 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 /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 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 (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 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 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..."

Cash-out

Enviar Pix

POST/payouts

Envia Pix para qualquer chave (CPF, CNPJ, e-mail, telefone ou aleatória). As condições comerciais aplicáveis ao seu perfil são descontadas automaticamente. 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 do limite configurado passam por 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": 250.00,
  "gross_amount": 257.00,
  "net_amount": 250.00,
  "fee_amount": 7.00,
  "test_mode": false,
  "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": 250.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"
}

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

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

Política de compatibilidade

A API v1 é estável. Nenhuma integração existente precisa ser alterada quando lançamos algo novo — seguimos estas garantias:

  • Nunca removemos nem renomeamos campos, endpoints ou valores de status já documentados na v1.
  • Novos campos são sempre opcionais e aditivos. Trate objetos JSON como abertos: ignore chaves que você ainda não conhece em vez de falhar a leitura.
  • Novos métodos de pagamento entram como novos endpoints (ex.: /crypto_payments, futuramente /card_payments) — nunca mudando o comportamento de /charges, que continua exclusivo de Pix.
  • Novos eventos de webhook podem surgir a qualquer momento. Sua rota deve responder 200 e ignorar tipos desconhecidos, sem quebrar.
  • Novos códigos de erro mantêm o mesmo envelope (error.code, error.message) e o mesmo padrão snake_case.
  • Qualquer mudança incompatível viraria uma nova versão (/v2) com anúncio prévio e a v1 permanecendo ativa.

Recomendação prática: ao processar respostas e webhooks, faça switch por payment_method / type com um caso default neutro. Assim, quando um novo método (cartão, por exemplo) for ativado na sua conta, seu código continua rodando sem deploy.

Roadmap

Cartão de crédito/débito (ainda não disponível)

Status: não implementado. Hoje a PhanterPay opera Pix e cripto. Nenhum adquirente conectado oferece cartão via API. Esta seção existe apenas para você já modelar sua integração de forma que o dia do lançamento não exija reescrita.

Quando (e se) cartão for lançado, o desenho será este — reservado desde já:

  • Novo recurso isolado: POST /card_payments e GET /card_payments/:id. /charges não muda.
  • Novo prefixo de ID: crd_, seguindo o mesmo padrão de chg_ / pyt_.
  • Mesma autenticação (Bearer API Key), mesma Idempotency-Key obrigatória, mesmo external_id.
  • Novos eventos aditivos: card_payment.authorized, card_payment.paid, card_payment.refused, card_payment.refunded, card_payment.chargeback.
  • Dados sensíveis do cartão nunca trafegariam pela sua aplicação: tokenização no front (PCI) e envio apenas do token.
  • Taxas de cartão seriam configuradas por conta, exatamente como já ocorre hoje com Pix e cripto — sem impacto nas taxas vigentes.

Enquanto isso, chamadas a /card_payments retornam 404 not_found. Não escreva lógica que dependa desse endpoint até o anúncio oficial no Changelog.

Roadmap

Split de pagamentos (previsto)

Split de pagamentos: recurso previsto para versões futuras. Caso sua operação necessite dessa funcionalidade, entre em contato com a equipe da PhanterPay.

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