postreef
View as Markdown

Webhooks

Instead of polling, let PostReef POST the result to you. Register up to 5 endpoints in the dashboard, each with its own signing secret, or pass a per-run webhookUrl when submitting.

Events

  • extraction.completed. The extraction finished, and data matches GET /v1/extractions/:id/result (summary + structured extraction). data.outcome is the content-match verdict: ok means extraction holds your data; no_match / uncertain mean the video's content didn't match your schema, so extraction is null and verdictReason explains why. Always check outcome before treating a null extraction as a failure.
  • extraction.failed. The extraction failed, and data.error explains why. Credits are refunded in full before the event fires.
  • test. Sent on demand from the dashboard, delivered and signed exactly like real events.

The event name is also sent in the X-Vidextract-Event header.

Payloads

// extraction.completed
{
  "event": "extraction.completed",
  "data": {
    "id": "run_8f3a2b1c",
    "status": "complete",
    "outcome": "ok",              // "ok" | "no_match" | "uncertain" | null
    "summary": { "title": "...", "durationSeconds": 212, "files": [ ... ], ... },
    "extraction": { ... }         // your schema-shaped object, null for download-only runs
  }
}

// extraction.completed: content didn't match the schema
{
  "event": "extraction.completed",
  "data": {
    "id": "run_2c9d4e5f",
    "status": "complete",
    "outcome": "no_match",        // the video isn't about your schema's subject
    "verdictReason": "This video is a car review, not a recipe.",
    "summary": { ... },
    "extraction": null            // no data: check outcome before treating null as an error
  }
}

// extraction.failed
{
  "event": "extraction.failed",
  "data": {
    "id": "run_8f3a2b1c",
    "status": "failed",
    "error": "This video is private or unavailable."
  }
}

Verifying signatures

Every delivery from a registered endpoint carries an HMAC signature over the raw body:

X-Vidextract-Signature: t=1765459200,v1=5257a869e7...

t is a unix timestamp (seconds); v1 is the hex HMAC-SHA256 of the string {t}.{rawBody} keyed with your endpoint's secret. Recompute it with your endpoint's secret and compare in constant time. Reject stale timestamps to block replays:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(signatureHeader: string, rawBody: string, secret: string) {
  const t = signatureHeader.match(/t=(\d+)/)?.[1];
  const v1 = signatureHeader.match(/v1=([0-9a-f]+)/)?.[1];
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // 5 min
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return (
    v1.length === expected.length &&
    timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
  );
}

Important: compute the HMAC over the raw request body bytes, not a re-serialized JSON object. Key order matters.

Delivery & retries

  • Respond with any 2xx within 10 seconds to acknowledge. Do heavy processing after responding.
  • Failed deliveries are retried up to 3 more times: after 30s, 2m, then 10m. After the fourth failure the delivery is marked failed, so fall back to polling GET /v1/extractions/:id/result.
  • Redirects are not followed; deliveries time out after 10 seconds.
  • Deliveries can arrive more than once (a timeout after your 200 still retries), so make your handler idempotent on data.id.

URL requirements

  • https only. Plain http is rejected.
  • Public hosts only. URLs resolving to private, loopback or link-local addresses are rejected, both at registration and again at every send.

Per-run webhookUrl

Pass webhookUrl in the submit request to receive that run's events at an extra URL without registering it. Same https/public-host rules apply. Signing: per-run deliveries are signed with your first registered endpoint's secret when you have one, and are unsigned when you have no registered endpoints. If the per-run URL duplicates a registered endpoint, only the registered delivery is sent.