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.
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.11costs: {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.11costs: {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.
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.jsjavascript
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 belowconst ENABLE_TESTING = true;// Express API endpointrouter.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, errorconst mode = req.header("X-Remotion-Mode"); // demo or productionconst payload = JSON.parse(req.body) as WebhookPayload;if (payload.type === "success") {// ...} else if (payload.type === "timeout") {// ...}});
server.jsjavascript
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 belowconst ENABLE_TESTING = true;// Express API endpointrouter.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, errorconst mode = req.header("X-Remotion-Mode"); // demo or productionconst 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.tstsx
import {validateWebhookSignature ,WebhookPayload ,} from "@remotion/lambda/client";// Enable testing through the tool below// You may disable it in productionconstENABLE_TESTING = true;export constPOST = async (req :Request ,res :Response ) => {letheaders = {};if (ENABLE_TESTING ) {consttestingheaders = {"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 newResponse (null, {headers ,});}// Parse the body properlyconstbody = awaitreq .json ();validateWebhookSignature ({secret :process .env .WEBHOOK_SECRET as string,body :body ,signatureHeader :req .headers .get ("X-Remotion-Signature") as string,});constpayload =body asWebhookPayload ;if (payload .type === "success") {//...} else if (payload .type === "timeout") {//...}return newResponse (JSON .stringify ({success : true }));};export constOPTIONS =POST ;
app/api/webhook.tstsx
import {validateWebhookSignature ,WebhookPayload ,} from "@remotion/lambda/client";// Enable testing through the tool below// You may disable it in productionconstENABLE_TESTING = true;export constPOST = async (req :Request ,res :Response ) => {letheaders = {};if (ENABLE_TESTING ) {consttestingheaders = {"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 newResponse (null, {headers ,});}// Parse the body properlyconstbody = awaitreq .json ();validateWebhookSignature ({secret :process .env .WEBHOOK_SECRET as string,body :body ,signatureHeader :req .headers .get ("X-Remotion-Signature") as string,});constpayload =body asWebhookPayload ;if (payload .type === "success") {//...} else if (payload .type === "timeout") {//...}return newResponse (JSON .stringify ({success : true }));};export constOPTIONS =POST ;
Example webhook endpoint (Next.JS Pages Router)
The same endpoint as above, but using the Pages Router.
pages/api/webhook.tstsx
import {validateWebhookSignature ,WebhookPayload ,} from "@remotion/lambda/client";// Enable testing through the tool below// You may disable it in productionconstENABLE_TESTING = true;export default async functionhandler (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.constpayload =req .body asWebhookPayload ;if (payload .type === "success") {// ...} else if (payload .type === "timeout") {// ...}res .status (200).json ({success : true,});}
pages/api/webhook.tstsx
import {validateWebhookSignature ,WebhookPayload ,} from "@remotion/lambda/client";// Enable testing through the tool below// You may disable it in productionconstENABLE_TESTING = true;export default async functionhandler (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.constpayload =req .body asWebhookPayload ;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"
.
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.
- Make sure your API endpoint is configured to accept requests from
- You can use a server listening on
localhost
and don't need to use a reverse proxy.