Respawn Tech Dev

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:

OrigemOnde se configuraCusto
Eventos da conta (ex.: qr.dynamic.scanned)Webhooks no dashboardGrátis
Callbacks de jobs (ex.: conversão de PDF)webhook_url em cada requisiçãoGrátis
Webhook Relay (seus próprios eventos)API do Webhook RelayPor 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.

EventoQuando dispara
qr.dynamic.scannedAlguém escaneou um QR dinâmico.
qr.batch.completedUm batch de geração de QR foi concluído.
rag.document.readyUm documento da base de conhecimento terminou de ser indexado e já responde às buscas.
rag.document.failedA indexação de um documento falhou; o payload traz o `error_code`.
rag.question.unansweredUma pergunta ficou sem resposta porque nenhum trecho da base passou do `min_score`.
billing.recharge.successUma recarga de créditos foi confirmada.
billing.bankruptcy.warningO saldo da conta ficou negativo; as ferramentas podem ser bloqueadas.
webhook.testPing 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
HeaderConteúdo
webhook-idIdentificador da mensagem. É o mesmo em todos os retries: use para descartar duplicatas.
webhook-timestampMomento do envio desta tentativa, em segundos Unix.
webhook-signaturev1,<base64>, uma assinatura por secret ativo, separadas por espaço (duas durante uma troca de secret).
X-Webhook-SignatureAssinatura 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:

  1. Leia o corpo bruto, antes de qualquer parse de JSON (reformatar o JSON muda a assinatura).
  2. Recuse timestamps com mais de 5 minutos de diferença do seu relógio (proteção contra replay).
  3. 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

DestinoOnde obter o secret
Endpoint de eventos da contaNa criação, ou pela opção de revelar o secret em Webhooks no dashboard.
Endpoint do Webhook RelayNa 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:

TentativaEspera antes dela
1—
25 segundos
35 minutos
430 minutos
52 horas
65 horas
7 e 810 horas

São cerca de 27 horas no total, com variação de ±10%. Casos especiais:

  • 429 e 503 com Retry-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 timestamp do 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