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
datamatchesGET /v1/extractions/:id/result(summary + structured extraction).data.outcomeis the content-match verdict:okmeansextractionholds your data;no_match/uncertainmean the video's content didn't match your schema, soextractionis null andverdictReasonexplains why. Always checkoutcomebefore treating a nullextractionas a failure. - extraction.failed. The extraction failed, and
data.errorexplains 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.