Webhooks allow you to receive real-time notifications when events occur in your Understory account. Instead of polling the API for changes, webhooks push updates to your application as they happen.
Understory webhooks follow a notification-based approach, sometimes called "thin events". Rather than including the complete resource data in each webhook payload, we send lightweight notifications containing only the resource identifiers.
This design ensures you always work with the most current data. When you receive a webhook, fetch the latest resource state via the API. This avoids issues where webhook payloads might be stale by the time you process them.
For example, when a booking is created, you receive the booking_id, event_id, experience_id, and host_id. Use these identifiers to fetch the complete booking details from the Bookings API.
All webhook events follow this general structure:
{
"id": "62564ee5-1a3d-5671-b4df-ca3bab46165a",
"type": "v1.booking.created",
"timestamp": "2026-01-29T19:29:01.856844592Z",
"payload": {
"booking_id": "bkg_abc123",
"host_id": "host_xyz",
"experience_id": "exp_456",
"event_id": "evt_789"
}
}| Field | Description |
|---|---|
id | Unique identifier for this event. Use for logging and debugging. |
type | The event type, indicating what occurred (e.g., v1.booking.created). |
timestamp | ISO 8601 timestamp with nanosecond precision of when the event occurred. |
payload | Event-specific data containing resource identifiers. |
Webhook security is critical. Always verify that incoming webhooks are genuinely from Understory before processing them.
Every webhook request includes three headers for verification:
| Header | Description |
|---|---|
webhook-id | Unique identifier for this webhook message. The same ID is used when retrying failed deliveries. Use this for deduplication. |
webhook-timestamp | Unix timestamp (seconds since epoch) of when the webhook was sent. |
webhook-signature | HMAC-SHA256 signature in the format v1,<base64> for verifying authenticity. |
The signature is computed over the string: {webhook-id}.{webhook-timestamp}.{body}
When you create a webhook subscription, you receive a secret in the response. Store this securely as it cannot be retrieved again. Use this secret to verify incoming webhooks.
import { createHmac, timingSafeEqual } from 'node:crypto';
// These values come from the incoming webhook request
const webhookId = 'msg_loFOjxBNrRLzqYUf';
const webhookTimestamp = '1731705121';
const webhookSignature = 'v1,rAvfW3dJ/X/qxhsaXPOyyCGmRKsaKWcsNccKXlIktD0=';
const body = '{"event_type":"ping","data":{"success":true}}';
// This is the secret you receive when registering the webhook subscription
const secret = 'whsec_plJ3nmyCDGBKInavdOK15jsl';
// Build the signed content string
const signedContent = `${webhookId}.${webhookTimestamp}.${body}`;
// Base64 decode the secret (remove the "whsec_" prefix first)
const secretBytes = Buffer.from(secret.split('_')[1], 'base64');
// Compute the expected signature
const expectedSignature = createHmac('sha256', secretBytes)
.update(signedContent)
.digest('base64');
// Extract the signature from the header (format: "v1,<base64>")
const actualSignature = webhookSignature.split(',')[1];
// Compare signatures using constant-time comparison to prevent timing attacks
const valid = timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(actualSignature)
);
console.log('Signature valid:', valid);The example above includes test values you can use to verify your implementation.
Always verify webhook signatures before processing. Never trust webhook data without verification, as attackers could send forged requests to your endpoint.
An attacker could capture a valid webhook and replay it later. Prevent this by validating the webhook-timestamp header.
Reject webhooks with timestamps that are too old. A tolerance of 5 minutes is recommended.
If your endpoint fails to respond with a 2xx status code within 15 seconds, the webhook is retried using an exponential backoff schedule:
- Immediately
- 5 seconds
- 5 minutes
- 30 minutes
- 2 hours
- 5 hours
- 10 hours
- 10 hours
After all retry attempts are exhausted, the message is marked as failed.
If all delivery attempts to an endpoint fail for a period of 5 days, the endpoint is automatically disabled. The 5-day period starts after multiple failures occur within a 24-hour span.
Follow these recommendations for a reliable webhook integration:
- Verify signatures: Always validate the webhook signature before processing any data.
- Check timestamps: Reject webhooks older than 5 minutes to prevent replay attacks.
- Use idempotency: Track processed
webhook-idvalues to handle duplicate deliveries gracefully. The same webhook may be sent multiple times if your endpoint returns an error. - Respond quickly: Return a
2xxstatus code within a few seconds. Process webhook data asynchronously to avoid timeouts. - Fetch fresh data: After receiving a notification, fetch the current resource state via the API. The webhook payload contains identifiers, not the full resource.
- Use HTTPS: Only configure HTTPS endpoints to protect data in transit.
Before building your webhook endpoint, you can inspect webhook payloads using testing tools.
For example, Svix Play is a free webhook testing tool that requires no signup. It provides an instant HTTPS endpoint you can use to receive and inspect webhooks.
- Visit play.svix.com to get a unique endpoint URL
- Create a webhook subscription in Understory using that URL
- Trigger events (e.g., create a booking) and inspect the incoming webhooks in your browser
- View the full request including headers, body, and signature
This is useful for understanding the webhook format before writing any code.