Skip to content

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.

subscribe.ts
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.secret

2. 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.

app/api/hooks/synento/route.ts
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.joined and participant.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.

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.