# Create Subscription Source: https://docs.purincash.com/api-reference/assinaturas/create-subscription /api-reference/openapi.yaml post /v1/subscriptions Cria uma assinatura recorrente via PIX vinculada a um produto ativo e retorna o código PIX (brCode) da primeira cobrança. Funciona também no modo sandbox (chaves ps_test_), com dados simulados. # List Subscriptions Source: https://docs.purincash.com/api-reference/assinaturas/list-subscriptions /api-reference/openapi.yaml get /v1/subscriptions Retorna a lista de assinaturas do ambiente da chave utilizada, ordenadas da mais recente para a mais antiga, com paginação por limit/offset e filtro opcional por status. # Create Card Payment Source: https://docs.purincash.com/api-reference/cartao/create-card-payment /api-reference/openapi.yaml post /v1/card-payments Cria um pagamento no cartão de crédito via Stripe Checkout e retorna a checkoutUrl para redirecionar o cliente. A sessão de checkout expira em 30 minutos. Não disponível no modo sandbox (chaves ps_test_). # Get Card Payment Source: https://docs.purincash.com/api-reference/cartao/get-card-payment /api-reference/openapi.yaml get /v1/card-payments/{orderCode} Consulta os detalhes de um pagamento no cartão pelo orderCode retornado na criação. Não disponível no modo sandbox (chaves ps_test_). # List Card Payments Source: https://docs.purincash.com/api-reference/cartao/list-card-payments /api-reference/openapi.yaml get /v1/card-payments Retorna a lista de pagamentos no cartão, ordenados do mais recente para o mais antigo, com paginação por limit/offset e filtro opcional por status. No modo sandbox (chaves ps_test_) a lista retorna vazia. # Get Wallet Source: https://docs.purincash.com/api-reference/carteira/get-wallet /api-reference/openapi.yaml get /v1/wallet Retorna o saldo da carteira da loja. O campo withdrawable é o saldo sacável agora (balance - disputeBlocked): o valor retido por disputas (MED) abertas ou perdidas não perdoadas é descontado do saldo bruto, e é esse o limite máximo aceito pelo POST /v1/payouts. Funciona com chaves ps_live_ e ps_test_ — no sandbox o saldo é simulado a partir dos pagamentos de teste (mesma regra do GET /v1/sandbox/wallet) e a resposta inclui "sandbox": true, com disputeBlocked e cryptoBalanceLtc sempre 0. # Create Charge Source: https://docs.purincash.com/api-reference/cobrancas/create-charge /api-reference/openapi.yaml post /v1/charges Cria uma cobrança PIX avulsa com valor customizado, sem produto vinculado. Retorna o BR Code (copia-e-cola) e a imagem do QR code para pagamento. Se `callbackUrl` for informado, um webhook `charge.paid` assinado com HMAC-SHA256 (header `X-Webhook-Signature`) é enviado quando o pagamento for confirmado. Para dividir a cobrança entre contas PurinCash, use o recurso Split Charges (POST /v1/split-charges). # Get Charge Source: https://docs.purincash.com/api-reference/cobrancas/get-charge /api-reference/openapi.yaml get /v1/charges/{paymentId} Consulta os detalhes de uma cobrança avulsa pelo paymentId, incluindo status, cliente, metadata e datas. Use para confirmar o pagamento (polling) como alternativa ao webhook do callbackUrl. Cobranças com split (prefixo psplit_) retornam o formato descrito em GET /v1/split-charges/{paymentId}. # Get Dispute Source: https://docs.purincash.com/api-reference/disputas/get-dispute /api-reference/openapi.yaml get /v1/disputes/{id} Consulta os detalhes de uma disputa pelo ID, incluindo as evidências já enviadas (com URLs assinadas regeneradas a cada consulta). No sandbox retorna sempre 404. # List Disputes Source: https://docs.purincash.com/api-reference/disputas/list-disputes /api-reference/openapi.yaml get /v1/disputes Lista as disputas (MED — Mecanismo Especial de Devolução) da loja, das mais recentes para as mais antigas. Enquanto uma disputa estiver aberta ou perdida (não perdoada), o valor contestado fica retido do saldo sacável (campo disputeBlocked do GET /v1/wallet). No sandbox retorna lista vazia com "sandbox": true. # Submit Dispute Evidence Source: https://docs.purincash.com/api-reference/disputas/submit-dispute-evidence /api-reference/openapi.yaml post /v1/disputes/{id}/evidence Envia evidência de defesa para uma disputa aberta. A disputa precisa estar com status "aberta" e ter ID no gateway (wooviDisputeId). Informe documents (até 10 URLs http/https) e/ou textForPdf (texto convertido automaticamente em PDF de defesa, hospedado em URL assinada com expiração). Não disponível no sandbox. # Get Delivery Source: https://docs.purincash.com/api-reference/entregas/get-delivery /api-reference/openapi.yaml get /v1/deliveries/{paymentId} Consulta o status de entrega de um pagamento e retorna o conteúdo entregue, se disponível. Aceita IDs de pagamento (prefixo psa_) e de cobrança (prefixo psc_). O campo deliveredContent é null se o pagamento estiver pendente ou se o produto tiver entrega manual. # Referência da API Source: https://docs.purincash.com/api-reference/introducao 29 endpoints, um formato de erro, uma autenticação. Com playground pra testar aqui mesmo. **Base URL:** `https://api.purincash.com` Todos os endpoints ficam sob `/v1` e exigem o header de autenticação: ``` Authorization: Bearer ps_live_sua_chave ``` Cada página desta seção tem um playground. Cole uma chave `ps_test_` e dispare a requisição direto do navegador, sem sair da documentação. ## Convenções `valueCents: 4990` é R\$ 49,90. Os campos terminados em `Cents` são sempre inteiros. Duas exceções, que vale conhecer antes de escrever o parser: cartão e saque usam `amount` em reais decimal, e produto da loja usa `price` como **texto** no padrão brasileiro (`"49,90"`). | Prefixo | Recurso | | ----------- | -------------------------------------------- | | `psa_` | Pagamento de `POST /v1/payments` | | `psa_sub_` | Assinatura | | `psc_` | Cobrança de `POST /v1/charges` | | `psplit_` | Cobrança com split | | Sem prefixo | Cartão usa `orderCode`, produto usa ObjectId | ```json theme={null} { "error": "Descrição do que deu errado" } ``` Trate pelo código HTTP, não pelo texto. Lista completa em [Erros](/guias/erros). `limit` vai de 1 a 100 (padrão 50) e `offset` pula resultados. A resposta traz `total`, `limit` e `offset` junto do array. Mande um JSON já serializado, de até 2 KB. Ele volta exatamente igual na consulta e no webhook, sem parse do nosso lado. ```json theme={null} { "metadata": "{\"pedido\":\"1042\"}" } ``` A única exceção é cobrança com [split](/guias/splits), que não aceita `metadata`. `2026-03-18T12:05:00.000Z`. Converta para o fuso do usuário só na exibição. ## O mapa `/v1/payments` para PIX e LTC. `/v1/charges` para PIX avulso e split. `/v1/card-payments` para cartão. `/v1/subscriptions` para recorrência. `/v1/products` para produtos de cobrança. `/v1/store/products` para os da loja do Discord. `/v1/deliveries` para o conteúdo entregue. `/v1/wallet` para saldo e retenções. `/v1/payouts` para sacar. `/v1/disputes` para contestações. `/v1/sandbox/*` para simular pagamento e conferir saldo de teste. Só com chave `ps_test_`. ## Limites que valem lembrar | O quê | Limite | | ---------------------- | ---------------------------------------------- | | Requisições | 120 por minuto, somando todas as rotas `/v1/*` | | Saques | 10 por hora, 5 se for turbo | | Chaves de API ativas | 10 por conta | | Produtos de cobrança | 500 por conta e por ambiente | | Cobrança com split | R\$ 5.000,00 por cobrança | | Beneficiários no split | 9, além de você | Detalhes e como lidar com `429` em [Limites de requisição](/guias/limites). O guia de primeira cobrança vai do zero ao webhook recebido, tudo em sandbox. # List Store Products Source: https://docs.purincash.com/api-reference/loja/list-store-products /api-reference/openapi.yaml get /v1/store/products Lista os produtos da loja do Discord, com categoria, variações e estoque. É uma rota de leitura: para criar ou editar produtos da loja, use o painel. Não confunda com `GET /v1/products`, que lista os produtos de cobrança criados pela própria API. São coleções separadas, e por isso `/v1/products` pode devolver uma lista vazia mesmo com a loja cheia. O conteúdo do estoque (as chaves, contas ou links entregues ao comprador) e as instruções de entrega não são retornados aqui. Variações de seller ainda não aprovadas ficam de fora da resposta, porque não são vendáveis. # Create Payment Source: https://docs.purincash.com/api-reference/pagamentos/create-payment /api-reference/openapi.yaml post /v1/payments Cria um pagamento e retorna os dados para o cliente pagar. Suporta PIX (padrão) e LTC (Litecoin) via `paymentMethod`. Envie `productId` (o preço vem do produto) ou `valueCents` (valor avulso em centavos); se ambos forem enviados, o preço do produto prevalece. Pagamentos LTC não estão disponíveis no sandbox. Se `callbackUrl` for informado, um webhook `payment.paid` assinado com HMAC-SHA256 (header `X-Webhook-Signature`) é enviado quando o pagamento for confirmado. # Get Payment Source: https://docs.purincash.com/api-reference/pagamentos/get-payment /api-reference/openapi.yaml get /v1/payments/{paymentId} Consulta os detalhes de um pagamento pelo paymentId, incluindo status, método de pagamento, cliente, metadata e datas. Use para confirmar o pagamento (polling) como alternativa ao webhook do callbackUrl. # List Payments Source: https://docs.purincash.com/api-reference/pagamentos/list-payments /api-reference/openapi.yaml get /v1/payments Retorna a lista de pagamentos com paginação e filtros por status e tipo, ordenada da mais recente para a mais antiga. # Create Product Source: https://docs.purincash.com/api-reference/produtos/create-product /api-reference/openapi.yaml post /v1/products Cria um produto para ser referenciado em pagamentos e assinaturas. Limite de 500 produtos por conta por ambiente. # Delete Product Source: https://docs.purincash.com/api-reference/produtos/delete-product /api-reference/openapi.yaml delete /v1/products/{id} Exclui permanentemente um produto pelo seu ID. # Get Product Source: https://docs.purincash.com/api-reference/produtos/get-product /api-reference/openapi.yaml get /v1/products/{id} Obtém os detalhes de um único produto pelo seu ID. # List Products Source: https://docs.purincash.com/api-reference/produtos/list-products /api-reference/openapi.yaml get /v1/products Retorna a lista de produtos criados, ordenada da mais recente para a mais antiga. Por padrão, apenas produtos ativos são retornados. # Update Product Source: https://docs.purincash.com/api-reference/produtos/update-product /api-reference/openapi.yaml put /v1/products/{id} Atualiza um produto existente. Todos os campos são opcionais — apenas os campos enviados são alterados. # Sandbox Transactions Source: https://docs.purincash.com/api-reference/sandbox/sandbox-transactions /api-reference/openapi.yaml get /v1/sandbox/transactions Lista as transações do ambiente de teste (pagamentos e cobranças avulsas), das mais recentes para as mais antigas. Requer chave ps_test_ (retorna 403 com chave ps_live_). # Sandbox Wallet Source: https://docs.purincash.com/api-reference/sandbox/sandbox-wallet /api-reference/openapi.yaml get /v1/sandbox/wallet Retorna o saldo simulado do ambiente de teste, calculado a partir dos pagamentos e cobranças de teste — os pagos somam em available e os pendentes em pending. Requer chave ps_test_ (retorna 403 com chave ps_live_). # Simulate Charge Paid Source: https://docs.purincash.com/api-reference/sandbox/simulate-charge-paid /api-reference/openapi.yaml post /v1/sandbox/charges/{paymentId}/simulate-paid Simula a confirmação de uma cobrança avulsa no ambiente de teste. Requer chave ps_test_ (retorna 403 com chave ps_live_). Marca a cobrança como "paid" e, se a cobrança tiver callbackUrl, dispara o webhook payment.paid com "sandbox": true. Cobranças já pagas apenas retornam o estado atual. # Simulate Payment Paid Source: https://docs.purincash.com/api-reference/sandbox/simulate-payment-paid /api-reference/openapi.yaml post /v1/sandbox/payments/{paymentId}/simulate-paid Simula a confirmação de um pagamento no ambiente de teste. Requer chave ps_test_ (retorna 403 com chave ps_live_). Marca o pagamento como "paid" e, se o pagamento tiver callbackUrl, dispara o webhook payment.paid com "sandbox": true. Pagamentos já pagos apenas retornam o estado atual. # Create Payout Source: https://docs.purincash.com/api-reference/saques/create-payout /api-reference/openapi.yaml post /v1/payouts Solicita um saque PIX ou LTC do saldo disponível. O débito do saldo é atômico. O valor máximo sacável é o campo withdrawable do GET /v1/wallet (balance - disputeBlocked): o saldo retido por disputas (MED) abertas ou perdidas não perdoadas não é sacável, e pedidos acima desse valor são rejeitados com 400 (o campo available da resposta indica o valor sacável). Saque PIX exige chave PIX verificada no dashboard (403 caso contrário). Saque LTC exige cryptoAmount e endereço LTC em formato válido. Rate limit de 10 solicitações por hora por chave de API (429 ao exceder). No sandbox retorna um saque simulado com status "completed", sem alterar o saldo real. # List Payouts Source: https://docs.purincash.com/api-reference/saques/list-payouts /api-reference/openapi.yaml get /v1/payouts Lista os saques solicitados pela loja, do mais recente para o mais antigo. O campo walletAddress é sempre mascarado (6 primeiros caracteres + ***). No sandbox retorna lista vazia. # Create Split Charge Source: https://docs.purincash.com/api-reference/splits/create-split-charge /api-reference/openapi.yaml post /v1/split-charges Cria uma cobrança PIX que, ao ser paga, é dividida automaticamente entre VOCÊ (dono da chave de API) e 1 a 9 beneficiários (contas PurinCash já cadastradas). Em `splits` você lista SOMENTE os outros beneficiários — você não se inclui: fica com o resto (100% menos a soma) e deve obrigatoriamente ter a maior fatia. A soma das percentages deve ser menor que 100.00. A taxa do gateway sai inteira da sua parte: cada beneficiário recebe a porcentagem cheia dele sobre o valor bruto; você recebe o restante menos a taxa (se a taxa passar da sua parte, você recebe 0). Emails dos beneficiários são mascarados em todas as respostas (LGPD). Se `callbackUrl` for informado, um POST assinado com HMAC-SHA256 (header X-Webhook-Signature) é enviado quando o pagamento for confirmado, com payload incluindo paymentId, status "paid" e splits mascarados. # Get Split Charge Source: https://docs.purincash.com/api-reference/splits/get-split-charge /api-reference/openapi.yaml get /v1/split-charges/{paymentId} Consulta o status de uma cobrança com split pelo paymentId (prefixo psplit_). Após o pagamento, retorna a taxa do gateway, o valor líquido e o quanto cada beneficiário recebeu (amountCents) com a data do crédito (creditedAt). Emails dos beneficiários são mascarados (LGPD). # Ambientes e sandbox Source: https://docs.purincash.com/guias/ambientes Como testar o fluxo inteiro, inclusive webhook, sem mover um centavo. A API tem dois ambientes e uma URL só. Quem decide onde a requisição cai é o prefixo da chave. Chave `ps_live_`. Cobrança de verdade, saldo de verdade, saque de verdade. Chave `ps_test_`. Nada é enviado a banco nenhum. O PIX gerado não é pagável. O `brCode` do sandbox não é um código PIX de verdade. Ele volta como `SANDBOX_PIX_`, uma string sem valor nenhum: não escaneia, não cola em app de banco, ninguém paga por acidente. Se você renderizar isso num QR Code na sua tela de teste, o QR vai existir e não vai funcionar. É esperado. Os dados são isolados de ponta a ponta. Uma cobrança criada com `ps_test_` não aparece para `ps_live_`, e o saldo de sandbox é calculado só a partir das transações de teste. Se o prefixo da chave não bater com o ambiente em que ela foi criada, a API responde `401` em vez de escolher um ambiente por conta própria. ## Simulando pagamento Como ninguém consegue pagar um PIX de sandbox, você marca como pago pela API: ```bash Cobrança (psc_) theme={null} curl -X POST https://api.purincash.com/v1/sandbox/charges/psc_a1b2c3d4/simulate-paid \ -H "Authorization: Bearer ps_test_sua_chave" ``` ```bash Pagamento (psa_) theme={null} curl -X POST https://api.purincash.com/v1/sandbox/payments/psa_a1b2c3d4/simulate-paid \ -H "Authorization: Bearer ps_test_sua_chave" ``` O status vira `paid` e o webhook sai para a `callbackUrl` que você informou na criação, exatamente como aconteceria em produção. Não existe aprovação de ninguém no meio. A chamada só funciona com chave `ps_test_` e só alcança cobrança da sua própria conta: com chave `ps_live_` a resposta é `403`. Chame `simulate-paid` **duas vezes** na mesma cobrança. O status não muda de novo, mas o webhook sai outra vez com o mesmo `X-Webhook-Id`. É o jeito mais barato de verificar se o seu handler é idempotente antes de precisar disso em produção. No webhook de simulação o `event` é sempre `payment.paid`, mesmo quando você simula uma cobrança `psc_`. O payload é enxuto: `event`, `paymentId`, `amountCents`, `status`, `paidAt`, `customer`, `metadata` e `sandbox: true`. ## Conferindo o resultado Saldo simulado, somado a partir das transações de teste. Extrato do que você criou em sandbox, com paginação. `GET /v1/wallet` também funciona com chave de teste. Nesse caso a resposta vem com `"sandbox": true`, e `disputeBlocked` e `cryptoBalanceLtc` ficam sempre em zero. ## O que não roda em sandbox `POST /v1/card-payments` depende do provedor de checkout e não é simulado. Teste o cartão direto em produção com um valor baixo. `paymentMethod: "ltc"` responde `400` com chave de teste. A cotação e o endereço vêm da rede real, então não há como simular. Os endpoints de `/v1/disputes` respondem `404` em sandbox. Contestação nasce de uma transação real. O saque em sandbox é registrado e dispara os eventos `withdrawal.test.requested` e `withdrawal.test.completed`, mas nenhum valor sai. ## Indo para produção Troque a variável de ambiente com a chave. Nada mais muda: mesma URL, mesmos campos, mesmos nomes de evento. Vale a pena rodar o mesmo teste automatizado nos dois ambientes, com a chave vindo de variável. Se o teste passa com `ps_test_` e quebra com `ps_live_`, o problema está em configuração de conta e não no seu código. # Assinaturas Source: https://docs.purincash.com/guias/assinaturas PIX recorrente amarrado a um produto, com a cobrança sendo gerada sozinha. Assinatura é PIX recorrente. Você cria uma vez, escolhe a frequência, e a cobrança passa a ser gerada automaticamente. Cada cobrança gerada dispara o webhook na mesma `callbackUrl`. Assinatura sempre nasce de um produto cadastrado. O valor e o nome saem dele, então antes de criar a assinatura você precisa de um `productId` ativo. Veja [Produtos](/guias/produtos). ## Criando ```bash theme={null} curl -X POST https://api.purincash.com/v1/subscriptions \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "productId": "65f0c2a1e4b0a1b2c3d4e5f6", "customer": { "name": "João Silva", "externalId": "user_42" }, "frequency": "MONTHLY", "dayGenerateCharge": 15, "callbackUrl": "https://minhaloja.com/webhooks/purincash", "metadata": "{\"plano\":\"premium\"}" }' ``` Produto ativo que define valor e nome da assinatura. Nome do assinante. Aqui ele é obrigatório, diferente do resto da API. `WEEKLY`, `MONTHLY`, `SEMIANNUALLY` ou `ANNUALLY`. Dia do mês em que a cobrança é gerada, entre 4 e 28. Recebe o webhook de **cada** cobrança da assinatura, não só da primeira. String JSON de até 2 KB, devolvida sem alteração em toda cobrança. ```json Resposta theme={null} { "paymentId": "psa_sub_a1b2c3d4", "status": "pending", "type": "subscription", "subscriptionId": "sub_9f8e7d6c", "amountCents": 4990, "currency": "BRL", "productName": "Plano Premium", "frequency": "MONTHLY", "environment": "live", "pix": { "brCode": "00020126580014br.gov.bcb.pix...", "paymentLinkUrl": "https://pay.exemplo.com/sub_9f8e7d6c" }, "dayGenerateCharge": 15 } ``` O `dayGenerateCharge` vai até 28 de propósito. Assinatura marcada para dia 30 pularia fevereiro, e isso é uma classe inteira de bug que ninguém quer no faturamento. ## Por que 4 a 28, e não 1 a 31 O limite inferior existe porque a cobrança precisa de alguns dias de antecedência para o cliente pagar antes do vencimento. O superior evita mês sem aquele dia. Se o seu produto tem data comercial fixa fora dessa faixa, o caminho é gerar a cobrança você mesmo com `POST /v1/payments` no dia que quiser. ## Acompanhando ```bash theme={null} curl "https://api.purincash.com/v1/subscriptions?limit=20&status=paid" \ -H "Authorization: Bearer $PURINCASH_KEY" ``` A listagem aceita `limit` (1 a 100, padrão 50), `offset` e `status`. ```json theme={null} { "subscriptions": [ { "paymentId": "psa_sub_a1b2c3d4", "subscriptionId": "sub_9f8e7d6c", "status": "pending", "amountCents": 4990, "currency": "BRL", "productName": "Plano Premium", "customer": { "name": "João Silva", "email": "", "externalId": "user_42" }, "metadata": "{\"plano\":\"premium\"}", "paidAt": null, "createdAt": "2026-03-18T10:00:00.000Z" } ], "total": 5, "limit": 20, "offset": 0 } ``` ## Controlando o acesso do assinante A API não bloqueia o cliente sozinho. Quem decide se o acesso continua é você, e o dado que sustenta essa decisão é o webhook. É a chave estável entre as cobranças. O `paymentId` muda a cada ciclo. Ao receber o webhook com `status: "paid"`, empurre a data de expiração do acesso para frente conforme a frequência. Se o ciclo passou sem pagamento, a data vence e o acesso cai. É mais confiável do que tentar reagir a um evento de cancelamento. Sempre confirme por `subscriptionId` antes de renovar. Um assinante com duas assinaturas ativas do mesmo produto é raro, mas acontece, e renovar pelo produto em vez da assinatura dá acesso de graça. # Autenticação Source: https://docs.purincash.com/guias/autenticacao Uma chave, um header. Como pegar, como guardar e o que fazer quando vaza. Toda requisição para `https://api.purincash.com` leva a chave de API no header `Authorization`, no esquema Bearer: ``` Authorization: Bearer ps_live_sua_chave_aqui ``` Não existe OAuth, não existe login por usuário e senha na API, e não existe chave no query string. ## Pegando a chave [purincash.com/dashboard/api](https://purincash.com/dashboard/api) Produção gera `ps_live_`. Sandbox gera `ps_test_`. A chave completa aparece uma vez só. Depois disso o painel mostra apenas os últimos caracteres, para você identificar qual é qual. Cada conta pode manter até 10 chaves ativas ao mesmo tempo. Use isso a seu favor: uma chave por ambiente e por serviço deixa você revogar uma sem derrubar o resto. ## O prefixo define o ambiente | Prefixo | Ambiente | O que acontece | | ---------- | -------- | ---------------------------------------- | | `ps_live_` | Produção | Cobrança real, dinheiro real, saldo real | | `ps_test_` | Sandbox | Tudo simulado, nada movimenta dinheiro | Não existe parâmetro de ambiente. A chave decide, e os dados são isolados: uma chave de teste nunca lista uma cobrança de produção. Mais detalhes em [Ambientes e sandbox](/guias/ambientes). ## Guardando a chave Quem tem a sua chave consegue criar cobrança em seu nome e, com [saque turbo](/guias/saques#saque-turbo), mandar dinheiro pra fora sem passar por aprovação. Trate a chave com o mesmo cuidado da senha do painel. Variável de ambiente ou cofre de segredos. Chamada sempre de servidor pra servidor. Uma chave por serviço, para revogar sem parar tudo. `ps_test_` no desenvolvimento e na CI. Chave no bundle do front, no app mobile ou no `.env` versionado. Chave em log, em print, em ticket de suporte. A mesma chave em produção e homologação. Chave hardcoded "só pra testar rápido". Se desconfiar que vazou, revogue no painel e gere outra. A revogação vale na hora: a chave antiga passa a responder `401` na requisição seguinte. ## Erros de autenticação Todos vêm como `401` com o corpo padrão `{ "error": string }`. | Mensagem | O que aconteceu | | ---------------------------------------------------------- | -------------------------------------------------------------------------- | | `API key required. Use: Authorization: Bearer ps_live_...` | Header ausente, ou sem o `Bearer ps_` na frente | | `Invalid API key prefix. Use ps_live_ or ps_test_` | A chave não começa com nenhum dos dois prefixos | | `Invalid or revoked API key` | Chave inexistente, digitada errada ou revogada | | `API key environment mismatch. Please generate a new key.` | O prefixo não bate com o ambiente em que a chave foi criada. Gere uma nova | ```json theme={null} { "error": "Invalid or revoked API key" } ``` Recebeu `401` com a chave certa? Confira se sobrou espaço, quebra de linha ou aspas na variável de ambiente. É de longe a causa mais comum. # Receber por cartão Source: https://docs.purincash.com/guias/cartao Checkout hospedado: você redireciona, a gente cobra e devolve o resultado. No cartão você não recebe o número do cartão em momento nenhum. A API devolve uma URL de checkout hospedado, o cliente paga lá, e você recebe o resultado por webhook e por consulta. Isso mantém dado de cartão fora do seu servidor, que é exatamente onde ele deve ficar. Cartão não funciona em sandbox. Com chave `ps_test_` a resposta é erro. Para testar, use produção com um valor baixo. ## Criando a cobrança ```bash theme={null} curl -X POST https://api.purincash.com/v1/card-payments \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "valueCents": 4990, "description": "Plano Premium", "callbackUrl": "https://minhaloja.com/webhooks/purincash", "customer": { "name": "João Silva", "email": "joao@exemplo.com", "externalId": "user_42" }, "successUrl": "https://minhaloja.com/obrigado", "cancelUrl": "https://minhaloja.com/carrinho", "metadata": "{\"pedido\":\"1042\"}" }' ``` ```json Resposta 201 theme={null} { "orderCode": "JUE7Y9MPSX", "status": "pending", "amountCents": 4990, "currency": "BRL", "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_live_...", "expiresAt": "2026-03-18T12:30:00.000Z" } ``` Redirecione o cliente para `checkoutUrl`. A sessão vale 30 minutos. Cartão é o único recurso identificado por `orderCode` em vez de `paymentId`. É esse código que você usa para consultar e é ele que volta no webhook. ## Para onde o cliente volta | Campo | Quando é usado | | ------------ | ------------------------------ | | `successUrl` | O cliente terminou o pagamento | | `cancelUrl` | O cliente desistiu e voltou | Cair na `successUrl` não significa pagamento aprovado. É só o navegador voltando, e qualquer pessoa consegue abrir essa URL na mão. Libere o produto pelo webhook `card_payment.paid` ou pela consulta, nunca pelo redirecionamento. ## Consultando ```bash theme={null} curl https://api.purincash.com/v1/card-payments/JUE7Y9MPSX \ -H "Authorization: Bearer $PURINCASH_KEY" ``` ```json theme={null} { "orderCode": "JUE7Y9MPSX", "status": "paid", "amount": 49.90, "amountCents": 4990, "currency": "BRL", "description": "Plano Premium", "checkoutUrl": null, "paidAt": "2026-03-18T12:05:00.000Z", "createdAt": "2026-03-18T12:00:00.000Z" } ``` Status possíveis: `pending`, `paid`, `expired`, `refunded` e `failed`. Para listar, use `GET /v1/card-payments` com `limit`, `offset` e `status`. ## Cartão tem prazo de liberação Diferente do PIX, o valor do cartão entra como **a liberar** antes de virar saldo sacável. Em `GET /v1/wallet` esse montante aparece em `pendingRelease`, e ele não entra no `withdrawable`. Vale considerar isso no seu fluxo de caixa: vendeu no cartão hoje não quer dizer que dá pra sacar hoje. O detalhe de cada campo está em [Carteira](/guias/carteira). ## Diferenças no webhook O evento é `card_payment.paid` e a forma muda um pouco em relação ao PIX: ```json theme={null} { "event": "card_payment.paid", "orderCode": "JUE7Y9MPSX", "amount": 49.90, "status": "paid", "paidAt": "2026-03-18T12:05:00.000Z", "customer": { "name": "João Silva", "email": "joao@exemplo.com" }, "metadata": "{\"pedido\":\"1042\"}" } ``` Repare em duas coisas: o identificador é `orderCode` e não `paymentId`, e o valor vem em `amount` (reais, decimal) e não em `amountCents`. Se o seu handler for genérico, trate os dois formatos. Detalhes completos em [Webhook de cartão](/webhooks/cartao). # Carteira Source: https://docs.purincash.com/guias/carteira Saldo bruto, retido, a liberar e sacável. Quatro números que não são a mesma coisa. `GET /v1/wallet` devolve o saldo da loja separado por situação. A confusão comum é achar que "saldo" é o quanto dá pra sacar, e não é. ```bash theme={null} curl https://api.purincash.com/v1/wallet \ -H "Authorization: Bearer $PURINCASH_KEY" ``` ```json theme={null} { "currency": "BRL", "balance": 1250.00, "balanceCents": 125000, "disputeBlocked": 230.70, "withdrawable": 1019.30, "withdrawableCents": 101930, "pendingRelease": 480.00, "pendingReleaseCents": 48000, "cryptoBalanceLtc": 0.15068859 } ``` ## O que cada número significa Saldo bruto da carteira, em reais. É o total que entrou e ainda não saiu, sem descontar retenção. Valor retido por [disputas](/guias/disputas) abertas ou perdidas que ainda não foram perdoadas. Fica indisponível até a contestação ser resolvida. **É este que importa na hora de sacar.** Vale `balance - disputeBlocked`, e é o teto aceito por `POST /v1/payouts`. Vendas no cartão aguardando o prazo de liberação. Ainda **não** está em `balance` nem em `withdrawable`. É previsão de caixa, não dinheiro disponível. Saldo em Litecoin, com 8 casas decimais. Sacável por `POST /v1/payouts` com `method: "ltc"`. Todos os valores em reais têm um par em centavos (`balanceCents`, `withdrawableCents`, `pendingReleaseCents`). Use os campos em centavos para qualquer conta. Ponto flutuante em dinheiro é como você ganha uma diferença de um centavo no relatório e perde uma tarde procurando. ## A relação entre eles ``` balance = o que entrou e liquidou − disputeBlocked = retido por contestação = withdrawable = o que POST /v1/payouts aceita hoje pendingRelease = cartão que ainda vai virar balance ``` Pedir um saque acima de `withdrawable` devolve `400` por saldo insuficiente, mesmo que `balance` cubra o valor. Se isso te pegou de surpresa, quase sempre a resposta está em `disputeBlocked`. ## Em sandbox A rota funciona com chave `ps_test_`. O saldo é somado a partir das transações de teste, a resposta traz `"sandbox": true`, e `disputeBlocked` e `cryptoBalanceLtc` ficam sempre em zero. PIX, LTC ou USDT, direto pela API. Como responder uma contestação e liberar o valor. # Receber em Litecoin Source: https://docs.purincash.com/guias/cripto Mesmo endpoint do PIX, um campo a mais. E três diferenças que mudam o seu fluxo. Não existe endpoint separado para cripto. Você usa o mesmo `POST /v1/payments` com `paymentMethod: "ltc"`. O valor continua indo em `valueCents`, em reais. A conversão para LTC é feita na hora, pela cotação do momento. ```bash theme={null} curl -X POST https://api.purincash.com/v1/payments \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "paymentMethod": "ltc", "valueCents": 4990, "description": "Plano Premium", "callbackUrl": "https://minhaloja.com/webhooks/purincash" }' ``` ```json Resposta theme={null} { "paymentId": "psa_a1b2c3d4e5f6", "status": "pending", "type": "one_time", "paymentMethod": "ltc", "amountCents": 4990, "currency": "BRL", "ltc": { "address": "ltc1q...", "amount": 0.15068859, "amountBrl": 49.9, "ltcPriceBrl": 331.2 }, "expiresAt": "2026-03-18T12:25:00.000Z" } ``` ## O valor exato importa Cobre exatamente o `ltc.amount`, com todas as casas decimais. Os últimos satoshis são aleatórios de propósito: é assim que a gente sabe qual pagamento chegou quando duas cobranças têm o mesmo preço. Valor diferente não é reconhecido automaticamente. Na prática isso significa que você mostra `ltc.amount` como está, sem arredondar para exibição. Um `toFixed(4)` na interface é o suficiente para o cliente pagar o valor errado. ## Três diferenças em relação ao PIX PIX expira em 30 minutos, LTC em 25. O `expiresAt` sempre manda. Não é instantâneo. O webhook sai quando a rede fecha as confirmações. Com chave `ps_test_` a resposta é `400`. Cotação e endereço vêm da rede real. ## Erros específicos | Código | Motivo | | ------ | ------------------------------------------------------------------ | | `400` | Chave de sandbox, ou `paymentMethod` fora de `pix` e `ltc` | | `502` | Cotação de LTC indisponível no momento. Tente de novo em instantes | | `503` | A loja não tem carteira LTC configurada no painel | ## Consulta e webhook Iguais aos do PIX. A consulta é `GET /v1/payments/{paymentId}` e o webhook chega em `callbackUrl` quando as confirmações fecham. Como a confirmação demora, cripto combina bem com entrega assíncrona: confirme o pedido na hora com status "aguardando rede" e libere o produto quando o webhook chegar. Prender o cliente numa tela de espera por vários minutos derruba conversão. ## E o USDT? USDT aparece na PurinCash só na saída, como forma de saque, não como forma de receber. Está em [Saques](/guias/saques#saque-em-usdt). # Disputas Source: https://docs.purincash.com/guias/disputas O cliente contestou o PIX. O que fica retido, o que você manda e em quanto tempo. Disputa é o mecanismo de contestação do PIX (MED). Quando o comprador aciona o banco dizendo que não recebeu o produto, o valor daquela transação fica retido no seu saldo até a operadora decidir. Enquanto está aberta, o valor aparece em `disputeBlocked` na [carteira](/guias/carteira) e sai do `withdrawable`. Não dá pra sacar o que está contestado. Disputas não existem em sandbox. Os endpoints respondem `404` com chave `ps_test_`, porque contestação nasce de uma transação real. ## Monitorando ```bash theme={null} curl "https://api.purincash.com/v1/disputes?status=aberta&limit=50" \ -H "Authorization: Bearer $PURINCASH_KEY" ``` ```json theme={null} { "disputes": [ { "id": "665f1a2b3c4d5e6f7a8b9c0d", "code": "MED-2024-001", "endToEndId": "E123456782026...", "orderCode": "JUE7Y9MPSX", "buyer": "João Silva", "product": "Plano Premium", "amount": 23.07, "reason": "Produto não recebido", "status": "aberta", "evidences": [], "resolvedAt": null, "createdAt": "2026-03-18T10:00:00.000Z" } ], "total": 3, "limit": 50, "offset": 0 } ``` Status possíveis: `aberta`, `resolvida` e `perdida`. Vale rodar essa listagem uma vez por dia filtrando por `aberta`. Disputa tem prazo, e perder por não ter respondido é o pior jeito de perder. ## Respondendo Só disputa com status `aberta` aceita evidência. Você pode mandar documentos, um texto que vira PDF, ou os dois. ```bash theme={null} curl -X POST https://api.purincash.com/v1/disputes/665f1a2b3c4d5e6f7a8b9c0d/evidence \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "documents": [ { "url": "https://minhaloja.com/comprovantes/1042.pdf", "description": "Comprovante de entrega", "correlationID": "MED-2024-001-EV1" } ], "textForPdf": "Produto entregue em 15/03 às 10h05. Acesso registrado pelo IP do comprador em 15/03 às 10h12." }' ``` ```json Resposta 200 theme={null} { "uploaded": 2 } ``` Até 10 itens. Cada um precisa de `url` http ou https. Itens sem URL válida são descartados em silêncio. Texto convertido automaticamente em PDF de defesa. Serve sozinho, sem `documents`. Um dos dois é obrigatório. Sem nenhum, a resposta é `400`. As URLs precisam apontar direto para o arquivo (PDF, PNG, JPEG ou WebP). Link de página HTML, tipo encurtador de print, é recusado pela operadora. Se o seu comprovante está numa página, gere um PDF e hospede o arquivo. ## O que costuma funcionar como evidência Log de acesso, e-mail de entrega com data e hora, código de rastreio, print do sistema mostrando a liberação. Termos aceitos, confirmação de recebimento, conversa em que o comprador reconhece que recebeu. O `endToEndId` da transação, o e-mail usado na compra, o `externalId` que amarra ao usuário da sua base. Histórico mostrando que a conta foi usada depois da compra. É o que mais derruba "produto não recebido". Junte a evidência de forma programática assim que o pagamento confirma, não quando a disputa chega. Trinta dias depois o log já rotacionou e o print não existe mais. ## Consultando uma disputa ```bash theme={null} curl https://api.purincash.com/v1/disputes/665f1a2b3c4d5e6f7a8b9c0d \ -H "Authorization: Bearer $PURINCASH_KEY" ``` O retorno traz as evidências já enviadas, com URLs assinadas que são regeneradas a cada consulta. Elas expiram, então não vale guardar a URL: guarde o id e consulte de novo quando precisar. Disputa que não é da sua conta responde `404`, e não `403`. É proposital: a API não confirma a existência de recurso de terceiro. ## Erros | Código | Motivo | | ------ | ----------------------------------------------------------------------------------------------------------- | | `400` | Disputa já resolvida, corpo sem `documents` nem `textForPdf`, nenhuma URL válida, ou ID em formato inválido | | `404` | Disputa inexistente, de outra conta, ou chamada em sandbox | | `502` | Falha ao enviar a evidência à operadora. Tente de novo | | `503` | Provedor de pagamento não configurado | # Entrega automática Source: https://docs.purincash.com/guias/entrega Como buscar a chave, a conta ou o link que o comprador recebeu. Quando a cobrança está amarrada a um produto da loja, o conteúdo (chave, conta, link) é liberado no instante em que o pagamento confirma. `GET /v1/deliveries/{paymentId}` é como você lê esse conteúdo do seu lado, para reenviar por e-mail, mostrar numa tela de pedido ou registrar no seu banco. ## Amarrando o produto A ligação é feita na **criação** da cobrança, pelo campo `supplier`. Ele funciona igual em `POST /v1/charges` e em `POST /v1/payments`: ```bash theme={null} curl -X POST https://api.purincash.com/v1/charges \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "valueCents": 1990, "description": "Conta Premium", "callbackUrl": "https://minhaloja.com/webhooks/purincash", "supplier": { "productId": "prod_aim_assist", "variationIndex": 0 } }' ``` ID **público** do produto da loja, no formato `prod_xxx`. Você pega no painel, no produto. Não é o `_id` que aparece em `GET /v1/store/products`, e não é o `productId` de produto de cobrança. Qual variação, pela posição na lista: `0` é a primeira, `1` a segunda. Índice que não existe devolve `400` dizendo quantas variações o produto tem. O `supplier` **não** define o preço. Quem manda no valor continua sendo `valueCents` (ou o produto de cobrança, se você usou `productId`). Amarrar um produto de R\$ 50 numa cobrança de R\$ 5 não corrige o valor: gera uma cobrança de R\$ 5. Quando a variação vem de fornecedor externo, a API valida duas coisas na criação e recusa na hora se algo não fecha: | Código | Motivo | | ------ | ------------------------------------------------------------------------------- | | `403` | Você não tem acesso aprovado àquele fornecedor | | `400` | O valor da cobrança é menor que o custo do fornecedor. O erro diz o custo exato | Isso evita vender abaixo do custo por engano, que é um erro que só apareceria no fechamento do mês. ## Lendo o que foi entregue ```bash theme={null} curl https://api.purincash.com/v1/deliveries/psa_a1b2c3d4e5f6 \ -H "Authorization: Bearer $PURINCASH_KEY" ``` ```json theme={null} { "paymentId": "psa_a1b2c3d4e5f6", "status": "paid", "amountCents": 4990, "paidAt": "2026-03-18T12:05:00.000Z", "deliveredContent": "LICENSE-KEY-ABC-123", "customer": { "name": "João Silva", "email": "joao@exemplo.com", "externalId": "user_42" } } ``` Aceita tanto ID de pagamento (`psa_`) quanto de cobrança (`psc_`). ## Quando `deliveredContent` vem `null` Enquanto o `status` for `pending`, não existe entrega. Espere o webhook. Nesse caso o conteúdo não é gerado automaticamente, e não há nada para esta rota devolver. Sem `supplier` na criação, não existe o que entregar. É só um valor. Esse é o caso mais comum de "criei tudo certo e `deliveredContent` vem `null`": o vínculo precisa ser feito na criação, não dá pra amarrar depois. Nenhum desses casos é erro: a resposta é `200` com `deliveredContent: null`. `404` só acontece quando o pagamento não existe ou não é da sua conta. ## O conteúdo também chega no webhook O evento `payment.paid` traz `deliveredContent` quando há entrega automática. Se você já processa o webhook, normalmente não precisa chamar esta rota. Ela é útil em dois momentos: quando o cliente pede a chave de novo e você não quer guardar o conteúdo no seu banco, e quando você está reconciliando um pagamento antigo cujo webhook se perdeu. `deliveredContent` costuma ser exatamente o que o cliente comprou: licença, credencial ou link. Trate como dado sensível, não jogue em log e não exponha em endpoint público sem autenticação. # Erros Source: https://docs.purincash.com/guias/erros Um formato só, sete códigos, e quais deles vale reenviar. Todo erro da API vem como JSON com um campo: ```json theme={null} { "error": "Descrição do que deu errado" } ``` Trate erro pelo **código HTTP**, nunca pelo texto. A mensagem existe para você ler no log e pode mudar sem aviso. O código não muda. ## Códigos | Código | Significa | Reenviar? | | ------ | ------------------------------------------------------------------------------------ | --------------------------------------------- | | `400` | Parâmetro ausente, inválido ou fora do limite | Só depois de corrigir | | `401` | Chave ausente, inválida, revogada ou com prefixo errado | Não. Veja [Autenticação](/guias/autenticacao) | | `403` | Chave válida, operação não permitida (sandbox com chave live, conta sem verificação) | Não. Resolva no painel | | `404` | Recurso não existe ou não é da sua conta | Não | | `409` | Conflito de estado (saque turbo já em andamento) | Sim, depois de esperar | | `429` | Passou do [limite de requisições](/guias/limites) | Sim, com backoff | | `500` | Erro inesperado no servidor | Sim, com backoff | | `502` | Falha temporária junto ao provedor | Sim, com backoff | | `503` | Recurso indisponível no momento (carteira LTC ausente, cripto fora do ar) | Sim, mais tarde | `404` em vez de `403` para recurso de outra conta é proposital. A API não confirma que um id existe se ele não é seu. ## Mensagens que você vai encontrar | Mensagem | Causa | | ---------------------------------------------------------- | --------------------------------------------------------------------------------- | | `valueCents must be >= 80 (R$ 0.80), or provide productId` | Valor abaixo do mínimo da loja. O número na mensagem é o mínimo real da sua conta | | `priceCents must be >= 100 (R$ 1.00)` | Produto abaixo de R\$ 1,00 | | `callbackUrl must be a valid, public HTTPS URL` | URL sem HTTPS, com IP, `localhost` ou rede privada | | `paymentMethod must be 'pix' or 'ltc'` | Método desconhecido | | `Invalid product ID format` | O id não é um ObjectId válido | | `Turbo payouts are PIX-only.` | `turbo: true` com `method` diferente de `pix` | | Mensagem | Causa | | ---------------------------------------------------------- | ---------------------------------------- | | `API key required. Use: Authorization: Bearer ps_live_...` | Header ausente ou malformado | | `Invalid API key prefix. Use ps_live_ or ps_test_` | Prefixo desconhecido | | `Invalid or revoked API key` | Chave inexistente ou revogada | | `API key environment mismatch. Please generate a new key.` | Prefixo não bate com o ambiente da chave | | Mensagem | Causa | | ----------------------------------------------------------------- | --------------------------------------------- | | `Sandbox endpoint requires ps_test_ key` | Rota de sandbox chamada com chave de produção | | `PIX not verified. Complete verification in the dashboard first.` | Saque antes de verificar a chave PIX | | Mensagem | Causa | | ----------------------- | ---------------------------------------------- | | `Product not found` | Produto inexistente, inativo ou de outra conta | | `Charge not found` | Cobrança inexistente ou de outra conta | | `Payment not found` | Pagamento inexistente ou de outra conta | | `LTC price unavailable` | Cotação de Litecoin fora do ar no momento | ## Como tratar ```js theme={null} async function chamar(url, opcoes, tentativas = 4) { for (let i = 0; i < tentativas; i++) { const res = await fetch(url, opcoes); if (res.ok) return res.json(); const { error } = await res.json().catch(() => ({ error: res.statusText })); // Erro do cliente: reenviar não resolve, só queima requisição. if (res.status < 500 && res.status !== 429) { throw new Error(`${res.status}: ${error}`); } // 429, 5xx: vale esperar. Backoff com jitter pra não sincronizar rajada. const espera = 2 ** i * 1000 + Math.random() * 1000; await new Promise((r) => setTimeout(r, espera)); } throw new Error("limite de tentativas excedido"); } ``` Registre o corpo `{ "error": ... }` junto do código HTTP e do `paymentId` nos seus logs. Quando você chamar a gente [no Discord](https://discord.gg/8eyQQFZZxY), é essa tripla que resolve em uma resposta em vez de cinco. # Limites de requisição Source: https://docs.purincash.com/guias/limites 120 por minuto no geral, 10 por hora em saque. E como não bater neles. ## Os limites | Escopo | Limite | Janela | | ------------------------------ | --------------- | ----------------------------------------- | | Todas as rotas `/v1/*` | 120 requisições | 60 segundos | | `POST /v1/payouts` | 10 saques | 1 hora, por chave | | `POST /v1/payouts` com `turbo` | 5 saques | 1 hora, por chave, dentro do limite acima | O limite geral é somado entre todos os endpoints. Não existe cota separada por recurso. ## Headers Toda resposta traz os headers padrão, então dá pra desacelerar antes de bater no teto: | Header | Conteúdo | | --------------------- | ------------------------------- | | `RateLimit-Limit` | Total permitido na janela | | `RateLimit-Remaining` | Quanto ainda resta | | `RateLimit-Reset` | Segundos até a janela reiniciar | ## Quando estoura ``` HTTP/1.1 429 Too Many Requests RateLimit-Limit: 120 RateLimit-Remaining: 0 RateLimit-Reset: 42 { "error": "Rate limit exceeded. Max 120 requests/minute." } ``` Não faça retry imediato de um `429`. Você continua bloqueado até a janela virar, e cada tentativa só empurra o problema pra frente. Espere pelo menos o `RateLimit-Reset`, e use backoff exponencial com jitter: ```js theme={null} async function comRetry(fn, maxTentativas = 5) { for (let i = 0; i <= maxTentativas; i++) { const res = await fn(); if (res.status !== 429) return res; const reset = Number(res.headers.get("RateLimit-Reset")) || 2 ** i; const jitter = Math.random() * 1000; await new Promise((r) => setTimeout(r, reset * 1000 + jitter)); } throw new Error("rate limit: máximo de tentativas excedido"); } ``` O jitter não é enfeite: sem ele, todos os seus workers acordam no mesmo instante e batem no limite de novo, juntos. ## O jeito de não chegar perto É de longe a maior economia. Uma cobrança consultada a cada 3 segundos por 30 minutos são 600 requisições. O webhook resolve em uma. Intervalo de 5 segundos ou mais, e pare no primeiro status final (`paid`, `expired`, `refunded`). As listagens aceitam `limit` até 100. Buscar de 10 em 10 gasta 10 vezes mais requisição pelo mesmo resultado. Catálogo de produto não precisa ser relido a cada request do seu usuário. Se você tem vários serviços integrando, dê uma chave para cada um. Assim o limite de saque de um job não é consumido pelo checkout, e você descobre rápido qual serviço está gastando requisição à toa. # Receber por PIX Source: https://docs.purincash.com/guias/pix Os dois endpoints de PIX, quando usar cada um e como reconciliar sem passar vergonha. Existem dois jeitos de gerar um PIX, e a diferença não é estilo: eles aceitam recursos diferentes. **Os dois aceitam valor solto.** Não é que um seja "com produto" e o outro "sem". Em `/v1/payments` o `productId` é opcional: sem ele, você manda `valueCents` e funciona igual. O que separa de verdade é o que cada um faz **além** disso. | Você quer | Use | ID | | ------------------------------------------- | ---------------------------------------------- | ---------- | | Só gerar um PIX de um valor | `POST /v1/charges` | `psc_` | | Dividir o valor com outras contas | `POST /v1/charges` com `splits` | `psplit_` | | O preço vir de um produto cadastrado | `POST /v1/payments` com `productId` | `psa_` | | Cobrar em [Litecoin](/guias/cripto) | `POST /v1/payments` com `paymentMethod: "ltc"` | `psa_` | | [Assinatura](/guias/assinaturas) recorrente | `POST /v1/subscriptions` | `psa_sub_` | Regra prática: **quer só um valor, usa `charges`.** É o caminho mais curto, e é o único que faz split. Vá pra `payments` quando precisar de catálogo, cripto ou assinatura. Uma coisa que **os dois** aceitam: o campo `supplier`, que amarra a cobrança a um produto da sua loja pra [entrega automática](/guias/entrega#amarrando-o-produto). Isso é independente de qual endpoint você escolheu. ## Criando ```bash Valor livre theme={null} curl -X POST https://api.purincash.com/v1/charges \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "valueCents": 1990, "description": "Plano Pro", "callbackUrl": "https://minhaloja.com/webhooks/purincash", "customer": { "name": "João Silva", "email": "joao@exemplo.com" }, "metadata": "{\"pedido\":\"1042\"}" }' ``` ```bash A partir de um produto theme={null} curl -X POST https://api.purincash.com/v1/payments \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "productId": "665f1a2b3c4d5e6f7a8b9c0d", "callbackUrl": "https://minhaloja.com/webhooks/purincash", "customer": { "name": "João Silva", "externalId": "user_42" } }' ``` ```json Resposta theme={null} { "paymentId": "psc_a1b2c3d4e5f6", "status": "pending", "amountCents": 1990, "currency": "BRL", "environment": "live", "pix": { "brCode": "00020126580014br.gov.bcb.pix0136...", "qrCodeImage": "https://qr.exemplo.com/psc_a1b2c3d4e5f6.png" }, "expiresAt": "2026-03-18T12:30:00.000Z" } ``` Em `/v1/payments`, se você mandar `productId` e `valueCents` ao mesmo tempo, o preço do produto vence. Produto em moeda diferente de BRL é convertido para real na hora da cobrança. Sem `productId`, o `valueCents` manda e a `description` vira o nome do pagamento (padrão `"Pagamento"` quando você não mandar nenhuma). Cuidado pra não confundir dois campos com nome parecido: `productId` é produto **de cobrança**, criado por `POST /v1/products`. Ele define o preço. `supplier.productId` é produto **da loja**, no formato `prod_xxx`. Ele não define preço nenhum: serve pra entrega automática. Passar um no lugar do outro devolve `404`. ## Valor mínimo O piso da plataforma é **R\$ 0,80** (`valueCents: 80`), mas cada loja pode configurar um mínimo maior no painel. Quando o valor não passa, o erro `400` já diz qual é o mínimo daquela conta: ```json theme={null} { "error": "valueCents must be >= 500 (R$ 5.00), or provide productId" } ``` Vale ler esse número em vez de fixar `80` no seu código. ## Ciclo de vida A cobrança existe e o `brCode` funciona. Dura 30 minutos. O PIX caiu. O webhook sai nesse momento e o valor entra no seu saldo. Passou dos 30 minutos sem pagamento. O código não funciona mais. Para tentar de novo, crie outra cobrança. Estorno ou cancelamento posterior. Vale conferir esses status antes de entregar algo de valor alto. ## Reconciliando Faça as duas coisas. Elas cobrem falhas diferentes. A entrega é feita assim que o pagamento confirma, com reentrega automática em caso de falha. Ainda assim, a sua URL é pública e qualquer um pode chamá-la. Sempre [valide a assinatura](/webhooks/assinatura) e deduplique pelo header `X-Webhook-Id`. `GET /v1/charges/{paymentId}` e `GET /v1/payments/{paymentId}` devolvem o estado atual. Use antes de entregar algo caro, e como rede de segurança quando o webhook não chegou. ```bash theme={null} curl https://api.purincash.com/v1/charges/psc_a1b2c3d4e5f6 \ -H "Authorization: Bearer $PURINCASH_KEY" ``` Se precisar mesmo, use intervalo de 5 segundos ou mais e pare no primeiro status final. O [limite de 120 requisições por minuto](/guias/limites) vale para tudo somado. Antes de liberar o produto, confira `amountCents` além do `status`. Um pagamento pago com valor menor que o esperado não deveria virar entrega. ## Dados do pagador No webhook `charge.paid` chegam campos que não existem na criação, porque vêm do banco: | Campo | Conteúdo | | ------------ | ---------------------------------------------- | | `payer` | Nome de quem pagou | | `bank` | Banco de origem, no formato `código - nome` | | `endToEndId` | Identificador end-to-end da transação no Bacen | | `txId` | txid da transação | Servem para conciliação bancária e para responder [disputa](/guias/disputas). CPF, telefone e endereço não são enviados em webhook. Marketplace, comissão ou sociedade, resolvido no momento do pagamento. Chave ou conta liberada assim que o PIX confirma. # Sua primeira cobrança Source: https://docs.purincash.com/guias/primeira-cobranca Do zero ao PIX pago em sandbox, sem mover dinheiro de verdade. Este guia vai até o fim: chave, cobrança, pagamento simulado e webhook recebido. Tudo em sandbox, então nada aqui movimenta dinheiro. Entre no painel, abra [API e Desenvolvedores](https://purincash.com/dashboard/api) e gere uma chave de sandbox. Ela começa com `ps_test_`. A chave inteira aparece uma única vez, na hora da criação. Se perder, revogue e gere outra. Cada conta pode manter até 10 chaves ativas. Chave de API é credencial de servidor. Ela nunca deve aparecer em JavaScript de navegador, app mobile, repositório público ou print de tela. O endpoint mais direto é `POST /v1/charges`: valor livre, sem precisar cadastrar produto antes. ```bash cURL theme={null} curl -X POST https://api.purincash.com/v1/charges \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "valueCents": 1990, "description": "Plano Pro", "callbackUrl": "https://minhaloja.com/webhooks/purincash", "customer": { "name": "João Silva", "email": "joao@exemplo.com" }, "metadata": "{\"pedido\":\"1042\"}" }' ``` ```js Node.js theme={null} const res = await fetch("https://api.purincash.com/v1/charges", { method: "POST", headers: { Authorization: `Bearer ${process.env.PURINCASH_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ valueCents: 1990, description: "Plano Pro", callbackUrl: "https://minhaloja.com/webhooks/purincash", customer: { name: "João Silva", email: "joao@exemplo.com" }, metadata: JSON.stringify({ pedido: "1042" }), }), }); const cobranca = await res.json(); console.log(cobranca.paymentId, cobranca.pix.brCode); ``` ```python Python theme={null} import os, json, requests resposta = requests.post( "https://api.purincash.com/v1/charges", headers={"Authorization": f"Bearer {os.environ['PURINCASH_KEY']}"}, json={ "valueCents": 1990, "description": "Plano Pro", "callbackUrl": "https://minhaloja.com/webhooks/purincash", "customer": {"name": "João Silva", "email": "joao@exemplo.com"}, "metadata": json.dumps({"pedido": "1042"}), }, timeout=15, ) cobranca = resposta.json() print(cobranca["paymentId"], cobranca["pix"]["brCode"]) ``` ```php PHP theme={null} $ch = curl_init("https://api.purincash.com/v1/charges"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("PURINCASH_KEY"), "Content-Type: application/json", ], CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ "valueCents" => 1990, "description" => "Plano Pro", "callbackUrl" => "https://minhaloja.com/webhooks/purincash", "customer" => ["name" => "João Silva", "email" => "joao@exemplo.com"], "metadata" => json_encode(["pedido" => "1042"]), ]), ]); $cobranca = json_decode(curl_exec($ch), true); echo $cobranca["paymentId"], PHP_EOL, $cobranca["pix"]["brCode"]; ``` A resposta vem assim: ```json Resposta 201 theme={null} { "paymentId": "psc_a1b2c3d4e5f6", "status": "pending", "amountCents": 1990, "currency": "BRL", "environment": "sandbox", "pix": { "brCode": "00020126580014br.gov.bcb.pix0136...", "qrCodeImage": "https://qr.exemplo.com/psc_a1b2c3d4e5f6.png" }, "expiresAt": "2026-03-18T12:30:00.000Z" } ``` Guarde o `paymentId` junto do seu pedido. É por ele que você vai reconciliar tudo depois. Quer amarrar a cobrança ao seu banco de dados sem tabela auxiliar? Use `metadata`. Ele volta intacto no webhook e na consulta, então dá pra guardar ali o id do pedido. São dois caminhos, e você normalmente oferece os dois na mesma tela: | Campo | O que fazer com ele | | ----------------- | ---------------------------------------------------------------- | | `pix.brCode` | Renderize como texto com um botão de copiar. É o copia e cola. | | `pix.qrCodeImage` | URL da imagem PNG do QR. Trate como opaca, o domínio pode mudar. | Se preferir gerar o QR você mesmo, use qualquer biblioteca de QR Code passando o `brCode` como conteúdo. O resultado é o mesmo. Cobrança PIX expira em 30 minutos (`expiresAt`). Depois disso o código para de funcionar e o status vira `expired`. Se o cliente sumiu e voltou, crie outra. Você está em sandbox, então esse `brCode` é falso: vem como `SANDBOX_PIX_...` e nenhum banco reconhece. Serve pra você montar a tela; quem confirma o pagamento é o passo seguinte. Em sandbox ninguém vai pagar de verdade, então você marca como pago na mão: ```bash theme={null} curl -X POST https://api.purincash.com/v1/sandbox/charges/psc_a1b2c3d4e5f6/simulate-paid \ -H "Authorization: Bearer $PURINCASH_KEY" ``` Isso muda o status para `paid` e dispara o webhook na sua `callbackUrl`, igualzinho ao que acontece em produção. Não passa por aprovação de ninguém: é essa chamada e pronto. Para pagamentos criados em `POST /v1/payments` o endpoint é `/v1/sandbox/payments/{paymentId}/simulate-paid`. A diferença é só o recurso. A gente faz `POST` na sua URL com o corpo do evento e o header `X-Webhook-Signature`: ```json theme={null} { "event": "charge.paid", "paymentId": "psc_a1b2c3d4e5f6", "amountCents": 1990, "status": "paid", "paidAt": "2026-03-18T12:05:00.000Z", "customer": { "name": "João Silva", "email": "joao@exemplo.com" }, "metadata": "{\"pedido\":\"1042\"}", "sandbox": true } ``` Antes de liberar qualquer coisa pro cliente, [valide a assinatura](/webhooks/assinatura). A sua URL é pública, então qualquer pessoa consegue mandar um JSON dizendo `"status": "paid"`. Sem servidor exposto ainda? Use um túnel local (`ngrok http 3000`, `cloudflared tunnel`) ou um coletor tipo webhook.site só pra ver o payload chegando. Webhook é notificação, não fonte da verdade. Antes de entregar o produto, confirme: ```bash theme={null} curl https://api.purincash.com/v1/charges/psc_a1b2c3d4e5f6 \ -H "Authorization: Bearer $PURINCASH_KEY" ``` Se o `status` vier `paid` e o `amountCents` bater com o que você cobrou, pode liberar. ## Passando pra produção Troque `ps_test_` por `ps_live_`. Só isso. Nenhuma URL muda, nenhum parâmetro muda. Antes de virar a chave, dá uma passada em [Antes de ir pra produção](/guias/producao). Cadastre o preço uma vez e cobre por `productId`. Chave, conta ou link liberado no instante do pagamento. # Antes de ir pra produção Source: https://docs.purincash.com/guias/producao A lista curta do que costuma quebrar no primeiro dia com dinheiro de verdade. Você testou em sandbox e funcionou. Estes são os pontos que sandbox não pega. ## Segurança Procure por `ps_live_` no build final, não só no código. Um `console.log` esquecido ou uma variável sem prefixo de servidor já entrega a chave. Se ela estiver exposta, alguém pode criar cobrança em seu nome e, com [saque turbo](/guias/saques#saque-turbo), mandar dinheiro pra fora. A sua `callbackUrl` é pública. Sem [validar o `X-Webhook-Signature`](/webhooks/assinatura), qualquer pessoa manda um JSON com `"status": "paid"` e leva o produto de graça. Confira também que você está assinando o **corpo cru**, antes do parse. Um `express.json()` no lugar errado quebra a validação de um jeito que passa em teste e falha em produção. Não basta o `status` ser `paid`. Compare o `amountCents` recebido com o valor que você esperava para aquele pedido antes de liberar qualquer coisa. Variável de ambiente ou cofre. Se vazou, regenere no painel: o antigo para de funcionar na hora. ## Confiabilidade Webhook é reentregue quando a entrega falha, e a mesma confirmação pode chegar mais de uma vez. Deduplique pelo header `X-Webhook-Id`, que é estável entre as tentativas. O teste é simples: mande o mesmo payload duas vezes e veja se o cliente recebe o produto duas vezes. O timeout é de 5 segundos. Se o seu handler manda e-mail, gera licença e escreve em três tabelas antes de responder, ele vai estourar e você vai receber reentrega de coisa que já processou. Grave o evento, responda `200`, processe em fila. Webhook é notificação, não fonte da verdade. Tenha um job que varre pedidos parados em `pending` há mais de uma hora e confirma o estado por `GET`. Em [saques](/guias/saques), `202` e `status: "processing"` significam que o dinheiro pode já ter saído. Reenviar paga duas vezes. ## Configuração da conta Sem verificação, o primeiro saque devolve `403`. Resolva isso antes de precisar do dinheiro, não depois. Os webhooks de [pedido](/webhooks/pedidos) e de [saque](/webhooks/saques) usam a URL configurada na conta, não a `callbackUrl` da cobrança. O painel permite subir o mínimo da loja acima de R\$ 0,80. Se o seu produto mais barato fica abaixo dele, a cobrança falha em produção e passava em sandbox. Sem carteira, `paymentMethod: "ltc"` devolve `503`. ## Operação Código HTTP, corpo do erro e `paymentId`. Com esses três, uma dúvida [no Discord](https://discord.gg/8eyQQFZZxY) vira uma resposta em vez de cinco. Rode `GET /v1/disputes?status=aberta` diariamente. Contestação tem prazo. Dá pra revogar uma sem derrubar o resto, e dá pra saber quem gastou o limite. Com jitter. Sem ele, seus workers batem no `429` todos juntos. ## O teste final Faça uma venda real de valor baixo, do início ao fim: cobrança, pagamento, webhook, entrega e saque. É o único teste que cobre configuração de conta, e leva dez minutos. Chama no Discord com o código HTTP, o corpo do erro e o `paymentId`. Costuma sair na primeira resposta. # Produtos Source: https://docs.purincash.com/guias/produtos Existem dois tipos de produto com o mesmo nome. Aqui está qual é qual. Esta é a parte da API que mais gera ticket de suporte, então vale começar pelo ponto: `GET /v1/products` pode devolver uma lista vazia com a sua loja cheia de produtos. Não é bug. São duas coleções diferentes. Criado **pela API**, com `POST /v1/products`. Tem `priceCents` (número inteiro). É o único aceito em `POST /v1/payments` e `POST /v1/subscriptions`. Vive em `/v1/products`. Criado **no painel**, na sua loja do Discord. Tem `price` (texto tipo `"49,90"`), categoria, variações e estoque. Não serve para gerar cobrança pela API. Vive em `/v1/store/products`, somente leitura. Os dois não vêm juntos por padrão porque a forma é diferente. Código fazendo `priceCents / 100` viraria `NaN` ao receber um produto de loja, e quem repassasse um id de loja para criar cobrança levaria `404` sem entender o motivo. ## Pedindo os dois de uma vez | Chamada | Devolve | | -------------------------------- | ----------------------------------------- | | `GET /v1/products` | Só os de cobrança. É o padrão e não mudou | | `GET /v1/products?include=all` | Os dois, com um campo `type` em cada item | | `GET /v1/products?include=store` | Só os da loja | | `GET /v1/store/products` | Só os da loja | ```json GET /v1/products?include=all theme={null} { "products": [ { "type": "api", "_id": "60f7...", "name": "Plano Premium", "priceCents": 4990 }, { "type": "store", "id": "70a2...", "name": "Conta Full", "price": "249,90", "variations": [] } ] } ``` Todas aceitam `?includeInactive=true` para incluir os desativados. Se `GET /v1/products` vier vazio e você tiver produtos na loja, a resposta traz um campo `hint` apontando a rota certa. Ele é aditivo, então quem só lê `products` não sente diferença. ## Produtos de cobrança ```bash Criar theme={null} curl -X POST https://api.purincash.com/v1/products \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Plano Premium", "description": "Acesso completo por 30 dias", "priceCents": 4990, "metadata": "{\"sku\":\"premium-01\"}" }' ``` Até 200 caracteres. Preço em centavos. Mínimo de 100, ou seja R\$ 1,00. Até 500 caracteres. Produto em outra moeda é convertido para real na hora de gerar a cobrança. String JSON de até 2 KB, devolvida sem alteração. O `PUT` aceita os mesmos campos, todos opcionais, mais `active` para ativar ou desativar. Mande apenas o que quer mudar. Limite de 500 produtos por conta e por ambiente. Repare que o mínimo de um produto (R\$ 1,00) é diferente do mínimo de uma cobrança avulsa (R\$ 0,80, ajustável pela loja). Não é inconsistência: produto é catálogo, cobrança é transação. ## Produtos da loja Leitura apenas. Para criar ou editar, use o painel. ```bash theme={null} curl "https://api.purincash.com/v1/store/products?includeInactive=true" \ -H "Authorization: Bearer $PURINCASH_KEY" ``` ```json theme={null} { "products": [ { "type": "store", "id": "70a2b1c3d4e5f6a7b8c9d0e1", "publicId": "plano-premium", "name": "Plano Premium", "description": "Acesso completo por 30 dias", "price": "49,90", "category": "Assinaturas", "active": true, "deliveryType": "automatica", "variations": [ { "id": "a1b2...", "name": "Mensal", "price": "49,90", "stock": 42, "unlimited": false }, { "id": "c3d4...", "name": "Anual", "price": "499,00", "stock": null, "unlimited": true } ] } ] } ``` ### Estoque ilimitado vem como `null` Variação com fornecedor externo, estoque via API ou estoque fictício infinito não tem um número para contar. Nesses casos `stock` vem `null` e `unlimited` vem `true`. O valor **não** é `0`, justamente porque zero você leria como esgotado e esconderia um produto que está à venda. Na prática, a checagem correta é: ```js theme={null} const disponivel = variacao.unlimited || variacao.stock > 0; ``` ### O que não sai nessa rota O conteúdo do estoque (a chave, a conta ou o link que o comprador recebe) e as instruções de entrega não são retornados. Variações de seller ainda não aprovadas também ficam de fora, porque não são vendáveis. Para ver o que foi entregue em um pagamento específico, existe a rota de [entrega](/guias/entrega). # Saques Source: https://docs.purincash.com/guias/saques Tirar o saldo por PIX, LTC ou USDT sem sair do código. Inclui o turbo, que é irreversível. Um endpoint só, `POST /v1/payouts`, com três destinos possíveis. O que muda é o `method`. Cai como pendente e passa por aprovação. Com `turbo`, sai na hora. Debita o saldo em Litecoin e envia após aprovação. Converte BRL e entrega USDT na rede BEP20 em segundos. **Limite de 10 saques por hora, por chave de API.** O turbo tem um teto próprio de 5 por hora, que corre por dentro desse. ## Saque PIX ```bash theme={null} curl -X POST https://api.purincash.com/v1/payouts \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "method": "pix", "amount": 100.00, "walletAddress": "loja@exemplo.com" }' ``` ```json Resposta 201 theme={null} { "id": "SAQ-API-A1B2C3D4", "code": "SAQ-API-A1B2C3D4", "method": "pix", "amount": 100.00, "status": "pending", "sandbox": false } ``` `pix`, `ltc` ou `usd`. Valor em reais. Mínimo de R\$ 5,00, máximo de R\$ 999.999,99, sempre limitado ao `withdrawable` da [carteira](/guias/carteira). Chave PIX, endereço LTC ou endereço USDT BEP20, conforme o `method`. Quantidade em LTC. Obrigatório quando `method` é `ltc`. Só com `method: "pix"`. Envia o PIX na hora, sem fila de aprovação. A chave PIX precisa estar verificada no painel antes do primeiro saque. Sem isso a resposta é `403`. ## Saque LTC ```bash theme={null} curl -X POST https://api.purincash.com/v1/payouts \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "method": "ltc", "amount": 100.00, "cryptoAmount": 0.5, "walletAddress": "ltc1q..." }' ``` O `cryptoAmount` é obrigatório aqui e precisa ser maior que zero. O saldo debitado é o `cryptoBalanceLtc` da carteira, não o saldo em reais. ## Saque turbo Com `"turbo": true`, o PIX é enviado dentro da própria requisição. É o mesmo caminho do botão de saque turbo do painel. ```bash theme={null} curl -X POST https://api.purincash.com/v1/payouts \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "method": "pix", "amount": 100.00, "walletAddress": "loja@exemplo.com", "turbo": true }' ``` ```json Resposta 201 theme={null} { "id": "TURBO-A1B2C3D4", "code": "TURBO-A1B2C3D4", "method": "pix_turbo", "amount": 100.00, "fee": 1.00, "netAmount": 99.00, "status": "completed", "txId": "...", "recipientName": "NOME DO RECEBEDOR", "turbo": true, "sandbox": false } ``` **O turbo é irreversível.** O dinheiro sai no mesmo request e não existe cancelamento depois. Quem tem a sua chave de API consegue mandar dinheiro pra fora sem passar por nenhuma aprovação. Guarde a chave como você guardaria a senha do painel. Regras do turbo: | Regra | Detalhe | | ------------ | --------------------------------------------------------------------------------------- | | Método | Só `pix`. Com `ltc` ou `usd` devolve `400` | | Valor | R\$ 5,00 a R\$ 5.000,00 por saque | | Limite | 5 por hora, dentro do limite geral de 10 | | Taxa | Cobrada da loja (padrão R\$ 1,00) e descontada do envio. Veja `netAmount` | | Concorrência | Um por vez. Com outro em processamento, devolve `409` | | Recusa | Turbo recusado **não** vira saque pendente. O request falha e você repete sem o `turbo` | Sem o campo `turbo`, nada muda: o saque continua caindo como `pending` para aprovação. ## Saque em USDT Converte o saldo em reais e envia USDT na rede BEP20 na hora. ```bash theme={null} curl -X POST https://api.purincash.com/v1/payouts \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "method": "usd", "amount": 100.00, "walletAddress": "0xAbC1230000000000000000000000000000000000", "confirmNotCoinbase": true }' ``` ```json Resposta 201 theme={null} { "success": true, "withdrawal": { "id": "665f1a2b3c4d5e6f7a8b9c0d", "code": "SAQ-U-1A2B3C4D5E6F7A8B", "amountBRL": 100.00, "receiveUSDT": 17.42, "rateBRLPerUSDT": 5.74, "status": "processando", "estimatedTime": "menos de 30 segundos" } } ``` `confirmNotCoinbase: true` é obrigatório. Depósito da Coinbase não aceita USDT BEP20, e o valor enviado para lá é perdido, sem recuperação. A confirmação existe para você parar e conferir a rede do endereço antes de mandar. Por segurança contra fraude, o saque em USDT só é liberado depois que a loja fizer o primeiro saque em PIX. Antes disso a resposta é `403`. ## Status Aguardando aprovação. Pago. Negado. O valor volta para o saldo. O pagamento foi enviado e a confirmação ainda não voltou. **O dinheiro já saiu.** Não repita o request. ## Erros e o que fazer | Código | Situação | O que fazer | | ------ | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `400` | Saldo insuficiente, valor fora da faixa, endereço inválido, `confirmNotCoinbase` ausente | Corrija e reenvie | | `403` | Chave PIX não verificada, ou USDT antes do primeiro saque PIX | Resolva no painel | | `409` | Já existe um saque turbo em processamento | Espere e tente de novo | | `429` | Passou de 10 por hora, ou 5 no turbo | Espere a janela virar | | `502` | O banco recusou. O saldo é devolvido (`"refunded": true`) | Pode reenviar | | `503` | Saque em cripto indisponível no momento | Tente mais tarde | | `202` | Sem resposta do banco no turbo | **Não repita.** O PIX pode ter saído e um admin vai revisar | `202` e `processing` são os dois casos em que reenviar o request paga duas vezes. Trate ambos como sucesso provisório e reconcilie por `GET /v1/payouts`. ## Listando ```bash theme={null} curl "https://api.purincash.com/v1/payouts?limit=20&status=pendente" \ -H "Authorization: Bearer $PURINCASH_KEY" ``` ```json theme={null} { "payouts": [ { "id": "SAQ-API-A1B2C3D4", "code": "SAQ-API-A1B2C3D4", "method": "pix", "amount": 100.00, "cryptoAmount": null, "walletAddress": "12345***", "status": "pendente", "createdAt": "2026-04-27T12:00:00.000Z" } ] } ``` `walletAddress` volta sempre mascarado, com os primeiros caracteres e `***`. Dado sensível (CPF, chave PIX completa) não aparece em resposta nem em log. Cada mudança de status também dispara um [webhook de saque](/webhooks/saques). # Dividir o valor (split) Source: https://docs.purincash.com/guias/splits Marketplace, comissão e sociedade resolvidos no instante em que o dinheiro entra. Split é uma cobrança PIX comum com uma lista de beneficiários. Quando o cliente paga, o valor já cai partido na carteira de cada conta, com evento financeiro auditável. Você não precisa receber tudo e repassar depois. Funciona em `POST /v1/charges` com o campo `splits`, ou em `POST /v1/split-charges`, que é o mesmo endpoint com outro nome. ## A regra que confunde todo mundo **Você não entra na lista.** O `splits` leva só os *outros* beneficiários. Como dono da chave de API, você é participante implícito e fica com o que sobrar: `100 - soma das percentages`. Isso tem duas consequências práticas: 1. A soma das `percentage` precisa ser **menor que 100**, nunca igual. 2. A sua fatia precisa ser **estritamente a maior** de todas. Se sobrar para você menos do que para algum beneficiário, a criação é rejeitada com `400`. ## Criando ```bash theme={null} curl -X POST https://api.purincash.com/v1/charges \ -H "Authorization: Bearer $PURINCASH_KEY" \ -H "Content-Type: application/json" \ -d '{ "amountCents": 10000, "description": "Venda compartilhada", "callbackUrl": "https://minhaloja.com/webhooks/purincash", "splits": [ { "recipientEmail": "socio@exemplo.com", "percentage": 30 } ], "customer": { "name": "Cliente", "email": "cliente@exemplo.com" } }' ``` ```json Resposta 201 theme={null} { "paymentId": "psplit_a1b2c3d4", "status": "pending", "amountCents": 10000, "currency": "BRL", "environment": "live", "pix": { "brCode": "00020126360014BR.GOV.BCB.PIX...", "qrCodeImage": null }, "splits": [ { "recipientEmail": "vo***@exemplo.com", "percentage": 70, "isOwner": true }, { "recipientEmail": "so***@exemplo.com", "percentage": 30, "isOwner": false } ], "expiresAt": "2026-05-28T12:30:00.000Z" } ``` Repare que a resposta devolve **três** coisas que você não mandou: a sua própria fatia (`isOwner: true`), os e-mails mascarados e o prefixo `psplit_`. E-mail de beneficiário vem sempre mascarado, em toda resposta e todo webhook. Se você precisa exibir o parceiro na sua interface, guarde o e-mail do seu lado no momento em que criou a cobrança. ## Validações | Regra | Limite | | --------------------------- | ----------------------------------------------------------- | | Quantidade de beneficiários | 1 a 9, além de você (10 no total) | | `percentage` de cada um | 0.01 a 99.99 | | Soma das `percentage` | Menor que 100.00 | | Sua fatia | Estritamente a maior | | `recipientEmail` | Conta PurinCash existente, única na lista, diferente da sua | | `amountCents` | 80 a 500000 (R\$ 0,80 a R\$ 5.000,00) | Qualquer uma quebrada devolve `400` na criação. Nada é criado pela metade. O teto de R\$ 5.000 vale só para cobrança com split. Cobrança PIX comum não tem esse limite. ## Quem paga a taxa A taxa do gateway sai **inteira da sua parte**. Os outros beneficiários recebem a porcentagem cheia sobre o valor bruto. R\$ 100,00, ou seja, 10000 centavos. O sócio com 30% leva R\$ 30,00. Supondo 2% + R\$ 0,50, a taxa é R\$ 2,50. 10000 − 3000 − 250 = 6750 centavos, ou R\$ 67,50. Se a taxa for maior que a sua fatia, você recebe 0. Nunca fica negativo, mas fica em zero. Vale conferir isso quando a sua margem é apertada e o valor da cobrança é baixo. ## Arredondamento Cada beneficiário recebe `floor(bruto × percentage)`. O resto é seu, o que garante que a soma creditada mais a taxa feche exatamente com o bruto, sem centavo sumido. Um caso com dízima, para deixar concreto. Cobrança de R\$ 100,00, taxa de R\$ 2,50, dois beneficiários com 33.33% cada, você com 33.34%: | Quem | Conta | Recebe | | -------------- | --------------------------- | ------------- | | Beneficiário A | `floor(10000 × 0.3333)` | 3333 centavos | | Beneficiário B | `floor(10000 × 0.3333)` | 3333 centavos | | Você | `10000 − 3333 − 3333 − 250` | 3084 centavos | 3333 + 3333 + 3084 + 250 = 10000. Fecha. ## Consultando o resultado `GET /v1/charges/{paymentId}` com um ID `psplit_` traz o detalhamento de cada fatia, com o valor em centavos que foi creditado e quando: ```json theme={null} { "paymentId": "psplit_a1b2c3d4", "status": "paid", "amountCents": 10000, "gatewayFeeCents": 250, "netAmountCents": 9750, "splits": [ { "recipientEmail": "vo***@exemplo.com", "percentage": 70, "isOwner": true, "amountCents": 6750, "creditedAt": "2026-05-28T12:15:00.000Z" }, { "recipientEmail": "so***@exemplo.com", "percentage": 30, "isOwner": false, "amountCents": 3000, "creditedAt": "2026-05-28T12:15:00.000Z" } ], "paidAt": "2026-05-28T12:15:00.000Z" } ``` Use `amountCents` de cada split, e não `percentage × total` recalculado por você. O arredondamento já foi decidido no crédito, e refazer a conta gera divergência de um centavo no seu relatório. ## Limitação conhecida `metadata` não é aceito em cobrança com splits. Se você precisa amarrar a cobrança a um pedido interno, guarde o `paymentId` do seu lado na hora da criação. # Documentação PurinCash Source: https://docs.purincash.com/index Receba por PIX, cartão, cripto e assinatura com uma chamada HTTP. Sandbox liberado, sem contrato e sem SDK obrigatório. A PurinCash é um gateway de pagamento. Você cria uma cobrança pela API, o cliente paga, e a gente te avisa por webhook. É basicamente isso. A API é REST, fala JSON, autentica com um header e não tem SDK obrigatório: se o seu stack faz `POST`, ele integra. Do zero ao PIX pago, em sandbox, sem gastar um centavo. Os 29 endpoints, com playground pra testar na hora. Como a gente te avisa, e como você confere que fomos nós. A lista curta do que costuma quebrar no primeiro dia. ## O caminho mais curto No painel, em [API e Desenvolvedores](https://purincash.com/dashboard/api), gere uma chave `ps_test_`. Ela é mostrada uma vez só. ```bash theme={null} curl -X POST https://api.purincash.com/v1/charges \ -H "Authorization: Bearer ps_test_sua_chave" \ -H "Content-Type: application/json" \ -d '{ "valueCents": 1990, "description": "Plano Pro" }' ``` A resposta traz `pix.brCode`, que é o copia e cola, e `pix.qrCodeImage`, que é a imagem do QR. Com chave `ps_test_` esse código é falso de propósito (vem como `SANDBOX_PIX_...`). Ninguém consegue pagar, e é isso que você quer enquanto testa. Como ninguém vai pagar um PIX de teste, quem confirma é você. Não tem fila nem aprovação de ninguém: ```bash theme={null} curl -X POST https://api.purincash.com/v1/sandbox/charges/psc_a1b2c3d4/simulate-paid \ -H "Authorization: Bearer ps_test_sua_chave" ``` A cobrança vira `paid` e a gente faz `POST` na sua `callbackUrl` com o evento, igual ao que acontece em produção. Em produção esse passo acontece sozinho quando o PIX cai de verdade. ## O que dá pra cobrar Copia e cola ou QR, confirmação em segundos. É o caminho padrão. Checkout hospedado. Você redireciona e recebe o resultado. PIX recorrente, semanal a anual, cobrança gerada sozinha. Endereço LTC com o valor convertido na cotação do momento. Divide o valor entre até 10 contas no momento em que o dinheiro entra. Tira o saldo por PIX, LTC ou USDT sem sair do código. ## Dois detalhes que economizam uma tarde `ps_live_` opera em produção e `ps_test_` em sandbox. Não existe campo de ambiente no body nem na URL. Os dados são isolados: chave de teste nunca enxerga cobrança real, e o contrário também vale. Detalhes em [Ambientes](/guias/ambientes). `GET /v1/products` lista os produtos de cobrança criados pela API. `GET /v1/store/products` lista os produtos da sua loja do Discord. São coleções separadas, então a primeira pode devolver lista vazia com a loja cheia. O [guia de produtos](/guias/produtos) explica quando usar cada uma. Integrando com ajuda de IA? Esta documentação inteira existe em texto puro em [docs.purincash.com/llms-full.txt](https://docs.purincash.com/llms-full.txt), gerado a partir destas páginas. Cole no seu assistente e ele responde sobre a API sem inventar endpoint. O suporte fica no Discord, com gente que mexe na API. Traga o código HTTP, o corpo do erro e o `paymentId`. # Validando a assinatura Source: https://docs.purincash.com/webhooks/assinatura Sem isso, qualquer pessoa na internet consegue dizer que pagou. Todo webhook chega assinado com HMAC-SHA256 no header `X-Webhook-Signature`. Validar essa assinatura é o que separa "recebi uma notificação" de "recebi uma notificação nossa". A sua `callbackUrl` é pública. Sem validação, um `curl` com `{"status":"paid"}` é suficiente para alguém levar o seu produto sem pagar. ## Onde fica o secret Cada loja tem o seu, em [Dashboard → API e Desenvolvedores → Webhooks](https://purincash.com/dashboard/api). Guarde numa variável de ambiente do seu backend. Se vazar, regenere pelo painel. O antigo para de funcionar na hora. ## Como calcular O HMAC é sobre o **corpo cru** da requisição, a string exata que chegou, antes de qualquer `JSON.parse`. Se você parsear e re-serializar, a ordem das chaves ou o espaçamento mudam e a assinatura nunca vai bater. Compare com `timingSafeEqual` ou equivalente. Comparação com `===` vaza informação pelo tempo de execução. ```js Node.js (Express) theme={null} import crypto from "node:crypto"; import express from "express"; const app = express(); // express.raw, não express.json: precisamos dos bytes originais. app.post("/webhooks/purincash", express.raw({ type: "application/json" }), (req, res) => { const assinatura = req.header("X-Webhook-Signature") || ""; const esperado = crypto .createHmac("sha256", process.env.PURINCASH_WEBHOOK_SECRET) .update(req.body) .digest("hex"); const valida = assinatura.length === esperado.length && crypto.timingSafeEqual(Buffer.from(assinatura), Buffer.from(esperado)); if (!valida) return res.status(401).send("assinatura inválida"); const evento = JSON.parse(req.body.toString()); const id = req.header("X-Webhook-Id"); // Grave, responda, processe depois. enfileirar({ id, evento }); res.json({ ok: true }); }); ``` ```php PHP theme={null} true]); ``` ```python Python (Flask) theme={null} import hmac, hashlib, os, json from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ["PURINCASH_WEBHOOK_SECRET"].encode() @app.post("/webhooks/purincash") def webhook(): corpo = request.get_data() # bytes crus assinatura = request.headers.get("X-Webhook-Signature", "") esperado = hmac.new(SECRET, corpo, hashlib.sha256).hexdigest() if not hmac.compare_digest(esperado, assinatura): abort(401) evento = json.loads(corpo) enfileirar(request.headers.get("X-Webhook-Id"), evento) return {"ok": True} ``` ```go Go theme={null} package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "os" ) func webhook(w http.ResponseWriter, r *http.Request) { corpo, _ := io.ReadAll(r.Body) mac := hmac.New(sha256.New, []byte(os.Getenv("PURINCASH_WEBHOOK_SECRET"))) mac.Write(corpo) esperado := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(r.Header.Get("X-Webhook-Signature")), []byte(esperado)) { http.Error(w, "assinatura inválida", http.StatusUnauthorized) return } var evento map[string]any json.Unmarshal(corpo, &evento) enfileirar(r.Header.Get("X-Webhook-Id"), evento) w.WriteHeader(http.StatusOK) } ``` ## Idempotência O header `X-Webhook-Id` vem no formato `evento:id`, como `payment.paid:psa_abc123`. Reentrega do mesmo evento usa o **mesmo** valor. ```js theme={null} async function processar(webhookId, evento) { // Chave única no banco: a segunda inserção falha e você sai fora. const novo = await db.webhooksProcessados.insertIfAbsent(webhookId); if (!novo) return; // já tratamos, nada a fazer await entregarProduto(evento); } ``` Deduplique **antes** de creditar saldo, enviar licença ou disparar e-mail. Depois de entregar não tem como voltar atrás, e a reentrega não é hipótese remota: é o comportamento normal quando o seu servidor demora mais de 5 segundos para responder. ## Erros comuns Quase sempre é middleware de JSON rodando antes do handler e consumindo o corpo. Em Express, `express.json()` global quebra a validação. Registre a rota do webhook com `express.raw()` antes do parser global. Proxy ou CDN reescrevendo o corpo. Verifique se não há compressão, reformatação de JSON ou WAF alterando a requisição no caminho. Você está comparando a assinatura contra um corpo re-serializado. Guarde os bytes originais em vez de reconstruir o JSON. # card_payment.paid Source: https://docs.purincash.com/webhooks/cartao Mesma ideia do PIX, dois campos com nome diferente. É onde o handler genérico quebra. Quando um pagamento no cartão é confirmado, a gente faz `POST` na `callbackUrl` daquela cobrança. ```json theme={null} POST https://minhaloja.com/webhooks/purincash Content-Type: application/json X-Webhook-Signature: a3f5b8c2e1d4f7a9b6c8d2e5f1a4b7c9d2e5f8a1b4c7d0e3f6a9b2c5d8e1f4a7 X-Webhook-Id: card_payment.paid:JUE7Y9MPSX { "event": "card_payment.paid", "orderCode": "JUE7Y9MPSX", "amount": 49.90, "status": "paid", "paidAt": "2026-03-18T12:05:00.000Z", "customer": { "name": "João Silva", "email": "joao@exemplo.com" }, "metadata": "{\"pedido\":\"1042\"}" } ``` ## As duas diferenças que importam O identificador é **`orderCode`**, não `paymentId`. O valor vem em **`amount`** (reais, decimal), não em `amountCents`. Se o seu handler é genérico, normalize na entrada em vez de espalhar `if` pelo código: ```js theme={null} function normalizar(evento) { if (evento.event === "card_payment.paid") { return { id: evento.orderCode, valorCents: Math.round(evento.amount * 100), pagoEm: evento.paidAt, customer: evento.customer, metadata: evento.metadata, }; } // payment.paid e charge.paid return { id: evento.paymentId, valorCents: evento.amountCents, pagoEm: evento.paidAt, customer: evento.customer, metadata: evento.metadata, }; } ``` `Math.round(amount * 100)` e não `amount * 100`. Ponto flutuante transforma `49.90 * 100` em `4989.999999999999`, e o `!==` contra o valor esperado passa a falhar de forma aleatória. ## Campos Sempre `card_payment.paid`. Código do pedido no cartão. É o mesmo usado em `GET /v1/card-payments/{orderCode}`. Valor em reais, decimal. Sempre `paid` neste evento. Data e hora da confirmação, em ISO 8601. `name` e `email`, quando informados na criação. A string JSON enviada na criação. Vem `null` quando não houve. ## Garantias As mesmas do PIX: assinatura HMAC em `X-Webhook-Signature`, idempotência em `X-Webhook-Id`, timeout de 5 segundos e até 7 tentativas com backoff. Detalhes em [Visão geral](/webhooks/visao-geral). Cartão não tem webhook em sandbox, porque cartão não roda em sandbox. Para testar o handler, faça uma cobrança real de valor baixo em produção. ## Lembrete sobre a successUrl O cliente cair na sua `successUrl` não é confirmação de pagamento. É só o navegador voltando, e a URL pode ser aberta na mão por qualquer um. Libere o produto por este webhook ou por `GET /v1/card-payments/{orderCode}`. # payment.paid e charge.paid Source: https://docs.purincash.com/webhooks/pagamentos O evento que confirma que o PIX caiu, com os dados que só o banco tem. Quando o PIX é confirmado, a gente faz `POST` na `callbackUrl` informada na criação. O nome do evento depende do recurso: | Evento | Origem | | -------------- | ------------------------------- | | `payment.paid` | `POST /v1/payments`, IDs `psa_` | | `charge.paid` | `POST /v1/charges`, IDs `psc_` | O corpo é praticamente o mesmo. `charge.paid` traz alguns campos a mais, que vêm do banco. ```json theme={null} POST https://minhaloja.com/webhooks/purincash Content-Type: application/json X-Webhook-Signature: a3f5b8c2e1d4f7a9b6c8d2e5f1a4b7c9d2e5f8a1b4c7d0e3f6a9b2c5d8e1f4a7 X-Webhook-Id: charge.paid:psc_a1b2c3d4 { "event": "charge.paid", "paymentId": "psc_a1b2c3d4", "amountCents": 1990, "amountReais": 19.90, "status": "paid", "paidAt": "2026-03-18T12:05:00.000Z", "description": "Plano Pro", "customer": { "name": "João Silva", "email": "joao@exemplo.com", "externalId": "user_42" }, "metadata": "{\"pedido\":\"1042\"}", "payer": "JOAO DA SILVA", "bank": "077 - Banco Inter", "endToEndId": "E1818773820260318120500abc12345", "txId": "abc123def456", "deliveredContent": "LICENSE-KEY-ABC-123" } ``` ## Campos `payment.paid` ou `charge.paid`. Identificador do pagamento. É a chave para reconciliar do seu lado. Valor em centavos. **Confira contra o esperado antes de entregar.** Sempre `paid` neste evento. Data e hora da confirmação, em ISO 8601. `name`, `email` e `externalId`, como você informou na criação. A string JSON que você mandou, devolvida sem alteração. O mesmo valor em reais, decimal. Conveniência para exibição. Descrição informada na criação. O que foi entregue automaticamente, quando o produto tem entrega automática. Veja [Entrega](/guias/entrega). `true` quando o evento veio de um endpoint de simulação. ### Dados do pagador (só em `charge.paid`) Nome de quem pagou, como consta no banco. Banco de origem, no formato `código - nome`. Identificador end-to-end da transação no Bacen. Serve para conciliação bancária e para responder [disputa](/guias/disputas). txid da transação PIX. Campo opcional é omitido quando não se aplica, e não enviado como `null`. Dado sensível (CPF, telefone, endereço) nunca vai em webhook. ## Em sandbox O `simulate-paid` sempre envia `event: "payment.paid"`, inclusive ao simular uma cobrança `psc_`. O corpo é enxuto: `event`, `paymentId`, `amountCents`, `status`, `paidAt`, `customer`, `metadata` e `sandbox: true`. Se o seu handler faz `if (evento.event === "charge.paid")` para cobranças, ele não vai disparar em sandbox. Trate os dois nomes no mesmo caminho. ## Tratando ```js theme={null} app.post("/webhooks/purincash", express.raw({ type: "application/json" }), async (req, res) => { if (!assinaturaValida(req)) return res.status(401).end(); const evento = JSON.parse(req.body.toString()); const webhookId = req.header("X-Webhook-Id"); // 1. Idempotência antes de tudo. if (!(await marcarComoVisto(webhookId))) return res.json({ ok: true }); // 2. Responder rápido. O resto vai pra fila. res.json({ ok: true }); if (evento.event === "payment.paid" || evento.event === "charge.paid") { const pedido = await buscarPorPaymentId(evento.paymentId); // 3. Valor precisa bater. Status "paid" sozinho não é suficiente. if (!pedido || pedido.valorCents !== evento.amountCents) { return registrarDivergencia(evento); } await liberarAcesso(pedido, evento.deliveredContent); } }); ``` O evento é outro e a forma do corpo muda. Vale a leitura antes de escrever um handler genérico. # order.paid Source: https://docs.purincash.com/webhooks/pedidos Para quem vende pelo bot no Discord e quer sincronizar estoque ou CRM. Este evento é específico da loja no Discord. Quando um pedido feito pelo bot é pago, a gente avisa a URL configurada na conta. Diferente do PIX e do cartão, aqui não existe `callbackUrl` por cobrança. A URL é configurada uma vez em [Dashboard → API e Desenvolvedores → Webhooks](https://purincash.com/dashboard/api) e vale para todos os pedidos do bot. ```json theme={null} POST https://minhaloja.com/webhooks/purincash Content-Type: application/json X-Webhook-Signature: a3f5b8c2e1d4f7a9b6c8d2e5f1a4b7c9d2e5f8a1b4c7d0e3f6a9b2c5d8e1f4a7 X-Webhook-Id: order.paid:665f1a2b3c4d5e6f7a8b9c0d { "event": "order.paid", "orderId": "665f1a2b3c4d5e6f7a8b9c0d", "orderCode": "ORD-7K3B9X", "total": 49.90, "paidAt": "2026-03-18T12:05:00.000Z", "acquirer": "..." } ``` ## Campos Sempre `order.paid`. Identificador interno do pedido. Código curto do pedido, o mesmo que aparece para o comprador no Discord. Valor total em reais, decimal. Data e hora da confirmação, em ISO 8601. Identificador interno de qual processadora tratou o pagamento. Pode mudar sem aviso, então não construa lógica em cima dele. ## Para que serve Baixar a quantidade no seu ERP ou planilha assim que a venda confirma, sem depender de consulta periódica. Registrar o cliente e a compra no seu funil no momento certo. Lançar a receita com data e valor exatos, em vez de exportar relatório no fim do mês. Avisar a equipe no canal certo quando entra venda acima de um valor. Este evento avisa que o pedido foi pago. Ele não substitui o webhook de [pagamento](/webhooks/pagamentos) nas cobranças criadas pela API: são fluxos separados, com origens diferentes. As garantias são as mesmas: assinatura HMAC, `X-Webhook-Id` para deduplicar, timeout de 5 segundos e reentrega com backoff. # withdrawal.* Source: https://docs.purincash.com/webhooks/saques Solicitado, pago ou negado. Os três momentos em que o seu saldo muda de lado. Cada mudança de estado de um saque dispara um evento na URL configurada na conta, em [Dashboard → API e Desenvolvedores → Webhooks](https://purincash.com/dashboard/api). | Evento | Quando | | ---------------------- | --------------------------------------------- | | `withdrawal.requested` | O saque foi solicitado e entrou na fila | | `withdrawal.completed` | O saque foi pago | | `withdrawal.denied` | O saque foi negado e o valor voltou pro saldo | Em sandbox os nomes mudam para `withdrawal.test.requested` e `withdrawal.test.completed`. ```json Solicitado theme={null} { "event": "withdrawal.requested", "withdrawalId": "665f1a2b3c4d5e6f7a8b9c0d", "code": "SAQ-A1B2C3", "amount": 150.00, "method": "pix", "walletAddress": "chave-pix-ou-carteira", "status": "pendente", "requestedAt": "2026-03-18T14:00:00.000Z", "sandbox": false } ``` ```json Pago theme={null} { "event": "withdrawal.completed", "withdrawalId": "665f1a2b3c4d5e6f7a8b9c0d", "code": "SAQ-A1B2C3", "amount": 150.00, "method": "pix", "walletAddress": "chave-pix-ou-carteira", "status": "concluido", "processedAt": "2026-03-18T15:30:00.000Z" } ``` ```json Negado theme={null} { "event": "withdrawal.denied", "withdrawalId": "665f1a2b3c4d5e6f7a8b9c0d", "code": "SAQ-A1B2C3", "amount": 150.00, "method": "pix", "walletAddress": "chave-pix-ou-carteira", "status": "negado", "processedAt": "2026-03-18T15:30:00.000Z" } ``` ## Campos `withdrawal.requested`, `withdrawal.completed` ou `withdrawal.denied`. Identificador do saque. Código legível do saque, como `SAQ-A1B2C3`. É por ele que o suporte localiza a operação. Valor em reais, decimal. `pix`, `pix_turbo`, `ltc` ou `usd`. Destino do saque. Chave PIX, endereço LTC ou endereço USDT, conforme o método. `pendente`, `concluido` ou `negado`. Presente em `withdrawal.requested`. Presente em `withdrawal.completed` e `withdrawal.denied`. Repare que o `status` aqui vem em português (`pendente`, `concluido`, `negado`), enquanto as cobranças usam inglês (`pending`, `paid`). São domínios diferentes da API e cada um manteve o vocabulário da sua área. ## Um uso que compensa Saque negado devolve o valor pro saldo, mas ninguém fica olhando o painel esperando isso. Um alerta no `withdrawal.denied` costuma ser a diferença entre resolver no mesmo dia e descobrir na semana seguinte: ```js theme={null} if (evento.event === "withdrawal.denied") { await avisarFinanceiro({ titulo: `Saque ${evento.code} negado`, valor: evento.amount, metodo: evento.method, em: evento.processedAt, }); } ``` Este webhook não cobre todos os estados. O `processing` do [saque turbo](/guias/saques#saque-turbo), que significa "o dinheiro saiu e o banco ainda não confirmou", é visto por `GET /v1/payouts`. Não trate ausência de `withdrawal.completed` como saque não realizado. As garantias são as mesmas dos outros: assinatura HMAC em `X-Webhook-Signature`, idempotência em `X-Webhook-Id` e reentrega com backoff. # Como funcionam os webhooks Source: https://docs.purincash.com/webhooks/visao-geral Quem avisa quem, o que a sua URL precisa ter e o que acontece quando ela cai. Quando algo acontece com o seu dinheiro, a gente faz um `POST` na sua URL com o evento em JSON. É assim que você sabe que o PIX caiu sem ficar consultando a API. ## Duas configurações diferentes Você manda no corpo da requisição ao criar. Vale só para aquela cobrança. Usado por [pagamento PIX](/webhooks/pagamentos), [cartão](/webhooks/cartao), assinatura e split. Configurada uma vez em [API e Desenvolvedores](https://purincash.com/dashboard/api). Vale para tudo. Usado por [pedido da loja](/webhooks/pedidos) e [saque](/webhooks/saques). ## O que a sua URL precisa ter | Requisito | Detalhe | | --------------- | -------------------------------------------------------------------------------- | | HTTPS | Obrigatório. `http://` é recusado na criação | | Domínio público | Só nome de domínio. IP numérico, IPv6, `localhost` e rede privada são bloqueados | | Resposta rápida | `2xx` em até **5 segundos**. Fora disso conta como falha | | Tamanho | Até 500 caracteres | URL inválida devolve `400` na criação da cobrança, com `callbackUrl must be a valid, public HTTPS URL`. O erro vem na hora, não na entrega. Em desenvolvimento, use um túnel (`ngrok http 3000`, `cloudflared tunnel`) para expor o seu localhost com HTTPS público. Passar `http://localhost:3000` não funciona por design. ## Headers | Header | Conteúdo | | --------------------- | ------------------------------------------------------------------------------------ | | `Content-Type` | Sempre `application/json` | | `X-Webhook-Signature` | HMAC-SHA256 em hex do corpo cru. Veja [Validando a assinatura](/webhooks/assinatura) | | `X-Webhook-Id` | Chave de idempotência no formato `evento:id`, por exemplo `payment.paid:psa_abc123` | `X-Webhook-Signature` vem em **todo** webhook, sem opt-in. Se você não valida, a sua URL aceita qualquer POST da internet dizendo que um pagamento foi aprovado. ## Reentrega Se a entrega falhar (timeout, erro de rede ou status fora de `2xx`), o evento entra na fila e é reenviado. | Tentativa | Quando | | --------- | ----------------- | | 1 | Na hora do evento | | 2 | 1 minuto depois | | 3 | 5 minutos | | 4 | 30 minutos | | 5 | 2 horas | | 6 | 6 horas | | 7 | 12 horas | São até **7 tentativas**, cobrindo pouco mais de 20 horas. O corpo reenviado é idêntico ao original e o `X-Webhook-Id` não muda. É exatamente por causa da reentrega que o seu handler precisa ser idempotente. Deduplique pelo `X-Webhook-Id` antes de fazer qualquer coisa que não dá pra desfazer, como creditar saldo ou enviar licença. ## Eventos | Evento | Dispara quando | Configurado em | | ---------------------- | ------------------------------ | -------------- | | `payment.paid` | Pagamento `psa_` confirmado | `callbackUrl` | | `charge.paid` | Cobrança `psc_` confirmada | `callbackUrl` | | `card_payment.paid` | Pagamento no cartão confirmado | `callbackUrl` | | `order.paid` | Pedido da loja do Discord pago | Painel | | `withdrawal.requested` | Saque solicitado | Painel | | `withdrawal.completed` | Saque pago | Painel | | `withdrawal.denied` | Saque negado | Painel | Em sandbox, os eventos de saque saem como `withdrawal.test.requested` e `withdrawal.test.completed`. ## O jeito certo de tratar Antes de qualquer parse de JSON. A assinatura é calculada sobre os bytes exatos que chegaram. Com comparação timing-safe. Se não bater, responda `401` e pare por aí. Já processou esse id? Responda `200` e não faça nada. Salve o evento e responda imediatamente. O processamento pesado vai para uma fila. `amountCents` precisa bater com o que você esperava. Status `paid` sozinho não basta. Não conte com a ordem de chegada. Se a ordem importa para você, confirme o estado atual com um `GET` na API antes de decidir. Código pronto em Node, PHP, Python e Go.