Skip to main content

Webhooks

When rendering on AWS Lambda, Remotion can send webhooks to notify you when the render ends, successfully or with failures. This page describes the webhook payloads and how to set up a webhook API endpoint.

Refer to the renderMediaOnLambda() documentation to learn how to trigger a render with webhooks enabled.

Setup

You will need to set up an API endpoint with a POST request handler. Make sure that the endpoint is reachable and accepts requests from AWS.

info

If you run the webhook endpoint on your local machine (i.e. on localhost), you will need to set up a public reverse proxy using a tool like tunnelmole, an open source tunneling tool or ngrok, a popular closed source tunneling tool. Running either tool will generate a Public URL that will forward to your service on localhost.

Response

Every webhook has the following headers:

json
{
"Content-Type": "application/json",
"X-Remotion-Mode": "production" | "demo",
"X-Remotion-Signature": "sha512=HASHED_SIGNATURE" | "NO_SECRET_PROVIDED",
"X-Remotion-Status": "success" | "timeout" | "error",
}
json
{
"Content-Type": "application/json",
"X-Remotion-Mode": "production" | "demo",
"X-Remotion-Signature": "sha512=HASHED_SIGNATURE" | "NO_SECRET_PROVIDED",
"X-Remotion-Status": "success" | "timeout" | "error",
}

You can use these headers to verify the authenticity of the request, to check the status of your rendering process and to check whether the webhook was called from production code deployed to AWS or a demo application such the tool below or your own test suite.

The request body has the following structure:

ts
type WebhookPayload =
| {
type: 'error';
errors: {
message: string;
name: string;
stack: string;
}[];
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
}
| {
type: 'success';
lambdaErrors: EnhancedErrorInfo[];
outputUrl: string | undefined;
outputFile: string | undefined;
timeToFinish: number | undefined;
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
// Available from v3.3.11
costs: {
estimatedCost: number;
estimatedDisplayCost: string;
currency: string;
disclaimer: string;
};
}
| {
type: 'timeout';
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
};
ts
type WebhookPayload =
| {
type: 'error';
errors: {
message: string;
name: string;
stack: string;
}[];
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
}
| {
type: 'success';
lambdaErrors: EnhancedErrorInfo[];
outputUrl: string | undefined;
outputFile: string | undefined;
timeToFinish: number | undefined;
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
// Available from v3.3.11
costs: {
estimatedCost: number;
estimatedDisplayCost: string;
currency: string;
disclaimer: string;
};
}
| {
type: 'timeout';
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
};

The fields renderId, bucketName will be returned just like they are returned by renderMediaOnLambda() itself.

You can use the field customData to set a JSON-serializable object, which is useful to pass on custom data to the webhook endpoint. The customData field must be less than 1KB (1024 bytes) when serialized, otherwise an error is thrown. Store larger data in inputProps and retrieve it back by calling getRenderProgress() and reading progress.renderMetadata.inputProps.

If the render process times out, the reponse body will not contain any other fields.

The outputUrl, outputFile and timeToFinish keys are only returned if the render was successful. Note that a successful render process may still have non-fatal lambdaErrors:

json
{
"s3Location": "string",
"explanation": "string" | null,
"type": "renderer" | "browser" | "stitcher",
"message": "string",
"name": "string",
"stack": "string",
"frame": "number"| null,
"chunk": "number"| null,
"isFatal": "boolean",
"attempt": "number",
"willRetry": "boolean",
"totalAttempts": "number",
"tmpDir": {
"files": [{
"filename": "string",
"size": "number",
}],
"total": "number"
} | null,
}
json
{
"s3Location": "string",
"explanation": "string" | null,
"type": "renderer" | "browser" | "stitcher",
"message": "string",
"name": "string",
"stack": "string",
"frame": "number"| null,
"chunk": "number"| null,
"isFatal": "boolean",
"attempt": "number",
"willRetry": "boolean",
"totalAttempts": "number",
"tmpDir": {
"files": [{
"filename": "string",
"size": "number",
}],
"total": "number"
} | null,
}

The errors array will contain the error message and stack trace of any fatal error that occurs during the render process.

Validate Webhooks

Remotion will sign all webhook requests if you provide a webhook secret in the CLI arguments.

warning

If you don't provide a secret, the X-Remotion-Signature will be set to NO_SECRET_PROVIDED. It is not possible to verify the authenticity and data integrity of a webhook request that is sent with a NO_SECRET_PROVIDED signature. If you want to verify incoming webhooks, you must provide a webhook secret.

Remotion uses HMAC with the SHA-512 algorithm to cryptographically sign the webhook requests it sends. This allows you to verify the authenticity and data integrity of incoming webhook requests.

In order to verify a webhook request, you will need to create a hex digest of a SHA-512 HMAC signature using your provided webhook key and the request body. If it matches the X-Remotion-Signature header, the request was indeed sent by Remotion and its request body is complete.

If it does not match, either the data integrity is compromised and the request body is incomplete or the request was not sent by Remotion.

This is how Remotion calculates the signature:

javascript
import * as Crypto from "crypto";
function calculateSignature(payload: string, secret?: string) {
if (!secret) {
return "NO_SECRET_PROVIDED";
}
const hmac = Crypto.createHmac("sha512", secret);
const signature = "sha512=" + hmac.update(payload).digest("hex");
return signature;
}
javascript
import * as Crypto from "crypto";
function calculateSignature(payload: string, secret?: string) {
if (!secret) {
return "NO_SECRET_PROVIDED";
}
const hmac = Crypto.createHmac("sha512", secret);
const signature = "sha512=" + hmac.update(payload).digest("hex");
return signature;
}

In your webhook endpoint, the payload parameter is the request body and the secret parameter is your webhook secret.

Instead of validating the signature yourself, you can use the validateWebhookSignature() function to throw an error if the signature is invalid.

Example webhook endpoint (Express)

You can use any web framework and language to set up your webhook endpoint. The following example is written in JavaScript using the Express framework.

server.js
javascript
import express from "express";
import bodyParser from "body-parser";
import * as Crypto from "crypto";
import {
validateWebhookSignature,
WebhookPayload,
} from "@remotion/lambda/client";
const router = express();
// You'll need to add a JSON parser middleware globally or
// for the webhook route in order to get access to the request
// body.
const jsonParser = bodyParser.json();
// Enable testing through the tool below
const ENABLE_TESTING = true;
// Express API endpoint
router.post("/my-remotion-webhook-endpoint", jsonParser, (req, res) => {
if (ENABLE_TESTING) {
res.setHeader("Access-Control-Allow-Origin", "https://www.remotion.dev");
res.setHeader("Access-Control-Allow-Methods", "OPTIONS,POST");
res.setHeader(
"Access-Control-Allow-Headers",
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode"
);
}
if (req.method === "OPTIONS") {
res.status(200).end();
return;
}
validateWebhookSignature({
signatureHeader: req.header("X-Remotion-Signature"),
body: req.body,
secret: process.env.WEBHOOK_SECRET as string
});
const status = req.header("X-Remotion-Status"); // success, timeout, error
const mode = req.header("X-Remotion-Mode"); // demo or production
const payload = JSON.parse(req.body) as WebhookPayload;
if (payload.type === "success") {
// ...
} else if (payload.type === "timeout") {
// ...
}
});
server.js
javascript
import express from "express";
import bodyParser from "body-parser";
import * as Crypto from "crypto";
import {
validateWebhookSignature,
WebhookPayload,
} from "@remotion/lambda/client";
const router = express();
// You'll need to add a JSON parser middleware globally or
// for the webhook route in order to get access to the request
// body.
const jsonParser = bodyParser.json();
// Enable testing through the tool below
const ENABLE_TESTING = true;
// Express API endpoint
router.post("/my-remotion-webhook-endpoint", jsonParser, (req, res) => {
if (ENABLE_TESTING) {
res.setHeader("Access-Control-Allow-Origin", "https://www.remotion.dev");
res.setHeader("Access-Control-Allow-Methods", "OPTIONS,POST");
res.setHeader(
"Access-Control-Allow-Headers",
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode"
);
}
if (req.method === "OPTIONS") {
res.status(200).end();
return;
}
validateWebhookSignature({
signatureHeader: req.header("X-Remotion-Signature"),
body: req.body,
secret: process.env.WEBHOOK_SECRET as string
});
const status = req.header("X-Remotion-Status"); // success, timeout, error
const mode = req.header("X-Remotion-Mode"); // demo or production
const payload = JSON.parse(req.body) as WebhookPayload;
if (payload.type === "success") {
// ...
} else if (payload.type === "timeout") {
// ...
}
});

Example webhook endpoint (Next.JS App Router)

Similary, here is an example endpoint in Next.JS for the App Router.

Since this endpoint is going to be executed in an AWS Lambda function on its own, you want to import the Remotion functions from @remotion/lambda/client.

app/api/webhook.ts
tsx
import {
validateWebhookSignature,
WebhookPayload,
} from '@remotion/lambda/client';
 
// Enable testing through the tool below
// You may disable it in production
const ENABLE_TESTING = true;
 
export const POST = async (req: Request, res: Response) => {
let headers = {};
 
if (ENABLE_TESTING) {
const testingheaders = {
'Access-Control-Allow-Origin': 'https://www.remotion.dev',
'Access-Control-Allow-Headers':
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode',
'Access-Control-Allow-Methods': 'OPTIONS,POST',
};
headers = {...headers, ...testingheaders};
}
 
if (req.method === 'OPTIONS') {
return new Response(null, {
headers,
});
}
 
// Parse the body properly
const body = await req.json();
 
validateWebhookSignature({
secret: process.env.WEBHOOK_SECRET as string,
body: body,
signatureHeader: req.headers.get('X-Remotion-Signature') as string,
});
 
const payload = body as WebhookPayload;
 
if (payload.type === 'success') {
//...
} else if (payload.type === 'timeout') {
//...
}
 
return new Response(JSON.stringify({success: true}));
};
 
export const OPTIONS = POST;
app/api/webhook.ts
tsx
import {
validateWebhookSignature,
WebhookPayload,
} from '@remotion/lambda/client';
 
// Enable testing through the tool below
// You may disable it in production
const ENABLE_TESTING = true;
 
export const POST = async (req: Request, res: Response) => {
let headers = {};
 
if (ENABLE_TESTING) {
const testingheaders = {
'Access-Control-Allow-Origin': 'https://www.remotion.dev',
'Access-Control-Allow-Headers':
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode',
'Access-Control-Allow-Methods': 'OPTIONS,POST',
};
headers = {...headers, ...testingheaders};
}
 
if (req.method === 'OPTIONS') {
return new Response(null, {
headers,
});
}
 
// Parse the body properly
const body = await req.json();
 
validateWebhookSignature({
secret: process.env.WEBHOOK_SECRET as string,
body: body,
signatureHeader: req.headers.get('X-Remotion-Signature') as string,
});
 
const payload = body as WebhookPayload;
 
if (payload.type === 'success') {
//...
} else if (payload.type === 'timeout') {
//...
}
 
return new Response(JSON.stringify({success: true}));
};
 
export const OPTIONS = POST;

Example webhook endpoint (Next.JS Pages Router)

The same endpoint as above, but using the Pages Router.

pages/api/webhook.ts
tsx
import {
validateWebhookSignature,
WebhookPayload,
} from '@remotion/lambda/client';
 
// Enable testing through the tool below
// You may disable it in production
const ENABLE_TESTING = true;
 
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (ENABLE_TESTING) {
res.setHeader('Access-Control-Allow-Origin', 'https://www.remotion.dev');
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS,POST');
res.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode',
);
}
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}
 
validateWebhookSignature({
secret: process.env.WEBHOOK_SECRET as string,
body: req.body,
signatureHeader: req.headers['x-remotion-signature'] as string,
});
 
// If code reaches this path, the webhook is authentic.
const payload = req.body as WebhookPayload;
if (payload.type === 'success') {
// ...
} else if (payload.type === 'timeout') {
// ...
}
 
res.status(200).json({
success: true,
});
}
pages/api/webhook.ts
tsx
import {
validateWebhookSignature,
WebhookPayload,
} from '@remotion/lambda/client';
 
// Enable testing through the tool below
// You may disable it in production
const ENABLE_TESTING = true;
 
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (ENABLE_TESTING) {
res.setHeader('Access-Control-Allow-Origin', 'https://www.remotion.dev');
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS,POST');
res.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode',
);
}
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}
 
validateWebhookSignature({
secret: process.env.WEBHOOK_SECRET as string,
body: req.body,
signatureHeader: req.headers['x-remotion-signature'] as string,
});
 
// If code reaches this path, the webhook is authentic.
const payload = req.body as WebhookPayload;
if (payload.type === 'success') {
// ...
} else if (payload.type === 'timeout') {
// ...
}
 
res.status(200).json({
success: true,
});
}

Test your webhook endpoint

You can use this tool to verify that your webhook endpoint is working properly. The tool will send an appropriate demo payload and log the response to the screen. All requests sent by this tool will have the "X-Remotion-Mode" header set to "demo".

info

This tool sends the demo webhook requests directly from your browser, which has the following implications:

  • CORS requirements:
    • Make sure your API endpoint is configured to accept requests from remotion.dev by setting "Access-Control-Allow-Origin": "https://www.remotion.dev". This is necessary for this tool to work, but not for your production webhook endpoint.
    • You must set "Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode"
    • You must set "Access-Control-Allow-Methods": "OPTIONS,POST".
    • Read the error messages in the DevTools to debug potential CORS issues.
  • You can use a server listening on localhost and don't need to use a reverse proxy.
info
Your webhook URL:
Your webhook secret:

See also