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 osecretsobre 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 campotypedo 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();
});
import hmac
import time
from hashlib import sha256
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.strip().partition("=")[::2] for p in header.split(","))
stamp, sent = parts.get("t", ""), parts.get("v1", "")
if not stamp.isdigit() or abs(int(time.time()) - int(stamp)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{stamp}.".encode() + body, sha256).hexdigest()
return hmac.compare_digest(sent.lower(), expected)
# FastAPI: use o corpo bruto, não o JSON já analisado.
@app.post("/webhooks/smskit", status_code=204)
async def webhook(request: Request):
body = await request.body()
if not verify(SECRET, request.headers.get("X-SMSKit-Signature", ""), body):
raise HTTPException(status_code=400)
event = json.loads(body)
# event["type"], event["data"] …
Nos SDKs, essa verificação é uma chamada:
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 });
from smskit import verify_signature
ok = verify_signature(
SECRET,
request.headers.get("X-SMSKit-Signature"),
await request.body(), # bytes do corpo bruto
)
if not ok:
raise HTTPException(status_code=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.
{
"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.