> ## Documentation Index
> Fetch the complete documentation index at: https://docs.purincash.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Como funcionam os webhooks

> 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

<Columns cols={2}>
  <Card title="callbackUrl, por cobrança" icon="receipt">
    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.
  </Card>

  <Card title="URL da conta, no painel" icon="gear">
    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).
  </Card>
</Columns>

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

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

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

<Warning>
  `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.
</Warning>

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

<Warning>
  É 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.
</Warning>

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

<Steps>
  <Step title="Leia o corpo cru">
    Antes de qualquer parse de JSON. A assinatura é calculada sobre os bytes exatos que
    chegaram.
  </Step>

  <Step title="Valide a assinatura">
    Com comparação timing-safe. Se não bater, responda `401` e pare por aí.
  </Step>

  <Step title="Deduplique pelo X-Webhook-Id">
    Já processou esse id? Responda `200` e não faça nada.
  </Step>

  <Step title="Grave e responda 200">
    Salve o evento e responda imediatamente. O processamento pesado vai para uma fila.
  </Step>

  <Step title="Confira o valor antes de entregar">
    `amountCents` precisa bater com o que você esperava. Status `paid` sozinho não basta.
  </Step>
</Steps>

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

<Card title="Validando a assinatura" icon="signature" href="/webhooks/assinatura" horizontal>
  Código pronto em Node, PHP, Python e Go.
</Card>
