Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.lucenthq.com/llms.txt

Use this file to discover all available pages before exploring further.

Webhooks let Lucent push events to your service in real time, instead of you polling the Data API. When an event fires — for example, a new issue is created — Lucent makes a signed POST to the URL you register, with a JSON body describing the event.

Available events

EventWhen it fires
issue.createdA new issue is created from session analysis or AI insights.
More event types (issue.status_changed, issue.recurred, signal.matched, insight.generated) are on the roadmap.

Register an endpoint

Webhooks are configured per organization, in Organization → Webhooks.
1

Open the Webhooks section

In the dashboard, go to Organization → Webhooks and click Add endpoint.
2

Set the URL and events

Enter:
  • a name (max 64 characters)
  • the URL Lucent should POST to (HTTPS, max 2048 characters)
  • the events you want delivered to this endpoint
3

Copy the signing secret

On save, Lucent shows the signing secret once. It looks like whsec_…. Store it somewhere safe (a secret manager, your .env). The dashboard only ever shows the prefix (whsec_xxxx…) again — there is no way to retrieve the full secret later. If you lose it, revoke the endpoint and create a new one.
4

Send a test event

Use Send test to deliver a synthetic event of the type you chose. Test events carry the Lucent-Webhook-Test: 1 header so you can branch on them in your handler.

Limits

  • Up to 3 active endpoints per organization.
  • Up to 5 endpoint creations per hour per organization.
  • Test event sends are rate-limited per organization and per endpoint.
Webhooks are available on paid plans.

Payload shape

Every delivery is a single JSON object with stable top-level fields:
FieldTypeDescription
eventstringThe event type, e.g. issue.created.
iduuidEnvelope id. Identical across every retry of the same event — use it to dedupe on your side.
occurredAtstringISO 8601 timestamp of when the event happened in Lucent.
dataobjectEvent-specific payload. The shape depends on event.

issue.created

{
  "event": "issue.created",
  "id": "ea0c8c0b-4f3d-4b1c-9e7a-d4b1c8e7a4f3",
  "occurredAt": "2026-05-12T08:42:13.512Z",
  "data": {
    "issueId": "9b1c0f8b-a4e2-4ff7-8c5b-7d8a9e2f4c1b",
    "orgId": "00000000-0000-0000-0000-000000000000",
    "title": "Checkout button unresponsive on mobile",
    "description": "Multiple sessions show the checkout button failing to register taps...",
    "status": "unresolved",
    "priority": "high",
    "previewUrl": null,
    "aiVerified": true,
    "sourceType": "session_analysis"
  }
}
data fieldTypeNotes
issueIduuidThe issue id. Use with GET /api/v1/issues/{issueId}.
orgIduuidThe Lucent organization that owns the issue.
titlestringShort human-readable summary.
descriptionstring | nullLong-form summary, truncated to 1,000 characters with ... if longer.
statusstringOne of unresolved, ticket_created, transient, resolved.
prioritystringOne of critical, high, medium, low.
previewUrlstring | nullURL to a preview frame of the offending session, if available.
aiVerifiedboolean | nullWhether AI verification has run and confirmed the issue.
sourceTypestringsession_analysis (issue derived from a session) or insight (issue derived from an AI insight).

Headers

HeaderExampleNotes
Lucent-Signaturet=1715500000,v1=3c35a6b4…HMAC-SHA256 over ${t}.${rawBody}. See Verify the signature.
Lucent-Webhook-Idea0c8c0b-4f3d-4b1c-9e7a-d4b1c8e7a4f3Same value as the body’s id. Identical across all retries — persist for dedup.
Lucent-Eventissue.createdThe event type. Same value as the body’s event.
Content-Typeapplication/json
User-AgentLucent-Webhooks/1.0
Lucent-Webhook-Test1Only present on test events sent from the dashboard.

Verify the signature

Lucent signs every delivery using a Stripe-compatible scheme: HMAC-SHA256 over ${timestamp}.${rawBody}, using your endpoint’s signing secret as the key. The Lucent-Signature header is a comma-separated key/value list:
Lucent-Signature: t=1715500000,v1=3c35a6b4d9e2f0...
  • t — Unix timestamp (seconds) of when Lucent generated the signature.
  • v1 — hex-encoded HMAC-SHA256.
To verify:
  1. Read the raw request body. Sign the bytes Lucent sent — re-serializing parsed JSON will not match.
  2. Recompute HMAC_SHA256(secret, "${t}.${rawBody}").
  3. Compare with v1 in constant time.
  4. Reject the request if t is more than 5 minutes from your server clock — this prevents replay of an old, valid request.
import crypto from "node:crypto";

function verifyLucentSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
) {
  // signatureHeader looks like: "t=1715500000,v1=3c35a6b4..."
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => {
      const [k, v] = p.split("=", 2);
      return [k, v ?? ""];
    }),
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;

  // Reject replays older than 5 minutes.
  const ageSec = Math.floor(Date.now() / 1000) - Number(t);
  if (Number.isNaN(ageSec) || Math.abs(ageSec) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  // timingSafeEqual throws if buffers differ in length — guard first.
  const expectedBuf = Buffer.from(expected);
  const v1Buf = Buffer.from(v1);
  if (expectedBuf.length !== v1Buf.length) return false;
  return crypto.timingSafeEqual(expectedBuf, v1Buf);
}
Read the raw request body before any JSON parsing or middleware that might rewrite it. In Express, mount express.raw({ type: "application/json" }) for the webhook route. In Next.js Route Handlers, use await request.text().

Retries and idempotency

  • Delivery has a 10-second per-request timeout.
  • Non-2xx responses, timeouts, and network errors trigger a retry — up to 3 retries with exponential backoff after the initial attempt.
  • The same Lucent-Webhook-Id is sent on every attempt. Persist it on success and treat duplicate ids as no-ops.
  • Lucent recomputes the signature on each retry, so t (and the Lucent-Signature value) will differ between attempts. The body bytes and Lucent-Webhook-Id stay the same.
  • Because the timeout is 10 seconds, your handler should acknowledge fast: validate the signature, enqueue the work, and return 200. Do the actual processing asynchronously.

Security model

Webhook URLs go through several checks before every delivery, not just at creation:
  • HTTPS only. Plain http:// URLs are rejected. URLs with user:pass@ credentials are also rejected — fetch would silently send them as Basic auth.
  • Public destinations only. The hostname is resolved fresh on every delivery and its IPs are checked against ipaddr.js ranges. Loopback, private, link-local, and other non-unicast addresses are rejected. This defends against DNS rebinding (an attacker-controlled hostname pointing at 127.0.0.1 between checks).
  • Service ports are blocked. Common service ports — SSH (22), SMTP (25), Postgres (5432), MySQL (3306), Redis (6379), Mongo (27017), Elasticsearch (9200/9300), Memcached (11211), Docker (2375/2376), and others — are rejected so webhooks can’t be used to probe internal infrastructure.
  • Redirects are not followed. Set the final URL on your endpoint directly.
If a delivery is blocked for any of these reasons, the endpoint’s last delivery status is recorded as url_unsafe and no request leaves Lucent.

Endpoint management

Each endpoint surfaces, in Organization → Webhooks:
  • last delivery time and HTTP status
  • last delivery error label, if any (bad_status:NNN, timeout, tls_error, network_error, url_unsafe)
  • a Send test button
  • a Revoke button — revoked endpoints stop receiving deliveries immediately and free a slot against the per-org cap.
The full signing secret is shown only at creation. The dashboard always shows the prefix (whsec_xxxx…) so you can tell endpoints apart without exposing the secret.