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.