Webhooks
Subscribe
Add an endpoint under Settings → API (owners and admins), or from code. The signing secret (whsec_…) is shown once, when the endpoint is made. Leave events empty to get every event except the opt-in ones (audit.event). Up to 10 endpoints per workspace; URLs must be public and use https.
curl -X POST https://reeltopus.com/api/v1/webhooks -H "Authorization: Bearer $REELTOPUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/reeltopus", "events": ["project.ready", "clip.exported"]}'
# The response carries "secret": "whsec_…" once. Keep it to verify deliveries.Events
| Event | When | data holds |
|---|---|---|
project.ready | A video finished processing; its clips are ready. | project |
project.failed | A video could not be processed. Its hours were refunded. | project (with error) |
clip.exported | A clip's MP4 export finished; download links are in the payload. | clip, export |
clip.export_failed | A clip's MP4 export failed. | clip, export (with error) |
post.published | A post went live on the platform. | post (with platformUrl) |
post.failed | A post could not be published. | post (with error) |
audit.event | Something was recorded in the audit log (Settings → Audit log). Sent only to endpoints that list it. | event |
Payloads
Every event is { id, type, createdAt, data }, and data holds the same objects the API returns, so the code that reads GET /projects/{id} reads the webhook too. The samples below are shortened; every field is in the OpenAPI document under webhooks.
project.ready, with its headers:
POST /hooks/reeltopus HTTP/1.1
Content-Type: application/json
User-Agent: Reeltopus-Webhooks/1.0 (+https://reeltopus.com/docs/api#webhooks)
webhook-id: evt_3f1c9a2b7d4e5f60718293a4
webhook-timestamp: 1792854000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"id": "evt_3f1c9a2b7d4e5f60718293a4",
"type": "project.ready",
"createdAt": "2026-10-24T12:00:00.000Z",
"data": {
"project": {
"id": "…",
"object": "project",
"title": "Episode 12",
"status": "ready",
"mode": "standard",
"source": { "type": "link", "url": "https://drive.google.com/file/d/…/view", "host": "google_drive" },
"durationMs": 3127000,
"language": "en",
"clipCount": 14,
"error": null,
…
}
}
}clip.exported. The links work without a key for an hour; GET /clips/{id}/export gives fresh ones.
{
"id": "evt_9a0b…",
"type": "clip.exported",
"createdAt": "2026-10-24T12:04:10.000Z",
"data": {
"clip": { "id": "…", "object": "clip", "projectId": "…", "rank": 1, "score": 87, "title": "The one habit that doubled our retention", … },
"export": {
"clipId": "…",
"status": "ready",
"resolution": "1080p",
"error": null,
"renderedAt": "2026-10-24T12:04:09.000Z",
"files": {
"mp4": { "url": "https://…", "expiresAt": "2026-10-24T13:04:10.000Z" },
"captions": { "srt": { "url": "…", "expiresAt": "…" }, "vtt": { … }, "ass": { … } },
"editorTimeline": { "url": "…", "expiresAt": "…" }
}
}
}
}post.published:
{
"id": "evt_51d2…",
"type": "post.published",
"createdAt": "2026-10-25T09:00:04.000Z",
"data": {
"post": {
"id": "…", "object": "post", "clipId": "…", "connectionId": "…",
"title": "The one habit that doubled our retention",
"status": "published",
"platformUrl": "https://www.youtube.com/shorts/…",
"error": null,
…
}
}
}Delivery and retries
- Answer with any 2xx within 10 seconds. Do the slow work afterwards, in a queue.
- Anything else (a timeout, a 4xx or 5xx, a redirect) is retried with backoff for about a day, 8 retries in all. Redirects are not followed.
- A
410 Gonedeletes the endpoint: nothing more is sent to it. Zapier answers that for a deleted Zap. - The event id stays the same on every retry and is also sent as
webhook-id. Use it to de-duplicate: a retry after a lost answer delivers the same event twice. - Events can arrive out of order. If order matters, compare
createdAtor fetch the resource.
Verify the signature
Signatures follow Standard Webhooks, so its libraries for Go, Ruby, PHP, Java, Rust and C# work too. The signature is an HMAC-SHA256 of ${webhook-id}.${webhook-timestamp}.${body}, keyed with the base64-decoded secret (after whsec_), base64-encoded, and sent as v1,<signature>. Reject timestamps more than 5 minutes off, and always check the raw body, before any JSON parsing.
With the SDK, once it's published (Node 20+, Deno, Bun, Workers, Vercel Edge):
import { verifyWebhook, WebhookVerificationError } from "@reeltopus/sdk/webhooks";
export async function POST(request: Request) {
try {
// The raw body, before any JSON parsing.
const event = await verifyWebhook(process.env.REELTOPUS_WEBHOOK_SECRET!, request.headers, await request.text());
if (event.type === "project.ready") console.log(event.data.project.clipCount, "clips");
if (event.type === "clip.exported") console.log(event.data.export.files.mp4?.url);
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof WebhookVerificationError) return new Response(error.message, { status: 400 });
throw error;
}
}Without a library:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret: string, headers: Headers, body: string) {
const id = headers.get("webhook-id");
const ts = headers.get("webhook-timestamp");
const signatures = headers.get("webhook-signature");
if (!id || !ts || !signatures) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // older than 5 minutes
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest();
return signatures.split(" ").some((s) => {
const [version, mac] = s.split(",");
const given = Buffer.from(mac ?? "", "base64");
return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
});
}With Express, read the body with express.raw({ type: "application/json" }) and pass req.headers and req.body. In Django, pass request.body.
Test an endpoint
Send test next to an endpoint in Settings → API, or POST /webhooks/{id}/test, delivers a signed event of type webhook.test straight away and returns the status your endpoint answered. Recent deliveries, with their status codes and attempts, are listed under each endpoint in Settings → API.
curl -X POST https://reeltopus.com/api/v1/webhooks/$WEBHOOK_ID/test -H "Authorization: Bearer $REELTOPUS_API_KEY"