Update a webhook subscription
Authorization
bearerAuth 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
Path Parameters
uuidRequest Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
curl -X PATCH "https://example.com/v1/webhooks/497f6eca-6276-4993-bfeb-53cbbbba6f08" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/sautikit", "events": [ "call.completed", "call.failed" ], "status": "active" }'{ "subscription": { "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" }}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"])) } ```
Delete a webhook subscription DELETE
Next Page