Skip to Content
APIWebhooks
last verified · 2026-07-16

Webhooks

A webhook subscription tells Lumis to POST an event payload to your own URL whenever something happens in your project — instead of you polling Tickets or Decisions for changes.

There’s no update endpoint yet — to change a subscription’s URL, secret, or event types, delete it and create a new one.

Event types

  • ticket.created
  • ticket.updated
  • decision.resolved
  • audit.event
GET/api/v1/webhooks/subscriptions

List your project’s webhook subscriptions.

curl
curl https://lumis.work/api/v1/webhooks/subscriptions \ -H "Authorization: Bearer $LUMIS_MCP_TOKEN"
TypeScript
const res = await fetch('https://lumis.work/api/v1/webhooks/subscriptions', { headers: { authorization: `Bearer ${process.env.LUMIS_MCP_TOKEN}` }, }); const { subscriptions } = await res.json();
Python
res = requests.get( "https://lumis.work/api/v1/webhooks/subscriptions", headers={"authorization": f"Bearer {os.environ['LUMIS_MCP_TOKEN']}"}, ) subscriptions = res.json()["subscriptions"]
{ "version": "v1", "subscriptions": [ { "id": "w4e7...", "tenant_id": "acme-co", "url": "https://example.com/hooks/lumis", "event_types": ["ticket.created", "ticket.updated"], "active": true, "created_at": "2026-07-10T09:00:00.000Z", "updated_at": "2026-07-10T09:00:00.000Z" } ] }

The secret field is never included in list/read responses.

POST/api/v1/webhooks/subscriptions

Create a subscription.

NameTypeRequiredDescription
urlstringYesYour endpoint to receive POSTed events.
secretstringYesShared secret used to sign every delivery.
event_typesstring[]YesOne or more of the event types above.
curl
curl -X POST https://lumis.work/api/v1/webhooks/subscriptions \ -H "Authorization: Bearer $LUMIS_MCP_TOKEN" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/hooks/lumis", "secret": "whsec_...", "event_types": ["ticket.created", "ticket.updated"]}'
TypeScript
const res = await fetch('https://lumis.work/api/v1/webhooks/subscriptions', { method: 'POST', headers: { authorization: `Bearer ${process.env.LUMIS_MCP_TOKEN}`, 'content-type': 'application/json', }, body: JSON.stringify({ url: 'https://example.com/hooks/lumis', secret: process.env.WEBHOOK_SECRET, event_types: ['ticket.created', 'ticket.updated'], }), }); const { subscription } = await res.json();
Python
res = requests.post( "https://lumis.work/api/v1/webhooks/subscriptions", headers={"authorization": f"Bearer {os.environ['LUMIS_MCP_TOKEN']}"}, json={ "url": "https://example.com/hooks/lumis", "secret": os.environ["WEBHOOK_SECRET"], "event_types": ["ticket.created", "ticket.updated"], }, ) subscription = res.json()["subscription"]

An unrecognized event type returns 400 with the valid list. Returns 201 on success.

DELETE/api/v1/webhooks/subscriptions/:id

Delete a subscription. Returns { "version": "v1", "ok": true }, or 404 if it doesn’t exist.

Verifying a delivery

Every delivery is a POST with a JSON body and two headers:

NameTypeDescription
X-Lumis-EventstringThe event type, e.g. ticket.created.
X-Lumis-Signaturestringsha256=<hex hmac> — HMAC-SHA256 of the raw JSON body, keyed with your subscription's secret.
{ "event": "ticket.created", "id": "8b6b5b6e-...", "ts": "2026-07-16T18:04:22.000Z", "tenant_id": "acme-co", "data": { "id": "b3f1...", "project_key": "acme-co", "venture_key": "acme-co" } }

Recompute the HMAC over the exact raw request body with your secret and compare it to X-Lumis-Signature before trusting a delivery:

TypeScript
import { createHmac, timingSafeEqual } from 'node:crypto'; function isValidDelivery(rawBody: string, signatureHeader: string, secret: string): boolean { const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`; const a = Buffer.from(expected); const b = Buffer.from(signatureHeader); return a.length === b.length && timingSafeEqual(a, b); }

A delivery that fails (non-2xx response, or your endpoint unreachable) is retried with exponential backoff — up to 6 total attempts before it’s dead-lettered — rather than dropped after the first try.