Webhooks
Signed lifecycle callbacks, so the backend always knows the real on-device state.
A webhook endpoint is a URL the service POSTs to as each user's activity changes on the device. Register endpoints in the dashboard; each one is minted its own signing secret and can optionally be named by an identifier, unique within the team.
Routing by identifier
An activity started with a webhookIdentifier sends every event it fires to the one endpoint holding that identifier. An activity started without one sends them to every unnamed endpoint. An event that resolves to no endpoint is dropped.
That is also the whole test / sandbox story: there is no separate environment. Keep the live receiver unnamed, and start test activities with the test endpoint's identifier:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
curl -X POST https://api.webliveactivity.com/api/start \ -H 'Content-Type: application/json' \ -H 'x-api-key: wla_your_key' \ -d '{ "widgetContainerId": "acme-orders", "webhookIdentifier": "staging", "brand": { "title": "Order #1234" }, "state": { "layout": "progress", "primaryText": "Order received", "progress": 0.1 } }'
Events name the activity by its activityId and carry its group.
The events
| Event | Fired when |
|---|---|
active | The activity is confirmed live on the device: the user opened the launch link and it is on screen. |
ended | You called POST /api/end, or the activity was ended from the app. |
restarted | The activity was re-invoked: a keep-alive refresh, or a re-invoke from the app. |
The delivery
Each event is one POST with a JSON body:
1 2 3 4 5 6 7 8
{ "activityId": "act-8f2c1a", "group": "order-tracker", "event": "active", "active": true, "webhookIdentifier": "staging", "at": "2026-07-30T14:25:19.991Z" }
And three headers:
| Header | Meaning |
|---|---|
X-WebActivities-Event | The event name: active, ended, or restarted. |
X-WebActivities-Timestamp | Unix epoch seconds at delivery time. Reject anything too old to be legitimate. |
X-WebActivities-Signature | The HMAC signature described below. |
Verifying the signature
The signature is an HMAC-SHA256 of the timestamp and the raw request body, keyed with the endpoint's secret. The timestamp is inside the signed payload, not just alongside it, so a captured delivery cannot be replayed later with a fresh timestamp.
1 2 3 4 5 6 7 8 9 10 11 12
const { createHmac, timingSafeEqual } = require('node:crypto'); // rawBody is the request body EXACTLY as received, before any JSON parsing. const verify = (secret, headers, rawBody) => { const timestamp = headers['x-webactivities-timestamp']; const expected = 'sha256=' + createHmac('sha256', secret) .update(timestamp + '.' + rawBody) .digest('hex'); const presented = Buffer.from(headers['x-webactivities-signature']); return expected.length === presented.length && timingSafeEqual(Buffer.from(expected), presented); };
Compare in constant time (timingSafeEqual) so a wrong signature leaks nothing about the right one, and check the timestamp against a local clock to bound replays.
Retries
The endpoint has 8 seconds to answer with a 2xx. A failed attempt is retried up to five times, backing off exponentially from a minute, and only the fifth failure marks the delivery failed. Every delivery, its status, and its attempts are visible in the dashboard's delivery log, where you can also resend one manually.
Answering fast
Acknowledge first, work second: store the event and answer 200, then act on it. An endpoint that does slow work inline risks the 8 second timeout and a retry you will see as a duplicate. Deliveries can arrive out of order under retry, so treat the at field as the event's true time.









