API Reference
Webhooks

Create a webhook subscription

POST
/v1/webhooks

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:

HeaderDescription
X-Sautikit-SignatureHMAC envelope t=<unix>,v1=<hex> (see "Signature" below).
X-Sautikit-TimestampUnix epoch seconds the signature was minted at — same value as the t= term.
X-Sautikit-Event-IdUUID of the originating event. Identical across retries to the same subscription; use this for at-most-once application logic.
X-Sautikit-Event-KindThe event kind (e.g. call.completed). Matches the body's kind field.
X-Sautikit-Delivery-IdUUID of THIS delivery attempt. Unique per subscription and event.
X-Sautikit-Attempt1-indexed attempt counter. 1 on the first send; increments on each retry.
User-AgentSautikit-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

import hmac, hashlib, timedef 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

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

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"]))}

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

Header Parameters

Idempotency-Key?string

Optional client-supplied idempotency key. Webhook subscription create is idempotent at the DB level (URL+events uniqueness is NOT enforced — the key is accepted for client-side replay safety and surfaced in logs).

Lengthlength <= 64

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

curl -X POST "https://example.com/v1/webhooks" \  -H "Content-Type: application/json" \  -d '{    "url": "http://example.com",    "events": [      "string"    ]  }'
{  "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"  },  "secret": "string"}
{  "error": {    "code": "validation.bad_request",    "message": "string",    "request_id": "string"  }}
{  "error": {    "code": "validation.bad_request",    "message": "string",    "request_id": "string"  }}