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

# Webhooks

> O V4 Pay chama o seu servidor quando algo acontece. Aqui estão os eventos, os payloads e como conferir a assinatura.

## Como funciona

Você cadastra uma URL. Quando um evento acontece, o V4 Pay faz um `POST` nela com um
JSON e uma assinatura. Você confere a assinatura, processa, e responde `2xx`.

<Steps>
  <Step title="Cadastre o endpoint">
    ```bash theme={null}
    curl -X POST https://v4pay-api.vercel.app/webhook-endpoints \
      -H "Authorization: Bearer v4pay_SUA_CHAVE" \
      -H "Content-Type: application/json" \
      -d '{ "url": "https://seusite.com/webhooks/v4pay" }'
    ```

    A resposta traz um `secret`. É com ele que a assinatura é calculada — guarde.

    Dois campos opcionais: `events` (lista dos eventos que este endpoint quer receber —
    sem ela, recebe todos; o evento `test` passa sempre; `GET /webhook-endpoints/eventos`
    lista os nomes válidos) e `secret` (se você preferir gerar o seu, no formato exato
    `whsec_` + 32 hexadecimais).
  </Step>

  <Step title="Dispare um evento de teste">
    `POST /webhook-endpoints/{id}/test` manda um evento `test` para essa URL. Serve para
    conferir que o seu servidor recebe e valida antes de haver dinheiro envolvido.
  </Step>

  <Step title="Responda rápido">
    Responda `200` assim que tiver **gravado** o evento. Processe o resto depois. O V4 Pay
    espera até **8 segundos**.
  </Step>
</Steps>

## O envelope

Todo evento chega neste formato:

```json theme={null}
{
  "event": "charge.paid",
  "created_at": "2026-09-05T16:17:09.075Z",
  "data": { "…": "payload específico do evento" }
}
```

Com dois headers:

| Header              | Conteúdo                                                                   |
| ------------------- | -------------------------------------------------------------------------- |
| `X-V4Pay-Event`     | o nome do evento, igual ao campo `event`                                   |
| `X-V4Pay-Signature` | HMAC-SHA256 do **corpo bruto**, em hexadecimal, com o `secret` do endpoint |

## Eventos

<Tabs>
  <Tab title="charge.created">
    Uma cobrança acabou de ser criada e está `pending` — pelo `POST /pix`, pelo checkout de um
    link de pagamento, pelo cron de assinatura ou pelo `POST /charges`. É o evento para uma
    automação entregar o QR ao cliente (WhatsApp, e-mail) assim que ele existe. `qr_code_url` é a
    imagem em base64; `checkout_url` é a página onde o cliente paga.

    ```json theme={null}
    {
      "event": "charge.created",
      "created_at": "2026-09-06T15:40:12.000Z",
      "data": {
        "txid": "9f3a1c2b4d5e6f708192a3b4c5d6e7f8",
        "amount": 100,
        "description": "Pedido 1234",
        "status": "pending",
        "environment": "live",
        "customer_name": "Maria Silva",
        "customer_email": "maria@exemplo.com.br",
        "customer_phone": "11999999999",
        "subscription_id": null,
        "pix_code": "00020101021226…",
        "qr_code_url": "data:image/png;base64,…",
        "checkout_url": "https://pay.v4companyamaral.com/checkout/9f3a1c2b4d5e6f708192a3b4c5d6e7f8",
        "expires_at": "2026-09-06T16:40:12.000Z",
        "created_at": "2026-09-06T15:40:12.000Z"
      }
    }
    ```
  </Tab>

  <Tab title="charge.paid">
    Uma cobrança foi paga — confirmada pelo motor de pagamentos, ou por `POST /pix/simulate-payment` em modo teste.

    ```json theme={null}
    {
      "event": "charge.paid",
      "created_at": "2026-09-05T16:17:09.075Z",
      "data": {
        "txid": "9f3a1c2b4d5e6f708192a3b4c5d6e7f8",
        "amount": 100,
        "description": "Pedido 1234",
        "customer_name": "Maria Silva",
        "customer_email": "maria@exemplo.com.br",
        "paid_at": "2026-09-05T16:17:00.000Z",
        "end_to_end_id": "E18236120202609051617abcdef123456",
        "subscription_id": null,
        "simulado": true
      }
    }
    ```

    <Note>`simulado` só aparece (com `true`) quando o pagamento veio da simulação em modo teste.</Note>
  </Tab>

  <Tab title="subscription.charged">
    O cron diário gerou a cobrança de uma assinatura. A cobrança ainda está `pending`;
    quando for paga, chega um `charge.paid` com o mesmo `subscription_id`.

    ```json theme={null}
    {
      "event": "subscription.charged",
      "created_at": "2026-09-05T12:00:03.000Z",
      "data": {
        "subscription_id": "c1a2…",
        "txid": "7b8c…",
        "amount": 49.9,
        "description": "Assinatura mensal",
        "customer_name": "Maria Silva",
        "customer_email": "maria@exemplo.com.br",
        "expires_at": "2026-09-06T12:00:00.000Z"
      }
    }
    ```
  </Tab>

  <Tab title="withdrawal.completed">
    Um saque foi concluído.

    <Warning>
      Este evento **não é disparado hoje**: no modo conta única o saque pela plataforma
      está suspenso (`REPASSE_MANUAL`), e no modo subconta o dinheiro já cai na conta do
      lojista — não há saque a concluir. Está documentado porque o código existe e será
      usado se o fluxo de saque voltar. Veja [Modo de recebimento](/modo-de-recebimento).
    </Warning>

    ```json theme={null}
    {
      "event": "withdrawal.completed",
      "created_at": "…",
      "data": { "withdrawal_id": "…", "amount": 250, "pix_key": "…", "end_to_end_id": "…", "paid_at": "…" }
    }
    ```
  </Tab>

  <Tab title="withdrawal.rejected">
    Um saque foi recusado depois de 3 tentativas. Mesma ressalva do `withdrawal.completed`.

    ```json theme={null}
    {
      "event": "withdrawal.rejected",
      "created_at": "…",
      "data": { "withdrawal_id": "…", "amount": 250, "pix_key": "…", "reason": "…" }
    }
    ```
  </Tab>

  <Tab title="test">
    Disparado por `POST /webhook-endpoints/{id}/test`.

    ```json theme={null}
    {
      "event": "test",
      "created_at": "…",
      "data": { "message": "Evento de teste enviado pela V4 Pay. Seu webhook está configurado corretamente." }
    }
    ```
  </Tab>
</Tabs>

## Validar a assinatura

Calcule o HMAC-SHA256 do **corpo exatamente como chegou** (bytes brutos, antes de
qualquer parse) com o `secret` do endpoint, e compare com o header em tempo constante.

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  import crypto from 'node:crypto';
  import express from 'express';

  const app = express();

  // Precisa do corpo BRUTO: com express.json() o JSON é reserializado e a assinatura muda.
  app.post('/webhooks/v4pay', express.raw({ type: 'application/json' }), (req, res) => {
    const esperado = crypto
      .createHmac('sha256', process.env.V4PAY_WEBHOOK_SECRET)
      .update(req.body) // Buffer
      .digest('hex');
    const recebido = req.get('X-V4Pay-Signature') || '';

    const ok =
      esperado.length === recebido.length &&
      crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(recebido));
    if (!ok) return res.status(401).end();

    const evento = JSON.parse(req.body);
    // grave e responda; processe depois
    res.status(200).end();
  });
  ```

  ```python Python (Flask) theme={null}
  import hmac, hashlib, os
  from flask import Flask, request, abort

  app = Flask(__name__)

  @app.post("/webhooks/v4pay")
  def v4pay():
      corpo = request.get_data()  # bytes brutos
      esperado = hmac.new(
          os.environ["V4PAY_WEBHOOK_SECRET"].encode(), corpo, hashlib.sha256
      ).hexdigest()
      recebido = request.headers.get("X-V4Pay-Signature", "")
      if not hmac.compare_digest(esperado, recebido):
          abort(401)
      evento = request.get_json()
      # grave e responda; processe depois
      return "", 200
  ```
</CodeGroup>

## Reentrega e idempotência

<Warning>
  Hoje o V4 Pay faz **uma única tentativa** por endpoint, com timeout de 8 segundos. Se o
  seu servidor estiver fora do ar naquele instante, o evento **não é reenviado**. Trate isso
  como fato: mantenha `GET /pix/status?txid=…` como rede de segurança para reconciliar.
</Warning>

Se você tiver mais de um endpoint cadastrado, cada um recebe o evento — e o mesmo
`txid` pode chegar mais de uma vez ao longo do tempo (um `subscription.charged` seguido
de um `charge.paid`, por exemplo). Use `event` + `data.txid` como chave de idempotência
do seu lado.

## Gerenciar endpoints

| Ação                           | Rota                                |
| ------------------------------ | ----------------------------------- |
| Listar                         | `GET /webhook-endpoints`            |
| Editar URL ou ativar/desativar | `PUT /webhook-endpoints/{id}`       |
| Remover                        | `DELETE /webhook-endpoints/{id}`    |
| Testar                         | `POST /webhook-endpoints/{id}/test` |
