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 signedDocumentation Index
Fetch the complete documentation index at: https://docs.lucenthq.com/llms.txt
Use this file to discover all available pages before exploring further.
POST to the URL you
register, with a JSON body describing the event.
Available events
| Event | When it fires |
|---|---|
issue.created | A new issue is created from session analysis or AI insights. |
issue.status_changed, issue.recurred, signal.matched,
insight.generated) are on the roadmap.
Register an endpoint
Webhooks are configured per organization, in Organization → Webhooks.Open the Webhooks section
In the dashboard, go to Organization → Webhooks
and click Add endpoint.
Set the URL and events
Enter:
- a name (max 64 characters)
- the URL Lucent should
POSTto (HTTPS, max 2048 characters) - the events you want delivered to this endpoint
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.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.
Payload shape
Every delivery is a single JSON object with stable top-level fields:| Field | Type | Description |
|---|---|---|
event | string | The event type, e.g. issue.created. |
id | uuid | Envelope id. Identical across every retry of the same event — use it to dedupe on your side. |
occurredAt | string | ISO 8601 timestamp of when the event happened in Lucent. |
data | object | Event-specific payload. The shape depends on event. |
issue.created
data field | Type | Notes |
|---|---|---|
issueId | uuid | The issue id. Use with GET /api/v1/issues/{issueId}. |
orgId | uuid | The Lucent organization that owns the issue. |
title | string | Short human-readable summary. |
description | string | null | Long-form summary, truncated to 1,000 characters with ... if longer. |
status | string | One of unresolved, ticket_created, transient, resolved. |
priority | string | One of critical, high, medium, low. |
previewUrl | string | null | URL to a preview frame of the offending session, if available. |
aiVerified | boolean | null | Whether AI verification has run and confirmed the issue. |
sourceType | string | session_analysis (issue derived from a session) or insight (issue derived from an AI insight). |
Headers
| Header | Example | Notes |
|---|---|---|
Lucent-Signature | t=1715500000,v1=3c35a6b4… | HMAC-SHA256 over ${t}.${rawBody}. See Verify the signature. |
Lucent-Webhook-Id | ea0c8c0b-4f3d-4b1c-9e7a-d4b1c8e7a4f3 | Same value as the body’s id. Identical across all retries — persist for dedup. |
Lucent-Event | issue.created | The event type. Same value as the body’s event. |
Content-Type | application/json | |
User-Agent | Lucent-Webhooks/1.0 | |
Lucent-Webhook-Test | 1 | Only 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.
- Read the raw request body. Sign the bytes Lucent sent — re-serializing parsed JSON will not match.
- Recompute
HMAC_SHA256(secret, "${t}.${rawBody}"). - Compare with
v1in constant time. - Reject the request if
tis more than 5 minutes from your server clock — this prevents replay of an old, valid request.
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-Idis 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 theLucent-Signaturevalue) will differ between attempts. The body bytes andLucent-Webhook-Idstay 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 withuser:pass@credentials are also rejected —fetchwould silently send them as Basic auth. - Public destinations only. The hostname is resolved fresh on every
delivery and its IPs are checked against
ipaddr.jsranges. Loopback, private, link-local, and other non-unicast addresses are rejected. This defends against DNS rebinding (an attacker-controlled hostname pointing at127.0.0.1between 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.
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.
whsec_xxxx…) so you can tell endpoints apart without exposing
the secret.