Seedance 2.5 720P is live — BYOK at official provider prices, zero markup.See plans
Webhooks

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.

Status: The delivery channel — registration, HMAC signing, retries and exponential backoff — is live. However, the named event catalog (task.completed, balance.low, etc.) is not yet wired into the task queue. Events marked planned below are the target schema; they are not dispatched today.

Register an endpoint

Authenticated endpoints (you must be signed in):

MethodPathPurpose
GET/api/webhooksList your active subscriptions (max 50).
POST/api/webhooksCreate a subscription. Body: { "event": "task.completed", "url": "https://...", "secret": "whsec_..." }.
DELETE/api/webhooks/:idDisable 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:

{ "event": "task.completed", "delivered_at": "2026-11-06T12:00:00.000Z", "data": { /* event-specific payload, see below */ } }

task.completed planned

Trigger: a generation task finishes successfully and its result URL is ready.

Payload data:

{ "task_id": 1024, "project_id": 88, "user_id": 12, "status": "completed", "result_url": "https://cdn.lenswave.app/r/abc.mp4" }

task.failed planned

Trigger: a generation task exits with an error (provider error, content refusal, timeout).

Payload data:

{ "task_id": 1025, "project_id": 88, "user_id": 12, "status": "failed", "error": "provider returned 503" }

balance.low planned

Trigger: wallet balance drops below the configured low-water threshold after a charge.

Payload data:

{ "user_id": 12, "balance_fen": 150, "currency": "USD" }

subscription.changed planned

Trigger: subscription tier, billing cycle or renewal state changes (after Creem webhook settlement).

Payload data:

{ "user_id": 12, "tier": "pro", "cycle": "annual", "status": "active", "order_no": "LW20261106ab12cd" }

Signature verification (live algorithm)

Every request LensWave sends carries three headers, computed by server/reqsign.js — this is the shipping implementation, not a draft:

HeaderValue
X-Sign-TsUnix timestamp in seconds. Reject if |now − ts| > 300s (replay protection).
X-Sign-NonceRandom 16-byte hex string (32 chars). Reject any nonce shorter than 8 or longer than 256 chars; reject repeats within 5 minutes.
X-Signbase64( HMAC_SHA256(secret, stringToSign) ).

The stringToSign is built verbatim like this (newlines included):

# stringToSign = ts + "\n" + nonce + "\n" + rawBody signature = base64( HMAC_SHA256(secret, "1730898000\n3f2a...\n{\"event\":...}") )

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

1

LensWave POSTs the envelope to your URL with User-Agent: LensWave-Webhook/1.0 and a 10-second timeout.

2

Any 2xx response is marked success. Anything else (including network errors) counts as a failure.

3

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.

4

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):

curl -X POST "https://api.lenswave.app/api/webhooks" \ -H "Content-Type: application/json" \ -H "Cookie: session=..." \ -d '{ "event": "*", "url": "https://your-app.example.com/lenswave-hook", "secret": "whsec_replace_with_your_own" }'

Your endpoint will receive a POST like this (showing the signature headers):

POST /lenswave-hook HTTP/1.1 Host: your-app.example.com Content-Type: application/json User-Agent: LensWave-Webhook/1.0 X-Sign-Ts: 1730898000 X-Sign-Nonce: 3f2a9c1d7e4b8a0f X-Sign: dGhpcyBpcyBhIGJhc2U2NCBoYXNo... { "event": "task.completed", "delivered_at": "2026-11-06T12:00:00.000Z", "data": { "task_id": 1024, "result_url": "https://cdn.lenswave.app/r/abc.mp4" } }

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.