Documentation menu

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#

EventCarries
call.createdcall
call.endedcall
session.createdsession
message.createdsession, message
session.closedsession
JSON
{"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:

JavaScript
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))
  );
}
Python
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-Delivery to drop repeats: a delivery can arrive more than once.
  • Events can arrive out of order; use created_at and 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.