Help · admin

Outbound webhooks

Wire Cuelist events into your own tooling. Event reference + HMAC verification recipe.

Webhooks let your tooling react to what happens in Cuelist — a new cue gets created, a project is archived, a teammate joins, a subscription ends. We POST a signed JSON envelope to an HTTPS URL you control; you respond with a 2xx and we move on.

Configure

Open Admin → Integrations, expand "Add endpoint," paste an HTTPS URL, and tick the events you want. The signing secret is shown once on creation — store it on your endpoint side immediately.

Delivery envelope

Every request carries:

  • X-Cuelist-Event — the event kind (e.g. cue.created).
  • X-Cuelist-Delivery — a unique id for this delivery attempt. Use it to dedupe replays.
  • X-Cuelist-Attempt — 1-indexed retry counter.
  • X-Cuelist-Signature — the HMAC-SHA256 signature (see below).
  • User-Agent: Cuelist-Webhook/1.0.
  • A JSON body shaped like: {"kind":"cue.created","orgId":"...","occurredAt":"...","payload":{...}}

Verifying the signature

Compute HMAC-SHA256(secret, {timestamp} + "." + body) and compare to the v1 half of the signature header (t={ts},v1={hex}). Reject requests where the timestamp is more than 5 minutes off your clock — that defeats replay attacks.

Node.js example:

import crypto from "node:crypto";

function verify(req, secret) {
  const header = req.headers["x-cuelist-signature"];
  const [t, v1] = header.split(",");
  const ts = t.slice(2);
  const sig = v1.slice(3);
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${req.rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(sig),
    Buffer.from(expected),
  );
}

Event kinds

  • project.created — new project. Payload: slug, title, subtitle, venue.
  • project.updated — patch applied to project metadata. Payload: projectId, slug, patch.
  • project.archived / project.deleted — project lifecycle.
  • document.uploaded — new script bytes received. Payload: projectId, documentId, versionId, title, sizeBytes, converted (true when a Fountain or FDX upload was typeset to PDF on the way in).
  • cue.created / cue.updated / cue.deleted — cue lifecycle.
  • annotation.created / annotation.resolved — annotation lifecycle (producers ship in a follow-up release; subscribe now to opt in early).
  • member.added — invite was accepted. Payload: userId, email, name.
  • member.removed — member removed by an org admin.
  • billing.subscription.active — Stripe subscription became active (covers new + reactivation).
  • billing.subscription.updated — plan changed mid-term (upgrade/downgrade, proration applied).
  • billing.payment_failed — an invoice payment failed; the subscription is at risk until it’s resolved.
  • billing.subscription.canceled — Stripe sub deleted; org dropped back to free.

Retry policy

Any non-2xx response or a network timeout (30 s) triggers retry. Six attempts spaced exponentially:

  • +1 minute
  • +5 minutes
  • +30 minutes
  • +2 hours
  • +12 hours
  • +24 hours

After the sixth failure the delivery is marked failed and the endpoint's consecutive_failures counter increments. Ten consecutive failures auto-disables the endpoint — you'll see it greyed out in the admin UI until you re-enable. Each successful 2xx resets the counter to 0.

Replaying a delivery

The admin UI's "Recent deliveries" section has a Replay button next to each delivery. Replay re-queues the same payload (signature is recomputed with the current timestamp so the body bytes stay identical but the signature header rotates).

Tips

  • Idempotency: store the X-Cuelist-Delivery id and short-circuit duplicate deliveries — retries can race a slow consumer.
  • Respond fast: do the work async. We don't care what your endpoint does internally, only that it returns 2xx within 30 s.
  • Quiet 4xx: a 4xx response counts as a failed delivery. If you want to drop an event silently, return 204.