Last updated 2026-11-02

Webhooks and live progress

Two ways to know when work is finished without polling: a webhook, one signed POST to your server when a deck or a whole-deck edit is done or fails, and an event stream you hold open to watch a build as it happens.

Ask for a callback

Add callback_url to POST /v1/decks, or to a whole-deck edit (POST /v1/decks/{id}/edits without slide):

curl https://kinsy.ai/v1/decks \
  -H "Authorization: Bearer $KINSY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"brief": "Q3 board update", "callback_url": "https://example.com/hooks/kinsy"}'

The URL must be https, on a public address, with no username or password in it; anything else is refused with 400 before you are charged. Callbacks need an API key or a signed-in session: an assistant you connected by signing in cannot set one (403), so nothing it does can send a genuinely signed request to a server of its choosing.

Get your signing secret

Every callback is signed with your account's secret. Read it once and keep it on your server:

curl https://kinsy.ai/v1/webhooks/secret -H "Authorization: Bearer $KINSY_API_KEY"
# {"secret": "whsec_…", "created_at": "2026-11-02T09:00:00+00:00", "rotated_at": null}

POST /v1/webhooks/secret/rotate replaces it; the old secret stops working at once. Both calls need an API key or a signed-in session. An assistant you connected by signing in cannot read or rotate the secret.

What arrives

POST /hooks/kinsy HTTP/1.1
Content-Type: application/json
User-Agent: Kinsy-Webhooks/1
X-Kinsy-Event: deck.done
X-Kinsy-Delivery: dlv_3f9a0c1d2e4b5a6978c0d1e2
X-Kinsy-Signature: t=1793610000,v1=5d41402abc4b2a76b9719d911017c592…

{"id": "dlv_3f9a0c1d2e4b5a6978c0d1e2", "type": "deck.done", "created_at": "2026-11-02T09:01:40+00:00",
 "data": {"deck_id": "deck-4c9e01", "status": "done", "phase": "done", "progress": 1.0,
          "slides_rendered": 8, "slides_total": 8, "title": "Q3 board update", "client": "api"}}
EventWhen
deck.doneThe deck is finished. Fetch the files with GET /v1/decks/{id}.
deck.errorThe build failed; data.error says why in one sentence. Failed builds are refunded.
deck.edit.doneA whole-deck edit finished; data.edit.changed lists the slides it changed.
deck.edit.errorA whole-deck edit failed and was refunded; data.edit.error says why.

data is the same status GET /v1/decks/{id} returns, without download links: call that route with your key for fresh ones. Download links never travel in a callback, so they never end up in your request logs.

Verify the signature

The signature is HMAC-SHA256, keyed with your secret, over the timestamp, a dot, and the raw request body. Compare in constant time, and refuse a timestamp more than five minutes old so a captured request cannot be replayed.

import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes, tolerance_s: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = int(parts["t"]), parts["v1"]
    if abs(time.time() - t) > tolerance_s:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
import crypto from "node:crypto";

export function verify(secret, header, rawBody, toleranceS = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > toleranceS) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
  return expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Verify against the bytes you received, before any JSON parsing or re-serialising.

Delivery and retries

Follow a build live

GET /v1/decks/{id}/events streams the build's progress as server-sent events. Use the api.kinsy.ai host for streams:

curl -N https://api.kinsy.ai/v1/decks/deck-4c9e01/events \
  -H "Authorization: Bearer $KINSY_API_KEY"
id: 0
event: started
data: {"seq":0,"phase":"started","at":"2026-11-02T09:00:01.120+00:00"}

id: 3
event: plan_ready
data: {"seq":3,"phase":"plan_ready","at":"…","title":"Q3 board update","slides":[…]}

event: end
data: {"status":"done","reconnect":false}

Whole-deck edits

POST /v1/decks/{id}/edits with an instruction and no slide applies the instruction to every slide in the background. It answers 202 with an edit_id; follow it on GET /v1/decks/{id}, whose edit block moves from queued to running to done (with changed, the slides it changed) or error. It costs 2 credits per slide, charged when it is queued and refunded in full if it fails. One whole-deck edit runs at a time per deck, and a deck still being built cannot be edited (409).