Skip to content

Guides

Webhooks

Get a signed POST when a job succeeds, fails or is canceled, instead of polling.

Every slow operation is a job. Long-polling works well from scripts; for servers, register a webhook and Lumorig will POST to you when any job in the workspace finishes. Deliveries are signed with a per-webhook secret so you can prove they came from us.

Register an endpoint#

POST/webhooks

NameTypeDescription
urlrequiredstring (URL)Where to POST. Must be reachable from the internet.
eventsstring[]Any of job.succeeded, job.failed, job.canceled, or * for all. Default ["*"].
curl -X POST https://lumorig.com/api/v1/webhooks -H "Authorization: Bearer $LUMORIG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your.app/hooks/lumorig", "events": ["job.succeeded", "job.failed"] }'
# 201 → { "id": "whk_…", "object": "webhook", "url": "…", "events": [ … ], "secret": "whsec_…" }

Payload#

Each delivery is a POST with a JSON body: the event type, when it was sent, and the full job, exactly as GET /jobs/{id} would return it.

delivery
POST https://your.app/hooks/lumorig
content-type: application/json
lumorig-signature: t=1790000000,v1=5f2c9a…

{
  "type": "job.succeeded",
  "created_at": "2026-09-25T10:04:12.000Z",
  "data": {
    "id": "job_…",
    "object": "job",
    "type": "export",
    "status": "succeeded",
    "mascot_id": "msc_…",
    "result": { "file": { "id": "file_…", "url": "/api/v1/files/file_…", "mime": "application/json", "bytes": 48213 } },
    "error": null,
    "cost_usd": 0,
    "created_at": "…",
    "started_at": "…",
    "finished_at": "…"
  }
}

data.type tells you what finished (generate, reconstruct, edit, variant, animate, export, analyze, suggest, scene) and data.result has the same shape the job result has everywhere else. For failures, data.error holds the message.

Verifying signatures#

The signature header has a Unix timestamp and a hex HMAC-SHA256, keyed with your webhook secret, over the timestamp, a dot, and the raw body:

text
Lumorig-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, t + "." + body)>

Verify against the raw request body (before any JSON parsing), and reject old timestamps to stop replays. For now each delivery also carries the same value under the pre-rename name, MascotForge-Signature, so receivers written before the rename keep working.

import { verifyWebhook } from "@lumorig/sdk";

export async function POST(req: Request) {
  const body = await req.text();
  const ok = await verifyWebhook(body, req.headers.get("lumorig-signature") ?? "", process.env.LUMORIG_WEBHOOK_SECRET!);
  if (!ok) return new Response("bad signature", { status: 400 });

  const event = JSON.parse(body);
  if (event.type === "job.succeeded" && event.data.type === "export") {
    // event.data.result.file.id → download with GET /files/{id}
  }
  return new Response(null, { status: 204 });
}

verifyWebhook uses Web Crypto, so it runs in Node 18+, Bun, Deno and edge runtimes. Its default tolerance is five minutes; pass a fourth argument in seconds to change it.

Retries#

BehaviourValue
Attempts per delivery3
Timeout per attempt10 seconds
Wait after a failed attempt1 s, then 4 s, then 16 s
Counts as deliveredAny 2xx response

All attempts of one delivery carry the same timestamp and signature, so answer fast (queue the work, return 2xx) and make your handler idempotent on data.id. After the third failure the delivery is dropped; the job itself is unaffected, and GET /jobs/{id} still has the result.

Testing locally#

Point a webhook at a tunnel (for example cloudflared or ngrok) to your dev server, then run a free job such as an export to trigger a delivery without spending credits.

Managing webhooks#

EndpointDoes
GET/webhooksEvery webhook in the workspace: id, url, events, created_at (no secret).
DELETE/webhooks/{id}Stop deliveries to that endpoint.
ts
const hooks = await lumorig.webhooks.list();
await lumorig.webhooks.delete(hooks[0]!.id);

Webhooks belong to the workspace, not to an API key, so revoking a key doesn't stop them. Every job in the workspace is delivered, whichever surface (studio, API, MCP, CLI) started it.