Guides
Signed webhooks for recording-ready workflows
Webhooks tell your pipeline when the recording is ready. Verify the HMAC signature on every delivery, then fan out to transcription, review or delivery.
A recording pipeline starts with a question: how does my code know the call ended? Polling the API on a timer works until it does not — it hammers the API for hours of silence, notices endings minutes late, and misses nothing so much as it wastes everything. The alternative is webhooks: Synento calls you the moment something happens.
Every lifecycle moment worth reacting to has an event. session.created and session.started open the story; session.ended and recording.ready say the media is ready to read; export.completed says the MP4 finished rendering; participant.joined and participant.left track the room; replay.started says someone pressed play. Your pipeline subscribes to the ones it needs and ignores the rest.
This guide subscribes to the recording events, verifies the HMAC signature on every delivery — the step most webhook integrations skip until their first incident — and builds the fan-out: transcription on session end, review on demand, delivery on export.
1. Subscribe to the recording events
Endpoints belong to a project and are created with your secret key. The signing secret is returned exactly once, at creation: store it in your environment and never log it. A typical recording pipeline wants session.ended to start work, recording.ready as the explicit media-ready signal, and export.completed when it renders MP4s.
import { SynentoClient } from '@synento/node'
const synento = new SynentoClient()
// The secret is returned exactly once: store it, never log it.
const hook = await synento.createWebhook('https://app.example.com/hooks/synento', [
'session.ended',
'recording.ready',
'export.completed',
])
process.env.SYNENTO_WEBHOOK_SECRET = hook.secret2. Verify every delivery before trusting it
Your webhook URL is public, and anyone who finds it can POST to it. The signature is what separates Synento from anyone else: each delivery carries X-Synento-Signature, the hex HMAC-SHA256 of the raw request body under your endpoint secret, plus X-Synento-Event-Type naming the event. Recompute the HMAC over the exact bytes you received and compare in constant time. Reject anything else with a 401 before parsing the body.
Two details matter. First, verify the raw body — parse-then-reserialize changes whitespace and breaks the HMAC. Second, compare with a timing-safe equality: a naive string comparison leaks the secret byte by byte to anyone patient enough to measure.
import { createHmac, timingSafeEqual } from 'node:crypto'
// Every delivery carries X-Synento-Signature: the hex HMAC-SHA256 of the raw
// body under the endpoint secret, plus X-Synento-Event-Type with the event.
export function verifyWebhook(rawBody: string, signature: string, secret: string): boolean {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(signature)
const b = Buffer.from(expected)
return a.length === b.length && timingSafeEqual(a, b)
}
export async function POST(request: Request) {
const rawBody = await request.text()
const signature = request.headers.get('x-synento-signature') ?? ''
if (!verifyWebhook(rawBody, signature, process.env.SYNENTO_WEBHOOK_SECRET!)) {
return new Response('bad signature', { status: 401 })
}
const event = JSON.parse(rawBody) as {
id: string
project_id: string
session_id?: string
type: string
}
// session.ended and recording.ready both carry session_id: read the
// manifest, then fan out to transcription or review.
await handleEvent(event)
return new Response('ok')
}3. Fan out from session end
The delivery body is small on purpose: an id, the event type, the project and session ids, and a data object. It is a pointer, not the media. On session.ended, read the session manifest to confirm which tracks actually hold media, then hand each microphone track to your own transcription or evaluation models — per-speaker audio with no diarisation guesswork, because the tracks were never mixed.
On export.completed, fetch the finished MP4 for the destinations that need files. On recording.ready, notify reviewers with a replay link. Keep each handler idempotent on the delivery id: retries redeliver the same event, and doing the transcription twice is the kind of bug that bills you.
4. Retries, pauses and proof
Your endpoint will be down one day — deploys happen. Failed first deliveries are retried with backoff instead of hammering a dead URL, and every attempt is recorded: list an endpoint's deliveries to see statuses and errors per event instead of wondering whether Synento called. When an endpoint needs to stay quiet for a while, pause it with PATCH /v1/webhooks/:id and resume the same way; the management API lists paused endpoints too, because a paused endpoint you cannot see is one you can never resume.
When debugging, answer three questions in order: did the endpoint receive the delivery (check the deliveries log), did the signature verify (check your secret matches the endpoint's current one — rotation replaces it), and did the handler act on the right session (check the manifest, as always).
5. The event catalog
Subscribe narrowly: each event you accept is code you maintain. Most recording pipelines need three; the rest serve presence, analytics and review features.
session.created,session.started,session.ended— the session lifecycle. Ended starts the pipeline.recording.ready— the media is ready to read. Transcribe and review from here.export.completed— the MP4 finished rendering. Deliver files from here.participant.joinedandparticipant.left— who was there and when, with the user id in the data.replay.started— someone pressed play. Analytics and notifications, not pipeline triggers.
6. Store the secret like one
The endpoint secret is shown once, at creation, and never again — the same rule as every other credential here. Keep it in the environment next to your API key, never in the repo, and load it at boot rather than reading it per request. When it leaks or an engineer leaves, rotate it with POST /v1/webhooks/:id/rotate-secret: the new secret signs every delivery from that moment, including retries of older events, so deploy the new value and the rotation together.
One secret per endpoint, one endpoint per environment. Sharing a secret across staging and production means a staging log line compromises production deliveries; separate endpoints with separate secrets keep the blast radius where it belongs.
7. Prove the pipeline before the first real call
Do not discover a broken pipeline from a customer's missing transcript. Run one real session on staging — two publishers, ten seconds — and then check three things in order: the endpoint's deliveries log shows the events with 2xx statuses, your handler's own logs show it acted on the right session id, and the downstream artifact exists, whether that is a transcript row, a chat message or a file. Only then point the endpoint at production traffic.
Keep that session as a fixture. Re-run it after every change to the handler, and after every secret rotation, because rotation is exactly the kind of change that passes code review and fails at midnight. A pipeline with a standing test stays boring; a pipeline without one pages you.
Common questions
What happens when I rotate the signing secret?
The new secret signs every delivery from that moment on, including retries of older events. Update your environment at the same time or every delivery starts failing verification at once.
Do events arrive in order?
Do not assume it. session.ended precedes recording.ready in practice, but handlers should key on the event type and the delivery id, and tolerate any arrival order — retries make ordering guarantees impossible.
How do I develop webhooks locally?
Tunnel your dev server to a public URL and register that as the endpoint; deliveries need a reachable address. Keep a separate endpoint per environment so staging retries never transcribe production calls.
session.ended or recording.ready — which starts my pipeline?
session.ended tells you the call is over; recording.ready tells you the media is ready to read. Start cheap preparation on the first, and the media-dependent work — manifest reads, transcription, exports — on the second.
More guides
- Record a video call without a recording serverThe live streams are the recording. Create a session, publish from the browser, then replay or export — with no recording pipeline to run or pay for separately.
- Verify media actually arrivedGreen dashboards lie: a participant can join while publishing nothing. The session manifest is the ground truth — a track with a non-null start_ts carried real media.
- Add live video to an AI agent via the MCP serverA remote MCP server at api.synento.com/mcp takes your agent from nothing to a verified integration: projects, keys, sessions, tokens, and manifest proof.
- WebCodecs vs WebRTC for recorded callsIf the call is the product, use WebRTC. If the recording of the call is the product, the WebCodecs relay trade is worth understanding.
- Async video messages and standupsAsync video is mostly replay. Create one session per standup, let the team watch on their own time through expiring links, and export only what must be kept.
- Restricted keys and short-lived connection tokensTwo credentials, two jobs. A restricted key identifies your server to the API; a connection token admits one participant to one session for about an hour.
Give your agent a video platform it can verify.
Sign up, point your agent at llms.txt, and have a recorded call running before lunch.