
# Webhooks

Em vez de consultar a nota até ela sair da fila, cadastre uma URL e receba o resultado.

```json POST /v1/webhooks
{ "url": "https://seu-sistema.com.br/webhooks/emitza", "events": ["nfe.authorized", "nfe.rejected", "nfse.authorized", "nfse.rejected"] }
```

Sem `events`, você recebe todos. A resposta traz o `secret` (`whsec_...`) **uma única vez**: guarde para conferir a assinatura. Até 5 webhooks por conta. `POST /v1/webhooks/{id}/test` envia um `webhook.test` para você conferir a integração.

## O que chega

Um `POST` com JSON:

```json
{
  "id": "6710c3d4e5f6a7b8c9d0e1f2",
  "event": "nfe.authorized",
  "createdAt": "2026-10-15T14:03:11.204Z",
  "data": {
    "id": "6710c3d4e5f6a7b8c9d0e1aa",
    "externalId": "pedido-1234",
    "companyId": "6710a1b2c3d4e5f601234567",
    "environment": "homologation",
    "series": 1,
    "number": 42,
    "accessKey": "35261011222333000181550010000000421482917367",
    "protocol": "135260000000042",
    "authorizedAt": "2026-10-15T14:03:10.000Z"
  }
}
```

Cabeçalhos: `X-Fiscal-Event` (o evento), `X-Fiscal-Delivery` (o `id` da entrega) e `X-Fiscal-Signature` (a assinatura).

| Evento | Quando |
|---|---|
| `nfe.authorized`, `nfse.authorized` | Nota autorizada |
| `nfe.rejected`, `nfse.rejected` | Rejeitada, com o motivo |
| `nfe.denied` | Uso denegado |
| `nfe.failed`, `nfse.failed` | Parou sem resposta do órgão |
| `nfe.cancelled`, `nfse.cancelled` | Cancelamento registrado |
| `nfe.correction_registered` | Carta de correção registrada |
| `nfe.event_rejected`, `nfse.event_rejected` | Cancelamento, carta ou inutilização recusados |
| `nfe.disabled` | Faixa de números inutilizada |
| `certificate.expiring`, `certificate.expired` | Certificado vencendo (30, 15 e 7 dias antes) ou vencido |
| `webhook.test` | Teste manual |

O formato completo de cada evento está na [referência](../../referencia/).

## Responda rápido e trate repetições

- Responda com qualquer status 2xx em até 10 segundos. Processe depois, numa fila sua.
- Sem 2xx, a emitza tenta de novo depois de 1 min, 5 min, 15 min, 1 h, 3 h, 6 h e 12 h (cerca de 22 horas no total).
- A mesma entrega pode chegar mais de uma vez. Use o `id` do corpo para ignorar a repetição.
- A ordem de chegada não é garantida. Na dúvida, consulte `GET /v1/nfe/{id}`.

## Confira a assinatura

`X-Fiscal-Signature` tem o formato `t=<segundos unix>,v1=<hex>`, onde `v1` é o HMAC-SHA256 de `"<t>.<corpo>"` com o seu `secret`. Use o corpo **exatamente como chegou**, antes de qualquer conversão de JSON, e recuse timestamps com mais de 5 minutos de diferença.

Node.js:

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function assinaturaValida(secret, corpoBruto, cabecalho) {
  const partes = Object.fromEntries(String(cabecalho).split(',').map((p) => p.split('=', 2)));
  const t = Number(partes.t);
  if (!Number.isInteger(t) || !partes.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const esperado = createHmac('sha256', secret).update(`${t}.${corpoBruto}`).digest('hex');
  return esperado.length === partes.v1.length && timingSafeEqual(Buffer.from(esperado), Buffer.from(partes.v1));
}
```

PHP:

```php
function assinaturaValida(string $secret, string $corpoBruto, string $cabecalho): bool {
    parse_str(str_replace(',', '&', $cabecalho), $partes);
    $t = (int) ($partes['t'] ?? 0);
    if (!isset($partes['v1']) || abs(time() - $t) > 300) return false;
    $esperado = hash_hmac('sha256', $t . '.' . $corpoBruto, $secret);
    return hash_equals($esperado, $partes['v1']);
}
```

No PHP, leia o corpo com `file_get_contents('php://input')`.
