Webhooks de Rastreio

Receba cada mudança de status dos seus rastreios via HTTP POST, com assinatura HMAC, retry automático e auto-disable em falhas consecutivas.

Visão Geral

Webhooks disparam sempre que o último evento de um rastreio mudar, tanto para rastreios avulsos como para pedidos integrados (Lojaverso, Nuvemshop etc.). Você cadastra endpoints HTTPs e escolhe quais tipos de evento cada endpoint assina.

Um evento pode gerar zero, um ou vários POSTs — um para cada endpoint elegível. As entregas são enfileiradas de forma durável, entregues por workers em background e reentregues com backoff em caso de falha (não bloqueiam o processamento da fila de rastreio).

Autenticação (gerenciamento via API)

Para cadastrar, listar e excluir webhooks via API, use o header Authorization: Bearer sr_live_.... Cada chave pertence a um time e só enxerga os webhooks do próprio time.

Authorization: Bearer sr_live_sua_chave_aqui

Tipos de evento

Ao criar um webhook você informa a lista de eventos assinados. Use default como catch-all para receber qualquer tipo.

CampoTipoObrig.Descrição
em_cursoeventonãoObjeto em trânsito (qualquer evento não-terminal).
saiu_para_entregaeventonãoSaiu para entrega ao destinatário.
entregueeventonãoEntrega confirmada pelo rastreio.
nao_postadoeventonãoObjeto postado (primeiro registro na transportadora).
defaulteventonãoCatch-all — recebe qualquer evento mesmo se os outros não estiverem na lista.

Payload entregue

O corpo da requisição é JSON com o evento, informações do time, do rastreio e opcionalmente do pedido vinculado.

{
  "id": "d1e5b8a3-...",
  "event": "saiu_para_entrega",
  "eventLabel": "Saiu para entrega",
  "occurredAt": "2026-04-21T14:32:01.512Z",
  "team": { "id": "team_abc", "slug": "minha-loja" },
  "tracking": {
    "codigo": "AA123456789BR",
    "tipo": "normal",
    "lastEventDescription": "Objeto saiu para entrega ao destinatário",
    "lastEventLocation": "São Paulo/SP",
    "lastEventAt": "2026-04-21T14:30:00.000Z"
  },
  "order": {
    "orderId": "livexa_12345",
    "status": "shipped",
    "operationalStatus": "saiu_para_entrega"
  }
}

O campo order só está presente quando o evento foi disparado por uma atualização de pedido integrado.

Headers enviados

CampoTipoObrig.Descrição
X-SeuRastreio-EventstringsimTipo de evento (ex.: em_curso, entregue).
X-SeuRastreio-DeliverystringsimIdentificador único da tentativa de entrega (idempotência).
X-SeuRastreio-TimestampstringsimUnix timestamp (segundos) usado no cálculo do HMAC.
X-SeuRastreio-SignaturestringsimAssinatura HMAC-SHA256 no formato t=<unix_ts>,v1=<hex>.

Verificando a assinatura (HMAC-SHA256)

Compute HMAC-SHA256(secret, `$`{timestamp}.{rawBody}$``) e compare com o valor de v1 em X-SeuRastreio-Signature. Use comparação em tempo constante (ex.: crypto.timingSafeEqual no Node).

import crypto from "node:crypto";

export function verifySeuRastreioSignature(
  secret: string,
  rawBody: string,
  signatureHeader: string,
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=") as [string, string]),
  );
  const ts = Number(parts.t);
  const v1 = parts.v1;
  if (!ts || !v1) return false;

  // Rejeita requisições com mais de 5 minutos para mitigar replay.
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Idempotência: use X-SeuRastreio-Delivery como chave de idempotência no seu lado. O mesmo delivery pode ser re-tentado várias vezes em caso de timeout — processe uma vez só.

Política de retry & auto-disable

Uma resposta HTTP 2xx conta como sucesso. Qualquer outro status, timeout (10s) ou erro de rede agenda nova tentativa com backoff crescente:

StatusSignificado
1Após 1 minuto
2Após 5 minutos
3Após 30 minutos
4Após 2 horas
5Após 6 horas — se falhar, delivery marcado como failed

Após 10 falhas consecutivas no mesmo endpoint, ele é desativado automaticamente (enabled = false). Reative pela dashboard ou enviando PATCH com enabled: true.

Endpoints REST para gerenciar webhooks

Listar webhooks do time

GET/api/public/webhooks
Auth
curl -sS "https://seurastreio.com.br/api/public/webhooks" \
  -H "Authorization: Bearer sr_live_sua_chave"

Criar webhook

POST/api/public/webhooks
Auth
CampoTipoObrig.Descrição
namestringsimNome livre (máx. 120 caracteres).
urlstringsimURL HTTPS (HTTP permitido em dev).
eventsstring[]simLista de eventos a assinar (ex.: ["em_curso","entregue"]).
curl -sS -X POST "https://seurastreio.com.br/api/public/webhooks" \
  -H "Authorization: Bearer sr_live_sua_chave" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Integração ERP",
    "url": "https://meu-erp.com/webhooks/seurastreio",
    "events": ["em_curso","saiu_para_entrega","entregue"]
  }'

A resposta (HTTP 201) inclui o campo secret em claro — guarde agora, pois ele não é exibido em chamadas posteriores.

Consultar webhook

GET/api/public/webhooks/{id}
Auth

Atualizar webhook

PATCH/api/public/webhooks/{id}
Auth

Aceita qualquer combinação de name, url, events e enabled. Enviar {"enabled": true} reativa um endpoint que tenha sido desabilitado automaticamente.

Excluir webhook

DELETE/api/public/webhooks/{id}
Auth

Rotacionar o secret

POST/api/public/webhooks/{id}/rotate-secret
Auth

Gera um novo secret e invalida o anterior. A resposta expõe o novo secret em claro.

Disparar um evento de teste

POST/api/public/webhooks/{id}/test
Auth

Enfileira um delivery sintético (event=default e _test: true no payload) para validar conectividade e assinatura.

Listar entregas recentes

GET/api/public/webhooks/{id}/deliveries
Auth

Query opcional limit (default 50, máx 200). Retorna status, número de tentativas, último erro e nextAttemptAt.

Códigos de status (API de gerenciamento)

StatusSignificado
200Operação bem-sucedida
201Webhook criado
202Delivery de teste enfileirado
400Validação falhou (payload inválido)
401Chave ausente ou inválida
404Webhook não encontrado para o time
500Erro interno