Subscribe a URL and LensWave POSTs signed JSON events to your server. This page documents the channel as it exists in the current build — every algorithm, header and field name below is read straight from the server source.
Register an endpoint
Authenticated endpoints (you must be signed in):
| Method | Path | Purpose |
|---|---|---|
| GET | /api/webhooks | List your active subscriptions (max 50). |
| POST | /api/webhooks | Create a subscription. Body: { "event": "task.completed", "url": "https://...", "secret": "whsec_..." }. |
| DELETE | /api/webhooks/:id | Disable a subscription (soft delete, status='disabled'). |
Use "event": "*" to subscribe to every event as it ships. A delivery row is inserted into webhook_deliveries for every attempt, so you can audit failures server-side.
Event catalog
Envelope for every delivery:
task.completed planned
Trigger: a generation task finishes successfully and its result URL is ready.
Payload data:
task.failed planned
Trigger: a generation task exits with an error (provider error, content refusal, timeout).
Payload data:
balance.low planned
Trigger: wallet balance drops below the configured low-water threshold after a charge.
Payload data:
subscription.changed planned
Trigger: subscription tier, billing cycle or renewal state changes (after Creem webhook settlement).
Payload data:
Signature verification (live algorithm)
Every request LensWave sends carries three headers, computed by server/reqsign.js — this is the shipping implementation, not a draft:
| Header | Value |
|---|---|
| X-Sign-Ts | Unix timestamp in seconds. Reject if |now − ts| > 300s (replay protection). |
| X-Sign-Nonce | Random 16-byte hex string (32 chars). Reject any nonce shorter than 8 or longer than 256 chars; reject repeats within 5 minutes. |
| X-Sign | base64( HMAC_SHA256(secret, stringToSign) ). |
The stringToSign is built verbatim like this (newlines included):
Verify with a constant-time comparison. The secret is the value you provided (or received) when you created the subscription — it begins with whsec_. Always sign over the raw request body, not a re-serialized version; field order and whitespace must match byte-for-byte.
Retry & delivery semantics
LensWave POSTs the envelope to your URL with User-Agent: LensWave-Webhook/1.0 and a 10-second timeout.
Any 2xx response is marked success. Anything else (including network errors) counts as a failure.
Failed deliveries retry with exponential backoff: 1s · 2^(attempts−1), capped at 10s, up to 5 attempts. After that the row is marked failed and dropped.
Return 2xx as fast as possible. If you need to do heavy work, enqueue it on your side and acknowledge immediately.
cURL example
Register a subscription (run from a signed-in browser session, or swap in your session cookie):
Your endpoint will receive a POST like this (showing the signature headers):
Source references: server/index.js (dispatchOutbound, attemptDelivery, /api/webhooks CRUD), server/reqsign.js (signOutbound, verifySign), server/db.js (webhooks / webhook_deliveries tables). Internal events task_enqueued and order_paid exist on the outbox but are not yet bridged to outbound webhooks.