> ## Documentation Index
> Fetch the complete documentation index at: https://docs.purin.cash/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. Se
você não mandou nenhuma, o evento vai pras [URLs da conta](https://docs.purin.cash/webhooks/visao-geral.md). O nome do
evento depende do recurso:

| Evento | Origem |
| - | - |
| `payment.paid` | `POST /v1/payments`, IDs `psa_` |
| `charge.paid` | `POST /v1/charges`, IDs `psc_` |
| `charge.paid` | `POST /v1/split-charges`, IDs `psplit_`. [Veja abaixo](#cobrança-com-split) |
| `payment.paid` | `POST /v1/payments` com `paymentMethod: "ltc"` ou `"btc"`, quando a transação confirma na blockchain |

O corpo é o mesmo nos dois eventos. No pagamento em cripto não há pagador, CPF nem banco:
o `txId` é o hash da transação na blockchain, e `amountReais` é o valor em reais do que
chegou de fato.

```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",
  "cpf": "12345678909",
  "bank": "077 - Banco Inter",
  "endToEndId": "E1818773820260318120500abc12345",
  "txId": "abc123def456",
  "deliveredContent": "LICENSE-KEY-ABC-123"
}
```

## Campos

**event** `string` *required*

`payment.paid` ou `charge.paid`.

**paymentId** `string` *required*

Identificador do pagamento. É a chave para reconciliar do seu lado.

**amountCents** `number` *required*

Valor em centavos. **Confira contra o esperado antes de entregar.**

**status** `string` *required*

Sempre `paid` neste evento.

**paidAt** `string` *required*

Data e hora da confirmação, em ISO 8601.

**customer** `object`

`name`, `email` e `externalId`, como você informou na criação.

**metadata** `string`

A string JSON que você mandou, devolvida sem alteração.

**amountReais** `number`

O mesmo valor em reais, decimal. Conveniência para exibição.

**description** `string`

Descrição informada na criação.

**deliveredContent** `string`

O que foi entregue automaticamente, quando o produto tem entrega automática. Veja
[Entrega](https://docs.purin.cash/guias/entrega.md).

**subaccountId** `string`

Presente quando a cobrança foi criada com [subconta](https://docs.purin.cash/guias/subcontas.md). O ledger dela
já foi creditado quando este evento chega.

**sandbox** `boolean`

`true` quando o evento veio de um endpoint de simulação.

### Dados do pagador

Vêm do banco, não de você. Chegam nos dois eventos, e só quando o banco de origem informa:
PIX pago por certas instituições não traz o documento, e alguns não trazem nem o nome.

**payer** `string`

Nome de quem pagou, como consta no banco.

**cpf** `string`

CPF ou CNPJ de quem pagou, só com os dígitos: `12345678909`. Sem pontuação, para bater
direto com o que você guarda do seu lado. Use para conciliar com o extrato bancário e
para responder [disputa](https://docs.purin.cash/guias/disputas.md).

Alguns bancos de origem mandam o documento censurado. Nesse caso ele vai como veio,
com os asteriscos na posição em que o banco os pôs (`***456789**`), e `cpfMasked`
chega junto. Nunca chega um pedaço do número passando por número inteiro.

**cpfMasked** `boolean`

`true` quando o `cpf` veio censurado do banco de origem. Ausente quando o número chegou
completo.

**bank** `string`

Banco de origem, no formato `código - nome`.

**endToEndId** `string`

Identificador end-to-end da transação no Bacen. Serve para conciliação bancária e para
responder [disputa](https://docs.purin.cash/guias/disputas.md).

**txId** `string`

txid da transação PIX. Em pagamento cripto (LTC/BTC), o hash da transação na blockchain.

> [!NOTE]
>
> Campo opcional é omitido quando não se aplica, e não enviado como `null`.

> [!WARNING]
>
> `cpf` é dado pessoal de quem pagou, e chega inteiro sempre que o banco de origem o
> informa por inteiro. Receba em HTTPS, guarde só o tempo que a sua operação precisa e
> não exponha em log de aplicação nem em tela de terceiro. A
> chave PIX, o telefone e o endereço do pagador **não** saem em webhook nenhum.

## Cobrança com split

Cobrança criada em `POST /v1/split-charges` avisa pelo **mesmo evento**, `charge.paid`. É o
mesmo fato, um PIX foi pago, e o handler que você já tem continua valendo. O que muda vem
no corpo: o `paymentId` começa com `psplit_` e chegam três campos a mais.

```json theme={null}
{
  "event": "charge.paid",
  "paymentId": "psplit_9f3c1a7b2e5d8046",
  "amountCents": 10000,
  "amountReais": 100.00,
  "status": "paid",
  "paidAt": "2026-03-18T12:05:00.000Z",
  "customer": { "name": "João Silva", "email": "joao@exemplo.com" },
  "payer": "JOAO DA SILVA",
  "cpf": "12345678909",
  "bank": "077 - Banco Inter",
  "endToEndId": "E1818773820260318120500abc12345",
  "gatewayFeeCents": 199,
  "netAmountCents": 9801,
  "splits": [
    { "recipientEmail": "pa***@exemplo.com", "percentage": 30, "amountCents": 3000, "isOwner": false },
    { "recipientEmail": "vo***@minhaloja.com", "percentage": 70, "amountCents": 6801, "isOwner": true }
  ]
}
```

**gatewayFeeCents** `number`

A taxa retida, em centavos. Sai da **sua** fatia, não da dos beneficiários. A conta
inteira está em [Splits](https://docs.purin.cash/guias/splits.md).

**netAmountCents** `number`

`amountCents` menos `gatewayFeeCents`. É a soma do que foi creditado em `splits[]`.

**splits** `array`

A repartição de fato, já creditada. Cada item traz `recipientEmail` (mascarado),
`percentage`, `amountCents` e `isOwner`. Você entra na lista com `isOwner: true` e fica
com o resto: a soma dos beneficiários é sempre menor que 100%.

> [!WARNING]
>
> Use o `amountCents` de cada item, não `percentage × amountCents` recalculado por você. O
> arredondamento já foi decidido no crédito, e refazer a conta gera divergência de centavo
> no seu relatório, especialmente na sua própria fatia, de onde a taxa saiu.

> [!NOTE]
>
> Se o seu handler precisa distinguir os dois casos, use o prefixo do `paymentId`
> (`psplit_`) ou a presença de `splits`. O campo `event` é o mesmo de propósito: é o mesmo
> fato do domínio, e o handler que você já tem continua valendo.
>
> Split não aceita `metadata`, então esse campo não aparece neste corpo.

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

## 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] [Pagou com cartão?](https://docs.purin.cash/webhooks/cartao.md)
>
> O evento é outro e a forma do corpo muda. Vale a leitura antes de escrever um handler
> genérico.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.