Skip to navigation

Webhooks

Get an HTTPS request when something happens in your workspace, signed so you can trust it.

Webhooks tell your systems about events as they happen: a meeting ended, a summary is ready, a document finished processing. Rydhm sends each event as a JSON POST to the endpoints you choose.

Deliveries follow the Standard Webhooks format, so any Standard Webhooks or Svix library can verify them.

Add an endpoint

The Add endpoint panel with an endpoint URL, events and experts
Add endpoint.

Only workspace admins manage webhooks, in the app. API keys can’t create or change endpoints.

1

Open Webhooks

Go to Developers → Webhooks and choose Add endpoint.

2

Enter the URL

Under Endpoint URL, enter a full https:// address that the public internet can reach. Addresses with a username or password, and addresses that resolve to private or internal networks, are refused. A Description is optional.

3

Choose events

Under Events, pick the events you want, or Select all. Subscribing to all events also gets you event types added later.

4

Choose experts

Under Experts, keep All experts or pick Only these. An endpoint limited to some experts gets only the events about them.

5

Save the signing secret

Choose Add endpoint. Copy the Signing secret (whsec_…) and store it with your server’s other secrets. You need it to verify deliveries.

A workspace can have up to 10 endpoints. To check an endpoint, open it and choose Send test event. Rydhm sends a webhook.test event to that endpoint only, even if it doesn’t subscribe to it.

Events

EventGroupWhen it is sent
conversation.startedConversationsA conversation with an expert began: a chat in the app or on a public page, or a browser voice call.
conversation.endedConversationsA voice conversation ended. Sent once, when the call’s session report arrives. Text chats have no end and don’t send this.
call.endedConversationsA voice call with an expert finished, with its billable minutes. Sent once per call.
meeting.scheduledMeetingsAn expert was sent to (or booked into) a video meeting.
meeting.joiningMeetingsThe meeting bot is on its way in (dispatched, or waiting in the lobby).
meeting.joinedMeetingsThe expert is in the call: the meeting is live.
meeting.join_failedMeetingsThe expert couldn’t get into the meeting: the bot couldn’t be dispatched, was refused, or failed before the call went live.
meeting.endedMeetingsA meeting reached its final state: the call ended (completed), it was cancelled, or the bot failed after the call had gone live. meeting.status says which.
meeting.summary.readyMeetingsThe post-meeting summary is ready. Carries a short snippet and the counts; fetch the meeting for the full summary.
meeting.summary.failedMeetingsThe post-meeting summary couldn’t be generated. It can be regenerated from the meeting.
knowledge.document.readyKnowledgeA knowledge document finished processing and the expert can now answer from it.
knowledge.document.failedKnowledgeA knowledge document couldn’t be processed. Nothing from it is searchable.
tool.call.failedToolsAn expert called a tool and it failed (a custom webhook tool, a built-in action or an MCP tool). Arguments are never included.
expert.createdExpertsA new expert was created in the workspace.
expert.updatedExpertsAn expert’s settings changed. Lists which fields changed, never their values.
expert.deletedExpertsAn expert was deleted.
webhook.testWebhooksA test event, sent when you choose Send test event. Only ever goes to the endpoint you tested.

Every meeting that reaches a final state sends exactly one of meeting.ended or meeting.join_failed.

The payload

Every delivery has the same envelope:

{
"id": "evt_01JBX7Q8M4N2P6R9S3T5V7W9YA",
"type": "meeting.summary.ready",
"created_at": "2026-11-04T10:48:20.000Z",
"workspace_id": "6f1c2a9e-3b7d-4c55-9a0e-1d2f3a4b5c6d",
"data": {
"meeting": {
"id": "9d4e5f60-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
"title": "Weekly pipeline review",
"ended_at": "2026-11-04T10:47:40.000Z"
},
"expert_id": "2b8e4d10-7c3a-4f9b-8e21-5a6b7c8d9e0f",
"expert": { "id": "2b8e4d10-7c3a-4f9b-8e21-5a6b7c8d9e0f", "name": "Ava" },
"summary": {
"snippet": "The team agreed to move the Acme renewal to Q1 and to send pricing by Friday.",
"key_points": 6,
"action_items": 3,
"decisions": 2,
"topics": ["renewals", "pricing"],
"generated_at": "2026-11-04T10:48:20.000Z"
},
"links": {
"app": "https://app.rydhm.ai/conversations?id=9d4e5f60-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
"api": "https://api.rydhm.ai/v1/meetings/9d4e5f60-1a2b-4c3d-8e9f-0a1b2c3d4e5f"
}
}
}
FieldWhat it is
idThe event id, evt_…. A retried delivery carries the same id. Dedupe on it.
typeOne of the events.
created_atWhen the event happened (ISO 8601), not when this attempt was sent.
workspace_idThe workspace the event belongs to.
dataThe event’s details. Fields are snake_case.

What data contains

Payloads carry ids, names, statuses, timestamps, counts and links. They never carry a transcript, a message, an expert’s instructions, tool arguments or anything secret. To get the full record, call the API at data.links.api with a key that has the right scope. data.links.app opens the same record in the app.

Most events carry expert_id and expert (id and name).

EventsMain object in data
conversation.started, conversation.endedconversation: id, channel, times, and on ended also duration_seconds, message_count, end_reason
call.endedcall: id, conversation_id, times, duration_seconds, minutes, disconnect_reason
meeting.* (except summaries)meeting: id, title, status, previous_status, platform, mode, times, duration_seconds; plus reason on failures and transcript_segments on meeting.ended
meeting.summary.ready, meeting.summary.failedmeeting, and summary (snippet and counts) or reason
knowledge.document.*document: id, filename, mime_type, status, and chunks or error
tool.call.failedtool (id, name) and call: modality, status, duration_ms, error
expert.*expert, actor, and on expert.updated the changed_fields
webhook.testtest: true, message, endpoint_id

A payload larger than 256 KB is replaced by a stub with truncated: true, the expert_id and the links. Fetch the record through links.api instead.

Headers

Every delivery is a POST with these headers:

HeaderValue
content-typeapplication/json
user-agentRydhm-Webhooks/1
webhook-idThe event id, the same as the body’s id.
webhook-timestampWhen this attempt was signed, in Unix seconds.
webhook-signaturev1,<signature>. During a secret rotation, two space-separated entries.

Verify the signature

Check every delivery before you trust it. The signature is an HMAC-SHA256, computed like this:

  1. Take the signing secret, remove the whsec_ prefix and base64-decode the rest. That is the HMAC key.
  2. Build the signed content: {webhook-id}.{webhook-timestamp}.{raw body}.
  3. Compute HMAC-SHA256 of the content with the key, and base64-encode it.
  4. Compare it with each v1, entry in webhook-signature. Any match is valid.
  5. Reject the request if webhook-timestamp is more than 5 minutes (300 seconds) from your clock. This stops replays.

Verify the raw request body, exactly the bytes you received. If your framework parses the JSON first and you serialize it again, the bytes can change and every check fails.

verify.ts
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verifyRydhmWebhook(
secret: string, // "whsec_…"
headers: Record<string, string | string[] | undefined>,
rawBody: string | Buffer, // the exact bytes received
): boolean {
const id = String(headers["webhook-id"] ?? "");
const timestamp = String(headers["webhook-timestamp"] ?? "");
const signatures = String(headers["webhook-signature"] ?? "");
if (!id || !timestamp || !signatures) return false;
const sentAt = Number(timestamp);
if (!Number.isFinite(sentAt) || Math.abs(Date.now() / 1000 - sentAt) > TOLERANCE_SECONDS) {
return false;
}
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key)
.update(`${id}.${timestamp}.`)
.update(rawBody)
.digest();
// Two signatures arrive during a secret rotation: any match is enough.
return signatures.split(" ").some((entry) => {
const [version, signature] = entry.split(",");
if (version !== "v1" || !signature) return false;
const given = Buffer.from(signature, "base64");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}

With Express, read the body raw on this route:

server.ts
import express from "express";
import { verifyRydhmWebhook } from "./verify";
const app = express();
app.post("/webhooks/rydhm", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyRydhmWebhook(process.env.RYDHM_WEBHOOK_SECRET!, req.headers, req.body)) {
return res.status(400).send("Invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
// Dedupe on event.id, queue the work, and answer quickly.
res.sendStatus(204);
});

The same scheme signs the requests your experts send to your custom tools, with the workspace’s own secret under Developers → Webhooks → Tool call signing. The same code verifies them. See Tools.

Respond quickly

Answer with any 2xx status within 10 seconds. Do the real work in a background job.

  • A 2xx marks the delivery delivered.
  • A redirect (3xx) is a failure: redirects are not followed.
  • 410 Gone tells Rydhm to stop. The endpoint is disabled at once.
  • Anything else, a timeout, or a connection error is a failure and is retried.

Retries

The first attempt is immediate. After a failure, Rydhm retries on this schedule:

AttemptWait before it
230 seconds
32 minutes
410 minutes
51 hour
66 hours
712 hours
824 hours

That is 8 attempts over about two days. Each wait varies by up to 10% so retries don’t arrive in bursts. After the last attempt, the delivery is marked dead.

Retries carry the same webhook-id, so dedupe on it. Deliveries can arrive out of order; use created_at to order events.

Disabled endpoints

An endpoint that fails 15 or more times in a row, over at least 24 hours, is disabled automatically. Workspace admins get an email and a notification in the app, and an alert opens under Developers → Alerts.

Events that happen while an endpoint is disabled are not sent to it. Fix the endpoint, then open it in Developers → Webhooks and choose Enable.

The delivery log

Open an endpoint in Developers → Webhooks to see its deliveries: status, attempt number, your endpoint’s response status, and the time. Open a delivery to see the request body, the first 2 KB of your response, and any error.

From a delivery, Resend sends the same event again, with the same webhook-id.

Deliveries and events are kept for 30 days.

Rotate the signing secret

Open the endpoint and, under Signing secret, choose Rotate. For 24 hours the old secret keeps signing too: each delivery carries two signatures, one per secret. Deploy the new secret within that window. The verification code above accepts either.

Reveal shows the current secret again if you need it.