Integrating
Webhooks
Signed events to your server about calls and chats, stored and retried until they arrive.
Spiiksi can tell your server what happens: a call starts or ends, a chat starts, gets a message or closes.
Add an endpoint#
In API & integrations → Webhooks, add an https address and choose its events. Copy the signing secret (whsec_…), shown once. Test sends a ping at once.
The address must be https and on the public internet. Redirects aren't followed.
Events#
| Event | Carries |
|---|---|
call.created | call |
call.ended | call |
session.created | session |
message.created | session, message |
session.closed | session |
{"type": "call.ended", "created_at": "2026-10-08T09:12:00",
"call": {"id": "6f0c…", "status": "ended", "external_id": "ticket-1234",
"metadata": {"queue": "billing"}, "livemode": true,
"agent_language": "fi", "customer_language": "es", …}}The objects are the same as the API returns. livemode is false for objects made with a test key.
Check the signature#
Each delivery has these headers:
Spiiksi-Signature: t=<unix time>,v1=<hex>: HMAC-SHA256 of"<t>.<raw body>"with your secret.Spiiksi-Event: the event type.Spiiksi-Delivery: the delivery's id, the same on every retry.
Check the signature against the raw body, before parsing it, and reject timestamps older than a few minutes:
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return (
fresh &&
expected.length === parts.v1?.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < 300
return fresh and hmac.compare_digest(expected, parts.get("v1", ""))Delivery and retries#
Answer with any 2xx, quickly; do slow work afterwards. Deliveries are stored before they're sent, so a restart on our side never loses one. A delivery that doesn't get a 2xx is tried again after 10 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, then marked failed.
- Use
Spiiksi-Deliveryto drop repeats: a delivery can arrive more than once. - Events can arrive out of order; use
created_atand the objects'status. - The dashboard lists each endpoint's deliveries and can send one again.
- An endpoint whose deliveries run out of attempts ten times in a row is switched off; switch it back on in the dashboard.