Test Mux webhooks locally
Receive Mux Video webhook events on localhost with a Horizon tunnel, and verify the mux-signature header with the Mux SDK.
Receive Mux events on your laptop while you build, with a URL Mux can reach.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. - A Next.js app that uses the App Router and runs on port 3000
- A Mux account
Start your app
Install the Mux TypeScript SDK:
npm install @mux/tsCreate a route handler. Mux sends a mux-signature header that looks like t=<timestamp>,v1=<signature>. Mux builds v1 with HMAC-SHA256 (a keyed hash) over the timestamp, a dot and the raw request body. The SDK's webhooks.unwrap method checks this for you. It takes the raw body string, the headers and your signing secret, and throws when the signature is wrong.
import Mux from "@mux/ts";
const mux = new Mux({
webhookSecret: process.env.MUX_WEBHOOK_SECRET,
});
export async function POST(request: Request) {
const body = await request.text();
let event: Awaited<ReturnType<typeof mux.webhooks.unwrap>>;
try {
event = await mux.webhooks.unwrap(body, request.headers);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.log(`Mux signature verification failed: ${message}`);
return new Response("Invalid signature", { status: 400 });
}
console.log(`Received Mux event: ${event.type}`);
return new Response("ok", { status: 200 });
}Mux waits 5 seconds for a response. It retries for 24 hours when it doesn't get a 2xx. It can also send the same event twice, so make the handler safe to run twice.
Add the signing secret to .env.local. You copy it in a later step.
MUX_WEBHOOK_SECRET=replace-with-your-signing-secretStart the app on port 3000.
npm run devStart a tunnel
Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so your Mux webhook would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.
hrzn tunnel http://localhost:3000 -s my-appHORIZON: Tunnel connected
URL https://my-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-appYour public URL is https://my-app.hrzn.run. Keep this terminal open.
Add the webhook in Mux
Your webhook URL is the tunnel URL plus the route path: https://my-app.hrzn.run/api/webhooks/mux.
- Open the webhooks settings in the Mux Dashboard. They sit under Settings.
- Switch to the environment you build in. Webhooks are scoped to one environment.
- Add a webhook and enter your URL.
- Copy the signing secret from the same page into
MUX_WEBHOOK_SECRETin.env.local. - Restart
npm run devso Next.js loads the variable.
Every webhook endpoint has its own signing secret. If you create the webhook with the Webhooks API, the secret is the signing_secret value in the create response. Mux returns it once.
Send a test event
Mux has a CLI that sends synthetic events and re-sends stored ones. Install it from the Mux CLI docs.
Send a synthetic event to your app:
mux webhooks trigger video.asset.ready --forward-to http://localhost:3000/api/webhooks/muxMux's CLI replay command re-sends events it stored during a mux webhooks listen session:
mux webhooks events list
mux webhooks events replay <event-id> --forward-to http://localhost:3000/api/webhooks/muxThe CLI forwards straight to the URL you give it and signs the request with its own signing secret. It doesn't go through Horizon. To test the secret from your Mux webhook, trigger a real event instead, such as creating an asset in the same environment.
Check it works
Create an asset in the environment you registered the webhook for. When it finishes processing, Mux sends video.asset.ready to your Horizon URL. Your Horizon terminal prints one line per event:
POST 200 /api/webhooks/muxYour app terminal prints:
Received Mux event: video.asset.readyIf the line shows [400], see Troubleshooting.
Troubleshooting
The signature doesn't match
- Check that
MUX_WEBHOOK_SECRETis the signing secret of the endpoint you registered. Each endpoint has its own. - Pass
unwrapthe raw body fromrequest.text(). Don't runJSON.parseandJSON.stringifyfirst, because that can change the bytes. - Restart
npm run devafter you edit.env.local. - The Mux CLI signs with a different secret. Events from
mux webhooks triggerfail against your Dashboard endpoint's secret. SetMUX_WEBHOOK_SECRETto the secret thatmux webhooks listen --forward-toprints when you test with the CLI.
Verification fails on an old event
Mux's SDKs allow 5 minutes between the timestamp in the header and the current time by default. A stored event that you send again later can fall outside it.
The request returns 404
The Horizon line shows [404]. The webhook URL doesn't match your route. The file app/api/webhooks/mux/route.ts serves /api/webhooks/mux, and Mux sends POST requests.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and the URL stays the same. -s needs a subdomain you reserved, see Pricing.
Nothing reaches your app
- Check that the Horizon terminal is still running. If its last line is
Connection lost. Reconnecting…, wait forReconnected. - Check that the webhook is registered for the environment where the event happens.
Next steps
- Read Mux's guide to verifying webhook signatures.
- See the full list of events in Listen for webhooks.