Webhooks let your systems react to Timelines, Moments, Collections, and
Tracking changes without polling. Subscribe to one or more triggers on
POST /v1/webhooks and the
delivery engine will POST signed events to your URL.
Subscribing
| 1 | curl -X POST https://api.webmoment.app/v1/webhooks \ |
| 2 | -H "Authorization: Bearer $WEBMOMENT_TOKEN" \ |
| 3 | -H "Content-Type: application/json" \ |
| 4 | -d '{ |
| 5 | "name": "Production listener", |
| 6 | "url": "https://api.example.com/webhooks/webmoment", |
| 7 | "secret": "whsec_…", |
| 8 | "triggers": ["timeline.created", "timeline.deleted", |
| 9 | "moment.created", "moment.updated", "moment.completed", "moment.failed", "moment.deleted", |
| 10 | "collection.created", "collection.deleted", |
| 11 | "tracking.created", "tracking.deleted", |
| 12 | "quota.moments.75_percent_reached", "quota.moments.limit_reached", "quota.moments.reset", |
| 13 | "workspace.member.added", "workspace.member.removed"] |
| 14 | }' |
| 15 | |
The secret is what your server uses to verify the
X-Webmoment-Signature header. Generate it however you like — we
just need it to be ≥ 32 bytes of entropy.
Delivery
When a trigger fires, WebMoment sends a signed POST request to the
Webhook URL you configured.
The request your server receives looks like:
| 1 | POST /webhooks/webmoment HTTP/1.1 |
| 2 | Content-Type: application/json |
| 3 | User-Agent: WebMoment-Webhooks/1.0 |
| 4 | X-Webmoment-Event-Id: evt_abc… |
| 5 | X-Webmoment-Event-Type: moment.created |
| 6 | X-Webmoment-Delivery-Id: whd_xyz… |
| 7 | X-Webmoment-Signature: sha256=…hex… |
| 8 | |
| 9 | { |
| 10 | "id": "evt_abc…", |
| 11 | "event": "moment.created", |
| 12 | "createdAt": "2026-06-05T12:00:00.000Z", |
| 13 | "data": { /* MomentEvent shape */ } |
| 14 | } |
| 15 | |
Verifying the signature
The signature covers the raw request body, hex-encoded with the
prefix sha256=. The HMAC key is the secret you registered.
| 1 | import { createHmac, timingSafeEqual } from "node:crypto"; |
| 2 | |
| 3 | export function verify(req: Request, body: string, secret: string) { |
| 4 | const header = req.headers.get("X-Webmoment-Signature") ?? ""; |
| 5 | if (!header.startsWith("sha256=")) return false; |
| 6 | const expected = |
| 7 | "sha256=" + |
| 8 | createHmac("sha256", secret).update(body, "utf8").digest("hex"); |
| 9 | const a = Buffer.from(header); |
| 10 | const b = Buffer.from(expected); |
| 11 | return a.length === b.length && timingSafeEqual(a, b); |
| 12 | } |
| 13 | |
Triggers
| Trigger | Fired when | data shape |
|---|---|---|
timeline.created | The first Capture creates a Page and its Timeline. | TimelineEvent |
timeline.deleted | The final Moment is deleted and its Page Timeline no longer exists. | TimelineEvent |
moment.created | A Capture creates a Moment in INITIAL state. | MomentEvent |
moment.updated | A Moment starts processing or reaches PARTIAL; preview and per-slot Screenshot URLs are available once preserved. | MomentEvent |
moment.completed | A Moment reaches COMPLETE; replayUrl points to the preserved index.html when Replay is enabled. | MomentEvent |
moment.failed | A Capture cannot preserve its Moment. | MomentEvent |
moment.deleted | A Moment is deleted. | MomentEvent |
collection.created | A Collection is created. | CollectionEvent |
collection.deleted | A Collection is deleted. | CollectionEvent |
tracking.created | Tracking is created for a Page. | TrackingEvent |
tracking.deleted | Tracking is deleted. | TrackingEvent |
quota.moments.75_percent_reached | The Workspace reaches 75% of its Monthly Moments allowance. | QuotaEvent |
quota.moments.limit_reached | The Workspace reaches its Monthly Moments allowance. | QuotaEvent |
quota.moments.reset | A new billing cycle starts and the Monthly Moments allowance resets. | QuotaEvent |
workspace.member.added | A person joins a Workspace. | WorkspaceMemberEvent |
workspace.member.removed | A person leaves or is removed from a Workspace. | WorkspaceMemberEvent |
QuotaEvent includes the current count, limit, billing-cycle start, and the
threshold that was reached. A reset sets monthlyMomentsCount to 0 for the
new billing cycle. WorkspaceMemberEvent includes the member's identity and
role at the time of the change.
Idempotency
Each event has a unique id (also sent as X-Webmoment-Event-Id).
Treat deliveries as at-least-once and dedupe on that value. WebMoment
does not publish a fixed retry policy, so your endpoint must be safe
to receive the same event again.
Moment lifecycle
The same Moment can produce lifecycle events as its Capture progresses:
| 1 | moment.created INITIAL |
| 2 | moment.updated STARTING or PARTIAL |
| 3 | moment.completed COMPLETE |
| 4 | moment.failed FAILED |
| 5 | |
screenshotViewportUrl and screenshotFullPageUrl are null until
the screenshots are preserved. replayUrl is null until a completed
Replay is available; when present, it points to that Moment's index.html.

