TaadDocs
Help CenterAPIPolicies

Webhooks

Hear about new recordings, notes, to-dos and events the moment they happen.

A webhook endpoint is an address on your server. When something changes in Taad, we send it an HTTP POST with the event, signed so you know it came from us.

Add an endpoint

In Settings › Developers › Webhooks, or with the API (a key with the webhooks scope):

curl https://api.taad.app/v1/webhook_endpoints \
  -H "Authorization: Bearer $TAAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/taad-webhooks", "events": ["recording.ready", "todo.created"] }'

The answer carries the endpoint's signing secret (whsec_…), shown this once. Leave events out to get every event. The address must be https and reachable from the internet.

An endpoint added with a personal key hears about your Personal workspace; one added with an organization's key hears about that organization; one added with a test key hears about your sandbox.

Events

EventWhen
recording.createda recording you sent through the API was received
recording.readya recording's transcript and notes are ready
recording.faileda recording couldn't be transcribed
recording.updatedits title, transcript, notes, speakers or workspace changed (data.changed says which)
recording.removedit was deleted, merged into another, or moved out of reach (data.reason)
todo.created, todo.updated, todo.completed, todo.deleteda to-do changed
calendar_event.created, calendar_event.updated, calendar_event.deleteda calendar event changed
usage.balance_low, usage.exhaustedyour API wallet is under $1, or empty
endpoint.testyou pressed Send test (sent only to that endpoint, never listed)

What arrives

{
  "id": "evt_06F8Q3W1ZJ4K2M9N7P5R3T1V0X",
  "object": "event",
  "type": "recording.ready",
  "created_at": "2026-11-03T10:03:40.000Z",
  "livemode": true,
  "data": {
    "object": { "id": "rec_06F8Q3W1ZJ4K2M9N7P5R3T1V0X", "object": "recording", "title": "Pricing call with Northwind", "status": "ready" },
    "transcript": [{ "speaker": { "id": "sp_1", "name": "Sara", "is_you": true }, "start_ms": 1200, "end_ms": 5400, "text": "Shall we start with the numbers?" }],
    "notes": { "summary_markdown": "…", "taad_notes_markdown": "…" }
  }
}

Everything is in the event, the transcript and notes included, so most flows need no second call. When an event and a GET disagree (events can arrive late or twice), the GET is right.

Check the signature

Every delivery is signed with Standard Webhooks, so any of its libraries can check it. The headers:

HeaderWhat
webhook-idthe event's id: the same on every retry
webhook-timestampwhen this attempt was sent, in seconds
webhook-signaturev1, and a base64 HMAC-SHA256

The signature is over {webhook-id}.{webhook-timestamp}.{the raw body}, keyed with your secret's bytes (base64 after whsec_). Check it against the raw body, before parsing the JSON, and refuse timestamps more than five minutes old.

import { createHmac, timingSafeEqual } from "node:crypto";

export function isFromTaad(headers: Headers, rawBody: string, secret: string): boolean {
  const id = headers.get("webhook-id") ?? "";
  const ts = headers.get("webhook-timestamp") ?? "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = Buffer.from(createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64"));
  return (headers.get("webhook-signature") ?? "").split(" ").some((part) => {
    const [version, sig = ""] = part.split(",");
    const given = Buffer.from(sig);
    return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
  });
}

While you rotate a secret, deliveries are signed with both the old and the new one for 24 hours, so you can switch without missing any.

Answering, retries and replays

  • Answer 2xx within 10 seconds, then do the work. Anything else counts as a failure.
  • 408, 429, 5xx, timeouts and network errors are retried: after 30 seconds, 2, 5, 15 and 30 minutes, then 1, 2, 6, 12 and 24 hours. Redirects aren't followed; other 4xx answers aren't retried.
  • Deliveries can arrive twice or out of order: skip an id you've already handled.
  • After four days without a success the endpoint is turned off and we email you. When you turn it back on (Settings, or POST /v1/webhook_endpoints/{id}/enable), everything it missed is sent again.

Catching up

Every event is kept for 30 days, delivered or not, and any key can list them, though a key only sees the kinds of events its scopes cover (recordings need recordings:read, to-dos todos:read, calendar events calendar:read; a key with webhooks sees them all, as its endpoints receive them). GET /v1/events?after=<cursor> lists what happened since your last look, so a system that was down can catch up without webhooks at all. A cursor older than 30 days answers 410 cursor_expired: start again from GET /v1/recordings?updated_after=….

On this page