API Reference
Webhooks

List webhook subscriptions for the active workspace

GET
/v1/webhooks

Authorization

bearerAuth
AuthorizationBearer <token>

Long-lived ES256 JWT minted from the dashboard (https://app.sautikit.com/developers/api-keys). Signed by the platform keyring. Carries workspace_id and scopes claims; revoked via the platform deny-list.

In: header

Response Body

application/json

application/json

curl -X GET "https://example.com/v1/webhooks"
{  "data": [    {      "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",      "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",      "url": "http://example.com",      "secret_prefix": "string",      "events": [        "string"      ],      "status": "active",      "created_at": "2019-08-24T14:15:22Z",      "updated_at": "2019-08-24T14:15:22Z"    }  ]}
{  "error": {    "code": "validation.bad_request",    "message": "string",    "request_id": "string",    "details": [      "string"    ],    "resolution": "string",    "reason": "invalid_characters",    "suggested_e164": "+254727524723"  }}

Send a control command to a conference room on the PBX POST

Dispatches one of the allowlisted PBX conference commands to the named room. The room must belong to the active workspace. Unknown commands are rejected with 400. | Command | Effect | |---------|--------| | `mute` / `unmute` | Stop or restore what the room hears from a participant. | | `tmute` | Toggle a participant's mute state. | | `deaf` / `undeaf` | Stop or restore what a participant hears from the room. | | `play` | Play an audio file into the room. | | `say` | Speak text into the room. | | `moh` | Start or stop hold music in the room. | | `dtmf` | Send DTMF digits into the room. | | `relate` | Set the audio relationship between two participants. | | `list` | List the participants currently in the room. | | `exit` | Remove a participant **without ending their call**. | | `kill` | End the conference and hang up every participant. | `exit` is how a caller leaves a room and carries on: the leg does not hang up, it requests a new set of voice actions from `callback_url`, which is required for this command. That is what allows a queued caller to be taken out of a waiting room, offered to an agent, and returned to the room if the agent does not answer. Ending the call instead is what `kill` is for.

Create a webhook subscription POST

Mints a signing secret (32 random bytes, hex). The cleartext secret is returned ONCE in the response and must be stored client-side immediately — the API only persists `sha256(secret)` and the first 8 chars for display. ## Delivery contract Every delivery is a `POST <subscription.url>` with `Content-Type: application/json`. The body is the raw event payload (see the `WebhookEvent` discriminator below for the per-`kind` schema). The following request headers are emitted on every delivery: | Header | Description | |---|---| | `X-Sautikit-Signature` | HMAC envelope `t=<unix>,v1=<hex>` (see "Signature" below). | | `X-Sautikit-Timestamp` | Unix epoch seconds the signature was minted at — same value as the `t=` term. | | `X-Sautikit-Event-Id` | UUID of the originating event. Identical across retries to the same subscription; use this for at-most-once application logic. | | `X-Sautikit-Event-Kind` | The event kind (e.g. `call.completed`). Matches the body's `kind` field. | | `X-Sautikit-Delivery-Id` | UUID of THIS delivery attempt. Unique per subscription and event. | | `X-Sautikit-Attempt` | 1-indexed attempt counter. `1` on the first send; increments on each retry. | | `User-Agent` | `Sautikit-Webhooks/1.0` | Retries follow an exponential schedule: 30s, 2m, 10m, 30m, 2h, 6h, 24h, 7d, then dead-letter. A 2xx response marks the delivery `succeeded`; any other response (or a transport error) triggers the next backoff. ## Signature The signature header is canonically `X-Sautikit-Signature` with shape: ``` X-Sautikit-Signature: t=<unix>,v1=<hex> ``` Where: - `t` — Unix epoch seconds. MUST be within ±300s of the customer's clock to defeat replay attacks. - `v1` — hex-encoded HMAC-SHA256 over `<body> + "." + <t>`, keyed by the subscription's signing secret (the cleartext returned on POST `/v1/webhooks` or POST `/v1/webhooks/{id}/rotate-secret`). Customer verification (reject if any check fails): 1. Parse `t` and `v1` from the header. 2. Reject if `abs(now - t) > 300`. 3. Recompute `expected = hmac_sha256(secret, raw_body + "." + t)`. 4. Reject if `expected` != `v1` (constant-time compare). ### Python ```python import hmac, hashlib, time def verify(secret: str, header: str, raw_body: bytes, max_skew_seconds: int = 300) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) t = int(parts["t"]) if abs(time.time() - t) > max_skew_seconds: return False mac = hmac.new(secret.encode(), raw_body + b"." + str(t).encode(), hashlib.sha256) return hmac.compare_digest(mac.hexdigest(), parts["v1"]) ``` ### Node.js ```javascript const crypto = require("crypto"); function verify(secret, header, rawBody, maxSkewSeconds = 300) { const parts = Object.fromEntries( header.split(",").map((p) => p.split("=")), ); const t = parseInt(parts.t, 10); if (Math.abs(Date.now() / 1000 - t) > maxSkewSeconds) return false; const expected = crypto .createHmac("sha256", secret) .update(Buffer.concat([Buffer.from(rawBody), Buffer.from("." + t)])) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(parts.v1), ); } ``` ### Go ```go import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "strconv" "strings" "time" ) func Verify(secret, rawBody []byte, header string, maxSkew time.Duration) bool { parts := map[string]string{} for _, p := range strings.Split(header, ",") { kv := strings.SplitN(p, "=", 2) if len(kv) == 2 { parts[kv[0]] = kv[1] } } t, err := strconv.ParseInt(parts["t"], 10, 64) if err != nil { return false } if d := time.Since(time.Unix(t, 0)); d > maxSkew || d < -maxSkew { return false } mac := hmac.New(sha256.New, secret) mac.Write(rawBody) mac.Write([]byte(".")) mac.Write([]byte(strconv.FormatInt(t, 10))) want := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(want), []byte(parts["v1"])) } ```