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

# Webhooks & Eventos de Rastreio

> Como receber notificações push em tempo real sempre que o status de uma entrega mudar.

Em vez de realizar consultas repetitivas (polling) para saber se um pedido foi entregue, configure um Webhook no BulkRoute para receber chamadas `POST` automáticas em tempo real.

## Como funciona

```
Carrier atualiza status → BulkRoute normaliza → Dispara webhook outbound → Seu sistema recebe POST
```

O webhook é disparado **assincronamente** via BullMQ com retry automático (5 tentativas com backoff exponencial).

## 1. Configurando seu Endpoint de Webhook

1. No painel, vá em **Configurações** > **Webhooks**.
2. Informe a URL do seu servidor que receberá os eventos (ex: `https://meusistema.com.br/api/webhooks/bulkroute`).
3. Copie o **Webhook Secret** gerado (usado para validar a assinatura de segurança).
4. Selecione os eventos que deseja receber (ou "Todos").
5. Salve.

### Requisitos do seu endpoint

* Deve aceitar `POST` com body JSON
* Deve retornar `200 OK` em até 10 segundos (senão o BulkRoute considera timeout e faz retry)
* Deve ser acessível publicamente (não localhost)
* Deve validar a assinatura HMAC (ver abaixo)
* Deve ser idempotente (pode receber o mesmo evento mais de uma vez em caso de retry)

## 2. Eventos disponíveis

| Evento                    | Descrição             | Quando dispara                                           |
| :------------------------ | :-------------------- | :------------------------------------------------------- |
| `shipment.created`        | Envio criado          | Quando um shipment é criado (manual ou via API)          |
| `shipment.status_updated` | Status alterado       | Sempre que o status muda (PICKED\_UP, IN\_TRANSIT, etc.) |
| `shipment.delivered`      | Entrega concluída     | Quando o status muda para DELIVERED                      |
| `shipment.exception`      | Ocorrência registrada | Quando o status muda para FAILED                         |
| `shipment.cancelled`      | Envio cancelado       | Quando um shipment é cancelado                           |
| `label.generated`         | Etiqueta gerada       | Quando uma etiqueta é gerada com sucesso                 |
| `quote.completed`         | Cotação finalizada    | Quando uma cotação retorna resultados                    |
| `bid.created`             | Bid criado            | Quando um freight bid é criado (manual ou automático)    |
| `bid.acceptance_received` | Carrier propôs preço  | Quando um carrier responde a um bid                      |
| `bid.accepted`            | Bid aceito            | Quando o admin escolhe o vencedor do bid                 |

## 3. Exemplo de Payload Recebido

### shipment.status\_updated

```json theme={null}
{
  "event": "shipment.status_updated",
  "timestamp": "2026-09-06T18:30:00Z",
  "data": {
    "shipmentId": "shp_987654321",
    "trackingCode": "BR123456789BR",
    "carrierCode": "braspress",
    "previousStatus": "IN_TRANSIT",
    "currentStatus": "DELIVERED",
    "eventDescription": "Entrega realizada com sucesso",
    "city": "São Paulo",
    "state": "SP",
    "occurredAt": "2026-09-06T18:28:45Z"
  }
}
```

### shipment.created

```json theme={null}
{
  "event": "shipment.created",
  "timestamp": "2026-09-06T15:00:00Z",
  "data": {
    "shipmentId": "shp_987654321",
    "trackingCode": "BR123456789BR",
    "carrierCode": "braspress",
    "orderId": "PED-12345",
    "recipient": {
      "name": "João Silva",
      "city": "São Paulo",
      "state": "SP"
    }
  }
}
```

### bid.acceptance\_received

```json theme={null}
{
  "event": "bid.acceptance_received",
  "timestamp": "2026-09-06T16:30:00Z",
  "data": {
    "bidId": "bid_123",
    "carrierCode": "CORREIOS",
    "proposedPrice": 45.90,
    "proposedDeadline": 3
  }
}
```

## 4. Validação de Assinatura (Segurança HMAC)

Cada requisição enviada pelo BulkRoute inclui o cabeçalho:

```http theme={null}
X-BulkRoute-Signature: sha256=<hash_calculado>
```

Para garantir que o webhook veio legitimamente do BulkRoute, valide o hash HMAC-SHA256 utilizando o seu segredo cadastrado:

### Node.js

```javascript theme={null}
import crypto from 'crypto';

function verifySignature(payloadRaw, signatureHeader, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  const digest = 'sha256=' + hmac.update(payloadRaw).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signatureHeader));
}

// Express middleware
app.post('/webhooks/bulkroute', (req, res) => {
  const signature = req.headers['x-bulkroute-signature'];
  const rawBody = req.rawBody; // use raw body, not parsed JSON

  if (!verifySignature(rawBody, signature, process.env.BULKROUTE_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(rawBody);
  // Process event...
  res.status(200).send('OK');
});
```

### Python (Flask)

```python theme={null}
import hmac
import hashlib

def verify_signature(payload_raw, signature_header, secret):
    expected = 'sha256=' + hmac.new(
        secret.encode(),
        payload_raw,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

@app.route('/webhooks/bulkroute', methods=['POST'])
def webhook():
    signature = request.headers.get('X-BulkRoute-Signature', '')
    if not verify_signature(request.get_data(), signature, os.environ['BULKROUTE_WEBHOOK_SECRET']):
        return 'Invalid signature', 401

    event = request.json
    # Process event...
    return 'OK', 200
```

<Warning>
  Use o **raw body** (bytes brutos) para calcular o HMAC — não o body parseado como JSON. Se você usar `req.body` (já parseado), o hash não vai bater porque a serialização pode mudar a ordem das chaves.
</Warning>

## 5. Retry e Dead Letter Queue

O BulkRoute tenta entregar cada webhook até **5 vezes**:

| Tentativa | Delay       |
| :-------- | :---------- |
| 1ª        | Imediata    |
| 2ª        | 30 segundos |
| 3ª        | 2 minutos   |
| 4ª        | 10 minutos  |
| 5ª        | 1 hora      |

Se todas as 5 tentativas falharem, o evento vai para a **Dead Letter Queue (DLQ)** e fica visível no painel em **Configurações** > **Webhooks** > **DLQ**.

Você pode:

* **Reprocessar** um evento da DLQ manualmente
* **Baixar** o payload para debug
* **Descartar** eventos irrelevantes

## 6. Logs de Webhook

Cada disparo de webhook é logado no painel:

| Campo       | Descrição                                      |
| :---------- | :--------------------------------------------- |
| Evento      | Tipo do evento (ex: `shipment.status_updated`) |
| URL         | Endpoint que recebeu (ou tentou receber)       |
| Status HTTP | Resposta do seu servidor (200, 500, timeout)   |
| Tentativa   | Número da tentativa (1-5)                      |
| Latência    | Tempo de resposta em ms                        |
| Payload     | Body enviado (JSON)                            |
| Resposta    | Body recebido (se houver)                      |

## 7. Testando seu Webhook

### Com ngrok (desenvolvimento local)

```bash theme={null}
# Exponha seu servidor local
ngrok http 3000

# Configure a URL do ngrok no painel do BulkRoute
# https://abc123.ngrok.io/webhooks/bulkroute
```

### Com webhook.site (teste rápido)

1. Acesse [webhook.site](https://webhook.site)
2. Copie a URL gerada
3. Configure no painel do BulkRoute
4. Crie um shipment e observe os eventos chegando em tempo real

## Troubleshooting

| Problema                           | Causa                    | Solução                                 |
| :--------------------------------- | :----------------------- | :-------------------------------------- |
| `401 Unauthorized` no seu servidor | Assinatura inválida      | Verificar Webhook Secret                |
| Timeout (10s)                      | Seu servidor demora      | Otimizar processamento ou usar fila     |
| Eventos duplicados                 | Retry após timeout       | Garantir idempotência no seu endpoint   |
| Eventos não chegam                 | URL incorreta ou offline | Verificar URL e status do servidor      |
| DLQ cheia                          | Servidor fora do ar      | Reprocessar DLQ após restaurar servidor |
