Webhooks

Motioness uses Standard Webhooks — predictable headers, HMAC-SHA256 signatures, idempotency keys, retry schedule. Swap an existing Svix/Stripe verifier and it'll work.

Configure

Three controls, all in Project → Settings → Webhooks (or via API):

bash
curl -X PUT "https://motioness.com/api/projects/$PROJECT_ID/webhooks" \ -H 'Content-Type: application/json' -b cookie.txt \ -d '{ "webhook_url": "https://your-app.com/webhooks/motioness", "webhook_events": ["asset.completed", "asset.failed", "asset.retry_scheduled"], "webhooks_enabled": true }'

Response includes a webhook_secret_preview (first 12 chars + ellipsis). The full secret was set on first save — read it from the dashboard once, store it as MOTIONESS_WEBHOOK_SECRET.

To rotate:

bash
curl -X POST "https://motioness.com/api/projects/$PROJECT_ID/webhooks/rotate-secret" -b cookie.txt# → { "webhook_secret": "whsec_..." } ← shown once

Send a test event

bash
curl -X POST "https://motioness.com/api/projects/$PROJECT_ID/webhooks/test" -b cookie.txt

The first delivered test auto-flips webhooks_enabled to true if it wasn't already. Use this to confirm your endpoint is reachable + verifying signatures correctly.

Event types

Subscribe to any subset:

EventWhen
asset.queuedGeneration accepted
asset.stage_completedOne pipeline stage finished (vision, stage1, stage2, merge, r2_write)
asset.completedTerminal success — mp4 is hot
asset.failedTerminal failure — error_code field tells you why
asset.retry_scheduledTransient stage error, retry queued (observational)

Default subscription is [asset.completed, asset.failed].

Payload shape

json
{ "id": "evt_01HXY3K…", "type": "asset.completed", "ts": 1714801234567, "asset_id": "a25b330a71e2bc6b", "project_id": "proj_abc", "request_id": "req_xyz", "data": { "mp4_url": "https://motioness.com/r2/assets/a25b330a71e2bc6b.mp4", "bytes": 845321, "loop_mode": "native" }}

data varies by event type:

json
{ "mp4_url": "https://motioness.com/r2/...", "bytes": 845321, "loop_mode": "native" }
json
{ "error_code": "vision_blocked", "error_msg": "Source image rejected by vision model" }
json
{ "stage": "stage1", "latency_ms": 4820 }
json
{ "stage": "merge", "attempt": 2, "next_at_ms": 1714801234567 }

Headers

text
content-type: application/jsonwebhook-id: evt_01HXY3K… ← idempotency keywebhook-timestamp: 1714801234 ← unix secondswebhook-signature: v1,<base64-hmac> ← HMAC-SHA256 of `${id}.${ts}.${body}`

Standard Webhooks scheme. webhook-signature may carry multiple versions space-separated (v1,A v1,B) during secret rotation; verify any matches.

Verify

ts
import crypto from 'node:crypto';export function verify( secret: string, headers: Record<string, string>, body: string,): boolean { const id = headers['webhook-id']; const ts = headers['webhook-timestamp']; const sigHeader = headers['webhook-signature']; if (!id || !ts || !sigHeader) return false; // Reject events older than 5 minutes if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 5 * 60) return false; const raw = secret.startsWith('whsec_') ? Buffer.from(secret.slice(6), 'base64') : Buffer.from(secret); const expected = 'v1,' + crypto.createHmac('sha256', raw).update(`${id}.${ts}.${body}`).digest('base64'); return sigHeader .split(' ') .some((s) => crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));}

Idempotency

webhook-id is the idempotency key. Retries of the same logical event always carry the same id. Store seen ids for ~24h and skip duplicates:

ts
if (await seenIds.has(id)) return res.status(200).end();await seenIds.put(id, true, { ttl: 24 * 60 * 60 });

Retry schedule

Failed deliveries (non-2xx, timeout > 10s) retry on:

text
30s, 2m, 10m, 30m, 2h

After 5 failed attempts the delivery is marked dead. Inspect via GET /api/projects/:id/webhooks/deliveries and re-enqueue with POST /api/projects/:id/webhooks/deliveries/:deliveryId/redeliver.

Replay-attack window

Reject events older than 5 minutes (timestamp comparison). The verify recipes above already do this.

Local development

Use ngrok / cloudflared / Tailscale to forward your localhost:

bash
ngrok http 3000# → https://abc123.ngrok.io# Then in dashboard, set webhook_url to https://abc123.ngrok.io/webhooks/motioness

Run POST /webhooks/test from the dashboard to fire a test event into your local server.

Ask a question... ⌘I