Media Caption API v1

Webhooks

Configure job-level webhook delivery for final bulk job events.

Supported events

  • job.completed
  • job.completed_with_errors
  • job.failed
  • Only job-level events are sent in API v1. Item-level transcription events are not sent.

Payload

{
  "id": "evt_job_01jz7m91bxkpbqha0wt8whm76r_job.completed",
  "type": "job.completed",
  "eventId": "evt_job_01jz7m91bxkpbqha0wt8whm76r_job.completed",
  "eventType": "job.completed",
  "createdAt": "2026-05-11T10:00:25.000Z",
  "apiVersion": "2026-05-11",
  "data": {
    "jobId": "job_01jz7m91bxkpbqha0wt8whm76r",
    "jobType": "public_transcripts_bulk",
    "status": "completed",
    "progress": 100,
    "itemCount": 1,
    "completedCount": 1,
    "failedCount": 0,
    "creditsFrozen": 1,
    "creditsCharged": 1,
    "creditsReleased": 0,
    "items": [
      {
        "itemId": 101,
        "externalId": "lesson-001",
        "status": "completed",
        "transcriptionId": "tr_3f6e5c6a-0d31-4f8c-9b1d-7f3b9e6a2c11"
      }
    ]
  }
}

Signatures

  • Verify the raw request body before parsing JSON.
  • Compute HMAC-SHA256(signingSecret, timestamp + "." + rawBody) and compare it to the v1 signature with a constant-time comparison.
  • Reject stale timestamps. Five minutes is a reasonable default.
  • Treat MediaCaption-Event-Id as an idempotency key because retries can send the same event more than once.
MediaCaption-Event-Id: evt_...
MediaCaption-Timestamp: 1778493622
MediaCaption-Signature: v1=hex_hmac_sha256

signed_payload = "{timestamp}.{raw_request_body}"
import crypto from "node:crypto";

function verifyMediaCaptionSignature({
  rawBody,
  signatureHeader,
  timestampHeader,
  signingSecret,
}) {
  if (!signatureHeader || !timestampHeader) return false;

  const timestamp = Number(timestampHeader);
  if (!Number.isInteger(timestamp)) return false;

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > 5 * 60) return false;

  const [version, signature] = signatureHeader.split("=");
  if (version !== "v1" || !/^[0-9a-f]{64}$/i.test(signature ?? "")) {
    return false;
  }

  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(`${timestampHeader}.${rawBody}`)
    .digest();
  const received = Buffer.from(signature, "hex");

  return (
    received.length === expected.length &&
    crypto.timingSafeEqual(received, expected)
  );
}

Retries

Any 2xx response is success. Non-2xx responses and network failures are retried with recorded delivery attempts, up to 3 attempts.

Next: Billing