Set Up Your First Webhook

Configure outbound HTTP webhook notifications triggered by document events — from creating the webhook to verifying HMAC signatures in your handler.

beginner12 min read

What You'll Build

Webhooks let Lifestream Vault push real-time notifications to your own HTTP endpoint whenever vault events occur — document created, updated, deleted, moved, or copied. Unlike polling, webhooks are push-based: your server receives the payload the moment the event fires.

By the end of this guide you will have:

  • A working webhook endpoint subscribed to document events in a vault
  • HMAC-SHA256 signature verification in your handler to reject forged payloads
  • Delivery log monitoring to inspect past deliveries and diagnose failures

Webhooks are delivered by a BullMQ worker (webhook-delivery.worker) with exponential backoff retry on failures.

Prerequisites

  • A Pro tier subscription or higher (webhooks are a Pro-tier feature)
  • A public HTTPS URL to receive payloads — use ngrok (ngrok http 3000) for local testing
  • Node.js 18+ if using the SDK or CLI

Create a Webhook

Create a webhook by specifying:

  • URL — the HTTPS endpoint that will receive POST requests
  • Events — the event types you want to subscribe to (e.g. document.created, document.updated, document.deleted, document.moved)

The webhook is immediately active after creation. Any matching events in the vault will trigger a delivery attempt. The server auto-generates an HMAC signing secret and returns it once in the creation response — store it securely for signature verification.

typescript
import { LifestreamVaultClient } from '@lifestreamdynamics/vault-sdk';

const { client } = await LifestreamVaultClient.login(
  'https://vault.lifestreamdynamics.com',
  'you@example.com',
  'your-password',
);

const VAULT_ID = 'your-vault-id';

const webhook = await client.webhooks.create(VAULT_ID, {
  url: 'https://example.com/webhook',
  events: ['document.created', 'document.updated', 'document.deleted', 'document.moved', 'document.copied'],
});

console.log('Webhook created:', webhook.id);
console.log('Active:', webhook.isActive); // true
console.log('Secret (store this!):', webhook.secret); // auto-generated, shown only once

Verify HMAC Signatures

Every webhook delivery includes an X-Webhook-Signature header in the form sha256=<hex digest>, where the digest is an HMAC-SHA256 of the raw request body computed using the secret returned at creation time.

Always verify this signature before processing the payload. Without verification, anyone who discovers your endpoint URL could send forged events.

The verification algorithm:

  1. Read the raw request body as a Buffer (before any JSON parsing)
  2. Strip the sha256= prefix from the header value
  3. Compute HMAC-SHA256(secret, rawBody) as a hex string
  4. Compare against the stripped header digest using a constant-time comparison
typescript
import express from 'express';
import crypto from 'crypto';

const app = express();

// Use express.raw() to access the raw body before JSON parsing
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.headers['x-webhook-signature'] as string | undefined;
  const secret = process.env.WEBHOOK_SECRET!;

  if (!header) {
    res.status(400).send('Missing X-Webhook-Signature header');
    return;
  }

  // The header is "sha256=<hex digest>" — strip the prefix before comparing
  const signature = header.startsWith('sha256=') ? header.slice('sha256='.length) : header;

  // Compute expected signature
  const expected = crypto
    .createHmac('sha256', secret)
    .update(req.body) // req.body is a Buffer when using express.raw()
    .digest('hex');

  // Constant-time comparison to prevent timing attacks
  const sigBuffer = Buffer.from(signature, 'utf8');
  const expBuffer = Buffer.from(expected, 'utf8');

  if (
    sigBuffer.length !== expBuffer.length ||
    !crypto.timingSafeEqual(sigBuffer, expBuffer)
  ) {
    res.status(401).send('Invalid signature');
    return;
  }

  // Signature valid — parse and handle the payload
  const event = JSON.parse(req.body.toString('utf8'));
  console.log('Received event:', event.type, event.documentId);

  res.status(200).send('OK');
});

app.listen(3000, () => console.log('Webhook server listening on :3000'));

Always use constant-time comparison. Never compare signatures with === or == — standard string comparison short-circuits on the first differing byte, making it vulnerable to timing attacks that allow an attacker to forge a valid signature byte by byte. Use crypto.timingSafeEqual() (Node.js) or hmac.compare_digest() (Python).

Test Your Webhook

After creating the webhook, trigger an event by creating or updating a document in the subscribed vault. Then check the delivery log to confirm the payload was dispatched and your handler returned a 2xx response.

Delivery log entries include:

FieldDescription
idUnique delivery ID — use as idempotency key in your handler
statusCodeHTTP response code returned by your endpoint, or null if delivery failed
attemptDelivery attempt number (retried with exponential backoff on failure)
errorError message if the delivery failed, or null
deliveredAtISO 8601 timestamp of successful delivery, or null
typescript
const VAULT_ID = 'your-vault-id';
const WEBHOOK_ID = 'your-webhook-id';

const deliveries = await client.webhooks.listDeliveries(VAULT_ID, WEBHOOK_ID);

deliveries.forEach((d) => {
  console.log(`HTTP ${d.statusCode ?? '—'} | attempt: ${d.attempt} | ${d.deliveredAt ?? 'not delivered'}`);
  if (d.error) console.log(`  Error: ${d.error}`);
  console.log(`  Delivery ID: ${d.id}`);
});

Tips & Best Practices

  • Respond within 10 seconds. The delivery worker times out after 10 seconds and marks the delivery as failed, then retries with exponential backoff. Offload heavy processing to a background queue and return 200 OK immediately.

  • Use the payload id field for deduplication. Retried deliveries carry the same event id in the payload body. Store processed IDs in Redis or a database to make your handler idempotent.

  • Subscribe only to events you need. Subscribing to all events generates unnecessary traffic. Use the minimal set your integration requires — for example, a documentation deployment pipeline only needs document.created.

  • Your handler URL must be publicly reachable. The delivery worker runs server-side and cannot reach private/internal IP addresses. SSRF protection blocks requests to localhost, RFC 1918 ranges (e.g. 192.168.x.x, 10.x.x.x), and link-local addresses. Use ngrok or a similar tunnel for local development.

What's Next

  • Automate Your Vault with Hooks and Webhooks — combine outbound webhooks with internal hooks (auto-tag, template, etc.) for full event-driven automation
  • API Keys & Scoping — create scoped API keys for your webhook handler to call back into Lifestream Vault and update documents or trigger workflows
  • Webhook Reference — full payload format, all event types, retry policy, and delivery header reference