Webhooks

Early access. Capvo is rolling out gradually. API and webhook access is enabled for early-access accounts — join the waitlist.

Webhooks let your systems react to call events the moment they happen. No polling. The Capvo backend POSTs a JSON payload to your endpoint, signs it with HMAC-SHA256, and retries on failure.

Setup

You can register a webhook endpoint in two ways:

From the desktop app. Open Settings → Webhooks → Add endpoint. Paste your URL, choose the events you care about, and save. The signing secret is shown once; copy it before navigating away.

From the API. See POST /v1/webhooks. The response returns the same secret, again shown only once.

Endpoint URLs must be https:// and publicly reachable — URLs that resolve to private or loopback addresses are rejected at registration and re-checked at delivery time. Each endpoint can subscribe to one or more events.

Events

EventFires when
meeting.startedA recording has started.
meeting.transcribedA call has finished and its transcript is ready.
meeting.completedAlias for meeting.transcribed.
summary.readyAn AI summary has finished generating.

meeting.transcribed and meeting.completed are aliases for the same moment. An endpoint subscribed to either (or both) receives a single delivery, labelled with the event name it subscribed to.

Payload format

Every webhook delivery has the same envelope:

webhook.json
{
  "event": "meeting.transcribed",
  "created_at": "2026-04-27T14:32:55Z",
  "data": {
    "id": "fd1c2b0e-08a2-4ee0-ab26-9f3a7a14b2c1",
    "title": "Q2 Revenue Review",
    "platform": "meet",
    "duration_seconds": 1842,
    "started_at": "2026-04-27T14:02:11Z",
    "ended_at": "2026-04-27T14:32:53Z",
    "transcript": [
      { "sequence": 1, "speaker": "Albert", "content": "Let's start with the numbers.", "start_time": 4.2, "end_time": null },
      { "sequence": 2, "speaker": "Maya", "content": "Revenue is up 18% quarter over quarter.", "start_time": 9.1, "end_time": null }
    ],
    "summary": null
  }
}

data.id is the meeting UUID and is stable across retries, so use it as your idempotency key: if you process the same id twice for the same event, drop the second.

The summary.ready event has the same envelope with data.summary populated (context, decisions, action items, participants — see the API reference for the full shape).

interface WebhookPayload<T> {
  event: 'meeting.started' | 'meeting.transcribed' | 'meeting.completed' | 'summary.ready'
  created_at: string
  data: T
}

Alongside the signature headers below, every delivery carries an X-Capvo-Event header with the event name, so you can route before parsing the body.

HMAC verification

Capvo signs every delivery with HMAC-SHA256 using the endpoint's secret. The signature is sent in the X-Capvo-Signature header in the form:

X-Capvo-Signature: t=1714224728,v1=8a3f5c…
X-Capvo-Timestamp: 1714224728
  • t is the Unix timestamp at which the signature was generated (also sent separately in X-Capvo-Timestamp).
  • v1 is the hex-encoded HMAC-SHA256 of t + "." + raw_body keyed with your secret.

Reject requests where the timestamp is more than 5 minutes old, or where the signature does not match.

Node.js

verify.ts
import crypto from 'node:crypto'
import express from 'express'
 
const app = express()
 
app.post(
  '/capvo',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.header('x-capvo-signature') ?? ''
    const parts = Object.fromEntries(
      header.split(',').map((kv) => kv.split('='))
    ) as { t?: string; v1?: string }
 
    if (!parts.t || !parts.v1) return res.status(400).end()
 
    const tooOld = Math.abs(Date.now() / 1000 - Number(parts.t)) > 5 * 60
    if (tooOld) return res.status(400).end()
 
    const expected = crypto
      .createHmac('sha256', process.env.CAPVO_WEBHOOK_SECRET!)
      .update(`${parts.t}.${req.body.toString('utf8')}`)
      .digest('hex')
 
    const ok = crypto.timingSafeEqual(
      Buffer.from(expected, 'hex'),
      Buffer.from(parts.v1, 'hex')
    )
    if (!ok) return res.status(400).end()
 
    const payload = JSON.parse(req.body.toString('utf8'))
    // …handle payload
    res.status(200).end()
  }
)

Python

verify.py
import hmac
import hashlib
import os
import time
from flask import Flask, request, abort
 
app = Flask(__name__)
SECRET = os.environ["CAPVO_WEBHOOK_SECRET"].encode()
 
@app.post("/capvo")
def capvo():
    header = request.headers.get("X-Capvo-Signature", "")
    parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
 
    t = parts.get("t")
    v1 = parts.get("v1")
    if not t or not v1:
        abort(400)
 
    if abs(time.time() - int(t)) > 5 * 60:
        abort(400)
 
    body = request.get_data()  # raw bytes, do NOT use request.json here
    expected = hmac.new(SECRET, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
 
    if not hmac.compare_digest(expected, v1):
        abort(400)
 
    payload = request.get_json(force=True)
    # …handle payload
    return "", 200

Always verify against the raw request body, not a parsed-and-reserialized version. Whitespace and key ordering matter for the signature.

Retry policy

A delivery is considered successful if your endpoint returns any 2xx status code within 10 seconds. Anything else (4xx, 5xx, timeout, TLS error, DNS failure) counts as a failed attempt.

If the initial delivery fails, Capvo retries up to 3 more times with increasing backoff:

AttemptDelay after previous attempt
1 — initial delivery
2 — first retry30 seconds
3 — second retry5 minutes
4 — third retry30 minutes

If all 4 attempts fail, the event is considered permanently failed and the endpoint is marked inactive: no further deliveries are sent to it, and the account owner is notified by email. Re-enable the endpoint from Settings → Webhooks once you've fixed the issue. Events that occurred while the endpoint was inactive are not replayed automatically — fetch them via the API if you need to backfill.

Every attempt (status code, attempt number, payload) is recorded and queryable via GET /v1/webhooks/:id/deliveries, so you can debug failures without waiting for the next event.