Skip to content

Webhooks

Webhooks push lifecycle events to a URL you control, sparing you from polling GET /tasks/{id} in a loop.

Events

Type When
task.started First progress tick of a task. At most once per task.
task.progress Whenever the task crosses a 10% bucket.
task.succeeded Successful terminal state.
task.failed Unhandled exception OR business-logic failure.
task.cancelled Cooperative cancellation.
quote.ready Emitted after task.succeeded for automation.automated_cam_planning.

Register an endpoint

curl -X POST https://app.autonomiq.de/backend-api/api/public/v1/webhooks \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://your-app.example/sm-webhook",
        "event_types": ["task.succeeded","task.failed","quote.ready"]
      }'

The response is the only place you'll see the HMAC secret, so store it.

{
  "id": "0188...",
  "url": "https://your-app.example/sm-webhook",
  "event_types": ["task.succeeded","task.failed","quote.ready"],
  "enabled": true,
  "created_at": "2026-05-09T12:00:00Z",
  "secret": "5f3c7e..."
}

Empty event_types means all events.

Delivery format

Each delivery is a JSON POST with these headers:

Header Example Notes
Content-Type application/json
X-SimplyMill-Event-Type task.succeeded Same as the body's type.
X-SimplyMill-Delivery-Id UUID Dedupe on this. Retries reuse it.
X-SimplyMill-Signature t=1715260800,v1=<hex> HMAC-SHA256, see below.

Body:

{
  "id": "<delivery_id>",
  "type": "task.succeeded",
  "created_at": "2026-05-09T12:00:00Z",
  "data": {
    "task_id": "...",
    "project_id": "...",
    "task_name": "automation.automated_cam_planning",
    "status": "success",
    "progress": 1.0,
    "result": { ... }
  }
}

Verifying the signature

The signature value is HMAC-SHA256(secret, f"{t}.{raw_body}"), hex-encoded. Always verify before trusting the payload.

Python

import hashlib, hmac, time

REPLAY_WINDOW = 300  # seconds

def verify(secret: str, body: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    try:
        ts = int(parts["t"]); v1 = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(int(time.time()) - ts) > REPLAY_WINDOW:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{ts}.".encode() + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, v1)

Node.js

import crypto from "node:crypto";
const REPLAY = 300;

function verify(secret, body, header) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const ts = Number(parts.t);
  if (!ts || Math.abs(Date.now() / 1000 - ts) > REPLAY) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.`)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(parts.v1, "hex"),
  );
}

Retries

Outcome Behaviour
2xx Marked delivered.
4xx Marked failed. No retry: the receiver has rejected the payload.
5xx / network error Exponential backoff with jitter, up to 6 attempts (~24h total).

Retries reuse the same X-SimplyMill-Delivery-Id. Make your handler idempotent.

Listing and revoking

curl https://app.autonomiq.de/backend-api/api/public/v1/webhooks -H "X-API-Key: $KEY"
curl -X DELETE https://app.autonomiq.de/backend-api/api/public/v1/webhooks/<id> -H "X-API-Key: $KEY"