Webhooks

Receive signed lifecycle events, verify every request, and connect OLVA to your product or internal systems.

Quick start

1

Add an endpoint

Choose events in Settings.

2

Save the secret

It is shown only once.

3

Verify and acknowledge

Validate the raw body, then return 2xx.

Verify signatures

Read the raw request bytes before JSON parsing. Compute HMAC-SHA256 over timestamp.rawBody, encode it as Base64, and compare it with Olva-Signature using a constant-time comparison. Reject an Olva-Timestamp older than five minutes.

import crypto from 'node:crypto';
import express from 'express';

const app = express();
app.post('/webhooks/olva', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.header('Olva-Timestamp') ?? '';
  const supplied = (req.header('Olva-Signature') ?? '').replace(/^v1=/, '');
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(401);

  const expected = crypto.createHmac('sha256', process.env.OLVA_WEBHOOK_SECRET)
    .update(`${timestamp}.${req.body.toString('utf8')}`).digest('base64');
  const valid = supplied.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(supplied), Buffer.from(expected));
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  queueEventOnce(event.id, event);
  return res.sendStatus(204);
});

Keep the signing secret in a secret manager. Never commit or log it, and avoid logging transcript payloads.

Supported events

Complete event payloads

These production-shaped fixtures are exactly what Send test signs and delivers.

Meeting started: Live capture begins. The context block identifies the originating app version, desktop platform, OS version, and architecture without device identifiers.
{
  "specversion": "1.0",
  "id": "evt_test_01",
  "type": "meeting.started",
  "source": "https://olva.ai/webhooks",
  "subject": "meeting/mtg_test_01",
  "time": "2026-07-17T22:01:00.000Z",
  "datacontenttype": "application/json",
  "dataschema": "https://olva.ai/schemas/webhooks/meeting-started.json",
  "api_version": "2026-07-17",
  "livemode": false,
  "context": {
    "app": {
      "version": "1.6.8",
      "platform": "desktop"
    },
    "os": {
      "name": "macos",
      "version": "15.5",
      "arch": "arm64"
    }
  },
  "data": {
    "id": "mtg_test_01",
    "title": "Weekly product review",
    "status": "active",
    "started_at": "2026-07-17T21:30:00.000Z",
    "completed_at": null,
    "duration_seconds": null,
    "transcript_finalized_at": null,
    "metadata": {
      "participants": [
        "You"
      ],
      "transcription_language": "en"
    }
  }
}
contextPrivacy-safe app and OS metadata captured when the meeting starts; null for older meetings.
dataThe complete event-specific resource with no extra object wrapper.
transcriptPresent only for meeting.transcribed when transcript inclusion is enabled.

Delivery, retries, and idempotency

Return a 2xx quickly and process expensive work asynchronously. OLVA retries network errors, 5xx, and selected transient 4xx responses with backoff.

Store the envelope id and ignore duplicates. Automatic retries and manual redelivery preserve the event ID and exact payload while using a new delivery ID and signature timestamp.

Local testing

The development receiver lives in memory and is never available in production. Start the web app, then run the worker in another terminal so real meeting events leave the durable outbox.

pnpm dev:web
pnpm webhook:worker:dev -- --watch

# Inspect captured requests
curl http://localhost:5173/api/dev/webhooks/receiver

Send test is delivered immediately. Meeting-started and other real lifecycle events require the worker command above.