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

# Configurar webhooks

> Receba notificações automáticas quando eventos acontecem no seu projeto.

Webhooks permitem que sua aplicação seja notificada automaticamente quando eventos ocorrem — como um pagamento confirmado ou uma transação expirada. Em vez de fazer polling, a Bob Payments avisa você.

## Configurando um webhook

<Steps>
  <Step title="Acesse o Dashboard">
    Vá em **Webhooks → Adicionar webhook** no seu projeto.
  </Step>

  <Step title="Informe a URL">
    Insira a URL HTTPS da sua aplicação que receberá os eventos (ex: `https://suaapi.com/webhooks/bob`).
  </Step>

  <Step title="Copie o secret">
    Um secret é gerado automaticamente. Guarde-o para verificar a autenticidade das requisições.
  </Step>
</Steps>

<Note>
  Webhooks funcionam normalmente no sandbox. Use-os para testar o fluxo completo antes de ir a produção.
</Note>

## Versões do payload (v1 e v2)

O payload de webhook é **versionado**. A versão é fixada por canal (webhooks do dashboard) ou por sessão (checkout):

|            | **v2** (recomendada)                                            | **v1** (legado, congelado) |
| ---------- | --------------------------------------------------------------- | -------------------------- |
| Valor      | `data.amountCents` em **centavos** — consistente com toda a API | `data.amount` em **reais** |
| Envelope   | inclui `apiVersion: "v2"`                                       | sem marcador               |
| Assinatura | `t=<unix>,v1=<hmac>` com **proteção anti-replay**               | HMAC-SHA256 hex simples    |

* **Canais novos** criados no dashboard nascem em **v2**.
* **Canais existentes** permanecem em v1 até você migrar — quando seu consumidor estiver pronto, atualize o canal com `webhookApiVersion: "v2"` (via dashboard ou `PATCH /api/v1/notification-channels/:id`).
* O shape v1 é **congelado**: nunca muda. Toda evolução de payload acontece na v2.

Exemplo de `transaction.paid` na **v2** (repare em `apiVersion` e `amountCents`):

```json theme={"system"}
{
  "event": "transaction.paid",
  "apiVersion": "v2",
  "type": "transaction",
  "title": "Transação Paga",
  "message": "Transação de R$ 150,00 confirmada",
  "data": {
    "transactionId": "clx1abc123",
    "externalId": "pedido-001",
    "amountCents": 15000,
    "product": "Plano Premium",
    "customerName": "João Silva",
    "isSandbox": false,
    "createdAt": "2026-03-10T14:30:00.000Z"
  },
  "timestamp": "2026-03-10T14:35:00.000Z"
}
```

## Eventos disponíveis

<Note>
  Os exemplos abaixo mostram o formato **v1** (legado). Na **v2**, a única diferença de dados é que `data.amount` (reais) é substituído por `data.amountCents` (centavos) e o envelope ganha `apiVersion: "v2"` — os demais campos são idênticos.
</Note>

<AccordionGroup>
  <Accordion title="transaction.created" icon="plus">
    Disparado quando uma cobrança PIX é criada. Inclui `pixCode` e `expirationDate`.

    ```json theme={"system"}
    {
      "event": "transaction.created",
      "type": "transaction",
      "title": "Transação Criada",
      "message": "Nova transação de R$ 150,00 aguardando pagamento",
      "data": {
        "transactionId": "clx1abc123",
        "externalId": "pedido-001",
        "amount": 150.00,
        "product": "Plano Premium",
        "customerName": "João Silva",
        "customer": {
          "name": "João Silva",
          "email": "joao@email.com",
          "phone": "11999990000",
          "document": "12345678901",
          "documentType": "CPF",
          "address": {
            "street": "Rua das Flores",
            "streetNumber": "123",
            "neighborhood": "Centro",
            "complement": "",
            "zipCode": "01310100",
            "city": "São Paulo",
            "state": "SP",
            "country": "BR"
          }
        },
        "originDomain": "meusite.com.br",
        "isSandbox": false,
        "pixCode": "00020126580014br.gov.bcb.pix...",
        "expirationDate": "2026-03-10T15:30:00.000Z",
        "createdAt": "2026-03-10T14:30:00.000Z"
      },
      "timestamp": "2026-03-10T14:30:00.000Z"
    }
    ```
  </Accordion>

  <Accordion title="transaction.paid" icon="circle-check">
    Disparado quando um pagamento PIX é confirmado. Use para liberar o produto ou serviço ao cliente. Inclui `fee` quando há taxa.

    ```json theme={"system"}
    {
      "event": "transaction.paid",
      "type": "transaction",
      "title": "Transação Paga",
      "message": "Transação de R$ 150,00 confirmada",
      "data": {
        "transactionId": "clx1abc123",
        "externalId": "pedido-001",
        "amount": 150.00,
        "product": "Plano Premium",
        "customerName": "João Silva",
        "customer": {
          "name": "João Silva",
          "email": "joao@email.com",
          "phone": "11999990000",
          "document": "12345678901",
          "documentType": "CPF",
          "address": {
            "street": "Rua das Flores",
            "streetNumber": "123",
            "neighborhood": "Centro",
            "complement": "",
            "zipCode": "01310100",
            "city": "São Paulo",
            "state": "SP",
            "country": "BR"
          }
        },
        "originDomain": "meusite.com.br",
        "isSandbox": false,
        "fee": { "amount": 3.50 },
        "createdAt": "2026-03-10T14:30:00.000Z"
      },
      "timestamp": "2026-03-10T14:35:00.000Z"
    }
    ```
  </Accordion>

  <Accordion title="transaction.expired" icon="clock">
    Disparado quando uma transação expira sem pagamento. Use para notificar o cliente ou criar nova cobrança.

    ```json theme={"system"}
    {
      "event": "transaction.expired",
      "type": "transaction",
      "title": "Transação Expirada",
      "message": "Transação de R$ 150,00 expirou",
      "data": {
        "transactionId": "clx1abc123",
        "externalId": "pedido-001",
        "amount": 150.00,
        "product": "Plano Premium",
        "customerName": "João Silva",
        "customer": {
          "name": "João Silva",
          "email": "joao@email.com",
          "phone": "11999990000",
          "document": "12345678901",
          "documentType": "CPF",
          "address": { "...": "..." }
        },
        "originDomain": "meusite.com.br",
        "isSandbox": true,
        "createdAt": "2026-03-10T14:30:00.000Z"
      },
      "timestamp": "2026-03-11T03:00:00.000Z"
    }
    ```
  </Accordion>

  <Accordion title="transaction.refunded" icon="rotate-left">
    Disparado quando uma transação é estornada.

    ```json theme={"system"}
    {
      "event": "transaction.refunded",
      "type": "transaction",
      "title": "Transação Reembolsada",
      "message": "Transação de R$ 150,00 reembolsada",
      "data": {
        "transactionId": "clx1abc123",
        "externalId": "pedido-001",
        "amount": 150.00,
        "product": "Plano Premium",
        "customerName": "João Silva",
        "customer": { "...": "igual ao paid" },
        "originDomain": "meusite.com.br",
        "isSandbox": false,
        "createdAt": "2026-03-10T14:30:00.000Z"
      },
      "timestamp": "2026-03-10T15:00:00.000Z"
    }
    ```
  </Accordion>

  <Accordion title="transaction.cancelled" icon="circle-xmark">
    Disparado quando uma transação é cancelada manualmente.

    ```json theme={"system"}
    {
      "event": "transaction.cancelled",
      "type": "transaction",
      "title": "Transação Cancelada",
      "message": "Transação de R$ 150,00 cancelada",
      "data": {
        "transactionId": "clx1abc123",
        "externalId": "pedido-001",
        "amount": 150.00,
        "product": "Plano Premium",
        "customerName": "João Silva",
        "customer": { "...": "igual ao paid" },
        "originDomain": "meusite.com.br",
        "isSandbox": false,
        "createdAt": "2026-03-10T14:30:00.000Z"
      },
      "timestamp": "2026-03-10T14:40:00.000Z"
    }
    ```
  </Accordion>

  <Accordion title="transaction.failed" icon="triangle-exclamation">
    Disparado quando uma transação falha no processamento.

    ```json theme={"system"}
    {
      "event": "transaction.failed",
      "type": "transaction",
      "title": "transaction_failed",
      "message": "transaction_failed",
      "data": {
        "transactionId": "clx1abc123",
        "externalId": "pedido-001",
        "amount": 150.00,
        "product": "Plano Premium",
        "customerName": "João Silva",
        "customer": { "...": "igual ao paid" },
        "originDomain": "meusite.com.br",
        "isSandbox": false,
        "createdAt": "2026-03-10T14:30:00.000Z"
      },
      "timestamp": "2026-03-10T14:33:00.000Z"
    }
    ```
  </Accordion>
</AccordionGroup>

## Verificando a assinatura

Cada requisição inclui dois headers de segurança:

| Header                | Descrição                                                        |
| --------------------- | ---------------------------------------------------------------- |
| `X-Webhook-Signature` | Assinatura HMAC-SHA256 (formato depende da versão — veja abaixo) |
| `X-Webhook-Timestamp` | ISO 8601 do momento do envio (informativo)                       |

Verifique a assinatura sempre para garantir que a requisição veio da Bob Payments.

<Warning>
  Use sempre o **corpo bruto (raw body)** da requisição para calcular o HMAC. Se o framework parsear o JSON e você re-serializar, a assinatura pode não conferir.
</Warning>

### Com o SDK Node.js (recomendado)

O SDK detecta automaticamente o formato (v1 ou v2), compara em tempo constante e aplica a proteção anti-replay:

```typescript theme={"system"}
import { constructWebhookEvent } from '@bobpayments/sdk';

app.post('/webhooks/bob', express.raw({ type: 'application/json' }), (req, res) => {
  const event = constructWebhookEvent(
    req.body, // Buffer bruto
    req.headers['x-webhook-signature'],
    process.env.BOB_WEBHOOK_SECRET,
  );

  processEvent(event).catch(console.error); // assíncrono
  res.status(200).send('ok');               // 200 imediato
});
```

### Assinatura v2 (anti-replay)

Na v2, o header tem o formato `t=<unix>,v1=<hex>`. O HMAC cobre `timestamp + "." + corpo`, então um webhook capturado não pode ser reenviado depois — rejeite eventos com `t` fora de uma janela de tolerância (recomendado: 5 minutos).

<CodeGroup>
  ```javascript JavaScript theme={"system"}
  const crypto = require('crypto');

  function verifyV2(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
    const match = signatureHeader?.match(/^t=(\d+),v1=([0-9a-f]{64})$/);
    if (!match) return false;
    const [, t, digest] = match;

    const expected = crypto
      .createHmac('sha256', secret)
      .update(`${t}.${rawBody}`)
      .digest('hex');

    const valid = crypto.timingSafeEqual(
      Buffer.from(digest, 'hex'),
      Buffer.from(expected, 'hex'),
    );
    const fresh = Math.abs(Date.now() / 1000 - Number(t)) <= toleranceSeconds;
    return valid && fresh;
  }
  ```

  ```python Python theme={"system"}
  import hmac, hashlib, re, time

  def verify_v2(raw_body: bytes, signature: str, secret: str, tolerance: int = 300) -> bool:
      match = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", signature or "")
      if not match:
          return False
      t, digest = match.groups()
      expected = hmac.new(
          secret.encode(),
          f"{t}.".encode() + raw_body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(digest, expected) and abs(time.time() - int(t)) <= tolerance
  ```
</CodeGroup>

### Assinatura v1 (legado)

Na v1, o header é o HMAC-SHA256 hex do corpo, sem timestamp:

<CodeGroup>
  ```javascript JavaScript theme={"system"}
  const crypto = require('crypto');

  // use express.raw() para receber o corpo bruto (Buffer)
  function verifyV1(rawBody, signature, secret) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(rawBody)
      .digest('hex');

    const sigBuffer = Buffer.from(signature ?? '', 'utf8');
    const expectedBuffer = Buffer.from(expected, 'utf8');
    return (
      sigBuffer.length === expectedBuffer.length &&
      crypto.timingSafeEqual(sigBuffer, expectedBuffer)
    );
  }
  ```

  ```python Python theme={"system"}
  import hmac
  import hashlib

  def verify_v1(raw_body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(signature or "", expected)
  ```
</CodeGroup>

<Tip>
  Retorne HTTP 200 imediatamente e processe o evento de forma assíncrona para evitar timeouts. Entregas podem se repetir em retry — processe de forma **idempotente**.
</Tip>

## Próximos passos

<CardGroup cols={2}>
  <Card title="Idempotência" icon="rotate" href="/pages/idempotency">
    Trate entregas repetidas sem liberar o pedido duas vezes.
  </Card>

  <Card title="Testar no sandbox" icon="flask" href="/pages/sandbox">
    Dispare `transaction.paid` e `transaction.expired` sem dinheiro real.
  </Card>
</CardGroup>
