Guide to Mux Webhooks: Features and Best Practices
Mux webhooks tell your application when video work finishes outside the request cycle: an asset is ready to play, an upload becomes an asset, a live stream connects or disconnects, or a Robots AI job completes. Mux asks you to use webhooks rather than polling the Asset API, which it rate limits, so for most Mux integrations webhooks are the main source of state changes.
This guide covers how Mux webhooks are configured and signed, the event types and the body they share, how retries and duplicates behave, and the practices that keep a Mux receiver reliable in production.
What are Mux webhooks?
A Mux webhook is an endpoint URL configured for one Mux environment. Once it exists, Mux sends it every event in that environment as a JSON POST. You cannot subscribe to a subset of event types; your receiver filters on the type field.
Each delivery carries a mux-signature header with a Unix timestamp and an HMAC-SHA256 signature of the timestamp and the raw body. The signing secret is different for each endpoint.
Delivery is at least once. Mux retries for 24 hours if it does not get a 2xx, and says duplicates "may be sent even if your service responds with a 2xx response code".
Mux webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard (Settings > Webhooks) or the Webhooks API |
| Scope | One Mux environment per endpoint |
| Subscription model | Every event in the environment. No per-type selection |
| Signature header | mux-signature: t=<timestamp>,v1=<signature> |
| Scheme | HMAC-SHA256, hex digest |
| Signed content | <timestamp>.<raw body> |
| Secret | One per endpoint. The API returns it only once, at creation |
| Timestamp tolerance | Mux's SDKs default to 5 minutes |
| Response window | 5 seconds per attempt |
| Retries | For 24 hours, with increasing delays |
| Duplicates | Possible even after a 2xx. Deduplicate on id |
| Local testing | Mux CLI: mux webhooks listen, trigger and events replay |
| Source IPs | Not published |
Common events
Mux's machine-readable webhook spec lists 117 event types: 53 for Mux Video and 64 for Mux Robots. The type field holds the full event name.
| Area | Example events |
|---|---|
| Assets | video.asset.created, video.asset.ready, video.asset.errored, video.asset.updated, video.asset.deleted, video.asset.live_, video.asset.warning |
| Asset outputs | video.asset.static_, video.asset.master.ready, video.asset.track.ready, video.asset.shots.ready |
| Direct uploads | video.upload.created, video.upload.asset_, video.upload.cancelled, video.upload.errored |
| Live streams | video.live_, video.live_, video.live_stream.active, video.live_, video.live_stream.idle |
| Simulcast targets | video.live_, video.live_, video.live_ |
| Robots jobs | robots.job.<workflow>.pending, .processing, .completed, .errored, .cancelled |
| Robots directives | robots.directive_, .partial, .completed, .errored |
The Robots workflows are ask_questions, edit_captions, find_best_thumbnails, find_key_moments, find_scenes, generate_chapters, generate_, generate_, moderate, summarize, translate_audio and translate_captions.
Here is the complete video.asset.ready delivery from Mux's docs:
{
"type": "video.asset.ready",
"object": {
"type": "asset",
"id": "0201p02fGKPE7MrbC269XRD7LpcHhrmbu0002"
},
"id": "3a56ac3d-33da-4366-855b-f592d898409d",
"environment": {
"name": "Demo pages",
"id": "j0863n"
},
"data": {
"tracks": [
{ "type": "video", "max_width": 1280, "max_height": 544, "max_frame_rate": 23.976, "id": "0201p02fGKPE7MrbC269XRD7LpcHhrmbu0002", "duration": 153.361542 },
{ "type": "audio", "max_channels": 2, "max_channel_layout": "stereo", "id": "FzB95vBizv02bYNqO5QVzNWRrVo5SnQju", "duration": 153.361497 }
],
"status": "ready",
"max_stored_resolution": "SD",
"max_stored_frame_rate": 23.976,
"id": "0201p02fGKPE7MrbC269XRD7LpcHhrmbu0002",
"duration": 153.361542,
"created_at": "2018-02-15T01:04:45.000Z",
"aspect_ratio": "40:17"
},
"created_at": "2018-02-15T01:04:45.000Z",
"accessor_source": null,
"accessor": null,
"request_id": null
}
id is the event ID, which is what you deduplicate on. object.type names the resource (asset here), not the event, so do not route on it. data is the resource itself, and its shape follows the resource type.
Mux's own pages disagree in two places. The webhook spec marks an attempts array as a required envelope field, but the example above has none. And the Robots Directives guide shows directive-run deliveries with the run nested under a key named after the event and no type field, while the spec gives them the same envelope as every other event. Until you have seen a real directive-run delivery, handle both shapes.
Setting up Mux webhooks
In the Mux Dashboard, open Settings > Webhooks, choose the environment, and add your endpoint URL. Copy the endpoint's signing secret from the same page and store it, for example as MUX_WEBHOOK_SECRET.
You can also create endpoints with the Webhooks API. The create response includes signing_secret, and that is the only time the API returns it. Disabling an endpoint with enabled: false keeps its secret; deleting and recreating it issues a new one.
For local development, the Mux CLI can forward events to your machine:
mux webhooks listen --forward-to http://localhost:3000/webhooks/mux
mux webhooks trigger video.asset.ready --forward-to http://localhost:3000/webhooks/mux
The CLI prints its own signing secret and signs forwarded requests with it, so use that secret locally rather than the endpoint's. Mux notes CLI forwarding has no delivery guarantees and is for development only.
Securing Mux webhooks
Split mux-signature on commas, take the t and v1 values, compute an HMAC-SHA256 of t + "." + rawBody with the endpoint's signing secret, hex-encode it, and compare it with v1 in constant time.
const crypto = require('crypto');
const express = require('express');
function verifyMuxSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => {
const index = part.indexOf('=');
return [part.slice(0, index).trim(), part.slice(index + 1).trim()];
})
);
const timestamp = parts.t;
const signature = parts.v1;
if (!timestamp || !signature) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest('hex');
const received = Buffer.from(signature.toLowerCase(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
if (received.length !== computed.length || !crypto.timingSafeEqual(received, computed)) {
return false;
}
// Optional: Mux's SDKs also reject timestamps more than 5 minutes old.
// if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
return true;
}
const app = express();
app.post('/webhooks/mux', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyMuxSignature(req.body, req.get('mux-signature'), process.env.MUX_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on event.id, route on event.type, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
import os
def verify_mux_signature(raw_body: bytes, signature_header, secret: str) -> bool:
if not signature_header or not secret:
return False
parts = dict(
part.strip().split("=", 1) for part in signature_header.split(",") if "=" in part
)
timestamp, signature = parts.get("t"), parts.get("v1")
if not timestamp or not signature:
return False
signed = timestamp.encode("utf-8") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature.lower().encode("utf-8"), expected.encode("utf-8"))
# In FastAPI: raw_body = await request.body()
# verify_mux_signature(raw_body, request.headers.get("mux-signature"),
# os.environ["MUX_WEBHOOK_SECRET"])
If you use Mux's TypeScript or Elixir SDK, its webhook helper does this check, including the 5-minute tolerance, for you.
To test your own code, use the secret mux-test-signing-secret, the timestamp 1565220904 and this exact body:
{"type":"video.asset.ready","id":"3a56ac3d-33da-4366-855b-f592d898409d","object":{"type":"asset","id":"0201p02fGKPE7MrbC269XRD7LpcHhrmbu0002"}}
The header should be:
t=1565220904,v1=6755c58d0854eeecd3e026fb3a7107f557188d9142e2234309202de65245cc35
Leave any timestamp check off while you run this, since the timestamp is from 2019.
In Hookdeck, paste the endpoint's signing secret into the source. Hookdeck checks the v1 signature over the timestamp and body, and does not enforce a timestamp tolerance, so retried and replayed events keep passing verification. If you want the freshness check, add it in your own receiver.
Make Mux webhooks production-ready. Hookdeck Event Gateway verifies
mux-signature, answers Mux well inside the 5-second limit, and queues every asset, live stream and Robots event until your service has processed it.
Mux webhook limitations and pain points
Every event, whether you want it or not
The Problem: Your endpoint receives every event in the environment, including types you never handle.
Why It Happens: Webhooks are scoped to an environment, and "notifications will be sent for all events for that environment". There are 117 event types and no per-type subscription.
Workarounds:
- Return
200quickly for types you ignore, so they do not trigger retries. - Use separate environments for separate applications.
How Hookdeck Can Help: Hookdeck filters can drop or route events by type before they reach your service, so each service only sees the events it handles.
Five seconds per attempt
The Problem: Work done inside the request causes timeouts and retries.
Why It Happens: A single attempt times out after 5 seconds. Fetching the asset, writing to several systems or starting a transcode job in the handler can exceed that.
Workarounds:
- Verify, store and return
200, then do the work in a background job.
How Hookdeck Can Help: Hookdeck acknowledges Mux immediately and delivers to your service as a separate step, with its own retries.
Duplicates and ordering
The Problem: The same event can arrive twice, and events can arrive out of order.
Why It Happens: Mux retries for 24 hours with increasing delays and warns that duplicates can be sent even after a 2xx. It does not document delivery order, so you cannot rely on video.asset.created arriving before video.asset.ready.
Workarounds:
- Deduplicate on the event
id. - Use
data.statusor fetch the resource from the API rather than inferring state from the order events arrive in.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries on id before they reach your service.
Lost after 24 hours
The Problem: An outage longer than a day loses events.
Why It Happens: Mux stops retrying after 24 hours. It does not publish the number of attempts or the intervals.
Workarounds:
- Alert on errors at your webhook route so outages are caught within hours.
- Reconcile assets and live streams from the API after a long outage.
How Hookdeck Can Help: Hookdeck stores every delivery as it arrives and retries against your service on a schedule you control, and you can replay any event from its history once you have recovered.
A secret you only see once
The Problem: You lose the signing secret for an endpoint created through the API.
Why It Happens: The API returns signing_secret only in the create response. It does not appear in list or retrieve calls.
Workarounds:
- Store the secret as soon as you create the endpoint.
- If it is lost, delete and recreate the endpoint to get a new one.
How Hookdeck Can Help: Keep the secret in the Hookdeck source, so your services trust Hookdeck and never need the Mux secret.
Best practices
Verify against the raw body
The signature covers the exact bytes Mux sent, prefixed with the timestamp and a dot. Read the raw body with express.raw(), Buffer.from(await request.arrayBuffer()) in a Next.js route handler, or await request.body() in FastAPI.
Deduplicate on the event ID
Store each processed id and skip repeats. See our guide to webhook idempotency.
Route on type, not object.type
object.type is the resource type and is the same for every event about that resource.
Keep local and production secrets apart
The CLI's forwarding secret is not the endpoint's secret. Use different environment variables, or different .env files, for each.
Acknowledge fast and process asynchronously
Return 200 within the 5-second limit and do the real work afterwards. See why to process webhooks asynchronously.
Conclusion
Mux signs <timestamp>.<raw body> with HMAC-SHA256 and sends the result in mux-signature. Production problems tend to come from elsewhere: every environment event lands on one endpoint, attempts time out after 5 seconds, duplicates arrive even after success, and retries end after 24 hours.
Hookdeck Event Gateway sits in front of your receiver, checks every Mux signature, filters the event types you do not need, and holds the rest until your service has processed them, so a slow handler or an outage does not lose a ready asset.
Get started with Hookdeck for free and handle Mux webhooks reliably in minutes.