Publicamos prompts prontos, com o contrato completo da API, para que ChatGPT, Claude, Gemini e agentes autônomos gerem a integração correta sem inventar rotas, campos ou parâmetros.
Funciona em qualquer modelo. É o ponto de partida recomendado antes de usar as instruções específicas de cada IA.
Você é um engenheiro de software sênior. Implemente a integração de pagamentos Pix com a PhanterPay no meu projeto, seguindo o contrato abaixo.
AVISO CRÍTICO: A PhanterPay NÃO usa OAuth, Client ID nem Client Secret. A única credencial é PHANTERPAY_API_KEY. Não tente solicitar ou usar Client ID/Secret.
REQUISITO INICIAL: para começar a integrar peça SOMENTE a PHANTERPAY_API_KEY. NÃO peça PHANTERPAY_WEBHOOK_SECRET, URL de callback nem cadastro de webhook como pré-requisito. Webhook é OPCIONAL/RECOMENDADO (tempo real e menos polling); sem webhook, cash-in Pix, saque Pix e cash-in cripto continuam funcionais pelos GETs de consulta com sync-on-read. Só se o integrador optar por webhook é que se configura a URL de callback e a validação de assinatura/segredo.
CONTRATO DA API PHANTERPAY (v1)
Base URL (oficial): https://api.phanterpay.com.br/v1
OpenAPI 3.1: https://api.phanterpay.com.br/openapi.json
Documentação: https://docs.phanterpay.com.br
FLUXO BÁSICO DE COBRANÇA
- Crie a cobrança com POST /charges enviando apenas { "amount": 100.00 }.
- Utilize pix.copy_paste e pix.qr_code_image retornados pela API.
- Consulte GET /charges/{id} até que status seja "paid".
- O header Idempotency-Key é OBRIGATÓRIO em POST /charges (8 a 255 caracteres). webhook, postback_url e external_id continuam opcionais.
- Gere a Idempotency-Key UMA vez por intenção de pagamento e PERSISTA antes do POST; não gere nova chave em re-render.
CRIAÇÃO ÚNICA (REGRA OBRIGATÓRIA — EVITA COBRANÇA DUPLICADA)
- POST /charges deve ser executado APENAS UMA VEZ por intenção de pagamento.
- NUNCA use POST /charges como polling. Polling é somente GET.
- Não execute POST /charges automaticamente em re-render ou useEffect sem trava contra duplicidade.
- Enquanto a criação estiver pendente, bloqueie uma segunda mutation (ex.: if (createCharge.isPending) return;)
e mantenha o botão de criação desabilitado.
- Se usar external_id (opcional), gere uma única vez por intenção e PERSISTA antes do POST.
Não gere novos identificadores durante re-render.
- Desative o retry automático de POST /charges em clientes HTTP/frameworks que possam duplicar chamadas.
- Depois da criação, acompanhe o status SOMENTE por GET /charges/{id} ou GET /charges/ext:<external_id>.
- Se receber 409 external_id_in_progress: não repita o POST automaticamente, não gere novo external_id,
aguarde alguns segundos e consulte por GET /charges/ext:<external_id>.
- retryable:false em external_id_in_progress NÃO significa abandonar a operação; significa apenas
não repetir automaticamente o POST — consultar por GET continua correto.
REGRAS GERAIS
- Valor mínimo por operação: R$ 2,00 (amount >= 2).
- Split de pagamentos NÃO vem liberado por padrão: o recurso só funciona depois de ser HABILITADO
PELO ADMIN da PhanterPay na conta. O merchant não habilita sozinho pela API e Split não faz parte
da integração básica. Sem habilitação, enviar o array "split" em POST /charges devolve
403 split_disabled. O campo split_rule_id não é suportado no contrato público e devolve
422 split_not_available. Só implemente split se a conta já estiver habilitada pelo admin.
- Use apenas os endpoints e campos documentados. Não invente rotas, campos, filtros ou parâmetros.
AUTENTICAÇÃO
- Header: "Authorization: Bearer <chave>" (alternativa equivalente: "x-api-key: <chave>").
- Formato real da chave: bp_<8 hex>_<48 hex>. Exemplo: bp_a1b2c3d4_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a3928
- Não existem chaves públicas (pk_).
- Leia a credencial SEMPRE da variável de ambiente PHANTERPAY_API_KEY.
- NUNCA escreva a chave no código-fonte, em front-end, em app mobile ou em repositório.
ESCOPOS
- charges:read, charges:write, payouts:read, payouts:write, transactions:read, balance:read, med:read, med:write.
- GET /transactions aceita transactions:read ou, por compatibilidade, charges:read + payouts:read.
- Escopo ausente devolve 403 FORBIDDEN.
IDEMPOTÊNCIA
- Header "Idempotency-Key" é OBRIGATÓRIO em POST /charges e OPCIONAL em POST /payouts.
- Use 8 a 255 caracteres. Ausente em POST /charges -> 400 idempotency_key_required. Fora desse tamanho -> 400 invalid_idempotency_key.
- REGRA SIMPLES: operação nova -> chave nova. Retentativa da MESMA operação -> a MESMA chave.
- Derive a chave do identificador do pedido (ex.: "charge-pedido-847293") ou gere uma vez e PERSISTA
no seu banco antes de chamar a API, para reenviar exatamente a mesma chave na retentativa.
- Mesma chave + mesmo corpo já concluído -> devolve a resposta original com header "idempotent-replayed: true".
- Mesma chave + corpo diferente -> 409 IDEMPOTENCY_CONFLICT.
- Mesma chave ainda em processamento -> 409 idempotency_key_in_progress: aguarde alguns segundos e
repita a MESMA requisição com a MESMA chave e o MESMO corpo. Nunca gere outra chave nem outra
intenção de pagamento para contornar.
- Em timeout de rede, repita com a MESMA chave; nunca gere outra.
EXTERNAL_ID
- Em CHARGES, external_id é OPCIONAL. Não crie external_id automaticamente na integração básica.
Envie external_id apenas quando o seu sistema já possuir um identificador único de pedido/transação
(ex.: order_id do e-commerce) e quiser usar para deduplicação/conciliação.
NUNCA use apenas o ID do usuário/cliente como external_id, pois várias cobranças do mesmo usuário
representam intenções de pagamento diferentes e isso bloquearia novas cobranças.
- Em PAYOUTS, external_id é OBRIGATÓRIO (ex.: "saque_847293"): é o identificador da intenção de saque.
- O external_id de payout é imutável. O recurso continua ligado a ele mesmo em failed/reversed;
reenvio equivalente devolve o mesmo payout em HTTP 200.
- Uma nova tentativa operacional após failed/reversed exige NOVA intenção com NOVO external_id.
- Pedido novo -> external_id novo. Nunca reutilize o external_id de outro pedido.
- Reenvio do mesmo external_id com os mesmos dados materiais devolve o recurso existente (HTTP 200),
sem duplicar. Dados divergentes (amount na cobrança; amount ou pix_key no saque) -> 409 EXTERNAL_ID_CONFLICT
com retryable:false.
- Consulte por ele com GET /charges/ext:<external_id>.
- 409 external_id_in_progress -> já existe uma criação em andamento com esse external_id (bloqueio de
até 120 segundos): aguarde e consulte pelo mesmo external_id; não crie outro pedido nem outro external_id.
- Não confunda: Idempotency-Key evita duplicar o mesmo POST; external_id identifica o seu pedido.
RATE LIMIT
- 600 requisições por minuto por chave. Headers x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset; retry-after em 429.
PAGINAÇÃO (cursor, único formato existente)
- Query: limit (1 a 200, padrão 50) e starting_after.
- Resposta: { "data": [...], "has_more": bool, "next_starting_after": string|null }.
- NÃO existe offset, page nem filtro de status em /charges e /payouts.
ENDPOINTS DISPONÍVEIS
1) POST https://api.phanterpay.com.br/v1/charges (charges:write)
body mínimo: { "amount": 100.00 }
body: { amount (>=2, obrigatório), description?, payer_name?, payer_document?, expires_in?, external_id? (opcional), postback_url? (opcional) }
Idempotency-Key OBRIGATÓRIO (8-255). Ausente -> 400 idempotency_key_required.
201: { id: "chg_...", txid, external_id, amount, status, description,
pix: { copy_paste, qr_code_image }, end_to_end_id, paid_at, expires_at, created_at }
Atenção: o Pix copia e cola está em charge.pix.copy_paste e a imagem em charge.pix.qr_code_image.
2) GET https://api.phanterpay.com.br/v1/charges (charges:read) -> página com data/has_more/next_starting_after
3) GET https://api.phanterpay.com.br/v1/charges/{id} (charges:read) -> aceita chg_..., o txid do adquirente ou ext:<external_id> (checar status == "paid")
4) POST https://api.phanterpay.com.br/v1/payouts (payouts:write)
body mínimo: { "amount": 50.00, "pix_key": "...", "external_id": "saque_123" }
body: { amount (>=2, obrigatório, LÍQUIDO ao destinatário), pix_key (obrigatório), external_id (OBRIGATÓRIO),
pix_key_type? (cpf|cnpj|email|phone|random), recipient_name?, recipient_document?, description?, postback_url? }
Idempotency-Key opcional. external_id ausente -> 400 VALIDATION_ERROR.
Gere e persista external_id por intenção de saque (ex.: const externalId = `saque_${withdrawalId}`)
e reutilize o MESMO valor para recuperar a mesma intenção. RETRY da MESMA intenção (timeout, erro de
rede, 5xx, reenvio do job) DEVE usar exatamente o MESMO external_id. NUNCA gere external_id novo
por tentativa (ex.: sufixo com Date.now()): isso cria uma NOVA intenção e pode gerar SAQUE DUPLICADO
real, com dois Pix enviados. Após failed/reversed, uma nova tentativa
operacional deve criar nova intenção com NOVO external_id.
201: { id: "pyt_...", external_id, amount, fee, net_amount, gross_amount, status,
pix: { key, recipient_name, recipient_document }, description, end_to_end_id, error_message, created_at, completed_at }
402 insufficient_balance_for_fees: saldo insuficiente para líquido + taxa. O corpo traz
details.max_withdrawable (quanto pode ser sacado agora), além de requested_amount, available_balance,
fee_amount, total_required e additional_balance_needed — use esse valor para orientar o usuário,
não apenas o genérico INSUFFICIENT_BALANCE.
504 ambiguous_payout_status: o resultado do envio não pôde ser confirmado naquele instante.
NÃO crie outro payout, NÃO gere outra Idempotency-Key e NÃO repita o POST automaticamente.
Mesmo que o envelope traga retryable:true (política genérica de 504), trate ambiguous_payout_status
ANTES da lógica genérica de retry: a ação correta é consultar por GET.
Consulte primeiro GET https://api.phanterpay.com.br/v1/payouts/{id} ou GET https://api.phanterpay.com.br/v1/payouts/ext:<external_id> e só decida depois.
5) GET https://api.phanterpay.com.br/v1/payouts (payouts:read)
6) GET https://api.phanterpay.com.br/v1/payouts/{id} (payouts:read) -> aceita pyt_..., o uuid interno ou ext:<external_id>
Faz SYNC-ON-READ: se o saque ainda estiver em processing, a PhanterPay consulta a situação
autoritativa antes de responder, então esse GET pode concluir o saque quando o webhook atrasar
ou se perder. Use-o como fallback/reconciliação (polling moderado, com backoff).
Nunca marque um saque como pago localmente sem status terminal vindo deste GET ou do webhook.
7) GET https://api.phanterpay.com.br/v1/balance (balance:read)
200: { available_balance, pending_balance, blocked_balance, total_balance, currency: "BRL" }
Este endpoint devolve SOMENTE o saldo em BRL (Pix). O saldo de cripto é um ledger separado em USDT
e não é exposto por /balance — nunca some nem converta os dois saldos na sua integração.
8) GET https://api.phanterpay.com.br/v1/med (med:read) -> query: limit (1-100, padrão 20), starting_after, status
9) GET https://api.phanterpay.com.br/v1/med/{id} (med:read)
10) POST https://api.phanterpay.com.br/v1/med/{id} (med:write) -> body { defense_text (obrigatório), defense_evidence_url? }
ROTAS RESERVADAS (hoje respondem HTTP 501 FEATURE_NOT_AVAILABLE — não implemente fluxo de pagamento nelas)
- POST/GET https://api.phanterpay.com.br/v1/boletos, GET https://api.phanterpay.com.br/v1/boletos/{id}
- POST/GET https://api.phanterpay.com.br/v1/card/charges, GET https://api.phanterpay.com.br/v1/card/charges/{id}
CRIPTO (disponível na API pública — igual ao OpenAPI real)
11) POST https://api.phanterpay.com.br/v1/crypto/charges (charges:write) — depósito em cripto (cash-in)
body: { amount (USD, obrigatório), pay_currency (obrigatório), external_id?, customer_id?, description?, customer_email?, metadata? }
pay_currency aceitos: usdttrc20, usdtbsc, btc, eth, ltc, sol, usdc, trx, xrp, doge, bnbbsc, ton, ada, bch, dai.
Preço em USD; liquidação sempre no saldo USDT, com a taxa aplicável à conta descontada na liquidação, quando houver.
Mínimo comercial por moeda/rede (USDT: 20; demais moedas: 15) e, por cima dele, vale o mínimo técnico dinâmico da rede — aplica-se o maior dos dois.
Idempotência forte: external_id no corpo OU header Idempotency-Key — mesmo identificador com payload diferente retorna 409.
Em falha transitória (timeout, 202, erro de rede) REUTILIZE a mesma Idempotency-Key/external_id: trocar o identificador é a única forma de gerar um depósito duplicado.
Um external_id só serve a um depósito, mesmo depois de expirado — novo depósito exige novo identificador.
201 depósito criado | 200 idempotente (já existente) | 202 aceito e em provisionamento (status "processing": repita a MESMA Idempotency-Key até o status final)
409 idempotency_conflict | 422 invalid_request, unsupported_currency, below_minimum, charge_failed
Recurso: { id: "ccr_...", object: "crypto_charge", status, external_id, customer_id, metadata, price_amount, price_currency: "usd",
pay_amount, pay_currency, paid_amount, amount_status, received_amount, pay_address, pay_extra_id, network,
fee, net_amount, credited_amount, settlement_currency: "usdt", settled_amount, settled_currency,
expires_at, paid_at, created_at }
Campos de valor (regra inequívoca): paid_amount = quantidade on-chain recebida na pay_currency (auditoria, NÃO é USD/USDT);
received_amount = valor liquidado em USD/USDT — use ESTE para creditar o saldo do jogador; credited_amount = líquido do
merchant após taxas (não é saldo do jogador); amount_status = underpaid | exact | overpaid.
Se sua plataforma exibe saldo em BRL, converta received_amount no seu sistema (ex.: POST /crypto/quotes com
operation:"conversion", from "usdt", to "brl"). Nunca trate received_amount diretamente como BRL.
status: processing, waiting, confirming, paid, failed, expired, refunded.
"processing" é transitório: o endereço ainda está sendo provisionado ou recuperado — repita a MESMA Idempotency-Key/external_id até receber "waiting". "paid" é terminal para efeito de crédito.
customer_id é o identificador do jogador/cliente NO SEU sistema; não cria saldo próprio na PhanterPay.
12) GET https://api.phanterpay.com.br/v1/crypto/charges (charges:read) -> query: limit (1-200, padrão 50), external_id, customer_id
Resposta: { data: [...], has_more }
13) GET https://api.phanterpay.com.br/v1/crypto/charges/{id} (charges:read) -> aceita ccr_... ou ext:<external_id>. Nenhum lojista lê depósito de outro. 404 not_found.
14) POST https://api.phanterpay.com.br/v1/crypto/payouts (payouts:write) — saque em cripto (cash-out)
body: { amount (obrigatório, em USDT), currency (obrigatório), address (obrigatório, da rede da moeda), extra_id?, external_id? }
Saques criados pela API são automáticos QUANDO a conta estiver habilitada para cash-out cripto via API e a operação
estiver dentro das regras/limites definidos para a conta, sujeitos a saldo, mínimo, validações, idempotência e disponibilidade da rede.
currency aceitos no cash-out (SOMENTE estes): usdtbsc (USDT BEP20), trx (Tron), btc (Bitcoin), ltc (Litecoin), usdterc20 (USDT ERC20).
usdttrc20 NÃO é válido para cash-out (é aceito apenas no cash-in) — qualquer outra moeda/rede é recusada com unsupported_currency.
Idempotência é OBRIGATÓRIA: informe external_id no corpo ou o header Idempotency-Key. Mesmo identificador com payload idêntico
devolve o mesmo saque, sem novo débito; payload diferente devolve 409 idempotency_conflict. O identificador é escopado por conta.
Mínimo efetivo (dinâmico): max(mínimo comercial PhanterPay de 20 USDT, mínimo técnico dinâmico real de saque da rede).
Consulte GET https://api.phanterpay.com.br/v1/crypto/payouts/minimum antes de criar; o POST revalida o mínimo de qualquer forma.
Abaixo disso a criação é recusada (below_minimum / below_network_minimum).
O ledger é sempre USDT. amount é o principal/referência solicitado em USDT (NÃO é garantia do valor
final entregue on-chain); as taxas comerciais são cobradas POR FORA
(gross_amount = total debitado = amount + fee). A taxa de conversão (conversion_fee, 1%, quando aplicável)
JÁ ESTÁ CONTIDA em fee — nunca some duas vezes. A taxa de rede (network fee) é paga pelo RECEBEDOR:
é descontada do valor entregue on-chain, não do saldo. Por isso receive_estimated pode ser MENOR
que o equivalente a amount.
201: { id: "cpo_...", object: "crypto_payout", status, currency, amount, fee, conversion_fee, net_amount, gross_amount,
settlement_currency: "usdt", origin, external_id, tx_hash, error_message, created_at }
status: requested, processing, completed, failed. Saques da API entram em processing imediatamente (sem aprovação manual); completed é terminal.
403: account_banned, kyc_required, api_cashout_disabled, api_payout_disabled
422: invalid_request, unsupported_currency, invalid_address, below_minimum, below_network_minimum, insufficient_balance,
extra_id_required, currency_unavailable, crypto_disabled
15) GET https://api.phanterpay.com.br/v1/crypto/payouts (payouts:read) -> query: limit (1-200, padrão 50). Resposta: { data: [...], has_more }, do mais recente para o mais antigo.
16) GET https://api.phanterpay.com.br/v1/crypto/payouts/minimum (payouts:read)
Devolve o mínimo EFETIVO atual de saque em cripto: max(mínimo comercial PhanterPay, mínimo técnico dinâmico real da rede).
Consulta ao vivo, sem cache. 200: { currency, network, min_amount, updated_at }
503 minimum_unavailable quando o mínimo não puder ser determinado — nunca um valor estimado.
17) GET https://api.phanterpay.com.br/v1/crypto/payouts/estimate?amount=¤cy= (payouts:read)
Estimativa ANTES de criar, com as MESMAS fontes do POST (não cria saque nem reserva saldo).
200: { object: "crypto_payout_estimate", currency, network, amount, fee, conversion_fee, total_debit, rate,
send_amount, network_fee_estimated, receive_estimated, receive_estimated_usdt }
422 unsupported_currency / invalid_amount | 503 estimate_unavailable (nunca invente um número).
18) GET https://api.phanterpay.com.br/v1/crypto/currencies (charges:read OU payouts:read)
DESCOBERTA OFICIAL de moedas/redes. Chame ANTES de montar qualquer fluxo cripto e não chumbe códigos.
query opcional: flow=deposit | flow=withdrawal (filtra pelo fluxo).
200: { object: "list", data: [ { code, symbol, name, network, network_name, can_deposit, can_withdraw, requires_memo } ] }
"code" é o valor a enviar em pay_currency (depósito) e currency (saque). "network" é o slug estável da rede.
requires_memo=true significa que o destino exige memo/tag (extra_id) além do endereço.
19) POST https://api.phanterpay.com.br/v1/crypto/quotes (withdrawal: payouts:read | deposit: charges:read | conversion: charges:read OU payouts:read)
Cotação INFORMATIVA de conversão antes de criar depósito/saque. NÃO trava preço, não reserva saldo,
não cria recurso e NÃO é aceita como parâmetro em nenhum outro endpoint (não envie quote_id no POST
de depósito/saque). O valor final é sempre recalculado na criação.
body: { from, to, amount, network?, operation: "deposit" | "withdrawal" | "conversion" }
withdrawal: from = "usdt" (moeda do saldo), amount em USDT, to = moeda de saque (can_withdraw).
deposit: from = "usd" (precificação), to = moeda de depósito (can_deposit).
conversion: cotação INFORMATIVA de USD/USDT -> BRL (rotas válidas: from "usd" ou "usdt", to "brl", amount > 0).
Resposta traz network: null, minimum: null, price_locked: false. Serve APENAS para a plataforma converter
o saldo interno do jogador no próprio sistema; a PhanterPay liquida cash-in cripto em USD/USDT.
NUNCA trate received_amount como BRL: se exibe saldo em BRL, consulte /crypto/quotes com
operation:"conversion" e converta você mesmo. Rota fora dessas combinações -> 422 unsupported_route.
200: { object: "crypto_quote", quote_id, operation, from, to, network, amount, estimated_amount, rate,
minimum, minimum_currency, price_locked: false, expires_at, created_at }
NUNCA assuma paridade 1:1 (nem entre redes da mesma stablecoin): use estimated_amount/rate da resposta.
minimum vem da mesma fonte usada na criação, na moeda de "from"; pode vir null (indisponível) — não invente.
Não existe máximo nesta resposta: não invente campo nem limite.
expires_at expira em 60s — cotação vencida deve ser refeita antes de mostrar valor ao usuário.
422 unsupported_currency (moeda fora do catálogo) / unsupported_network (rede não confere com a moeda) /
unsupported_route (moeda não habilitada no fluxo, ou "from" errado) / invalid_amount / below_minimum
503 quote_unavailable = TRANSITÓRIO: repita com backoff curto. NÃO trate como saldo insuficiente,
não bloqueie o usuário e não crie depósito/saque às cegas.
WEBHOOKS
- Webhooks são opcionais. A integração básica pode acompanhar o status por GET /charges/{id}.
- NÃO peça segredo de webhook nem crie endpoint de webhook na integração básica.
- Se o usuário pedir webhooks explicitamente: cadastre no painel ou envie postback_url.
Assinatura "x-phanterpay-signature: t=<unix>,v1=<hex>" — HMAC-SHA256 do secret do endpoint sobre "<t>.<corpo cru>".
Headers auxiliares: x-phanterpay-event-id, x-phanterpay-event-type, x-phanterpay-attempt.
- Eventos Pix entregues hoje: charge.created, charge.paid, payout.created, payout.completed, payout.failed.
- Eventos de cripto entregues hoje: crypto.charge.created, crypto.charge.confirming, crypto.charge.paid,
crypto.charge.failed, crypto.charge.expired, crypto.payout.processing, crypto.payout.completed, crypto.payout.failed.
Cada transição emite um único evento.
- Reservados (aceitos no cadastro, ainda não disparados): charge.expired, charge.refunded, charge.failed,
payout.processing, payout.reversed, med.opened, med.updated, med.resolved. Não existem eventos de split.
- Envelope: { "id", "type", "created_at", "data" }. O campo "data" é o recurso público completo
(mesmo shape de GET /charges/{id} e GET /payouts/{id}).
- Entrega at-least-once: valide a assinatura sobre o corpo cru e deduplique por event.id.
Retentativas: até 6 tentativas, timeout 10s, backoff 1min, 5min, 30min, 2h e 12h.
Responda 2xx rápido e processe de forma assíncrona.
- Se utilizar postback_url, a URL deve estar previamente cadastrada e ativa nos endpoints de webhook
da conta (URL exatamente igual). Caso contrário a API devolve 400 postback_url_not_registered.
O quickstart e a integração básica NÃO devem enviar postback_url.
- Entrega de postback_url usa a MESMA fila persistente e as MESMAS retentativas do webhook global
(tentativa imediata + retries). Se a postback_url for igual a um endpoint global ativo que já
assina o evento, a entrega acontece uma única vez.
- GET /charges/{id} devolve status "paid" assim que a PhanterPay confirma o pagamento — é a fonte
de verdade e pode ser usada como fallback/reconciliação do webhook.
CONFIRMAÇÃO DE PAGAMENTO (ARQUITETURA RECOMENDADA — PIX E CRIPTO)
- Regra de ouro: NUNCA marque um pedido como pago localmente porque o usuário disse que pagou,
porque o QR foi exibido, porque o tempo passou, ou porque um saldo/endereço "parece" ter recebido.
A confirmação é SEMPRE da PhanterPay (webhook ou GET).
- Padrão resiliente: webhook como confirmação principal em tempo real + GET como fallback e
reconciliação. Os DOIS caminhos devem chamar a MESMA função idempotente de liberação do pedido
(liberar duas vezes é bug). Deduplique webhooks por event.id.
- PIX
1. Depois de POST /charges, PERSISTA os identificadores retornados (id chg_..., txid e o
external_id, se você usar) e exiba pix.copy_paste / pix.qr_code_image.
2. Webhook charge.paid é a confirmação principal.
3. GET /charges/{id} (ou /charges/ext:<external_id>) é o fallback/reconciliação até um estado
terminal (paid ou expirado).
4. Esse GET NÃO é leitura passiva: ele faz sync-on-read. Se a cobrança ainda estiver "pending" e
não expirada, a PhanterPay consulta a situação autoritativa do pagamento ANTES de responder.
Ou seja, o GET recupera o pagamento mesmo quando o webhook atrasa ou se perde.
5. Idempotency-Key continua OBRIGATÓRIA em POST /charges; retentativa da MESMA intenção usa a
MESMA chave.
- CRIPTO
1. Depois de POST /crypto/charges, PERSISTA id (ccr_...), payment_id (quando presente),
pay_address, pay_amount, pay_currency, network e status.
2. Webhooks crypto.charge.created/confirming/paid/failed/expired são a confirmação principal.
3. GET /crypto/charges/{id} (ou ext:<external_id>) é o fallback/reconciliação.
4. Em estados NÃO terminais (waiting, confirming) esse GET sincroniza com a situação real do
depósito antes de responder — por isso ele recupera confirmação atrasada ou perdida.
Terminais: paid, failed, expired, refunded.
5. paid = RECEBIMENTO CONFIRMADO. A diferença entre esperado e recebido NÃO muda o status, fica
nos CAMPOS DE VALOR: pay_amount = esperado na moeda on-chain; paid_amount = realmente recebido
on-chain na pay_currency (auditoria/reconciliação blockchain, NÃO é USD/USDT);
received_amount = valor efetivamente liquidado em USD/USDT — é ESTE o campo que integrações com
saldo em USD/USDT devem usar para creditar o cliente/jogador; credited_amount = líquido que
entrou no saldo USDT do merchant após taxas (não use para saldo do cliente final);
amount_status = underpaid | exact | overpaid. Nunca credite o cliente pelo valor solicitado que
não entrou. Sem valor confirmado a cobrança não vira paid (fica confirming).
7. Nunca infira pagamento olhando a blockchain/saldo por conta própria; use status da PhanterPay.
- POLLING
- Em checkout ativo (usuário na tela), consulte a cada 2–5 segundos por um período curto e depois
aplique backoff (ex.: 10s, 30s, 60s) até o estado terminal ou a expiração.
- Fora do checkout, reconcilie em background em intervalos maiores.
- Respeite o rate limit (600 req/min por chave; recue em 429 usando retry-after).
- Nunca prometa SLA de confirmação ao usuário final.
- INTEGRAÇÃO MÍNIMA: peça ao usuário basicamente a PHANTERPAY_API_KEY e, somente quando ele quiser
webhook, a URL de callback do sistema dele. Não invente campos, segredos ou configurações
obrigatórias que a API não exige.
ERROS (envelope único)
{ "success": false, "error": { "code", "message", "retryable", "issues"? }, "request_id" }
Códigos: VALIDATION_ERROR(400), UNAUTHORIZED(401), INSUFFICIENT_BALANCE(402), FORBIDDEN(403), NOT_FOUND(404),
(INSUFFICIENT_BALANCE(402) é o genérico de outros contextos; em POST /payouts o code é sempre o específico
insufficient_balance_for_fees, com details.max_withdrawable — não trate payout pelo genérico.)
IDEMPOTENCY_CONFLICT(409), AMOUNT_LIMIT_EXCEEDED(422), RATE_LIMIT_EXCEEDED(429), INTERNAL_ERROR(500),
FEATURE_NOT_AVAILABLE(501), PROVIDER_UNAVAILABLE(502), SERVICE_UNAVAILABLE(503).
Conflito semântico: EXTERNAL_ID_CONFLICT(409, retryable:false).
Códigos específicos em minúsculo devolvidos pelo runtime: invalid_idempotency_key(400),
postback_url_not_registered(400), idempotency_key_in_progress(409), external_id_in_progress(409),
split_not_available(422), split_disabled(403), ambiguous_payout_status(504), account_banned(403), kyc_required(403),
api_cashin_disabled(403), api_cashout_disabled(403), internal_error(500), provider_unavailable(502).
Decida retentar pelo campo booleano "retryable", nunca pelo texto de "message".
"retryable" controla se a MESMA requisição deve ser repetida automaticamente. Mesmo com retryable:false,
pode existir uma ação segura documentada — como consultar o recurso por GET.
EXCEÇÃO À REGRA GENÉRICA DE RETRY: "retryable" é o sinal padrão, EXCETO quando o contrato define uma ação
query-first. Em ambiguous_payout_status (504), mesmo com retryable:true, NÃO repita o POST /payouts —
consulte a operação existente por GET primeiro.
VARIÁVEIS DE AMBIENTE ESPERADAS
PHANTERPAY_API_KEY=bp_a1b2c3d4_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a3928
PHANTERPAY_API_BASE_URL=https://api.phanterpay.com.br/v1
O QUE EU PRECISO
1. Um cliente HTTP tipado para os endpoints acima, lendo a credencial de process.env.PHANTERPAY_API_KEY (ou equivalente na linguagem).
2. Autenticação por header "Authorization: Bearer <PHANTERPAY_API_KEY>".
3. Criação de cobrança Pix (POST /charges) enviando apenas amount (description é opcional).
4. Leitura e exibição imediata do código Pix copia e cola em charge.pix.copy_paste e imagem em charge.pix.qr_code_image.
5. Consulta de cobrança (GET /charges/{id}) para acompanhar o status (polling) até status == "paid".
6. Tratamento de erros pelo envelope real.
REGRAS DE INTEGRAÇÃO
- Gere a integração diretamente com POST /charges e GET /charges/{id}.
- Execute POST /charges apenas uma vez por intenção de pagamento; nunca implemente polling com POST.
- Proteja a criação contra double submit e chamadas concorrentes (mutation travada enquanto pendente,
botão desabilitado). Em frameworks com retry automático de mutations, desative o retry de POST /charges.
- Não exija webhook, postback_url nem external_id no cash-in; esses são opcionais.
- Idempotency-Key é OBRIGATÓRIA em POST /charges (8-255) e opcional em POST /payouts: chave nova para
operação nova e a mesma chave para retentativa da mesma operação (persista a chave junto do pedido).
- Se enviar external_id na cobrança, use o ID do pedido no meu sistema, um por pedido.
- Em POST /payouts, external_id é OBRIGATÓRIO: um por intenção de saque, persistido antes do POST.
- Trate 409 idempotency_key_in_progress e 409 external_id_in_progress aguardando e repetindo/consultando,
sem criar uma nova operação. Em external_id_in_progress, consulte por GET /charges/ext:<external_id>
em vez de repetir o POST.
- Se POST /payouts retornar ambiguous_payout_status, não crie outro payout e não repita o POST
automaticamente. Consulte primeiro GET /payouts/{id} ou GET /payouts/ext:<external_id>.
- Trate ambiguous_payout_status ANTES da lógica genérica de retry, mesmo quando retryable for true.
- Gere sempre um external_id por intenção de saque (ex.: const externalId = `saque_${withdrawalId}`)
e persista-o antes do POST /payouts.
- Não envie postback_url na integração básica; se enviar, a URL precisa estar cadastrada e ativa
nos webhooks da conta, senão a API devolve 400 postback_url_not_registered.
- Leia a chave do ambiente; não coloque no código.
- Não peça confirmação adicional para escrever ou configurar a integração.Os prompts abaixo ensinam a IA a listar as moedas/redes suportadas em GET /crypto/currencies e a criar uma cotação em POST /crypto/quotes antes de montar o fluxo — sem chumbar códigos de moeda, sem assumir paridade 1:1 e tratando 503, mínimo e expiração de cotação.
Você é um engenheiro de pagamentos sênior. Implemente saques em cripto (cash-out) com a PhanterPay no meu projeto, seguindo o contrato abaixo.
AVISO CRÍTICO: A PhanterPay NÃO usa OAuth, Client ID nem Client Secret. A única credencial é PHANTERPAY_API_KEY. Não tente solicitar ou usar Client ID/Secret.
REQUISITO INICIAL: para começar a integrar peça SOMENTE a PHANTERPAY_API_KEY. NÃO peça PHANTERPAY_WEBHOOK_SECRET, URL de callback nem cadastro de webhook como pré-requisito. Webhook é OPCIONAL/RECOMENDADO (tempo real e menos polling); sem webhook, cash-in Pix, saque Pix e cash-in cripto continuam funcionais pelos GETs de consulta com sync-on-read. Só se o integrador optar por webhook é que se configura a URL de callback e a validação de assinatura/segredo.
CONTRATO DA API PHANTERPAY (v1)
Base URL (oficial): https://api.phanterpay.com.br/v1
OpenAPI 3.1: https://api.phanterpay.com.br/openapi.json
Documentação: https://docs.phanterpay.com.br
FLUXO BÁSICO DE COBRANÇA
- Crie a cobrança com POST /charges enviando apenas { "amount": 100.00 }.
- Utilize pix.copy_paste e pix.qr_code_image retornados pela API.
- Consulte GET /charges/{id} até que status seja "paid".
- O header Idempotency-Key é OBRIGATÓRIO em POST /charges (8 a 255 caracteres). webhook, postback_url e external_id continuam opcionais.
- Gere a Idempotency-Key UMA vez por intenção de pagamento e PERSISTA antes do POST; não gere nova chave em re-render.
CRIAÇÃO ÚNICA (REGRA OBRIGATÓRIA — EVITA COBRANÇA DUPLICADA)
- POST /charges deve ser executado APENAS UMA VEZ por intenção de pagamento.
- NUNCA use POST /charges como polling. Polling é somente GET.
- Não execute POST /charges automaticamente em re-render ou useEffect sem trava contra duplicidade.
- Enquanto a criação estiver pendente, bloqueie uma segunda mutation (ex.: if (createCharge.isPending) return;)
e mantenha o botão de criação desabilitado.
- Se usar external_id (opcional), gere uma única vez por intenção e PERSISTA antes do POST.
Não gere novos identificadores durante re-render.
- Desative o retry automático de POST /charges em clientes HTTP/frameworks que possam duplicar chamadas.
- Depois da criação, acompanhe o status SOMENTE por GET /charges/{id} ou GET /charges/ext:<external_id>.
- Se receber 409 external_id_in_progress: não repita o POST automaticamente, não gere novo external_id,
aguarde alguns segundos e consulte por GET /charges/ext:<external_id>.
- retryable:false em external_id_in_progress NÃO significa abandonar a operação; significa apenas
não repetir automaticamente o POST — consultar por GET continua correto.
REGRAS GERAIS
- Valor mínimo por operação: R$ 2,00 (amount >= 2).
- Split de pagamentos NÃO vem liberado por padrão: o recurso só funciona depois de ser HABILITADO
PELO ADMIN da PhanterPay na conta. O merchant não habilita sozinho pela API e Split não faz parte
da integração básica. Sem habilitação, enviar o array "split" em POST /charges devolve
403 split_disabled. O campo split_rule_id não é suportado no contrato público e devolve
422 split_not_available. Só implemente split se a conta já estiver habilitada pelo admin.
- Use apenas os endpoints e campos documentados. Não invente rotas, campos, filtros ou parâmetros.
AUTENTICAÇÃO
- Header: "Authorization: Bearer <chave>" (alternativa equivalente: "x-api-key: <chave>").
- Formato real da chave: bp_<8 hex>_<48 hex>. Exemplo: bp_a1b2c3d4_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a3928
- Não existem chaves públicas (pk_).
- Leia a credencial SEMPRE da variável de ambiente PHANTERPAY_API_KEY.
- NUNCA escreva a chave no código-fonte, em front-end, em app mobile ou em repositório.
ESCOPOS
- charges:read, charges:write, payouts:read, payouts:write, transactions:read, balance:read, med:read, med:write.
- GET /transactions aceita transactions:read ou, por compatibilidade, charges:read + payouts:read.
- Escopo ausente devolve 403 FORBIDDEN.
IDEMPOTÊNCIA
- Header "Idempotency-Key" é OBRIGATÓRIO em POST /charges e OPCIONAL em POST /payouts.
- Use 8 a 255 caracteres. Ausente em POST /charges -> 400 idempotency_key_required. Fora desse tamanho -> 400 invalid_idempotency_key.
- REGRA SIMPLES: operação nova -> chave nova. Retentativa da MESMA operação -> a MESMA chave.
- Derive a chave do identificador do pedido (ex.: "charge-pedido-847293") ou gere uma vez e PERSISTA
no seu banco antes de chamar a API, para reenviar exatamente a mesma chave na retentativa.
- Mesma chave + mesmo corpo já concluído -> devolve a resposta original com header "idempotent-replayed: true".
- Mesma chave + corpo diferente -> 409 IDEMPOTENCY_CONFLICT.
- Mesma chave ainda em processamento -> 409 idempotency_key_in_progress: aguarde alguns segundos e
repita a MESMA requisição com a MESMA chave e o MESMO corpo. Nunca gere outra chave nem outra
intenção de pagamento para contornar.
- Em timeout de rede, repita com a MESMA chave; nunca gere outra.
EXTERNAL_ID
- Em CHARGES, external_id é OPCIONAL. Não crie external_id automaticamente na integração básica.
Envie external_id apenas quando o seu sistema já possuir um identificador único de pedido/transação
(ex.: order_id do e-commerce) e quiser usar para deduplicação/conciliação.
NUNCA use apenas o ID do usuário/cliente como external_id, pois várias cobranças do mesmo usuário
representam intenções de pagamento diferentes e isso bloquearia novas cobranças.
- Em PAYOUTS, external_id é OBRIGATÓRIO (ex.: "saque_847293"): é o identificador da intenção de saque.
- O external_id de payout é imutável. O recurso continua ligado a ele mesmo em failed/reversed;
reenvio equivalente devolve o mesmo payout em HTTP 200.
- Uma nova tentativa operacional após failed/reversed exige NOVA intenção com NOVO external_id.
- Pedido novo -> external_id novo. Nunca reutilize o external_id de outro pedido.
- Reenvio do mesmo external_id com os mesmos dados materiais devolve o recurso existente (HTTP 200),
sem duplicar. Dados divergentes (amount na cobrança; amount ou pix_key no saque) -> 409 EXTERNAL_ID_CONFLICT
com retryable:false.
- Consulte por ele com GET /charges/ext:<external_id>.
- 409 external_id_in_progress -> já existe uma criação em andamento com esse external_id (bloqueio de
até 120 segundos): aguarde e consulte pelo mesmo external_id; não crie outro pedido nem outro external_id.
- Não confunda: Idempotency-Key evita duplicar o mesmo POST; external_id identifica o seu pedido.
RATE LIMIT
- 600 requisições por minuto por chave. Headers x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset; retry-after em 429.
PAGINAÇÃO (cursor, único formato existente)
- Query: limit (1 a 200, padrão 50) e starting_after.
- Resposta: { "data": [...], "has_more": bool, "next_starting_after": string|null }.
- NÃO existe offset, page nem filtro de status em /charges e /payouts.
ENDPOINTS DISPONÍVEIS
1) POST https://api.phanterpay.com.br/v1/charges (charges:write)
body mínimo: { "amount": 100.00 }
body: { amount (>=2, obrigatório), description?, payer_name?, payer_document?, expires_in?, external_id? (opcional), postback_url? (opcional) }
Idempotency-Key OBRIGATÓRIO (8-255). Ausente -> 400 idempotency_key_required.
201: { id: "chg_...", txid, external_id, amount, status, description,
pix: { copy_paste, qr_code_image }, end_to_end_id, paid_at, expires_at, created_at }
Atenção: o Pix copia e cola está em charge.pix.copy_paste e a imagem em charge.pix.qr_code_image.
2) GET https://api.phanterpay.com.br/v1/charges (charges:read) -> página com data/has_more/next_starting_after
3) GET https://api.phanterpay.com.br/v1/charges/{id} (charges:read) -> aceita chg_..., o txid do adquirente ou ext:<external_id> (checar status == "paid")
4) POST https://api.phanterpay.com.br/v1/payouts (payouts:write)
body mínimo: { "amount": 50.00, "pix_key": "...", "external_id": "saque_123" }
body: { amount (>=2, obrigatório, LÍQUIDO ao destinatário), pix_key (obrigatório), external_id (OBRIGATÓRIO),
pix_key_type? (cpf|cnpj|email|phone|random), recipient_name?, recipient_document?, description?, postback_url? }
Idempotency-Key opcional. external_id ausente -> 400 VALIDATION_ERROR.
Gere e persista external_id por intenção de saque (ex.: const externalId = `saque_${withdrawalId}`)
e reutilize o MESMO valor para recuperar a mesma intenção. RETRY da MESMA intenção (timeout, erro de
rede, 5xx, reenvio do job) DEVE usar exatamente o MESMO external_id. NUNCA gere external_id novo
por tentativa (ex.: sufixo com Date.now()): isso cria uma NOVA intenção e pode gerar SAQUE DUPLICADO
real, com dois Pix enviados. Após failed/reversed, uma nova tentativa
operacional deve criar nova intenção com NOVO external_id.
201: { id: "pyt_...", external_id, amount, fee, net_amount, gross_amount, status,
pix: { key, recipient_name, recipient_document }, description, end_to_end_id, error_message, created_at, completed_at }
402 insufficient_balance_for_fees: saldo insuficiente para líquido + taxa. O corpo traz
details.max_withdrawable (quanto pode ser sacado agora), além de requested_amount, available_balance,
fee_amount, total_required e additional_balance_needed — use esse valor para orientar o usuário,
não apenas o genérico INSUFFICIENT_BALANCE.
504 ambiguous_payout_status: o resultado do envio não pôde ser confirmado naquele instante.
NÃO crie outro payout, NÃO gere outra Idempotency-Key e NÃO repita o POST automaticamente.
Mesmo que o envelope traga retryable:true (política genérica de 504), trate ambiguous_payout_status
ANTES da lógica genérica de retry: a ação correta é consultar por GET.
Consulte primeiro GET https://api.phanterpay.com.br/v1/payouts/{id} ou GET https://api.phanterpay.com.br/v1/payouts/ext:<external_id> e só decida depois.
5) GET https://api.phanterpay.com.br/v1/payouts (payouts:read)
6) GET https://api.phanterpay.com.br/v1/payouts/{id} (payouts:read) -> aceita pyt_..., o uuid interno ou ext:<external_id>
Faz SYNC-ON-READ: se o saque ainda estiver em processing, a PhanterPay consulta a situação
autoritativa antes de responder, então esse GET pode concluir o saque quando o webhook atrasar
ou se perder. Use-o como fallback/reconciliação (polling moderado, com backoff).
Nunca marque um saque como pago localmente sem status terminal vindo deste GET ou do webhook.
7) GET https://api.phanterpay.com.br/v1/balance (balance:read)
200: { available_balance, pending_balance, blocked_balance, total_balance, currency: "BRL" }
Este endpoint devolve SOMENTE o saldo em BRL (Pix). O saldo de cripto é um ledger separado em USDT
e não é exposto por /balance — nunca some nem converta os dois saldos na sua integração.
8) GET https://api.phanterpay.com.br/v1/med (med:read) -> query: limit (1-100, padrão 20), starting_after, status
9) GET https://api.phanterpay.com.br/v1/med/{id} (med:read)
10) POST https://api.phanterpay.com.br/v1/med/{id} (med:write) -> body { defense_text (obrigatório), defense_evidence_url? }
ROTAS RESERVADAS (hoje respondem HTTP 501 FEATURE_NOT_AVAILABLE — não implemente fluxo de pagamento nelas)
- POST/GET https://api.phanterpay.com.br/v1/boletos, GET https://api.phanterpay.com.br/v1/boletos/{id}
- POST/GET https://api.phanterpay.com.br/v1/card/charges, GET https://api.phanterpay.com.br/v1/card/charges/{id}
CRIPTO (disponível na API pública — igual ao OpenAPI real)
11) POST https://api.phanterpay.com.br/v1/crypto/charges (charges:write) — depósito em cripto (cash-in)
body: { amount (USD, obrigatório), pay_currency (obrigatório), external_id?, customer_id?, description?, customer_email?, metadata? }
pay_currency aceitos: usdttrc20, usdtbsc, btc, eth, ltc, sol, usdc, trx, xrp, doge, bnbbsc, ton, ada, bch, dai.
Preço em USD; liquidação sempre no saldo USDT, com a taxa aplicável à conta descontada na liquidação, quando houver.
Mínimo comercial por moeda/rede (USDT: 20; demais moedas: 15) e, por cima dele, vale o mínimo técnico dinâmico da rede — aplica-se o maior dos dois.
Idempotência forte: external_id no corpo OU header Idempotency-Key — mesmo identificador com payload diferente retorna 409.
Em falha transitória (timeout, 202, erro de rede) REUTILIZE a mesma Idempotency-Key/external_id: trocar o identificador é a única forma de gerar um depósito duplicado.
Um external_id só serve a um depósito, mesmo depois de expirado — novo depósito exige novo identificador.
201 depósito criado | 200 idempotente (já existente) | 202 aceito e em provisionamento (status "processing": repita a MESMA Idempotency-Key até o status final)
409 idempotency_conflict | 422 invalid_request, unsupported_currency, below_minimum, charge_failed
Recurso: { id: "ccr_...", object: "crypto_charge", status, external_id, customer_id, metadata, price_amount, price_currency: "usd",
pay_amount, pay_currency, paid_amount, amount_status, received_amount, pay_address, pay_extra_id, network,
fee, net_amount, credited_amount, settlement_currency: "usdt", settled_amount, settled_currency,
expires_at, paid_at, created_at }
Campos de valor (regra inequívoca): paid_amount = quantidade on-chain recebida na pay_currency (auditoria, NÃO é USD/USDT);
received_amount = valor liquidado em USD/USDT — use ESTE para creditar o saldo do jogador; credited_amount = líquido do
merchant após taxas (não é saldo do jogador); amount_status = underpaid | exact | overpaid.
Se sua plataforma exibe saldo em BRL, converta received_amount no seu sistema (ex.: POST /crypto/quotes com
operation:"conversion", from "usdt", to "brl"). Nunca trate received_amount diretamente como BRL.
status: processing, waiting, confirming, paid, failed, expired, refunded.
"processing" é transitório: o endereço ainda está sendo provisionado ou recuperado — repita a MESMA Idempotency-Key/external_id até receber "waiting". "paid" é terminal para efeito de crédito.
customer_id é o identificador do jogador/cliente NO SEU sistema; não cria saldo próprio na PhanterPay.
12) GET https://api.phanterpay.com.br/v1/crypto/charges (charges:read) -> query: limit (1-200, padrão 50), external_id, customer_id
Resposta: { data: [...], has_more }
13) GET https://api.phanterpay.com.br/v1/crypto/charges/{id} (charges:read) -> aceita ccr_... ou ext:<external_id>. Nenhum lojista lê depósito de outro. 404 not_found.
14) POST https://api.phanterpay.com.br/v1/crypto/payouts (payouts:write) — saque em cripto (cash-out)
body: { amount (obrigatório, em USDT), currency (obrigatório), address (obrigatório, da rede da moeda), extra_id?, external_id? }
Saques criados pela API são automáticos QUANDO a conta estiver habilitada para cash-out cripto via API e a operação
estiver dentro das regras/limites definidos para a conta, sujeitos a saldo, mínimo, validações, idempotência e disponibilidade da rede.
currency aceitos no cash-out (SOMENTE estes): usdtbsc (USDT BEP20), trx (Tron), btc (Bitcoin), ltc (Litecoin), usdterc20 (USDT ERC20).
usdttrc20 NÃO é válido para cash-out (é aceito apenas no cash-in) — qualquer outra moeda/rede é recusada com unsupported_currency.
Idempotência é OBRIGATÓRIA: informe external_id no corpo ou o header Idempotency-Key. Mesmo identificador com payload idêntico
devolve o mesmo saque, sem novo débito; payload diferente devolve 409 idempotency_conflict. O identificador é escopado por conta.
Mínimo efetivo (dinâmico): max(mínimo comercial PhanterPay de 20 USDT, mínimo técnico dinâmico real de saque da rede).
Consulte GET https://api.phanterpay.com.br/v1/crypto/payouts/minimum antes de criar; o POST revalida o mínimo de qualquer forma.
Abaixo disso a criação é recusada (below_minimum / below_network_minimum).
O ledger é sempre USDT. amount é o principal/referência solicitado em USDT (NÃO é garantia do valor
final entregue on-chain); as taxas comerciais são cobradas POR FORA
(gross_amount = total debitado = amount + fee). A taxa de conversão (conversion_fee, 1%, quando aplicável)
JÁ ESTÁ CONTIDA em fee — nunca some duas vezes. A taxa de rede (network fee) é paga pelo RECEBEDOR:
é descontada do valor entregue on-chain, não do saldo. Por isso receive_estimated pode ser MENOR
que o equivalente a amount.
201: { id: "cpo_...", object: "crypto_payout", status, currency, amount, fee, conversion_fee, net_amount, gross_amount,
settlement_currency: "usdt", origin, external_id, tx_hash, error_message, created_at }
status: requested, processing, completed, failed. Saques da API entram em processing imediatamente (sem aprovação manual); completed é terminal.
403: account_banned, kyc_required, api_cashout_disabled, api_payout_disabled
422: invalid_request, unsupported_currency, invalid_address, below_minimum, below_network_minimum, insufficient_balance,
extra_id_required, currency_unavailable, crypto_disabled
15) GET https://api.phanterpay.com.br/v1/crypto/payouts (payouts:read) -> query: limit (1-200, padrão 50). Resposta: { data: [...], has_more }, do mais recente para o mais antigo.
16) GET https://api.phanterpay.com.br/v1/crypto/payouts/minimum (payouts:read)
Devolve o mínimo EFETIVO atual de saque em cripto: max(mínimo comercial PhanterPay, mínimo técnico dinâmico real da rede).
Consulta ao vivo, sem cache. 200: { currency, network, min_amount, updated_at }
503 minimum_unavailable quando o mínimo não puder ser determinado — nunca um valor estimado.
17) GET https://api.phanterpay.com.br/v1/crypto/payouts/estimate?amount=¤cy= (payouts:read)
Estimativa ANTES de criar, com as MESMAS fontes do POST (não cria saque nem reserva saldo).
200: { object: "crypto_payout_estimate", currency, network, amount, fee, conversion_fee, total_debit, rate,
send_amount, network_fee_estimated, receive_estimated, receive_estimated_usdt }
422 unsupported_currency / invalid_amount | 503 estimate_unavailable (nunca invente um número).
18) GET https://api.phanterpay.com.br/v1/crypto/currencies (charges:read OU payouts:read)
DESCOBERTA OFICIAL de moedas/redes. Chame ANTES de montar qualquer fluxo cripto e não chumbe códigos.
query opcional: flow=deposit | flow=withdrawal (filtra pelo fluxo).
200: { object: "list", data: [ { code, symbol, name, network, network_name, can_deposit, can_withdraw, requires_memo } ] }
"code" é o valor a enviar em pay_currency (depósito) e currency (saque). "network" é o slug estável da rede.
requires_memo=true significa que o destino exige memo/tag (extra_id) além do endereço.
19) POST https://api.phanterpay.com.br/v1/crypto/quotes (withdrawal: payouts:read | deposit: charges:read | conversion: charges:read OU payouts:read)
Cotação INFORMATIVA de conversão antes de criar depósito/saque. NÃO trava preço, não reserva saldo,
não cria recurso e NÃO é aceita como parâmetro em nenhum outro endpoint (não envie quote_id no POST
de depósito/saque). O valor final é sempre recalculado na criação.
body: { from, to, amount, network?, operation: "deposit" | "withdrawal" | "conversion" }
withdrawal: from = "usdt" (moeda do saldo), amount em USDT, to = moeda de saque (can_withdraw).
deposit: from = "usd" (precificação), to = moeda de depósito (can_deposit).
conversion: cotação INFORMATIVA de USD/USDT -> BRL (rotas válidas: from "usd" ou "usdt", to "brl", amount > 0).
Resposta traz network: null, minimum: null, price_locked: false. Serve APENAS para a plataforma converter
o saldo interno do jogador no próprio sistema; a PhanterPay liquida cash-in cripto em USD/USDT.
NUNCA trate received_amount como BRL: se exibe saldo em BRL, consulte /crypto/quotes com
operation:"conversion" e converta você mesmo. Rota fora dessas combinações -> 422 unsupported_route.
200: { object: "crypto_quote", quote_id, operation, from, to, network, amount, estimated_amount, rate,
minimum, minimum_currency, price_locked: false, expires_at, created_at }
NUNCA assuma paridade 1:1 (nem entre redes da mesma stablecoin): use estimated_amount/rate da resposta.
minimum vem da mesma fonte usada na criação, na moeda de "from"; pode vir null (indisponível) — não invente.
Não existe máximo nesta resposta: não invente campo nem limite.
expires_at expira em 60s — cotação vencida deve ser refeita antes de mostrar valor ao usuário.
422 unsupported_currency (moeda fora do catálogo) / unsupported_network (rede não confere com a moeda) /
unsupported_route (moeda não habilitada no fluxo, ou "from" errado) / invalid_amount / below_minimum
503 quote_unavailable = TRANSITÓRIO: repita com backoff curto. NÃO trate como saldo insuficiente,
não bloqueie o usuário e não crie depósito/saque às cegas.
WEBHOOKS
- Webhooks são opcionais. A integração básica pode acompanhar o status por GET /charges/{id}.
- NÃO peça segredo de webhook nem crie endpoint de webhook na integração básica.
- Se o usuário pedir webhooks explicitamente: cadastre no painel ou envie postback_url.
Assinatura "x-phanterpay-signature: t=<unix>,v1=<hex>" — HMAC-SHA256 do secret do endpoint sobre "<t>.<corpo cru>".
Headers auxiliares: x-phanterpay-event-id, x-phanterpay-event-type, x-phanterpay-attempt.
- Eventos Pix entregues hoje: charge.created, charge.paid, payout.created, payout.completed, payout.failed.
- Eventos de cripto entregues hoje: crypto.charge.created, crypto.charge.confirming, crypto.charge.paid,
crypto.charge.failed, crypto.charge.expired, crypto.payout.processing, crypto.payout.completed, crypto.payout.failed.
Cada transição emite um único evento.
- Reservados (aceitos no cadastro, ainda não disparados): charge.expired, charge.refunded, charge.failed,
payout.processing, payout.reversed, med.opened, med.updated, med.resolved. Não existem eventos de split.
- Envelope: { "id", "type", "created_at", "data" }. O campo "data" é o recurso público completo
(mesmo shape de GET /charges/{id} e GET /payouts/{id}).
- Entrega at-least-once: valide a assinatura sobre o corpo cru e deduplique por event.id.
Retentativas: até 6 tentativas, timeout 10s, backoff 1min, 5min, 30min, 2h e 12h.
Responda 2xx rápido e processe de forma assíncrona.
- Se utilizar postback_url, a URL deve estar previamente cadastrada e ativa nos endpoints de webhook
da conta (URL exatamente igual). Caso contrário a API devolve 400 postback_url_not_registered.
O quickstart e a integração básica NÃO devem enviar postback_url.
- Entrega de postback_url usa a MESMA fila persistente e as MESMAS retentativas do webhook global
(tentativa imediata + retries). Se a postback_url for igual a um endpoint global ativo que já
assina o evento, a entrega acontece uma única vez.
- GET /charges/{id} devolve status "paid" assim que a PhanterPay confirma o pagamento — é a fonte
de verdade e pode ser usada como fallback/reconciliação do webhook.
CONFIRMAÇÃO DE PAGAMENTO (ARQUITETURA RECOMENDADA — PIX E CRIPTO)
- Regra de ouro: NUNCA marque um pedido como pago localmente porque o usuário disse que pagou,
porque o QR foi exibido, porque o tempo passou, ou porque um saldo/endereço "parece" ter recebido.
A confirmação é SEMPRE da PhanterPay (webhook ou GET).
- Padrão resiliente: webhook como confirmação principal em tempo real + GET como fallback e
reconciliação. Os DOIS caminhos devem chamar a MESMA função idempotente de liberação do pedido
(liberar duas vezes é bug). Deduplique webhooks por event.id.
- PIX
1. Depois de POST /charges, PERSISTA os identificadores retornados (id chg_..., txid e o
external_id, se você usar) e exiba pix.copy_paste / pix.qr_code_image.
2. Webhook charge.paid é a confirmação principal.
3. GET /charges/{id} (ou /charges/ext:<external_id>) é o fallback/reconciliação até um estado
terminal (paid ou expirado).
4. Esse GET NÃO é leitura passiva: ele faz sync-on-read. Se a cobrança ainda estiver "pending" e
não expirada, a PhanterPay consulta a situação autoritativa do pagamento ANTES de responder.
Ou seja, o GET recupera o pagamento mesmo quando o webhook atrasa ou se perde.
5. Idempotency-Key continua OBRIGATÓRIA em POST /charges; retentativa da MESMA intenção usa a
MESMA chave.
- CRIPTO
1. Depois de POST /crypto/charges, PERSISTA id (ccr_...), payment_id (quando presente),
pay_address, pay_amount, pay_currency, network e status.
2. Webhooks crypto.charge.created/confirming/paid/failed/expired são a confirmação principal.
3. GET /crypto/charges/{id} (ou ext:<external_id>) é o fallback/reconciliação.
4. Em estados NÃO terminais (waiting, confirming) esse GET sincroniza com a situação real do
depósito antes de responder — por isso ele recupera confirmação atrasada ou perdida.
Terminais: paid, failed, expired, refunded.
5. paid = RECEBIMENTO CONFIRMADO. A diferença entre esperado e recebido NÃO muda o status, fica
nos CAMPOS DE VALOR: pay_amount = esperado na moeda on-chain; paid_amount = realmente recebido
on-chain na pay_currency (auditoria/reconciliação blockchain, NÃO é USD/USDT);
received_amount = valor efetivamente liquidado em USD/USDT — é ESTE o campo que integrações com
saldo em USD/USDT devem usar para creditar o cliente/jogador; credited_amount = líquido que
entrou no saldo USDT do merchant após taxas (não use para saldo do cliente final);
amount_status = underpaid | exact | overpaid. Nunca credite o cliente pelo valor solicitado que
não entrou. Sem valor confirmado a cobrança não vira paid (fica confirming).
7. Nunca infira pagamento olhando a blockchain/saldo por conta própria; use status da PhanterPay.
- POLLING
- Em checkout ativo (usuário na tela), consulte a cada 2–5 segundos por um período curto e depois
aplique backoff (ex.: 10s, 30s, 60s) até o estado terminal ou a expiração.
- Fora do checkout, reconcilie em background em intervalos maiores.
- Respeite o rate limit (600 req/min por chave; recue em 429 usando retry-after).
- Nunca prometa SLA de confirmação ao usuário final.
- INTEGRAÇÃO MÍNIMA: peça ao usuário basicamente a PHANTERPAY_API_KEY e, somente quando ele quiser
webhook, a URL de callback do sistema dele. Não invente campos, segredos ou configurações
obrigatórias que a API não exige.
ERROS (envelope único)
{ "success": false, "error": { "code", "message", "retryable", "issues"? }, "request_id" }
Códigos: VALIDATION_ERROR(400), UNAUTHORIZED(401), INSUFFICIENT_BALANCE(402), FORBIDDEN(403), NOT_FOUND(404),
(INSUFFICIENT_BALANCE(402) é o genérico de outros contextos; em POST /payouts o code é sempre o específico
insufficient_balance_for_fees, com details.max_withdrawable — não trate payout pelo genérico.)
IDEMPOTENCY_CONFLICT(409), AMOUNT_LIMIT_EXCEEDED(422), RATE_LIMIT_EXCEEDED(429), INTERNAL_ERROR(500),
FEATURE_NOT_AVAILABLE(501), PROVIDER_UNAVAILABLE(502), SERVICE_UNAVAILABLE(503).
Conflito semântico: EXTERNAL_ID_CONFLICT(409, retryable:false).
Códigos específicos em minúsculo devolvidos pelo runtime: invalid_idempotency_key(400),
postback_url_not_registered(400), idempotency_key_in_progress(409), external_id_in_progress(409),
split_not_available(422), split_disabled(403), ambiguous_payout_status(504), account_banned(403), kyc_required(403),
api_cashin_disabled(403), api_cashout_disabled(403), internal_error(500), provider_unavailable(502).
Decida retentar pelo campo booleano "retryable", nunca pelo texto de "message".
"retryable" controla se a MESMA requisição deve ser repetida automaticamente. Mesmo com retryable:false,
pode existir uma ação segura documentada — como consultar o recurso por GET.
EXCEÇÃO À REGRA GENÉRICA DE RETRY: "retryable" é o sinal padrão, EXCETO quando o contrato define uma ação
query-first. Em ambiguous_payout_status (504), mesmo com retryable:true, NÃO repita o POST /payouts —
consulte a operação existente por GET primeiro.
VARIÁVEIS DE AMBIENTE ESPERADAS
PHANTERPAY_API_KEY=bp_a1b2c3d4_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a3928
PHANTERPAY_API_BASE_URL=https://api.phanterpay.com.br/v1
ORDEM OBRIGATÓRIA DE IMPLEMENTAÇÃO
1. GET https://api.phanterpay.com.br/v1/crypto/currencies?flow=withdrawal — descubra as moedas/redes reais e monte a seleção do usuário
a partir dessa resposta. NÃO chumbe códigos de moeda nem nomes de rede no código.
2. POST https://api.phanterpay.com.br/v1/crypto/quotes com { from: "usdt", to: <code>, amount: <USDT>, network: <network>, operation: "withdrawal" }
para mostrar o valor estimado ANTES de confirmar. Use estimated_amount e rate da resposta; nunca calcule 1:1.
3. GET https://api.phanterpay.com.br/v1/crypto/payouts/minimum e/ou o campo minimum da cotação para bloquear valores abaixo do mínimo.
4. GET https://api.phanterpay.com.br/v1/crypto/payouts/estimate?amount=¤cy= para exibir total debitado e estimativa a receber.
5. POST https://api.phanterpay.com.br/v1/crypto/payouts com external_id (ou Idempotency-Key) persistido ANTES da chamada.
6. Acompanhe por webhook crypto.payout.* e, como fallback, GET https://api.phanterpay.com.br/v1/crypto/payouts.
REGRAS QUE VOCÊ NÃO PODE VIOLAR
- A cotação é informativa: price_locked é sempre false e não há travamento de preço. NÃO envie quote_id no
POST /crypto/payouts (o endpoint não aceita esse campo) e não prometa ao usuário o valor exato da cotação.
- Se expires_at da cotação já passou, refaça a cotação antes de exibir/confirmar. Não reutilize cotação vencida.
- 422 unsupported_currency / unsupported_network / unsupported_route: o problema é a moeda ou a rede escolhida.
Recarregue GET /crypto/currencies e corrija a seleção; não tente outra moeda por conta própria.
- 422 below_minimum: mostre o mínimo devolvido na resposta e peça um valor maior. Se minimum vier null,
não invente número: apenas informe que o mínimo não está disponível agora.
- 503 quote_unavailable é TRANSITÓRIO: repita com backoff curto (ex.: 1s, 2s, 4s, máximo 3 tentativas).
NUNCA trate como saldo insuficiente, não bloqueie a conta e não crie o saque sem cotação válida.
- Não invente campos que não estão no contrato (não existe maximum, nem fee dentro da cotação).
- Idempotência: um external_id por intenção de saque; repetição da MESMA operação usa o MESMO identificador.
- Leia a credencial de process.env.PHANTERPAY_API_KEY (ou equivalente); nunca escreva a chave no código.
- Toda a integração fala apenas com a API da PhanterPay. Não adicione nenhum outro provedor ou SDK externo.
- Não peça confirmação adicional para escrever a integração.Você é um engenheiro de pagamentos sênior. Implemente depósitos em cripto (cash-in) com a PhanterPay no meu projeto, seguindo o contrato abaixo.
AVISO CRÍTICO: A PhanterPay NÃO usa OAuth, Client ID nem Client Secret. A única credencial é PHANTERPAY_API_KEY. Não tente solicitar ou usar Client ID/Secret.
REQUISITO INICIAL: para começar a integrar peça SOMENTE a PHANTERPAY_API_KEY. NÃO peça PHANTERPAY_WEBHOOK_SECRET, URL de callback nem cadastro de webhook como pré-requisito. Webhook é OPCIONAL/RECOMENDADO (tempo real e menos polling); sem webhook, cash-in Pix, saque Pix e cash-in cripto continuam funcionais pelos GETs de consulta com sync-on-read. Só se o integrador optar por webhook é que se configura a URL de callback e a validação de assinatura/segredo.
CONTRATO DA API PHANTERPAY (v1)
Base URL (oficial): https://api.phanterpay.com.br/v1
OpenAPI 3.1: https://api.phanterpay.com.br/openapi.json
Documentação: https://docs.phanterpay.com.br
FLUXO BÁSICO DE COBRANÇA
- Crie a cobrança com POST /charges enviando apenas { "amount": 100.00 }.
- Utilize pix.copy_paste e pix.qr_code_image retornados pela API.
- Consulte GET /charges/{id} até que status seja "paid".
- O header Idempotency-Key é OBRIGATÓRIO em POST /charges (8 a 255 caracteres). webhook, postback_url e external_id continuam opcionais.
- Gere a Idempotency-Key UMA vez por intenção de pagamento e PERSISTA antes do POST; não gere nova chave em re-render.
CRIAÇÃO ÚNICA (REGRA OBRIGATÓRIA — EVITA COBRANÇA DUPLICADA)
- POST /charges deve ser executado APENAS UMA VEZ por intenção de pagamento.
- NUNCA use POST /charges como polling. Polling é somente GET.
- Não execute POST /charges automaticamente em re-render ou useEffect sem trava contra duplicidade.
- Enquanto a criação estiver pendente, bloqueie uma segunda mutation (ex.: if (createCharge.isPending) return;)
e mantenha o botão de criação desabilitado.
- Se usar external_id (opcional), gere uma única vez por intenção e PERSISTA antes do POST.
Não gere novos identificadores durante re-render.
- Desative o retry automático de POST /charges em clientes HTTP/frameworks que possam duplicar chamadas.
- Depois da criação, acompanhe o status SOMENTE por GET /charges/{id} ou GET /charges/ext:<external_id>.
- Se receber 409 external_id_in_progress: não repita o POST automaticamente, não gere novo external_id,
aguarde alguns segundos e consulte por GET /charges/ext:<external_id>.
- retryable:false em external_id_in_progress NÃO significa abandonar a operação; significa apenas
não repetir automaticamente o POST — consultar por GET continua correto.
REGRAS GERAIS
- Valor mínimo por operação: R$ 2,00 (amount >= 2).
- Split de pagamentos NÃO vem liberado por padrão: o recurso só funciona depois de ser HABILITADO
PELO ADMIN da PhanterPay na conta. O merchant não habilita sozinho pela API e Split não faz parte
da integração básica. Sem habilitação, enviar o array "split" em POST /charges devolve
403 split_disabled. O campo split_rule_id não é suportado no contrato público e devolve
422 split_not_available. Só implemente split se a conta já estiver habilitada pelo admin.
- Use apenas os endpoints e campos documentados. Não invente rotas, campos, filtros ou parâmetros.
AUTENTICAÇÃO
- Header: "Authorization: Bearer <chave>" (alternativa equivalente: "x-api-key: <chave>").
- Formato real da chave: bp_<8 hex>_<48 hex>. Exemplo: bp_a1b2c3d4_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a3928
- Não existem chaves públicas (pk_).
- Leia a credencial SEMPRE da variável de ambiente PHANTERPAY_API_KEY.
- NUNCA escreva a chave no código-fonte, em front-end, em app mobile ou em repositório.
ESCOPOS
- charges:read, charges:write, payouts:read, payouts:write, transactions:read, balance:read, med:read, med:write.
- GET /transactions aceita transactions:read ou, por compatibilidade, charges:read + payouts:read.
- Escopo ausente devolve 403 FORBIDDEN.
IDEMPOTÊNCIA
- Header "Idempotency-Key" é OBRIGATÓRIO em POST /charges e OPCIONAL em POST /payouts.
- Use 8 a 255 caracteres. Ausente em POST /charges -> 400 idempotency_key_required. Fora desse tamanho -> 400 invalid_idempotency_key.
- REGRA SIMPLES: operação nova -> chave nova. Retentativa da MESMA operação -> a MESMA chave.
- Derive a chave do identificador do pedido (ex.: "charge-pedido-847293") ou gere uma vez e PERSISTA
no seu banco antes de chamar a API, para reenviar exatamente a mesma chave na retentativa.
- Mesma chave + mesmo corpo já concluído -> devolve a resposta original com header "idempotent-replayed: true".
- Mesma chave + corpo diferente -> 409 IDEMPOTENCY_CONFLICT.
- Mesma chave ainda em processamento -> 409 idempotency_key_in_progress: aguarde alguns segundos e
repita a MESMA requisição com a MESMA chave e o MESMO corpo. Nunca gere outra chave nem outra
intenção de pagamento para contornar.
- Em timeout de rede, repita com a MESMA chave; nunca gere outra.
EXTERNAL_ID
- Em CHARGES, external_id é OPCIONAL. Não crie external_id automaticamente na integração básica.
Envie external_id apenas quando o seu sistema já possuir um identificador único de pedido/transação
(ex.: order_id do e-commerce) e quiser usar para deduplicação/conciliação.
NUNCA use apenas o ID do usuário/cliente como external_id, pois várias cobranças do mesmo usuário
representam intenções de pagamento diferentes e isso bloquearia novas cobranças.
- Em PAYOUTS, external_id é OBRIGATÓRIO (ex.: "saque_847293"): é o identificador da intenção de saque.
- O external_id de payout é imutável. O recurso continua ligado a ele mesmo em failed/reversed;
reenvio equivalente devolve o mesmo payout em HTTP 200.
- Uma nova tentativa operacional após failed/reversed exige NOVA intenção com NOVO external_id.
- Pedido novo -> external_id novo. Nunca reutilize o external_id de outro pedido.
- Reenvio do mesmo external_id com os mesmos dados materiais devolve o recurso existente (HTTP 200),
sem duplicar. Dados divergentes (amount na cobrança; amount ou pix_key no saque) -> 409 EXTERNAL_ID_CONFLICT
com retryable:false.
- Consulte por ele com GET /charges/ext:<external_id>.
- 409 external_id_in_progress -> já existe uma criação em andamento com esse external_id (bloqueio de
até 120 segundos): aguarde e consulte pelo mesmo external_id; não crie outro pedido nem outro external_id.
- Não confunda: Idempotency-Key evita duplicar o mesmo POST; external_id identifica o seu pedido.
RATE LIMIT
- 600 requisições por minuto por chave. Headers x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset; retry-after em 429.
PAGINAÇÃO (cursor, único formato existente)
- Query: limit (1 a 200, padrão 50) e starting_after.
- Resposta: { "data": [...], "has_more": bool, "next_starting_after": string|null }.
- NÃO existe offset, page nem filtro de status em /charges e /payouts.
ENDPOINTS DISPONÍVEIS
1) POST https://api.phanterpay.com.br/v1/charges (charges:write)
body mínimo: { "amount": 100.00 }
body: { amount (>=2, obrigatório), description?, payer_name?, payer_document?, expires_in?, external_id? (opcional), postback_url? (opcional) }
Idempotency-Key OBRIGATÓRIO (8-255). Ausente -> 400 idempotency_key_required.
201: { id: "chg_...", txid, external_id, amount, status, description,
pix: { copy_paste, qr_code_image }, end_to_end_id, paid_at, expires_at, created_at }
Atenção: o Pix copia e cola está em charge.pix.copy_paste e a imagem em charge.pix.qr_code_image.
2) GET https://api.phanterpay.com.br/v1/charges (charges:read) -> página com data/has_more/next_starting_after
3) GET https://api.phanterpay.com.br/v1/charges/{id} (charges:read) -> aceita chg_..., o txid do adquirente ou ext:<external_id> (checar status == "paid")
4) POST https://api.phanterpay.com.br/v1/payouts (payouts:write)
body mínimo: { "amount": 50.00, "pix_key": "...", "external_id": "saque_123" }
body: { amount (>=2, obrigatório, LÍQUIDO ao destinatário), pix_key (obrigatório), external_id (OBRIGATÓRIO),
pix_key_type? (cpf|cnpj|email|phone|random), recipient_name?, recipient_document?, description?, postback_url? }
Idempotency-Key opcional. external_id ausente -> 400 VALIDATION_ERROR.
Gere e persista external_id por intenção de saque (ex.: const externalId = `saque_${withdrawalId}`)
e reutilize o MESMO valor para recuperar a mesma intenção. RETRY da MESMA intenção (timeout, erro de
rede, 5xx, reenvio do job) DEVE usar exatamente o MESMO external_id. NUNCA gere external_id novo
por tentativa (ex.: sufixo com Date.now()): isso cria uma NOVA intenção e pode gerar SAQUE DUPLICADO
real, com dois Pix enviados. Após failed/reversed, uma nova tentativa
operacional deve criar nova intenção com NOVO external_id.
201: { id: "pyt_...", external_id, amount, fee, net_amount, gross_amount, status,
pix: { key, recipient_name, recipient_document }, description, end_to_end_id, error_message, created_at, completed_at }
402 insufficient_balance_for_fees: saldo insuficiente para líquido + taxa. O corpo traz
details.max_withdrawable (quanto pode ser sacado agora), além de requested_amount, available_balance,
fee_amount, total_required e additional_balance_needed — use esse valor para orientar o usuário,
não apenas o genérico INSUFFICIENT_BALANCE.
504 ambiguous_payout_status: o resultado do envio não pôde ser confirmado naquele instante.
NÃO crie outro payout, NÃO gere outra Idempotency-Key e NÃO repita o POST automaticamente.
Mesmo que o envelope traga retryable:true (política genérica de 504), trate ambiguous_payout_status
ANTES da lógica genérica de retry: a ação correta é consultar por GET.
Consulte primeiro GET https://api.phanterpay.com.br/v1/payouts/{id} ou GET https://api.phanterpay.com.br/v1/payouts/ext:<external_id> e só decida depois.
5) GET https://api.phanterpay.com.br/v1/payouts (payouts:read)
6) GET https://api.phanterpay.com.br/v1/payouts/{id} (payouts:read) -> aceita pyt_..., o uuid interno ou ext:<external_id>
Faz SYNC-ON-READ: se o saque ainda estiver em processing, a PhanterPay consulta a situação
autoritativa antes de responder, então esse GET pode concluir o saque quando o webhook atrasar
ou se perder. Use-o como fallback/reconciliação (polling moderado, com backoff).
Nunca marque um saque como pago localmente sem status terminal vindo deste GET ou do webhook.
7) GET https://api.phanterpay.com.br/v1/balance (balance:read)
200: { available_balance, pending_balance, blocked_balance, total_balance, currency: "BRL" }
Este endpoint devolve SOMENTE o saldo em BRL (Pix). O saldo de cripto é um ledger separado em USDT
e não é exposto por /balance — nunca some nem converta os dois saldos na sua integração.
8) GET https://api.phanterpay.com.br/v1/med (med:read) -> query: limit (1-100, padrão 20), starting_after, status
9) GET https://api.phanterpay.com.br/v1/med/{id} (med:read)
10) POST https://api.phanterpay.com.br/v1/med/{id} (med:write) -> body { defense_text (obrigatório), defense_evidence_url? }
ROTAS RESERVADAS (hoje respondem HTTP 501 FEATURE_NOT_AVAILABLE — não implemente fluxo de pagamento nelas)
- POST/GET https://api.phanterpay.com.br/v1/boletos, GET https://api.phanterpay.com.br/v1/boletos/{id}
- POST/GET https://api.phanterpay.com.br/v1/card/charges, GET https://api.phanterpay.com.br/v1/card/charges/{id}
CRIPTO (disponível na API pública — igual ao OpenAPI real)
11) POST https://api.phanterpay.com.br/v1/crypto/charges (charges:write) — depósito em cripto (cash-in)
body: { amount (USD, obrigatório), pay_currency (obrigatório), external_id?, customer_id?, description?, customer_email?, metadata? }
pay_currency aceitos: usdttrc20, usdtbsc, btc, eth, ltc, sol, usdc, trx, xrp, doge, bnbbsc, ton, ada, bch, dai.
Preço em USD; liquidação sempre no saldo USDT, com a taxa aplicável à conta descontada na liquidação, quando houver.
Mínimo comercial por moeda/rede (USDT: 20; demais moedas: 15) e, por cima dele, vale o mínimo técnico dinâmico da rede — aplica-se o maior dos dois.
Idempotência forte: external_id no corpo OU header Idempotency-Key — mesmo identificador com payload diferente retorna 409.
Em falha transitória (timeout, 202, erro de rede) REUTILIZE a mesma Idempotency-Key/external_id: trocar o identificador é a única forma de gerar um depósito duplicado.
Um external_id só serve a um depósito, mesmo depois de expirado — novo depósito exige novo identificador.
201 depósito criado | 200 idempotente (já existente) | 202 aceito e em provisionamento (status "processing": repita a MESMA Idempotency-Key até o status final)
409 idempotency_conflict | 422 invalid_request, unsupported_currency, below_minimum, charge_failed
Recurso: { id: "ccr_...", object: "crypto_charge", status, external_id, customer_id, metadata, price_amount, price_currency: "usd",
pay_amount, pay_currency, paid_amount, amount_status, received_amount, pay_address, pay_extra_id, network,
fee, net_amount, credited_amount, settlement_currency: "usdt", settled_amount, settled_currency,
expires_at, paid_at, created_at }
Campos de valor (regra inequívoca): paid_amount = quantidade on-chain recebida na pay_currency (auditoria, NÃO é USD/USDT);
received_amount = valor liquidado em USD/USDT — use ESTE para creditar o saldo do jogador; credited_amount = líquido do
merchant após taxas (não é saldo do jogador); amount_status = underpaid | exact | overpaid.
Se sua plataforma exibe saldo em BRL, converta received_amount no seu sistema (ex.: POST /crypto/quotes com
operation:"conversion", from "usdt", to "brl"). Nunca trate received_amount diretamente como BRL.
status: processing, waiting, confirming, paid, failed, expired, refunded.
"processing" é transitório: o endereço ainda está sendo provisionado ou recuperado — repita a MESMA Idempotency-Key/external_id até receber "waiting". "paid" é terminal para efeito de crédito.
customer_id é o identificador do jogador/cliente NO SEU sistema; não cria saldo próprio na PhanterPay.
12) GET https://api.phanterpay.com.br/v1/crypto/charges (charges:read) -> query: limit (1-200, padrão 50), external_id, customer_id
Resposta: { data: [...], has_more }
13) GET https://api.phanterpay.com.br/v1/crypto/charges/{id} (charges:read) -> aceita ccr_... ou ext:<external_id>. Nenhum lojista lê depósito de outro. 404 not_found.
14) POST https://api.phanterpay.com.br/v1/crypto/payouts (payouts:write) — saque em cripto (cash-out)
body: { amount (obrigatório, em USDT), currency (obrigatório), address (obrigatório, da rede da moeda), extra_id?, external_id? }
Saques criados pela API são automáticos QUANDO a conta estiver habilitada para cash-out cripto via API e a operação
estiver dentro das regras/limites definidos para a conta, sujeitos a saldo, mínimo, validações, idempotência e disponibilidade da rede.
currency aceitos no cash-out (SOMENTE estes): usdtbsc (USDT BEP20), trx (Tron), btc (Bitcoin), ltc (Litecoin), usdterc20 (USDT ERC20).
usdttrc20 NÃO é válido para cash-out (é aceito apenas no cash-in) — qualquer outra moeda/rede é recusada com unsupported_currency.
Idempotência é OBRIGATÓRIA: informe external_id no corpo ou o header Idempotency-Key. Mesmo identificador com payload idêntico
devolve o mesmo saque, sem novo débito; payload diferente devolve 409 idempotency_conflict. O identificador é escopado por conta.
Mínimo efetivo (dinâmico): max(mínimo comercial PhanterPay de 20 USDT, mínimo técnico dinâmico real de saque da rede).
Consulte GET https://api.phanterpay.com.br/v1/crypto/payouts/minimum antes de criar; o POST revalida o mínimo de qualquer forma.
Abaixo disso a criação é recusada (below_minimum / below_network_minimum).
O ledger é sempre USDT. amount é o principal/referência solicitado em USDT (NÃO é garantia do valor
final entregue on-chain); as taxas comerciais são cobradas POR FORA
(gross_amount = total debitado = amount + fee). A taxa de conversão (conversion_fee, 1%, quando aplicável)
JÁ ESTÁ CONTIDA em fee — nunca some duas vezes. A taxa de rede (network fee) é paga pelo RECEBEDOR:
é descontada do valor entregue on-chain, não do saldo. Por isso receive_estimated pode ser MENOR
que o equivalente a amount.
201: { id: "cpo_...", object: "crypto_payout", status, currency, amount, fee, conversion_fee, net_amount, gross_amount,
settlement_currency: "usdt", origin, external_id, tx_hash, error_message, created_at }
status: requested, processing, completed, failed. Saques da API entram em processing imediatamente (sem aprovação manual); completed é terminal.
403: account_banned, kyc_required, api_cashout_disabled, api_payout_disabled
422: invalid_request, unsupported_currency, invalid_address, below_minimum, below_network_minimum, insufficient_balance,
extra_id_required, currency_unavailable, crypto_disabled
15) GET https://api.phanterpay.com.br/v1/crypto/payouts (payouts:read) -> query: limit (1-200, padrão 50). Resposta: { data: [...], has_more }, do mais recente para o mais antigo.
16) GET https://api.phanterpay.com.br/v1/crypto/payouts/minimum (payouts:read)
Devolve o mínimo EFETIVO atual de saque em cripto: max(mínimo comercial PhanterPay, mínimo técnico dinâmico real da rede).
Consulta ao vivo, sem cache. 200: { currency, network, min_amount, updated_at }
503 minimum_unavailable quando o mínimo não puder ser determinado — nunca um valor estimado.
17) GET https://api.phanterpay.com.br/v1/crypto/payouts/estimate?amount=¤cy= (payouts:read)
Estimativa ANTES de criar, com as MESMAS fontes do POST (não cria saque nem reserva saldo).
200: { object: "crypto_payout_estimate", currency, network, amount, fee, conversion_fee, total_debit, rate,
send_amount, network_fee_estimated, receive_estimated, receive_estimated_usdt }
422 unsupported_currency / invalid_amount | 503 estimate_unavailable (nunca invente um número).
18) GET https://api.phanterpay.com.br/v1/crypto/currencies (charges:read OU payouts:read)
DESCOBERTA OFICIAL de moedas/redes. Chame ANTES de montar qualquer fluxo cripto e não chumbe códigos.
query opcional: flow=deposit | flow=withdrawal (filtra pelo fluxo).
200: { object: "list", data: [ { code, symbol, name, network, network_name, can_deposit, can_withdraw, requires_memo } ] }
"code" é o valor a enviar em pay_currency (depósito) e currency (saque). "network" é o slug estável da rede.
requires_memo=true significa que o destino exige memo/tag (extra_id) além do endereço.
19) POST https://api.phanterpay.com.br/v1/crypto/quotes (withdrawal: payouts:read | deposit: charges:read | conversion: charges:read OU payouts:read)
Cotação INFORMATIVA de conversão antes de criar depósito/saque. NÃO trava preço, não reserva saldo,
não cria recurso e NÃO é aceita como parâmetro em nenhum outro endpoint (não envie quote_id no POST
de depósito/saque). O valor final é sempre recalculado na criação.
body: { from, to, amount, network?, operation: "deposit" | "withdrawal" | "conversion" }
withdrawal: from = "usdt" (moeda do saldo), amount em USDT, to = moeda de saque (can_withdraw).
deposit: from = "usd" (precificação), to = moeda de depósito (can_deposit).
conversion: cotação INFORMATIVA de USD/USDT -> BRL (rotas válidas: from "usd" ou "usdt", to "brl", amount > 0).
Resposta traz network: null, minimum: null, price_locked: false. Serve APENAS para a plataforma converter
o saldo interno do jogador no próprio sistema; a PhanterPay liquida cash-in cripto em USD/USDT.
NUNCA trate received_amount como BRL: se exibe saldo em BRL, consulte /crypto/quotes com
operation:"conversion" e converta você mesmo. Rota fora dessas combinações -> 422 unsupported_route.
200: { object: "crypto_quote", quote_id, operation, from, to, network, amount, estimated_amount, rate,
minimum, minimum_currency, price_locked: false, expires_at, created_at }
NUNCA assuma paridade 1:1 (nem entre redes da mesma stablecoin): use estimated_amount/rate da resposta.
minimum vem da mesma fonte usada na criação, na moeda de "from"; pode vir null (indisponível) — não invente.
Não existe máximo nesta resposta: não invente campo nem limite.
expires_at expira em 60s — cotação vencida deve ser refeita antes de mostrar valor ao usuário.
422 unsupported_currency (moeda fora do catálogo) / unsupported_network (rede não confere com a moeda) /
unsupported_route (moeda não habilitada no fluxo, ou "from" errado) / invalid_amount / below_minimum
503 quote_unavailable = TRANSITÓRIO: repita com backoff curto. NÃO trate como saldo insuficiente,
não bloqueie o usuário e não crie depósito/saque às cegas.
WEBHOOKS
- Webhooks são opcionais. A integração básica pode acompanhar o status por GET /charges/{id}.
- NÃO peça segredo de webhook nem crie endpoint de webhook na integração básica.
- Se o usuário pedir webhooks explicitamente: cadastre no painel ou envie postback_url.
Assinatura "x-phanterpay-signature: t=<unix>,v1=<hex>" — HMAC-SHA256 do secret do endpoint sobre "<t>.<corpo cru>".
Headers auxiliares: x-phanterpay-event-id, x-phanterpay-event-type, x-phanterpay-attempt.
- Eventos Pix entregues hoje: charge.created, charge.paid, payout.created, payout.completed, payout.failed.
- Eventos de cripto entregues hoje: crypto.charge.created, crypto.charge.confirming, crypto.charge.paid,
crypto.charge.failed, crypto.charge.expired, crypto.payout.processing, crypto.payout.completed, crypto.payout.failed.
Cada transição emite um único evento.
- Reservados (aceitos no cadastro, ainda não disparados): charge.expired, charge.refunded, charge.failed,
payout.processing, payout.reversed, med.opened, med.updated, med.resolved. Não existem eventos de split.
- Envelope: { "id", "type", "created_at", "data" }. O campo "data" é o recurso público completo
(mesmo shape de GET /charges/{id} e GET /payouts/{id}).
- Entrega at-least-once: valide a assinatura sobre o corpo cru e deduplique por event.id.
Retentativas: até 6 tentativas, timeout 10s, backoff 1min, 5min, 30min, 2h e 12h.
Responda 2xx rápido e processe de forma assíncrona.
- Se utilizar postback_url, a URL deve estar previamente cadastrada e ativa nos endpoints de webhook
da conta (URL exatamente igual). Caso contrário a API devolve 400 postback_url_not_registered.
O quickstart e a integração básica NÃO devem enviar postback_url.
- Entrega de postback_url usa a MESMA fila persistente e as MESMAS retentativas do webhook global
(tentativa imediata + retries). Se a postback_url for igual a um endpoint global ativo que já
assina o evento, a entrega acontece uma única vez.
- GET /charges/{id} devolve status "paid" assim que a PhanterPay confirma o pagamento — é a fonte
de verdade e pode ser usada como fallback/reconciliação do webhook.
CONFIRMAÇÃO DE PAGAMENTO (ARQUITETURA RECOMENDADA — PIX E CRIPTO)
- Regra de ouro: NUNCA marque um pedido como pago localmente porque o usuário disse que pagou,
porque o QR foi exibido, porque o tempo passou, ou porque um saldo/endereço "parece" ter recebido.
A confirmação é SEMPRE da PhanterPay (webhook ou GET).
- Padrão resiliente: webhook como confirmação principal em tempo real + GET como fallback e
reconciliação. Os DOIS caminhos devem chamar a MESMA função idempotente de liberação do pedido
(liberar duas vezes é bug). Deduplique webhooks por event.id.
- PIX
1. Depois de POST /charges, PERSISTA os identificadores retornados (id chg_..., txid e o
external_id, se você usar) e exiba pix.copy_paste / pix.qr_code_image.
2. Webhook charge.paid é a confirmação principal.
3. GET /charges/{id} (ou /charges/ext:<external_id>) é o fallback/reconciliação até um estado
terminal (paid ou expirado).
4. Esse GET NÃO é leitura passiva: ele faz sync-on-read. Se a cobrança ainda estiver "pending" e
não expirada, a PhanterPay consulta a situação autoritativa do pagamento ANTES de responder.
Ou seja, o GET recupera o pagamento mesmo quando o webhook atrasa ou se perde.
5. Idempotency-Key continua OBRIGATÓRIA em POST /charges; retentativa da MESMA intenção usa a
MESMA chave.
- CRIPTO
1. Depois de POST /crypto/charges, PERSISTA id (ccr_...), payment_id (quando presente),
pay_address, pay_amount, pay_currency, network e status.
2. Webhooks crypto.charge.created/confirming/paid/failed/expired são a confirmação principal.
3. GET /crypto/charges/{id} (ou ext:<external_id>) é o fallback/reconciliação.
4. Em estados NÃO terminais (waiting, confirming) esse GET sincroniza com a situação real do
depósito antes de responder — por isso ele recupera confirmação atrasada ou perdida.
Terminais: paid, failed, expired, refunded.
5. paid = RECEBIMENTO CONFIRMADO. A diferença entre esperado e recebido NÃO muda o status, fica
nos CAMPOS DE VALOR: pay_amount = esperado na moeda on-chain; paid_amount = realmente recebido
on-chain na pay_currency (auditoria/reconciliação blockchain, NÃO é USD/USDT);
received_amount = valor efetivamente liquidado em USD/USDT — é ESTE o campo que integrações com
saldo em USD/USDT devem usar para creditar o cliente/jogador; credited_amount = líquido que
entrou no saldo USDT do merchant após taxas (não use para saldo do cliente final);
amount_status = underpaid | exact | overpaid. Nunca credite o cliente pelo valor solicitado que
não entrou. Sem valor confirmado a cobrança não vira paid (fica confirming).
7. Nunca infira pagamento olhando a blockchain/saldo por conta própria; use status da PhanterPay.
- POLLING
- Em checkout ativo (usuário na tela), consulte a cada 2–5 segundos por um período curto e depois
aplique backoff (ex.: 10s, 30s, 60s) até o estado terminal ou a expiração.
- Fora do checkout, reconcilie em background em intervalos maiores.
- Respeite o rate limit (600 req/min por chave; recue em 429 usando retry-after).
- Nunca prometa SLA de confirmação ao usuário final.
- INTEGRAÇÃO MÍNIMA: peça ao usuário basicamente a PHANTERPAY_API_KEY e, somente quando ele quiser
webhook, a URL de callback do sistema dele. Não invente campos, segredos ou configurações
obrigatórias que a API não exige.
ERROS (envelope único)
{ "success": false, "error": { "code", "message", "retryable", "issues"? }, "request_id" }
Códigos: VALIDATION_ERROR(400), UNAUTHORIZED(401), INSUFFICIENT_BALANCE(402), FORBIDDEN(403), NOT_FOUND(404),
(INSUFFICIENT_BALANCE(402) é o genérico de outros contextos; em POST /payouts o code é sempre o específico
insufficient_balance_for_fees, com details.max_withdrawable — não trate payout pelo genérico.)
IDEMPOTENCY_CONFLICT(409), AMOUNT_LIMIT_EXCEEDED(422), RATE_LIMIT_EXCEEDED(429), INTERNAL_ERROR(500),
FEATURE_NOT_AVAILABLE(501), PROVIDER_UNAVAILABLE(502), SERVICE_UNAVAILABLE(503).
Conflito semântico: EXTERNAL_ID_CONFLICT(409, retryable:false).
Códigos específicos em minúsculo devolvidos pelo runtime: invalid_idempotency_key(400),
postback_url_not_registered(400), idempotency_key_in_progress(409), external_id_in_progress(409),
split_not_available(422), split_disabled(403), ambiguous_payout_status(504), account_banned(403), kyc_required(403),
api_cashin_disabled(403), api_cashout_disabled(403), internal_error(500), provider_unavailable(502).
Decida retentar pelo campo booleano "retryable", nunca pelo texto de "message".
"retryable" controla se a MESMA requisição deve ser repetida automaticamente. Mesmo com retryable:false,
pode existir uma ação segura documentada — como consultar o recurso por GET.
EXCEÇÃO À REGRA GENÉRICA DE RETRY: "retryable" é o sinal padrão, EXCETO quando o contrato define uma ação
query-first. Em ambiguous_payout_status (504), mesmo com retryable:true, NÃO repita o POST /payouts —
consulte a operação existente por GET primeiro.
VARIÁVEIS DE AMBIENTE ESPERADAS
PHANTERPAY_API_KEY=bp_a1b2c3d4_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a3928
PHANTERPAY_API_BASE_URL=https://api.phanterpay.com.br/v1
ORDEM OBRIGATÓRIA DE IMPLEMENTAÇÃO
1. GET https://api.phanterpay.com.br/v1/crypto/currencies?flow=deposit — monte a lista de moedas/redes a partir dessa resposta.
NÃO chumbe códigos. Se requires_memo for true, exiba e use o pay_extra_id devolvido na cobrança.
2. POST https://api.phanterpay.com.br/v1/crypto/quotes com { from: "usd", to: <code>, amount: <USD>, network: <network>, operation: "deposit" }
para mostrar quanto de cripto o pagador enviaria, ANTES de criar a cobrança. Nunca assuma paridade 1:1.
3. Valide o valor contra minimum da cotação (na moeda de "from", ou seja USD) antes de criar.
4. POST https://api.phanterpay.com.br/v1/crypto/charges com amount (USD), pay_currency e idempotência: external_id no corpo OU o header
Idempotency-Key (basta um dos dois; enviar os dois é recomendado para rastreabilidade). Exiba pay_address,
pay_amount, network, pay_extra_id e expires_at exatamente como vierem na resposta — o valor real a pagar é o
pay_amount da cobrança, não o estimated_amount da cotação.
5. Confirme por webhook crypto.charge.paid e, como fallback, GET https://api.phanterpay.com.br/v1/crypto/charges/ext:<external_id>.
REGRAS QUE VOCÊ NÃO PODE VIOLAR
- A cotação é informativa (price_locked: false) e expira em 60s: refaça se expires_at passou. NÃO envie quote_id
no POST /crypto/charges — o endpoint não aceita esse campo.
- 422 unsupported_currency / unsupported_network / unsupported_route: recarregue /crypto/currencies e corrija a
seleção do usuário. 422 below_minimum: exiba o mínimo retornado; se vier null, não invente valor.
- 503 quote_unavailable é transitório: backoff curto e nova tentativa. Nunca interprete como erro de saldo.
- status "processing" na cobrança é transitório: repita a MESMA Idempotency-Key/external_id com o MESMO corpo
até receber "waiting". Trocar o identificador cria depósito duplicado.
- Nunca marque um depósito como pago por leitura própria de blockchain/saldo: só por status "paid" da PhanterPay.
- Leia a credencial do ambiente; não escreva a chave no código. Não use nenhum outro provedor ou SDK externo.
- Não peça confirmação adicional para escrever a integração.O arquivo /llms.txt resume base URL, autenticação e regras principais para agentes que leem o site automaticamente. A especificação completa está em https://api.phanterpay.com.br/openapi.json.
bp_ no front-end ou em logs.Idempotency-Key é obrigatório em POST /charges (8–255) e opcional em POST /payouts; gere sempre no backend, por operação: chave nova para operação nova, a mesma chave na retentativa da mesma operação.external_id como identificador da operação no seu sistema — um por operação, nunca reaproveitado. Opcional em cobranças e obrigatório em saques.