# BluntPay > API REST da BluntPay para pagamentos Pix e Cripto (cash-in e cash-out). ## Início rápido 1. Envie sua API Key em `Authorization: Bearer `. 2. Crie a cobrança com `POST /charges` e o corpo mínimo `{"amount": 100.00}`. 3. Use `pix.copy_paste` e `pix.qr_code_image` retornados na resposta. 4. Consulte `GET /charges/{id}` até `status` ser `paid` (ou use webhook). O cash-in exige o header `Idempotency-Key` (8 a 255 caracteres); `external_id` continua opcional. Execute `POST /charges` uma única vez por intenção de pagamento. Depois que a criação for iniciada, não use `POST /charges` para acompanhar status — acompanhe somente com `GET /charges/{id}` ou `GET /charges/ext:`. Webhooks estão disponíveis para notificações automáticas em tempo real, mas não são necessários para a integração básica. Os campos `postback_url` e `external_id` também são opcionais. ## Evite criação duplicada - `external_id` é OPCIONAL em `POST /charges`. Não crie um `external_id` automaticamente na integração básica. Envie-o apenas quando o seu sistema já possuir um identificador único de pedido/transação (ex.: `order_id` do e-commerce). 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 seriam bloqueadas. - Se você usar `external_id`, gere-o uma única vez por intenção de pagamento e persista antes de disparar `POST /charges`. `Idempotency-Key` é OBRIGATÓRIA em `POST /charges`. - Execute `POST /charges` apenas uma vez; enquanto o POST estiver pendente, bloqueie uma segunda criação. - Não gere novos identificadores durante re-render. - Não coloque `POST /charges` em polling nem em `useEffect` sem trava. - Desative o retry automático do POST quando o cliente HTTP/framework puder duplicar chamadas. - Depois da criação, o polling usa apenas GET. - Em React, Next.js, tRPC ou similares: `if (createCharge.isPending) return;` e botão desabilitado enquanto a requisição estiver pendente. ## Base URL https://api.bluntpay.com.br/v1 ## Documentação - Documentação oficial: https://docs.bluntpay.com.br - Referência da API: https://docs.bluntpay.com.br/docs/api - Prompts prontos para IAs: https://docs.bluntpay.com.br/docs/ai - OpenAPI 3.1: https://api.bluntpay.com.br/openapi.json - Collection do Postman: https://bluntpay.com.br/bluntpay.postman_collection.json ## Autenticação A BluntPay utiliza autenticação baseada em chaves simples. **NÃO** utilizamos OAuth, Client ID nem Client Secret. - Header `Authorization: Bearer ` (alternativa: `x-api-key: `). - Única credencial: `BLUNTPAY_API_KEY`. Nunca solicitar ou tentar usar `BLUNTPAY_CLIENT_ID` ou `BLUNTPAY_CLIENT_SECRET`. - Formato real da chave: `bp_<8 hex>_<48 hex>`. Não existem chaves públicas (`pk_`). - Guarde a credencial na variável de ambiente `BLUNTPAY_API_KEY`. Nunca no código-fonte, no front-end 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. - Requisito inicial da integração: SOMENTE a `BLUNTPAY_API_KEY`. Não peça `BLUNTPAY_WEBHOOK_SECRET`, URL de callback nem cadastro de webhook como pré-requisito. Webhook é OPCIONAL/RECOMENDADO (tempo real e menos polling); sem ele, cash-in Pix, saque Pix e cash-in cripto funcionam pelos GETs de consulta com sync-on-read. Se o integrador optar por webhook, aí sim configure a URL de callback e valide a assinatura. ## Regras básicas - Valor mínimo por operação: R$ 2,00. - `Idempotency-Key` (8 a 255 caracteres) é OBRIGATÓRIA em POST /charges (ausente devolve 400 `idempotency_key_required`) e OPCIONAL em POST /payouts. Regra de uso: operação nova usa chave nova; retentativa da mesma operação reutiliza a mesma chave. Replay devolve a resposta original; corpo diferente devolve 409 IDEMPOTENCY_CONFLICT; mesma chave ainda em processamento devolve 409 idempotency_key_in_progress (aguarde e repita). Header presente porém com tamanho fora de 8–255 devolve 400 `invalid_idempotency_key`. - `external_id` é OPCIONAL em POST /charges e OBRIGATÓRIO em POST /payouts. É o identificador da operação no sistema do integrador (ex.: `pedido_847293`, `saque_847293`), um por operação. Reenvio do mesmo `external_id` com os mesmos dados materiais devolve o recurso existente (HTTP 200), sem duplicar. Em payouts isso vale em qualquer status, inclusive `failed` e `reversed`; uma nova tentativa operacional exige nova intenção com NOVO `external_id`. Dados materiais diferentes (amount na cobrança; amount ou pix_key no saque) devolvem 409 `EXTERNAL_ID_CONFLICT` com `retryable: false`. Criação concorrente com o mesmo external_id devolve 409 `external_id_in_progress`; o bloqueio expira em até 120 segundos. Nesse caso: não gere outro external_id e não repita o POST automaticamente — aguarde alguns segundos e consulte GET /charges/ext: ou GET /payouts/ext:. Consultar por GET continua correto mesmo com `retryable: false`. - Rate limit: 600 requisições por minuto por chave (headers x-ratelimit-*, retry-after em 429). - Paginação por cursor: `limit` (1–200, padrão 50) e `starting_after`; resposta com `data`, `has_more` e `next_starting_after`. Não existe offset nem page. - Erros usam sempre o envelope `{ "success": false, "error": { "code", "message", "retryable" }, "request_id" }`. `retryable` controla se a mesma requisição deve ser repetida automaticamente. Mesmo com `retryable: false`, pode haver outra ação segura documentada, como consultar o recurso por GET. `retryable` é o sinal padrão de retentativa, 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. - Códigos específicos em minúsculo também são devolvidos pelos handlers, incluindo `internal_error` (500), `provider_unavailable` (502), `account_banned`, `kyc_required`, `api_cashin_disabled` e `api_cashout_disabled` (403). - Split de pagamentos: `POST /charges` aceita o array `split`, mas o recurso NÃO vem liberado por padrão — ele só funciona em contas habilitadas pelo admin da BluntPay (o merchant não habilita pela API). Sem habilitação, o envio de `split` recebe 403 `split_disabled`. O campo `split_rule_id` não é suportado no contrato público e devolve 422 `split_not_available`. ## Endpoints - POST /charges — cria cobrança Pix (retorna `pix.copy_paste` e `pix.qr_code_image`) - GET /charges — lista cobranças (cursor) - GET /charges/{id} — aceita `chg_...`, o txid do adquirente ou `ext:` - POST /payouts — cria saque Pix (amount é o valor líquido; resposta traz fee e gross_amount) A criação pode devolver processing, completed ou failed conforme o resultado da sincronização inicial. `external_id` é OBRIGATÓRIO: gere um por intenção de saque (ex.: `saque_`) e persista antes do POST. Reutilize-o para recuperar a mesma intenção; após `failed`/`reversed`, uma nova tentativa operacional usa uma nova intenção e um NOVO external_id. RETRY da MESMA intenção (timeout, erro de rede, 5xx, reenvio de fila) DEVE reutilizar exatamente o MESMO `external_id`. Gerar um `external_id` novo a cada tentativa (ex.: sufixo com timestamp) cria uma NOVA intenção de saque e pode resultar em SAQUE DUPLICADO real, com dois Pix enviados. Saldo insuficiente devolve HTTP 402 com `code: "insufficient_balance_for_fees"` (NÃO o genérico "INSUFFICIENT_BALANCE") e `details: { requested_amount, available_balance, fee_amount, total_required, max_withdrawable, additional_balance_needed }`. Use `details.max_withdrawable` para mostrar ao usuário final quanto ele pode sacar agora. - GET /payouts — lista saques (cursor) - GET /payouts/{id} — consulta saque; aceita `pyt_...`, o uuid interno ou `ext:` Faz sync-on-read: em estado não terminal (`processing`) a BluntPay consulta a situação autoritativa antes de responder, recuperando confirmação quando o webhook atrasar ou se perder. Use como fallback/reconciliação com polling moderado e backoff; nunca marque pago localmente sem status terminal vindo deste GET ou do webhook. HTTP 504 `ambiguous_payout_status` no POST significa resultado não confirmado: não crie outro payout nem repita o POST automaticamente — consulte antes GET /payouts/{id} ou GET /payouts/ext:. - GET /balance — available_balance, pending_balance, blocked_balance, total_balance, currency - GET /med, GET /med/{id}, POST /med/{id} — infrações Pix e envio de defesa - GET /crypto/currencies — DESCOBERTA oficial das moedas/redes suportadas nos fluxos cripto (escopo charges:read OU payouts:read). Query opcional `flow=deposit|withdrawal`. Resposta: `{ "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. Consulte este endpoint em vez de chumbar códigos de moeda/rede na integração. - POST /crypto/quotes — cotação INFORMATIVA para depósito, saque OU conversão USD/USDT -> BRL (escopos: `withdrawal` = `payouts:read`; `deposit` = `charges:read`; `conversion` = `charges:read` OU `payouts:read`). `operation` aceita `deposit`, `withdrawal` e `conversion`. Body: `{ "from": "usdt", "to": "trx", "amount": 100, "network": "tron", "operation": "withdrawal" }`. Em `withdrawal`, `from` é sempre `usdt` (moeda do saldo) e `amount` está em USDT; em `deposit`, `from` é sempre `usd` (precificação). Em `conversion`, as rotas válidas são `from: "usd"` ou `from: "usdt"` com `to: "brl"` e `amount > 0`; a resposta vem com `network: null`, `minimum: null` e `price_locked: false`. É apenas cotação para a plataforma converter o saldo interno do jogador no PRÓPRIO sistema — a BluntPay liquida cash-in cripto em USD/USDT e NUNCA se deve tratar `received_amount` diretamente como BRL. Rota fora dessas combinações devolve 422 `unsupported_route`. Resposta: `{ "object": "crypto_quote", quote_id, operation, from, to, network, amount, estimated_amount, rate, minimum, minimum_currency, price_locked: false, expires_at, created_at }`. NÃO trava preço (`price_locked` é sempre false), não reserva saldo, não cria recurso e o `quote_id` NÃO é aceito por nenhum outro endpoint — o valor final é recalculado na criação. Nunca assuma paridade 1:1, nem entre redes da mesma stablecoin: use `estimated_amount`/`rate`. `minimum` vem da mesma fonte usada na criação e está na moeda de `from`; pode vir `null` (indisponível) — não invente número. Não existe máximo nesta resposta. `expires_at` vence em 60s. Erros: 422 `unsupported_currency`, `unsupported_network`, `unsupported_route`, `invalid_amount`, `below_minimum`; 503 `quote_unavailable` (TRANSITÓRIO — repita com backoff curto; nunca trate como saldo insuficiente). - POST /crypto/payouts — cria saque em cripto. `currency` aceita SOMENTE `trx` (TRX · Tron), `usdtbsc` (USDT · BNB Chain BEP20), `btc` (Bitcoin), `ltc` (Litecoin) e `usdterc20` (USDT · Ethereum ERC20); qualquer outra moeda, inclusive `usdttrc20`, devolve 422 `unsupported_currency`. O endereço precisa ser válido na rede da moeda escolhida — endereço de outra rede devolve 422 `invalid_address` sem débito. O saldo/referência do merchant permanece SEMPRE em USDT: o `amount` inteiro é convertido para a moeda escolhida pela cotação vigente no momento do saque; a taxa de conversão BluntPay de 1% é cobrada POR FORA do saldo (campo `conversion_fee` na resposta, em USDT, subset informativo já incluído em `fee` — nunca somar duas vezes). `fee` é o total de taxas comerciais cobradas por fora; total debitado = `amount` + `fee`. A taxa de rede é estimada dinamicamente, é variável, fica FORA de `fee` e é descontada do valor recebido. Idempotência OBRIGATÓRIA: `external_id` no corpo OU header `Idempotency-Key` (mesmo identificador + payload diferente = 409 `idempotency_conflict`). `amount` é o principal solicitado em USDT (referência do ledger), não uma garantia do valor final entregue: a taxa de rede fica FORA de `fee` e é descontada do valor entregue on-chain, então `receive_estimated` pode ser menor que o equivalente a `amount`. Mínimo comercial: 20 USDT — configure 20 USDT como mínimo no seu sistema. O mínimo efetivo é max(20 USDT comercial, mínimo técnico dinâmico real da rede) e pode ser MAIOR que 20 quando a rede exigir mais naquele instante — consulte GET /crypto/payouts/minimum. O total debitado do saldo vem em `gross_amount` na resposta. Saque em cripto via API exige HABILITAÇÃO PRÉVIA da conta — sem habilitação o POST devolve 403 `api_payout_disabled` (solicite acesso ao seu gerente BluntPay). Habilitado, o saque é automático dentro dos limites de API definidos para a conta (fora deles: 422 `below_minimum` / `above_maximum`), sujeito a saldo, validações, idempotência e disponibilidade da rede. - GET /crypto/payouts/minimum — mínimo EFETIVO atual de saque em cripto: `{ "currency": "USDT", "network": "TRC20", "min_amount": 20.00, "updated_at": "..." }`. O mínimo é sempre expresso na referência em USDT e vale para todas as moedas de saque (`trx`, `usdtbsc`, `btc`, `ltc`, `usdterc20`). - `GET /v1/crypto/payouts/estimate?amount=¤cy=` (escopo payouts:read) simula o saque antes de criar, com a MESMA matemática do POST: `amount` (principal em USDT), `fee` (comercial, cobrado por fora), `conversion_fee` (subset de `fee`), `total_debit`, `send_amount` na moeda escolhida, `network_fee_estimated` (estimativa conservadora, paga pelo destinatário, fora de `fee`) e `receive_estimated`. Escopo `payouts:read`, consulta ao vivo (sem cache). Consulte este endpoint antes do POST; o POST revalida o mínimo de qualquer forma. Quando o mínimo não puder ser determinado, devolve 503 `estimate_unavailable` — nunca um valor estimado. - GET /crypto/payouts — lista os saques em cripto da conta (`limit` de 1 a 200). Use polling aqui para conciliar: `completed` é terminal e traz o `tx_hash`. - POST /crypto/charges — cria depósito em cripto para o jogador. `amount` em USD. O mínimo comercial é definido por moeda/rede (USDT: 20; demais moedas: 15) e ainda vale o mínimo técnico dinâmico da rede — o maior dos dois é aplicado. `pay_currency` entre usdttrc20, usdtbsc, btc, eth, ltc, sol, usdc, trx, xrp, doge, bnbbsc, ton, ada, bch e dai. Idempotência OBRIGATÓRIA (`external_id` ou `Idempotency-Key`; payload diferente no mesmo id = 409). Aceita `customer_id` (id do jogador no seu sistema) e `metadata`. A resposta traz `pay_address`, `pay_amount`, `network`, `pay_extra_id` e `expires_at`; após o pagamento traz também `paid_amount` (on-chain, na `pay_currency`), `received_amount` (liquidado em USD/USDT — use ESTE para o saldo do jogador), `amount_status` e `credited_amount` (líquido do merchant, não é saldo do jogador). Liquidação sempre no saldo USDT — nunca em reais. Em falha transitória a resposta pode ser HTTP 202 com `status: "processing"`: o depósito já existe e o endereço está sendo provisionado/recuperado. REPITA a mesma `Idempotency-Key`/`external_id` com o mesmo corpo até receber `waiting`; trocar o identificador é a única forma de criar um depósito duplicado. Um `external_id` vale para um único depósito para sempre (mesmo depois de `expired`). - GET /crypto/charges — lista depósitos da conta; filtra por `external_id` e `customer_id`. - GET /crypto/charges/{id} — consulta por `ccr_...` ou `ext:`. `paid_amount` NUNCA serve como valor em USD/USDT para creditar o jogador: credite por `received_amount`. Se sua plataforma exibe saldo em BRL, converta `received_amount` no seu sistema (opcionalmente com POST /crypto/quotes, `operation: "conversion"`, `from: "usdt"`, `to: "brl"`). - Webhooks de cripto (mesma assinatura/retry dos webhooks Pix): 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. O campo `data` do webhook é o MESMO recurso público do GET correspondente (GET /crypto/charges/{id} e GET /crypto/payouts), com os mesmos campos. - /boletos e /card/charges — reservados: respondem HTTP 501 FEATURE_NOT_AVAILABLE ## Webhooks - Webhooks são opcionais: dá para integrar só com a API REST (POST para criar, GET para consultar status). Para atualizações em tempo real e menos polling, recomendamos usar webhooks. - `postback_url` é opcional e não faz parte do quickstart. Se utilizada, 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`. - 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. - Nenhum outro evento é emitido hoje (charge.expired/refunded/failed, payout.processing/reversed e med.* são reservados). Não existem eventos de split na API pública. - Envelope de todo evento: { "id", "type", "created_at", "data" }. `data` é o recurso público completo (mesmo formato de GET /charges/{id} e GET /payouts/{id}); o schema-base é sempre o mesmo e o que muda por evento é o status e os campos finais (end_to_end_id, paid_at, completed_at, error_message). - Entrega: tentativa imediata assim que o evento é registrado; a fila e as retentativas persistentes garantem novas tentativas se a primeira falhar. Trate como at-least-once e deduplique por `id` do evento. - `postback_url` usa exatamente a mesma fila persistente e as mesmas retentativas do endpoint global — não é entrega best-effort. Quando a `postback_url` é igual a um endpoint global ativo que já assina o evento, o envio acontece uma única vez (sem duplicidade). - Valide a assinatura sobre o corpo cru (sem reserializar), responda 2xx rápido e processe de forma assíncrona. - GET /charges/{id} devolve `paid` assim que a BluntPay confirma o pagamento; use como fonte de verdade e fallback de reconciliação. - Confirmação de pagamento (Pix e cripto): webhook é a confirmação principal em tempo real e o GET é o fallback/reconciliação até estado terminal. Os dois caminhos devem chamar a mesma função idempotente; deduplique webhooks por `id` do evento. - `GET /charges/{id}` faz sync-on-read: se a cobrança ainda estiver `pending` e não expirada, a BluntPay consulta a situação autoritativa antes de responder — recupera pagamento quando o webhook atrasa ou se perde. - `GET /crypto/charges/{id}` faz o mesmo em estados não terminais (`waiting`, `confirming`): sincroniza com a situação real do depósito antes de responder. - Em cripto, `paid` = RECEBIMENTO CONFIRMADO. A diferença entre esperado e recebido fica nos campos de valor, não no status: `pay_amount` é o esperado na moeda on-chain; `paid_amount` é o realmente recebido on-chain, na `pay_currency` (auditoria/reconciliação blockchain, não é USD/USDT); `received_amount` é o valor efetivamente liquidado em USD/USDT e é ESTE o campo que integrações com saldo em USD/USDT devem usar para creditar o cliente/jogador; `credited_amount` é o líquido que entrou no saldo USDT do merchant após as taxas (não use para saldo do cliente final); e `amount_status` (`underpaid` | `exact` | `overpaid`) resume a comparação. Nunca credite pelo valor solicitado que não entrou. Sem valor confirmado não há `paid` (fica `confirming`). - Nunca marque pago localmente (usuário afirmando que pagou, tempo decorrido, ou leitura própria de saldo/endereço on-chain). A confirmação é sempre da BluntPay. - Polling: em checkout ativo, 2–5s por um período curto e depois backoff, respeitando o rate limit. Não prometa SLA de confirmação. - Assinatura: `x-phanterpay-signature: t=,v1=`, HMAC-SHA256 de `.`. Valide o corpo cru, rejeite timestamps com mais de 5 minutos e compare em tempo constante.