Vidtreo

Webhooks

Receive signed HTTP notifications when platform events complete — payload anatomy, HMAC verification, and delivery guarantees.

Webhooks

Webhooks push platform events to your systems as they happen, so you never poll for status. Each delivery is a POST request with a JSON body, signed with HMAC-SHA256 so you can verify it came from VIDTREO.

Available Events

EventFires when
transcriptions.transcription.completedAI Transcription finishes processing a video

The event catalog is growing. The dashboard always shows the current list of events you can subscribe to when creating or editing an endpoint.

Creating an Endpoint

Webhook endpoints are managed per environment in the dashboard:

Sign in to the VIDTREO Dashboard

Open your environment and go to Settings → Webhooks

Click Add Endpoint, enter your https:// URL, and select the events to subscribe to

Copy the signing secret (whsec_...) shown after creation

The signing secret is displayed once. It is stored encrypted and can never be retrieved again — if you lose it, rotate it from the endpoint's settings to get a new one.

Delivery Headers

Every delivery includes identifying headers:

HeaderContent
X-Webhook-Signaturesha256=<hex> — HMAC-SHA256 of the raw request body
X-Vidtreo-Event-IdUnique event ID — use it to deduplicate processing
X-Vidtreo-Event-TypeThe event name, e.g. transcriptions.transcription.completed
X-Vidtreo-Event-VersionPayload schema version
X-Vidtreo-Webhook-Endpoint-IdThe receiving endpoint's ID
X-Vidtreo-Webhook-Delivery-IdThis specific delivery attempt — traceable in the dashboard

Payload: transcriptions.transcription.completed

{
  "id": "9f4c1a2e-8d3b-4f6a-b1c9-2e7d5a8f0c3b",
  "event": "transcriptions.transcription.completed",
  "eventVersion": "2",
  "occurredAt": "2026-08-03T14:22:07.000Z",
  "data": {
    "transcription": {
      "id": "b7e2...",
      "videoId": "vid_...",
      "environmentId": "env_...",
      "language": "en",
      "wordCount": 812,
      "duration": 294.5
    },
    "video": {
      "id": "vid_...",
      "publicId": "pub_...",
      "filename": "interview-final.mp4",
      "duration": 295,
      "status": "COMPLETED",
      "metadata": { },
      "userMetadata": {
        "candidateId": "cand_8231",
        "jobId": "job_114"
      }
    },
    "files": [
      { "format": "vtt",  "url": "https://...", "expiresAt": "...", "contentType": "text/vtt" },
      { "format": "json", "url": "https://...", "expiresAt": "...", "contentType": "application/json" },
      { "format": "txt",  "url": "https://...", "expiresAt": "...", "contentType": "text/plain" }
    ]
  }
}

Two fields deserve special attention:

  • data.video.userMetadata — whatever JSON you attached to the video at upload time, echoed back verbatim. Attach your own identifiers (candidate ID, course ID, ticket number) and your handler never needs a lookup table to know which record the video belongs to.
  • data.files[].url — presigned URLs that expire 5 minutes after the delivery is generated. Download what you need when the event arrives; fetch fresh URLs later via GET /api/v1/videos/{videoId}/transcription.

Verifying Signatures

Always verify the X-Webhook-Signature header before trusting a payload. The signature is an HMAC-SHA256 of the raw request body, hex-encoded, computed with your endpoint's signing secret:

import { createHmac, timingSafeEqual } from 'node:crypto'

function isFromVidtreo(rawBody: string, signature: string, secret: string) {
  const expected = `sha256=${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)
}

Two rules that prevent the classic mistakes:

  1. Compute the HMAC over the raw body string, before any JSON parsing or re-serialization touches it.
  2. Compare with a constant-time function (timingSafeEqual), never ===.

A minimal handler:

app.post('/webhooks/vidtreo', async (req) => {
  if (!isFromVidtreo(req.rawBody, req.headers['x-webhook-signature'], process.env.VIDTREO_WEBHOOK_SECRET)) {
    return res.status(401).end()
  }

  const { video, files } = req.body.data
  const { candidateId } = video.userMetadata

  const txt = files.find(f => f.format === 'txt')
  const transcript = await fetch(txt.url).then(r => r.text())

  await processTranscript(candidateId, transcript)
  return res.status(200).end()
})

Respond with a 2xx status quickly — do heavy processing after acknowledging, or asynchronously.

Secret Lifecycle

  • Secrets use the whsec_ prefix and are generated server-side; you can also supply your own when creating the endpoint
  • Stored encrypted at rest, shown in plaintext only at creation and rotation
  • Rotate any time from the endpoint's settings in the dashboard — update your handler's secret when you do

Delivery Guarantees

  • Automatic retries — failed processing is retried with a delay before giving up
  • Dead-letter capture — events that exhaust retries are preserved, not dropped
  • Full delivery history — every attempt, response code, and payload is inspectable per endpoint in the dashboard
  • Manual redelivery — redrive a single event, a single delivery, or every failed delivery in a time window, from the dashboard

Use X-Vidtreo-Event-Id to make your handler idempotent — a redelivered event carries the same event ID as the original.

On this page