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

# payment.paid e charge.paid

> 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

<ResponseField name="event" type="string" required>
  `payment.paid` ou `charge.paid`.
</ResponseField>

<ResponseField name="paymentId" type="string" required>
  Identificador do pagamento. É a chave para reconciliar do seu lado.
</ResponseField>

<ResponseField name="amountCents" type="number" required>
  Valor em centavos. **Confira contra o esperado antes de entregar.**
</ResponseField>

<ResponseField name="status" type="string" required>
  Sempre `paid` neste evento.
</ResponseField>

<ResponseField name="paidAt" type="string" required>
  Data e hora da confirmação, em ISO 8601.
</ResponseField>

<ResponseField name="customer" type="object">
  `name`, `email` e `externalId`, como você informou na criação.
</ResponseField>

<ResponseField name="metadata" type="string">
  A string JSON que você mandou, devolvida sem alteração.
</ResponseField>

<ResponseField name="amountReais" type="number">
  O mesmo valor em reais, decimal. Conveniência para exibição.
</ResponseField>

<ResponseField name="description" type="string">
  Descrição informada na criação.
</ResponseField>

<ResponseField name="deliveredContent" type="string">
  O que foi entregue automaticamente, quando o produto tem entrega automática. Veja
  [Entrega](/guias/entrega).
</ResponseField>

<ResponseField name="sandbox" type="boolean">
  `true` quando o evento veio de um endpoint de simulação.
</ResponseField>

### Dados do pagador (só em `charge.paid`)

<ResponseField name="payer" type="string">
  Nome de quem pagou, como consta no banco.
</ResponseField>

<ResponseField name="bank" type="string">
  Banco de origem, no formato `código - nome`.
</ResponseField>

<ResponseField name="endToEndId" type="string">
  Identificador end-to-end da transação no Bacen. Serve para conciliação bancária e para
  responder [disputa](/guias/disputas).
</ResponseField>

<ResponseField name="txId" type="string">
  txid da transação PIX.
</ResponseField>

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

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

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

## 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);
  }
});
```

<Card title="Pagou com cartão?" icon="credit-card" href="/webhooks/cartao" horizontal>
  O evento é outro e a forma do corpo muda. Vale a leitura antes de escrever um handler
  genérico.
</Card>
