Skip to main content
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

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:
  • 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.
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.