# Webhooks: eventos, assinatura e novas tentativas

> Webhooks do Praeko: cadastrar a URL, os eventos post.sent, post.failed e post.scheduled-fail, conferir a assinatura HMAC-SHA256 e entender as novas tentativas.

Fonte: https://praeko.app/docs/guides/webhooks

O Praeko avisa a sua URL quando um item sai, falha ou volta para a fila. Você confere a assinatura, responde 2xx e deduplica pelo id da entrega.

> **Antes de começar** Cadastrar webhook pede chave com escopo `admin`. A URL precisa ser `https://` com host público; localhost, IP privado e nomes internos são recusados com `unsafe_url` (a conferência roda no cadastro, na edição e a cada entrega). Máximo de 10 por workspace.

## O caminho de uma entrega

1. **Evento**: O item sai ou falha. O Praeko grava a entrega.
2. **Envio**: A cada minuto, POST na sua URL, com 10 s de limite.
3. **Seu servidor**: Confere a assinatura e responde 2xx.
4. **Resultado**: 2xx = entregue. Outro status = nova tentativa.

## Cadastrar

`POST /v1/webhooks` com `url` e, se quiser, `events` (sem `events`, o webhook recebe `post.sent` e `post.failed`). A resposta traz o segredo `whsec_…` **uma única vez** (e de novo em `POST /v1/webhooks/:id/rotate-secret`, que vale na hora). A lista mostra só a dica do segredo.

## Eventos

| Evento | Quando |
|---|---|
| `post.sent` | O item foi publicado na rede. |
| `post.scheduled-fail` | Uma tentativa falhou e o item voltou para a fila. Traz `attempts` e `nextAttemptAt`. |
| `post.failed` | O item desistiu: 3 tentativas ou falha definitiva. |
| `ping.test` | Só quando você chama `POST /v1/webhooks/:id/test`. |

Não existe evento de comentário novo: os comentários são lidos ao vivo das redes. Posts criados fora do Praeko também não geram webhook.

**Corpo de post.sent**

```json
{
  "id": "evt_…",
  "type": "post.sent",
  "createdAt": "2026-11-07T14:02:11.482Z",
  "data": {
    "post": { "id": "01M6…", "title": "Meu vídeo" },
    "item": { "id": "01M6…", "network": "youtube", "channel": "01KA…", "url": "https://youtu.be/…", "attempts": 1 }
  }
}
```

Cada entrega traz também os cabeçalhos `Praeko-Event`, `Praeko-Delivery` e `Praeko-Signature`. O campo `error`, quando existe, passa pela mesma limpeza dos logs (tokens viram `***`).

## Conferir a assinatura

`Praeko-Signature: t=<unix>,v1=<hex>` é o HMAC-SHA256 de `"<t>.<corpo cru>"` com o segredo `whsec_…`. Use o texto cru, antes do `JSON.parse`.

### 1. Ler o corpo cru

No Express, use `express.text({ type: "*/*" })` na rota.

### 2. Recalcular e comparar

Compare com `timingSafeEqual` e rejeite se `t` estiver a mais de 300 s do seu relógio.

**Node.js**

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

// corpo: o texto CRU recebido (antes de JSON.parse); cabecalho: req.headers["praeko-signature"]
export function assinaturaValida(segredo, cabecalho, corpo, toleranciaSeg = 300) {
  const p = Object.fromEntries(String(cabecalho).split(",").map((x) => x.split("=")));
  if (!p.t || !p.v1 || Math.abs(Date.now() / 1000 - Number(p.t)) > toleranciaSeg) return false;
  const esperado = createHmac("sha256", segredo).update(`${p.t}.${corpo}`).digest("hex");
  return esperado.length === p.v1.length && timingSafeEqual(Buffer.from(esperado), Buffer.from(p.v1));
}

// Express: app.post("/praeko", express.text({ type: "*/*" }), (req, res) => {
//   if (!assinaturaValida(process.env.PRAEKO_WEBHOOK_SECRET, req.get("praeko-signature"), req.body)) return res.sendStatus(401);
//   const evento = JSON.parse(req.body); /* deduplique por req.get("praeko-delivery") */ res.sendStatus(204);
// });
```

> **Responda rápido** Passou de 10 s ou veio redirecionamento (3xx), conta como falha. Faça o trabalho pesado depois de responder 204.

## Novas tentativas

| Tentativa | Espera depois da falha |
|---|---|
| 2ª | 1 min |
| 3ª | 5 min |
| 4ª | 25 min |
| 5ª | cerca de 2 h |
| 6ª | 6 h. Falhou de novo: a entrega fica `failed`. |

O cabeçalho `Praeko-Delivery` é o mesmo em todas as tentativas: guarde e ignore repetidos. Para reenviar à mão, `POST /v1/webhooks/:id/deliveries/:delivery/retry`; `GET /v1/webhooks/:id/deliveries` lista as entregas.

> **Pausa automática** Depois de 15 falhas seguidas o webhook pausa, com `disabledReason`, e as entregas pendentes são descartadas. Reativar zera a contagem (o teste manual não conta). As entregas ficam 30 dias.

Rotas: [`/v1/webhooks`](https://praeko.app/docs/api/webhooks).
