Pular para o conteúdo
Sursa Developers

Receber webhooks

Um webhook avisa o seu sistema sobre uma alteração no Sursa assim que ela acontece: um cartão foi publicado, verificado ou arquivado, uma pergunta ficou sem resposta. O Sursa envia um POST HTTPS com o evento para um endereço que você registra. Este guia monta um receptor que funciona corretamente diante de novas tentativas, mudanças de ordem e duplicatas.

Para saber o que cada evento significa e contém, veja a visão geral dos webhooks e o catálogo de eventos na referência da API.

1. Registrar um webhook

Um administrador registra webhooks no Sursa em Administration → Integrations → Webhooks and API (administração, integrações, webhooks) ou com POST /v1/webhooks e uma chave com a função admin e o escopo webhooks:manage:

curl -X POST https://api.sursa.com/v1/webhooks \
  -H "Authorization: Bearer $SURSA_ADMIN_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/sursa",
    "description": "Sync verified cards to the help center",
    "events": ["card.published", "card.updated", "card.verified", "card.archived"]
  }'

A resposta traz o segredo de assinatura em secret, começando com whsec_. Ele é mostrado uma única vez. Guarde-o junto com o seu receptor, por exemplo em uma variável de ambiente SURSA_WEBHOOK_SECRET.

O endereço precisa usar https e resolver para endereços IP públicos: os endereços de redes privadas, de loopback e de outras redes reservadas são rejeitados no registro e em cada entrega. Para desenvolver no seu próprio computador, exponha o seu receptor local por meio de um serviço de túnel que dê a ele um endereço HTTPS público.

Envie um evento de teste a qualquer momento com POST /v1/webhooks/{webhook_id}/test. A resposta mostra o que o seu receptor respondeu.

2. Como é uma entrega

POST /sursa HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
webhook-id: evt_0123456789abcdef0123456789abcdef
webhook-timestamp: 1791194400
webhook-signature: v1,l1LLl/aEl4dQq7Geb383KbyNeZgs7VZpiw6iuoJgunI=
X-KB-Signature: t=1791194400,v1=82aa6b27378d89d7d880d17c3fe96268598443ae0e64d059c0c41e94e5d3ffbe
X-KB-Event-Id: evt_0123456789abcdef0123456789abcdef
X-KB-Event: card.verified
X-KB-Delivery: 99812
X-KB-Attempt: 1
X-KB-Sequence: 1042

{"id":"evt_0123456789abcdef0123456789abcdef","type":"card.verified","created_at":"2026-10-05T10:00:00Z","org_id":1,"api_version":"1.60.0","sequence":1042,"object":{"type":"card","id":"c1"},"payload":"full","data":{"card":{"id":"c1"}},"test":false}
CabeçalhoSignificado
webhook-idO ID do evento. É o mesmo em todas as novas tentativas. Use-o para descartar duplicatas.
webhook-timestampQuando esta tentativa foi assinada, em segundos Unix.
webhook-signatureA assinatura no formato Standard Webhooks.
X-KB-SignatureA mesma entrega, assinada no formato anterior do Sursa.
X-KB-EventO tipo de evento.
X-KB-AttemptO número da tentativa, a partir de 1.
X-KB-SequenceA posição desta entrega na ordem do webhook.

Os nomes dos cabeçalhos não diferenciam maiúsculas de minúsculas; o seu framework pode mostrá-los como X-Kb-Signature ou x-kb-signature.

O corpo é o evento. O data dele depende do tipo. Um webhook configurado com o payload thin recebe o evento sem data; nesse caso, leia o objeto pela API com a sua própria chave.

3. Verificar a assinatura

Verifique a assinatura de cada entrega antes de confiar nela. Os dois formatos assinam os bytes exatos do corpo, então leia o corpo bruto antes de qualquer parsing de JSON: serializar o JSON de novo muda os bytes e quebra a verificação.

FormatoString assinadaChaveAssinatura
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>A parte do segredo depois de whsec_, decodificada de base64v1, + base64 do HMAC-SHA256
Formato anterior (X-KB-Signature)<t>.<body>A string inteira do segredo, como textov1= + hex do HMAC-SHA256

Verificar qualquer um dos dois basta. Prefira o cabeçalho Standard Webhooks: a mesma verificação está disponível em muitas bibliotecas prontas. Durante uma rotação do segredo, cada cabeçalho traz uma assinatura por segredo válido (separadas por espaço em webhook-signature, com v1= repetido em X-KB-Signature). Uma entrega é válida se qualquer uma delas corresponder.

Rejeite também um timestamp que esteja a mais de 5 minutos do seu relógio. Isso impede que alguém reenvie mais tarde uma entrega gravada.

# Check a saved delivery by hand: body.json holds the raw body, the values come from its headers.
SECRET="whsec_…"; WEBHOOK_ID="evt_…"; TIMESTAMP="1791194400"

# Standard Webhooks: compare with the value after "v1," in webhook-signature
KEY=$(printf '%s' "${SECRET#whsec_}" | tr '_-' '/+')
while [ $(( ${#KEY} % 4 )) -ne 0 ]; do KEY="$KEY="; done
KEY_HEX=$(printf '%s' "$KEY" | openssl base64 -d -A | xxd -p | tr -d '\n')
{ printf '%s.%s.' "$WEBHOOK_ID" "$TIMESTAMP"; cat body.json; } \
  | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY_HEX" -binary | openssl base64 -A

# Earlier format: compare with the value after "v1=" in X-KB-Signature
{ printf '%s.' "$TIMESTAMP"; cat body.json; } | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1

Testar a verificação de assinaturas

Execute o seu código com estes valores antes de conectá-lo às entregas reais. Com o relógio ajustado para o timestamp, as duas verificações precisam passar, e precisam falhar quando um byte do corpo muda.

EntradaValor
Segredowhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp e t1791194400
Corpo{"id":"evt_0123456789abcdef0123456789abcdef","type":"card.verified","created_at":"2026-10-05T10:00:00Z","org_id":1,"data":{"card":{"id":"c1"}}}
webhook-signaturev1,+xxWSXH6weQJ/MVg4+N78a1Dj4y/AQ4p94+PRNzpECg=
X-KB-Signaturet=1791194400,v1=ce26a19cb63cd3ae620217bf7688e1df1044fa7d59d9ae148d215f0292b768dc

4. Responder rápido, trabalhar depois

O Sursa espera até 10 segundos pela sua resposta. Qualquer status 2xx significa que a entrega deu certo; qualquer outra coisa, inclusive um redirecionamento, um tempo limite esgotado ou uma conexão fechada, significa que ela falhou e terá novas tentativas.

Por isso, o receptor deve apenas verificar, registrar o evento e responder. Faça o trabalho de verdade depois, a partir da sua própria fila.

// Node.js 20+, no framework. verifyStandard and verifyLegacy come from step 3.
import http from "node:http";

const SECRET = process.env.SURSA_WEBHOOK_SECRET;

http
  .createServer(async (req, res) => {
    const parts = [];
    for await (const part of req) parts.push(part);
    const body = Buffer.concat(parts); // the raw bytes, before any JSON parsing

    const valid = req.headers["webhook-signature"]
      ? verifyStandard(SECRET, req.headers, body)
      : verifyLegacy(SECRET, req.headers["x-kb-signature"], body);
    if (!valid) {
      res.writeHead(400).end();
      return;
    }

    const event = JSON.parse(body);
    await saveEventOnce(event); // insert into your table or queue, keyed by event.id; ignore a duplicate
    res.writeHead(204).end();
  })
  .listen(3000);

Responda 2xx só depois que o evento estiver guardado com segurança. Se você responder primeiro e o processo cair em seguida, o evento se perde para você, porque o Sursa já o contou como entregue.

5. Novas tentativas

Uma entrega com falha é tentada de novo com pausas cada vez maiores: depois de cerca de 1 minuto, 5 minutos, 30 minutos, 2 horas, 5 horas, 10 horas, 24 horas e 24 horas, nove tentativas ao longo de quase três dias. Cada pausa varia em até 20% para que as novas tentativas de muitos webhooks não cheguem no mesmo instante.

  • Uma resposta 429 ou 503 com Retry-After adia a próxima tentativa para pelo menos esse momento, até 24 horas depois. Use-a quando o seu sistema estiver sobrecarregado ou em manutenção.
  • Uma resposta 410 Gone desativa o webhook: envie-a só quando não quiser mais entregas nesse endereço.
  • Depois de 10 tentativas seguidas com falha, o webhook é pausado, e os administradores da organização são avisados. Os eventos continuam se acumulando e, quando o webhook é retomado, na interface de administração ou com POST /v1/webhooks/{webhook_id}/resume, são entregues em ordem.
  • Depois da última tentativa, a entrega é marcada como exhausted. Você pode enviá-la de novo manualmente.

As entregas de um webhook, com cada tentativa, o código da sua resposta e o tempo dela, são listadas por GET /v1/webhooks/{webhook_id}/deliveries e na interface de administração. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver envia uma delas de novo. Um reenvio tem o mesmo webhook-id.

6. Ordem e duplicatas

  • Ordem. As entregas a um webhook seguem estritamente uma depois da outra, na ordem de sequence. Enquanto uma entrega recebe novas tentativas, as posteriores esperam. Webhooks diferentes não esperam uns pelos outros.
  • Duplicatas. O mesmo evento pode chegar mais de uma vez, por exemplo quando a sua resposta se perdeu no caminho de volta. Guarde o webhook-id (ele também é o id no corpo) de cada evento que você processa e ignore um que já tenha visto.
  • Pensar em estados. Para alterações no mesmo objeto, compare created_at ou sequence com o que você guardou e ignore um evento mais antigo que o estado que você já tem. Na dúvida, leia o objeto atual pela API: o evento diz que algo mudou; a API diz como ele está agora.

7. Reconciliar com o feed de eventos

Todo evento também fica guardado por 30 dias no feed de eventos, haja ou não um webhook inscrito nele. Se o seu receptor ficou fora do ar por mais tempo do que as novas tentativas cobrem, ou se você quer conferir se nada se perdeu, leia o feed com uma chave que tenha a função admin e o escopo webhooks:manage:

curl -G https://api.sursa.com/v1/webhook-events \
  -H "Authorization: Bearer $SURSA_API_KEY" \
  --data-urlencode "type=card.*" \
  --data-urlencode "created_since=2026-10-20T00:00:00Z" \
  --data-urlencode "limit=200"

Um evento no feed tem o mesmo id e o mesmo formato que em uma entrega, sem os campos de entrega sequence, payload e test. Como o seu receptor ignora os eventos que já viu, você pode executar a reconciliação com a frequência que quiser, por exemplo uma vez por hora.

GET /v1/webhook-events/{event_id} lê um evento pelo ID dele.

8. Rotacionar o segredo

POST /v1/webhooks/{webhook_id}/rotate-secret emite um novo segredo e mantém o anterior válido por um período de sobreposição, de 24 horas por padrão e de até 7 dias. Durante a sobreposição, cada entrega é assinada com os dois segredos.

  1. Faça a rotação e guarde o novo segredo ao lado do antigo no seu receptor.
  2. Aceite uma entrega se ela for verificada com qualquer um dos dois segredos.
  3. Depois da sobreposição, remova o segredo antigo.

Se o segredo antigo vazou, encerre a sobreposição imediatamente com POST /v1/webhooks/{webhook_id}/revoke-previous-secret ou faça a rotação com "overlap_hours": 0.

Checklist

  • Verifique uma das assinaturas sobre o corpo bruto e confira o timestamp.
  • Guarde o evento, identificado pelo webhook-id, e então responda 2xx em até 10 segundos.
  • Ignore duplicatas e eventos desatualizados; leia o objeto atual pela API quando isso importar.
  • Responda 429 ou 503 com Retry-After quando precisar que o Sursa diminua o ritmo; nunca responda 410 por engano.
  • Ignore os tipos de evento e os campos que você não conhece.
  • Reconcilie com o feed de eventos de forma periódica.