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

# Receber eventos por webhook

> O Pictor chama você quando algo acontece. Como conferir a assinatura e não processar duas vezes.

Em vez de você perguntar de minuto em minuto se algo mudou, o Pictor **chama o
seu endereço** quando o fato acontece. Você escolhe quais tipos quer receber; a
lista completa está no [catálogo de eventos](/eventos).

O caso que costuma pagar a integração sozinho: assine `device.no_frame` e o seu
sistema de chamados abre a ordem de serviço sem ninguém digitar nada.

## Antes de escrever código

O endereço que recebe os eventos é cadastrado por um **administrador da conta**
no console, em **Configurações › Webhooks**. Ao criar, o console mostra um
**segredo** — ele aparece **uma única vez** e é com ele que você confere a
assinatura de cada entrega. Se perder, gere outro.

Requisitos do endereço:

* `https://` (sem TLS o cadastro é recusado);
* alcançável pela internet;
* responde em até **10 segundos**.

## O que chega

Um `POST` com corpo JSON e cinco cabeçalhos:

| Cabeçalho            | Para quê                                                            |
| -------------------- | ------------------------------------------------------------------- |
| `Pictor-Event-Id`    | identificador do evento. **É por ele que você descarta o repetido** |
| `Pictor-Event-Type`  | o tipo, para rotear sem abrir o corpo                               |
| `Pictor-Delivery-Id` | identificador desta entrega — cite-o ao abrir um chamado            |
| `Pictor-Attempt`     | qual tentativa é esta; `1` é a primeira                             |
| `Pictor-Signature`   | o carimbo de tempo e a assinatura                                   |

O corpo:

```json theme={null}
{
  "id": "6f1c9c2e-6a5c-4f6f-9a2e-2f2b7f0a1c33",
  "type": "device.offline",
  "version": 1,
  "created_at": "2026-09-09T18:00:00+00:00",
  "tenant_id": "0b7f2b1a-4c2d-4f11-9a3b-8e1d6c5a4b20",
  "data": {
    "device_id": "3a9e5c11-2b77-4d0e-bb51-9c2a1f6d7e04",
    "external_id": "EXEMPLO-CAM-014",
    "previous_status": "active",
    "changed_at": "2026-09-09T18:00:00+00:00"
  }
}
```

`version` é do **tipo**, não do envelope. Campo novo dentro de `data` é
acrescentado sem mudar a versão — então **ignore o que não conhece** em vez de
recusar o corpo.

## Conferir a assinatura

O cabeçalho tem esta forma:

```
Pictor-Signature: t=1757433600,v1=5257a1…
```

O que é assinado é `"{t}.{corpo cru}"` — o carimbo **está dentro** do que a
assinatura cobre. Assine sobre os **bytes recebidos**, nunca sobre o JSON
reserializado: a menor diferença de espaço ou de ordem de chave produz outro
resumo.

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

TOLERANCIA_S = 300


def assinatura_confere(cabecalho: str, corpo: bytes, segredos: list[str]) -> bool:
    """`True` quando alguma assinatura casa E o carimbo está na janela."""
    carimbo = None
    recebidas = []
    for parte in cabecalho.split(","):
        chave, _, valor = parte.strip().partition("=")
        if chave == "t":
            carimbo = valor
        elif chave == "v1" and valor:
            recebidas.append(valor)

    if carimbo is None or not recebidas:
        return False
    if abs(time.time() - int(carimbo)) > TOLERANCIA_S:
        return False

    assinado = carimbo.encode("ascii") + b"." + corpo
    for segredo in segredos:
        esperada = hmac.new(segredo.encode(), assinado, hashlib.sha256).hexdigest()
        # compare_digest: comparar com == vaza, pelo tempo de resposta, quantos
        # caracteres do início bateram.
        if any(hmac.compare_digest(esperada, r) for r in recebidas):
            return True
    return False
```

<Warning>
  **Recuse o que chegar fora da janela de 300 segundos.** Sem essa checagem, uma
  entrega capturada hoje continua válida amanhã, e a assinatura deixa de proteger
  contra reenvio.
</Warning>

### Durante uma troca de segredo

Ao girar o segredo você escolhe uma **janela de graça**. Dentro dela, cada
entrega leva **duas** `v1=` no mesmo cabeçalho: uma do segredo novo, outra do
anterior. Aceite se **qualquer uma** casar — é o que permite publicar a nova
configuração do seu lado sem perder um evento. O laço acima já faz isso.

## Não processar duas vezes

<Warning>
  **A entrega é pelo menos uma vez.** Uma reentrega depois de erro, uma resposta
  sua que demorou demais, um clique em *reenviar* no console — nos três casos o
  mesmo evento chega de novo, com o **mesmo** `Pictor-Event-Id`.
</Warning>

Guarde o identificador e descarte o repetido:

```python theme={null}
def receber(cabecalhos: dict, corpo: bytes, banco) -> int:
    if not assinatura_confere(cabecalhos["Pictor-Signature"], corpo, SEGREDOS):
        return 401

    event_id = cabecalhos["Pictor-Event-Id"]
    if banco.ja_processado(event_id):
        return 200  # repetido: aceite e não faça nada de novo

    banco.marcar_processado(event_id)
    enfileirar(corpo)  # o trabalho pesado sai da requisição
    return 200
```

## O que a sua resposta significa

| Você responde                                     | O Pictor faz                                                 |
| ------------------------------------------------- | ------------------------------------------------------------ |
| `2xx`                                             | considera entregue e não tenta de novo                       |
| `4xx` (fora de `408` e `429`)                     | **desiste** — reenviar o mesmo corpo não mudaria o resultado |
| `5xx`, `408`, `429`, tempo esgotado, erro de rede | tenta de novo, com espera crescente                          |

A espera cresce a cada tentativa — 30 segundos, 2 minutos, 8 minutos, 30
minutos, 2 horas, 6 horas, 12 horas — até **8 tentativas**. Isso dá cerca de 21
horas de tolerância para o seu endereço voltar.

<Warning>
  **Um endereço que falha muito é desligado.** Depois de 20 entregas seguidas que
  terminaram em falha, o endpoint é desativado e para de receber. O motivo fica
  visível no console, e reativar zera a contagem.
</Warning>

**Responda rápido e trabalhe depois.** O tempo limite é de 10 segundos: se você
processar dentro da requisição, uma lentidão sua vira reentrega nossa, e a
reentrega chega enquanto o primeiro processamento ainda está rodando.

## Conferir sem esperar um evento real

No console, cada endpoint tem um botão de **enviar teste**. Ele dispara um
`webhook.test` só para aquele endereço, pela mesma fila e com a mesma
assinatura de qualquer outro evento — se o teste chega e confere, o caminho
inteiro está de pé.

Cada tentativa fica registrada, com o corpo enviado, o código que você devolveu
e um trecho da sua resposta. É por aí que se descobre que o `401` era relógio
fora de hora, e não segredo errado.

## Ler o catálogo pelo código

```bash theme={null}
curl -s -H "Authorization: Bearer $PICTOR_KEY" \
  "$PICTOR_API/api/webhooks/event-types"
```

A resposta traz cada tipo com `status`, `version` e a lista de campos de `data`.
Tipos com `status` igual a `planned` já podem ser assinados: o nome e o formato
estão fechados, e no dia em que o emissor entrar no ar o seu receptor recebe sem
mudar nada.
