Webhooks
Coming soon Webhook endpoint management ships with the public beta. The events themselves already flow inside Waasl — see the event catalog for which are ready.
Webhooks push events to your HTTPS endpoint as they happen, so you never need to poll.
Registering an endpoint
Section titled “Registering an endpoint”curl -X POST https://api.waasl.io/v1/webhook-endpoints \ -H "Authorization: Bearer $WAASL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/waasl/webhook", "events": ["message.received", "message.status_updated"] }'Use ["*"] to receive everything. The response contains a secret (whsec_…) shown only once.
The payload
Section titled “The payload”Every event shares an envelope:
{ "id": "evt_2Nq8Ld", "type": "message.received", "created_at": "2026-10-01T08:15:00Z", "workspace_id": "ws_26N2SaWtMR", "api_version": "2026-10-01", "data": { "message": { "id": "msg_Vb3kQ9xP", "direction": "in", "kind": "text", "body": "مرحبا" }, "conversation": { "id": "cv_3PzA91" } }}Verifying signatures
Section titled “Verifying signatures”Each request carries a Waasl-Signature header:
Waasl-Signature: t=1759306500,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdv1 is the hex HMAC-SHA256 of {t}.{raw request body} keyed with your endpoint secret. Compute it over the raw bytes (before JSON parsing), compare in constant time, and reject timestamps older than five minutes to stop replays.
import crypto from "node:crypto";
export function verifyWaasl(rawBody, header, secret, toleranceSec = 300) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("="))); if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false; const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex"); return crypto.timingSafeEqual(Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex"));}import hmac, hashlib, time
def verify_waasl(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) if abs(time.time() - int(parts["t"])) > tolerance: return False signed = parts["t"].encode() + b"." + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts["v1"])Responding and retries
Section titled “Responding and retries”- Return any
2xxwithin 10 seconds. Do slow work in a background job. - Anything else — or a timeout — is retried with exponential backoff: 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h.
- After 3 days of consecutive failures the endpoint is disabled and workspace admins are emailed.
- Deliveries can arrive more than once and out of order. De-duplicate on
id, and comparecreated_at(or re-fetch the resource) before overwriting newer state. - Inspect recent attempts with
GET /webhook-endpoints/{id}/deliveries.