Webhooks
Receive signed Signals events at an organisation-owned public HTTPS endpoint.
Store the secret once
The signing secret is displayed only when the destination is created or rotated. Keep it in a server-side secret store and never log it.
Event types
| Event type | When it is sent |
|---|---|
| signals.monitor.delivery | When a configured monitor has new resources to deliver. |
| integration.test | When an owner or admin sends a test delivery. |
| posting.published | When an employer publishes a job posting. |
| posting.closed | When an employer closes a job posting. |
| application.received | When an employer receives an application. |
| application.updated | When an application changes. |
| grant.revoked | When a company revokes an employer authorization grant. |
Envelope
Every body is canonical JSON using schema version 1.
| Field | Type | Required |
|---|---|---|
| schema_version | 1 | Yes |
| event_id | UUIDv7 | Yes |
| delivery_id | UUIDv7 | Yes |
| event_type | event type | Yes |
| occurred_at | RFC 3339 timestamp | Yes |
| data | event payload | Yes |
Delivery headers
| Header | Purpose |
|---|---|
| X-Luranta-Delivery-Id | Unique delivery attempt identity. Use it when investigating delivery state. |
| X-Luranta-Event-Id | Stable event identity. Use it as the primary idempotency key. |
| X-Luranta-Event-Type | The event discriminator for routing and payload handling. |
| X-Luranta-Signature | The v0 HMAC-SHA256 signature. |
| X-Luranta-Timestamp | Unix time in seconds included in the signed input. |
Verify with the SDK
Use constructEvent / construct_event as the default path. It checks the timestamp window, verifies HMAC-SHA256 over v0.<timestamp>.<raw_body>, and returns a typed event.
Read the request body as raw bytes before verification. Deduplicate business processing by event_id; retain delivery_id for per-attempt diagnostics.
Receive and verify
import { Client } from "@luranta/sdk"
const client = new Client({ apiKey: process.env.LURANTA_API_KEY! })
export async function POST(request: Request) {
const payload = await request.text()
const event = await client.webhooks.constructEvent(
payload,
request.headers,
process.env.LURANTA_WEBHOOK_SECRET!,
)
switch (event.event_type) {
case "signals.monitor.delivery":
break
case "integration.test":
break
}
return new Response(null, { status: 200 })
}import os
from luranta import Client
client = Client(api_key=os.environ["LURANTA_API_KEY"])
def handle(raw_body: bytes, headers) -> None:
event = client.webhooks.construct_event(
raw_body,
headers,
os.environ["LURANTA_WEBHOOK_SECRET"],
)
if event.event_type == "signals.monitor.delivery":
return
if event.event_type == "integration.test":
returnbody='{"data":{"action_url":"https://luranta.com/signals/monitors","resource_ids":[],"schema_version":1,"summary":"A monitored hiring signal changed.","title":"Hiring signal update"},"delivery_id":"019c0000-0000-7000-8000-000000000201","event_id":"019c0000-0000-7000-8000-000000000202","event_type":"integration.test","occurred_at":"2026-08-02T12:00:00.000Z","schema_version":1}'
timestamp=$(date +%s)
signature=$(printf '%s' "v0.$timestamp.$body" \
| openssl dgst -sha256 -hmac "$LURANTA_WEBHOOK_SECRET" -hex \
| awk '{print $NF}')
curl --request POST "http://localhost:8787/luranta-webhook" \
--header "Content-Type: application/json" \
--header "X-Luranta-Event-Id: 019c0000-0000-7000-8000-000000000202" \
--header "X-Luranta-Delivery-Id: 019c0000-0000-7000-8000-000000000201" \
--header "X-Luranta-Event-Type: integration.test" \
--header "X-Luranta-Timestamp: $timestamp" \
--header "X-Luranta-Signature: v0=$signature" \
--data-binary "$body"Acknowledgement and retries
| Receiver result | Luranta behaviour |
|---|---|
| Any 2xx | Marks that destination delivery as complete. |
| 408, 409, 425, 429 or 5xx | Retries with bounded backoff, up to 12 attempts. |
| Other non-2xx | Treats the delivery as terminal. |
| Repeated terminal deliveries | Pauses the destination after 5 consecutive failures. |
| Redirect | Does not follow it; the delivery fails. |
Endpoint requirements
- Use a public HTTPS hostname on port 443.
- Do not use credentials in the URL.
- Luranta rejects loopback, private, link-local, metadata and non-public DNS answers before every attempt.
- The maximum body size is 64 KiB and the request timeout is 10 seconds.
- Return a 2xx as soon as the verified event is durably queued; process it asynchronously.
Rotate or troubleshoot
- Create a new secret in Settings → Integrations.
- Update your receiver and send a test event.
- Check the delivery receipt for destination, event, attempt count and terminal failure code. Receipts omit secrets and payloads.
- Fix the cause, then explicitly resume a paused destination.