Webhooks
Como a plataforma envia webhooks — eventos da conta, callbacks de jobs e Webhook Relay — e como validar a assinatura Standard Webhooks.
A plataforma faz um POST na sua URL quando algo acontece. Todos os envios passam pelo mesmo motor de entrega: a mensagem é gravada, cada destino recebe uma entrega com retries próprios e cada chamada HTTP fica registrada. Existem três origens:
| Origem | Onde se configura | Custo |
|---|---|---|
Eventos da conta (ex.: qr.dynamic.scanned) | Webhooks no dashboard | Grátis |
| Callbacks de jobs (ex.: conversão de PDF) | webhook_url em cada requisição | Grátis |
| Webhook Relay (seus próprios eventos) | API do Webhook Relay | Por entrega além da franquia mensal |
Eventos da conta
Cadastre um endpoint em Webhooks no dashboard e escolha os eventos. Cada endpoint tem o seu secret, mostrado na criação e depois pela opção de revelar o secret.
Esses endpoints são do Webhook Relay: um endpoint de qualquer projeto que assine um evento da conta o recebe, grátis. A antiga API de webhooks da conta está obsoleta; veja API legada da conta.
| Evento | Quando dispara |
|---|---|
qr.dynamic.scanned | Alguém escaneou um QR dinâmico. |
qr.batch.completed | Um batch de geração de QR foi concluído. |
rag.document.ready | Um documento da base de conhecimento terminou de ser indexado e já responde às buscas. |
rag.document.failed | A indexação de um documento falhou; o payload traz o `error_code`. |
rag.question.unanswered | Uma pergunta ficou sem resposta porque nenhum trecho da base passou do `min_score`. |
billing.recharge.success | Uma recarga de créditos foi confirmada. |
billing.bankruptcy.warning | O saldo da conta ficou negativo; as ferramentas podem ser bloqueadas. |
webhook.test | Ping de teste pedido na rota de teste de um endpoint. Grátis e assinado como os demais eventos. |
Requisição enviada
POST <url do endpoint>
Content-Type: application/json
User-Agent: RespawnTech-Webhook/2.0
webhook-id: msg_2Vf8tK1pQ9zX
webhook-timestamp: 1790338000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
X-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd| Header | Conteúdo |
|---|---|
webhook-id | Identificador da mensagem. É o mesmo em todos os retries: use para descartar duplicatas. |
webhook-timestamp | Momento do envio desta tentativa, em segundos Unix. |
webhook-signature | v1,<base64>, uma assinatura por secret ativo, separadas por espaço (duas durante uma troca de secret). |
X-Webhook-Signature | Assinatura legada sha256=<hex>, sem timestamp. Mantida para receptores antigos. |
Os headers seguem o padrão Standard Webhooks, então qualquer biblioteca desse padrão valida a requisição. Endpoints do Webhook Relay podem ter headers extras (custom_headers).
Corpo
Eventos da conta e mensagens do Webhook Relay usam o mesmo envelope:
{
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"type": "qr.dynamic.scanned",
"timestamp": 1790338000,
"payload": {
"resource_id": "…",
"slug": "promo-verao",
"ip": "hash_anônimo",
"user_agent": "Mozilla/5.0 …",
"referer": "https://…"
}
}O formato de payload varia conforme o type. Callbacks de jobs mantêm o corpo próprio (veja Callback de jobs), e mensagens recebidas de provedores pelo Webhook Relay chegam com o corpo original do provedor, sem alteração.
Validar a assinatura
O conteúdo assinado é webhook-id + "." + webhook-timestamp + "." + corpo bruto, com HMAC-SHA256 e o resultado em base64. Para validar:
- Leia o corpo bruto, antes de qualquer parse de JSON (reformatar o JSON muda a assinatura).
- Recuse timestamps com mais de 5 minutos de diferença do seu relógio (proteção contra replay).
- Calcule a assinatura e compare, em tempo constante, com cada item de
webhook-signature. Basta um bater.
Com uma biblioteca Standard Webhooks, use o standard_webhooks_secret (formato whsec_…). Sem biblioteca, a chave do HMAC são os bytes do secret bruto.
// npm i standardwebhooks
import express from 'express';
import { Webhook } from 'standardwebhooks';
const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_…
const app = express();
app.post('/hook', express.raw({ type: '*/*' }), (req, res) => {
let event;
try {
event = wh.verify(req.body, req.headers); // lança erro se inválido
} catch {
return res.sendStatus(401);
}
// processe `event` de forma assíncrona e responda rápido
res.sendStatus(204);
});# pip install standardwebhooks
import os
from flask import Flask, request
from standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["WEBHOOK_SECRET"]) # whsec_…
app = Flask(__name__)
@app.post("/hook")
def hook():
try:
event = wh.verify(request.get_data(), dict(request.headers))
except Exception:
return "", 401
# processe `event` de forma assíncrona e responda rápido
return "", 204// Sem biblioteca: secret é o valor bruto (não o whsec_).
func verify(secret string, r *http.Request, body []byte) bool {
id, ts := r.Header.Get("webhook-id"), r.Header.Get("webhook-timestamp")
t, err := strconv.ParseInt(ts, 10, 64)
if err != nil || math.Abs(time.Since(time.Unix(t, 0)).Seconds()) > 300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(id + "." + ts + "."))
mac.Write(body)
want := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))
for _, sig := range strings.Fields(r.Header.Get("webhook-signature")) {
if hmac.Equal([]byte(sig), []byte(want)) {
return true
}
}
return false
}Receptores antigos
Quem já valida X-Webhook-Signature continua funcionando: é sha256= + o HMAC-SHA256 em hexadecimal do corpo bruto, com o secret bruto como chave. Esse header não tem timestamp; prefira webhook-signature em integrações novas.
Secrets e rotação
| Destino | Onde obter o secret |
|---|---|
| Endpoint de eventos da conta | Na criação, ou pela opção de revelar o secret em Webhooks no dashboard. |
| Endpoint do Webhook Relay | Na criação, ou em Revelar secret. |
Callback de jobs (webhook_url) | Secret de assinatura da conta, único para todos os callbacks: GET /v1/account/webhooks/signing-secret (rota da conta, com a sessão do dashboard). |
Todo secret aparece em duas formas: secret (bruto) e standard_webhooks_secret (whsec_ + base64 dos mesmos bytes). As duas validam as mesmas assinaturas.
Ao trocar o secret de um endpoint, o antigo continua assinando por 24 horas: nesse período webhook-signature traz as duas assinaturas, e você atualiza o receptor sem perder eventos.
Entrega e retries
Só uma resposta 2xx conta como sucesso. Redirecionamentos (3xx) não são seguidos, e 3xx, 4xx, 5xx, timeouts e erros de rede são tentados de novo neste calendário:
| Tentativa | Espera antes dela |
|---|---|
| 1 | — |
| 2 | 5 segundos |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
| 6 | 5 horas |
| 7 e 8 | 10 horas |
São cerca de 27 horas no total, com variação de ±10%. Casos especiais:
429e503comRetry-After: a próxima tentativa respeita o header quando ele pede mais tempo (até 10 horas).410 Gone: a entrega para e o endpoint é desativado.- Depois da última tentativa, a entrega fica
exhausted. No Webhook Relay, dá para reenviar a mensagem ou recuperar as falhas de um endpoint.
Boas práticas no receptor:
- Responda 2xx o mais rápido possível e processe o evento de forma assíncrona.
- Descarte duplicatas pelo
webhook-id: uma entrega pode chegar mais de uma vez (ex.: seu servidor processou, mas a resposta se perdeu). - Não dependa da ordem de chegada; use o
timestampdo corpo quando a ordem importar.
Desativação automática
Um endpoint que falha sem parar por 5 dias é desativado automaticamente (is_active: false, com disabled_reason), assim como um que responde 410. Depois de corrigir o receptor, reative o endpoint (no dashboard ou com is_active: true no Webhook Relay); isso zera a sequência de falhas.
O histórico de mensagens, entregas e tentativas fica guardado por 30 dias.
Callback de jobs
Em POST /convert ou POST /batch do PDF Convert, envie webhook_url no body. Quando o job termina, a API faz um POST nessa URL:
{
"job_id": "job_abc123",
"success": true,
"result_url": "https://…",
"metadata": { "conversion_id": "…" }
}Em falha, success é false e o campo error traz a mensagem. O callback tem os mesmos headers, assinatura e retries dos outros webhooks, assinado com o secret de assinatura da conta (veja Secrets e rotação). A webhook_url precisa ser uma URL pública http ou https; caso contrário a requisição é recusada com webhook.invalid_url.
Seus próprios eventos
Para enviar webhooks da sua aplicação para os seus clientes, com a mesma assinatura, retries e log, ou para receber webhooks do Stripe, GitHub e outros e redistribuí-los, use o Webhook Relay.
Atualizado em