PL Log inOpen Studio
Preview version. Model catalog, prices, calculator and docs are live. Generation goes live at launch.

Webhooks

Docs: every page

Instead of asking the API over and over whether a task is done, you can let Kadraris tell you. Add webhook_url to a POST /v1/generate request, and when the task ends as succeeded or failed, a POST request goes to that address.

What you receive

Body

It is the task object, exactly as GET /v1/tasks/{id} would return it:

json
{
  "id": "task_8f2k...",
  "status": "succeeded",
  "model": "seedance-2.5",
  "output": [{ "type": "video", "url": "https://...", "duration": 5, "width": 1280, "height": 720 }],
  "cost": { "amount": "0.77", "currency": "USD" },
  "error": null,
  "created_at": "2026-09-27T10:15:02Z",
  "completed_at": "2026-09-27T10:16:41Z"
}

Headers

HeaderContent
Content-Typeapplication/json
x-kadraris-eventEither task.succeeded or task.failed
x-kadraris-signaturesha256=<hex>: an HMAC-SHA256 of the unmodified body, keyed with your webhook secret

The secret is in your dashboard. Treat it like an API key and keep it server-side.

Checking the signature

Anyone who knows your URL can send it a request, so verify each one:

  1. Take the body bytes as they arrived, before a JSON parser touches them.
  2. Compute HMAC-SHA256 over those bytes with your secret.
  3. Compare the result with x-kadraris-signature using a constant-time comparison.
  4. If they differ, reject the request.
python
# pip install flask
import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["KADRARIS_WEBHOOK_SECRET"].encode()
seen = set()  # in production, keep these in a database

@app.post("/kadraris-hook")
def kadraris_hook():
    raw = request.get_data()  # unparsed bytes
    expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    received = request.headers.get("x-kadraris-signature", "")
    if not hmac.compare_digest(expected, received):
        abort(401)

    task = request.get_json()
    event = request.headers.get("x-kadraris-event")
    if task["id"] in seen:
        return "", 200  # already handled
    seen.add(task["id"])

    enqueue_processing(task, event)  # your function; reply quickly
    return "", 200
javascript
// npm install express
import express from "express";
import crypto from "node:crypto";

const app = express();
const SECRET = process.env.KADRARIS_WEBHOOK_SECRET;
const seen = new Set(); // in production, keep these in a database

app.post("/kadraris-hook", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const received = req.get("x-kadraris-signature") ?? "";
  const ok =
    expected.length === received.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
  if (!ok) return res.sendStatus(401);

  const task = JSON.parse(req.body.toString("utf8"));
  if (seen.has(task.id)) return res.sendStatus(200);
  seen.add(task.id);

  queueMicrotask(() => processTask(task, req.get("x-kadraris-event"))); // your function
  res.sendStatus(200);
});

app.listen(3000);

The signature check fails most often because the handler hashes JSON.stringify(req.body) after a JSON middleware has already parsed the body. Serializing it again does not reproduce the original bytes, so the hashes never match. Hash the raw body.

Redelivery schedule

Kadraris sends the first attempt the moment the task completes. Any response outside the 2xx range, or no response at all, counts as a failure, and the delivery is repeated up to 5 more times with longer pauses:

Attempt after the firstPause before it
110 s
21 min
35 min
430 min
52 h

If the fifth retry fails too, that delivery is abandoned. The task is not lost: GET /v1/tasks/{id} still returns it.

Writing a reliable handler

  • Answer within moments. Store the body, push slow work such as file downloads or transcoding onto a queue, and reply 2xx. A slow reply is treated as a failure and causes a retry.
  • Expect the same task twice. Your 200 can get lost in transit, and then a retry arrives for something you already handled. Remember the task ids you processed and skip repeats.
  • Fetch the files without delay. The links in output expire.
  • Plan for downtime. If your server was offline longer than the retry schedule covers, look up the tasks you started but never got a call for with GET /v1/tasks/{id}.

Trying it on your machine

Kadraris has to reach webhook_url over the public internet. While developing, open a tunnel to your local port with any tunneling tool and use its public address as webhook_url. Another option is to send a saved payload to your handler with curl and sign it yourself:

bash
BODY='{"id":"task_test","status":"succeeded","model":"gpt-image-2.5","output":[],"cost":{"amount":"0.00","currency":"USD"},"error":null}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$KADRARIS_WEBHOOK_SECRET" | sed 's/^.* //')
curl -X POST http://localhost:3000/kadraris-hook \
  -H "Content-Type: application/json" \
  -H "x-kadraris-event: task.succeeded" \
  -H "x-kadraris-signature: sha256=$SIG" \
  -d "$BODY"