smskitdocs

Webhooks

Um webhook entrega no seu servidor o resultado de cada envio, sem que você precise consultar a API. Cadastre uma URL HTTPS em POST /v1/webhooks, ou em IntegraçõesWebhooks, com os eventos que quer receber. Cada entrega é um POST com JSON, assinado com HMAC-SHA256 sobre o corpo cru.

Cadastro e headers

O cadastro devolve o secret (whsec_) uma única vez, e é com ele que a assinatura é conferida. Guarde junto com as outras credenciais do projeto. Toda entrega leva quatro headers:

  • X-SMSKit-Signature: t=<unix>,v1=<hex> — HMAC-SHA256 com o secret sobre a string "<t>.<corpo bruto>".
  • X-SMSKit-Event-Id — o id do evento. Use para descartar uma entrega repetida.
  • X-SMSKit-Event-Type — o tipo do evento, igual ao campo type do corpo. Útil para rotear antes de ler o JSON.
  • X-SMSKit-Delivery-Attempt — a tentativa atual, de 1 a 6. Uma entrega repetida chega com um número maior e o mesmo {id}.

Confira a assinatura

Calcule o HMAC sobre os bytes que chegaram, compare em tempo constante e recuse carimbos com mais de cinco minutos. A conta precisa ser feita sobre o corpo cru: JSON reserializado muda o espaçamento e a ordem das chaves, e a assinatura deixa de bater.

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

// Verifique sobre o corpo BRUTO da requisição, nunca sobre JSON re-serializado.
export function verify(secret: string, header: string, rawBody: Buffer) {
  const parts = Object.fromEntries(
    header.split(",").map((part) => part.trim().split("=", 2)),
  );
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
  if (!parts.t || !parts.v1 || Number.isNaN(age) || age > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.`)
    .update(rawBody)
    .digest();
  const received = Buffer.from(parts.v1, "hex");
  return expected.length === received.length && timingSafeEqual(expected, received);
}

// Express: mantenha o corpo bruto no handler do webhook.
app.post("/webhooks/smskit", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.header("X-SMSKit-Signature") ?? "";
  if (!verify(process.env.SMSKIT_WEBHOOK_SECRET!, header, req.body)) {
    return res.status(400).end();
  }
  const event = JSON.parse(req.body.toString("utf8"));
  // event.type, event.data …
  res.status(204).end();
});

Nos SDKs, essa verificação é uma chamada:

verifySignature(secret, header, rawBody)
import { verifySignature } from "smskit";

// Assíncrono: usa Web Crypto, funciona em Node, Bun, Deno e edge runtimes.
const ok = await verifySignature(
  process.env.SMSKIT_WEBHOOK_SECRET!,
  request.headers.get("X-SMSKit-Signature"),
  rawBody, // string ou Uint8Array com o corpo bruto
);
if (!ok) return new Response(null, { status: 400 });

Formato do evento

Todo evento tem o mesmo formato. O campo data carrega o objeto do evento: a mensagem de um destinatário nos eventos message.* e o resumo do envio em sms.completed.

message.delivered
{
  "id": "evt_8n3v5t",
  "object": "event",
  "type": "message.delivered",
  "api_version": "v1",
  "created_at": "2026-09-11T14:03:05Z",
  "test": false,
  "data": {
    "id": "msg_2b9x4c",
    "sms_id": "sms_7f3k2m",
    "to": "+5511999990123",
    "status": "delivered",
    "error": null,
    "kind": "utility",
    "updated_at": "2026-09-11T14:03:05Z",
    "metadata": { "order_id": "1042" }
  }
}

Eventos disponíveis

message.sent
A operadora aceitou a mensagem para um destinatário.
message.delivered
A operadora confirmou a entrega no aparelho.
message.failed
A entrega falhou para um destinatário.
message.replied
Um destinatário respondeu à mensagem (MO).
sms.completed
Todos os destinatários de um envio têm resultado final.
webhook.test
Evento de verificação disparado pelo console ou pela API.

Reentrega

Qualquer resposta 2xx conta como entrega. Sem ela, o evento é reenviado até 6 vezes, com espera crescente: 1 min, 5 min, 30 min, 2 h, 12 h. Responda rápido e processe o evento depois, fora da requisição.

GET /v1/webhooks/{id}/deliveries lista as tentativas recentes, com o código de resposta de cada uma. POST /v1/webhooks/{id}/test dispara um evento webhook.test para validar a integração — o webhook precisa assinar esse evento, senão a resposta é 422.