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