POST to the URL you
register, with a JSON body describing the event.
Available events
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
POSTto (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.
Payload shape
Every delivery is a single JSON object with stable top-level fields:issue.created
Headers
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.