Webhooks
Motionpit can POST JSON payloads to a URL you control when render jobs complete or fail. Configure webhook endpoints in your dashboard settings. Deliveries are at-least-once — your handler must be idempotent on the X-VAM-Delivery header.
Events
render.completedA render job finished successfully. The MP4 is in Spaces at `outputSpacesKey`; request a presigned URL from `/v1/renders/{id}/signed-url` to download.Payload schema
| Field | Type | Description |
|---|---|---|
| renderJobId | string | — |
| outputSpacesKey | string | — |
Fired by: src/services/renderService.ts `markComplete()`
render.failedA render job failed terminally. The `error` string is human-readable only; do not pattern-match on it.Payload schema
| Field | Type | Description |
|---|---|---|
| renderJobId | string | — |
| error | string | — |
Fired by: src/services/renderService.ts `markFailed()`
Transport
Each delivery is an HTTP POST to your endpoint URL with the following headers:
| Header | Value |
|---|---|
| X-VAM-Event | The event type, e.g. render.completed |
| X-VAM-Signature | t=<unix_seconds>,v1=<hex_hmac> — HMAC-SHA256 signature |
| X-VAM-Delivery | Unique UUID for this delivery attempt (idempotency key) |
| Content-Type | application/json |
Delivery semantics
- At-least-once. De-duplicate on
X-VAM-Delivery+renderJobId. - Retry backoff (after first failure): 1m, 5m, 30m, 2h, 6h — 5 attempts total.
- Timeout per attempt: 15s. Respond with any
2xxto acknowledge. - After 10 consecutive dead deliveries, the endpoint is auto-disabled.
Signature verification
Verify the X-VAM-Signature header before processing the event. This prevents spoofed deliveries.
Signed string
<timestamp>.<raw_body>
Where timestamp is the t= value from the X-VAM-Signature header (Unix seconds), and raw_body is the exact request body bytes (do not parse JSON first).
Verification example (TypeScript / Node.js)
import crypto from "node:crypto";
function verifyWebhookSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
// Parse "t=<ts>,v1=<hex>"
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=")),
);
const timestamp = parts["t"];
const receivedHex = parts["v1"];
if (!timestamp || !receivedHex) return false;
// Reject replays older than 5 minutes
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) return false;
const signed = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signed)
.digest("hex");
// Constant-time compare to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(receivedHex, "hex"),
);
}Your endpoint secret is configured per-endpoint in the dashboard. Keep it private — anyone with the secret can forge valid signatures.