Endpoints & webhooks
Finish long-encoding videos, sync Mux-dashboard deletes, and ingest video in code — the endpoints and webhook that keep Payload and Mux in step.
The plugin mounts three HTTP endpoints and expects one Mux webhook. Together they finish slow-encoding videos, mirror Mux-dashboard deletes back into Payload, and let you create a video entirely in code.
Endpoints
The plugin registers three endpoints under Payload's API route. The upload pair is gated by
options.access.upload (default any logged-in user) and the webhook by options.access.webhook
(default Mux signature verification) — see Gating endpoints.
| Endpoint | What it does |
|---|---|
POST /api/mux/upload | Mint a Mux direct-upload. The admin uploader posts files to it. |
GET /api/mux/upload?id=… | Read a direct-upload, to pick up its asset_id. |
POST /api/mux/webhook | Verify and apply Mux events: set metadata on ready/updated, delete on deleted, mark the doc errored (with the Mux message) on errored, and (with autoCreateOnWebhook) backfill assets uploaded in Mux. |
GET /api/mux/refresh?id=… | Ask Mux for a doc's current asset state and write it back — the admin uploader polls it, and it recovers a missed webhook. Gated by options.access.refresh (any logged-in user by default). |
Webhook
On upload, the beforeChange hook polls the new asset for ~6 seconds. If Mux finishes encoding in that window, the hook fills in the playback metadata inline. Anything slower is saved with just its assetId and left for the webhook.
Point a Mux webhook at /api/mux/webhook (or <your routes.api>/mux/webhook if you've customized Payload's API route). Set MUX_WEBHOOK_SECRET so the plugin can verify each event's signature.
The webhook only works on a live site. Mux pushes events to your endpoint, so its servers
must be able to POST to it — and they can't reach localhost. Wire it once the site is deployed:
set MUX_WEBHOOK_SECRET and point the dashboard at https://your-site/api/mux/webhook. Local
development runs fine without it — the uploader's refresh polling and doc saves refetch asset
state from Mux directly (see the split below).
Not every flow depends on the webhook. Use the split below to know what breaks without it.
- Metadata for videos that take longer than ~6s to encode, without anyone touching the doc. The
common case. The upload's poll gives up at ~6s and the webhook is what fills
playbackOptionsin on its own. Without it the video stayspreparinguntil someone saves the doc, which refetches from Mux — so a missed event is recoverable, but only by hand. See Troubleshooting. - Mux → Payload delete sync. Deleting an asset in the Mux dashboard removes the Payload doc only
via the
video.asset.deletedevent. autoCreateOnWebhookbackfill. Importing assets you uploaded directly in Mux.
- The upload itself, and metadata for short videos (set synchronously by the 6s poll).
- Recovering a video stuck on
preparing. Saving the doc refetches the asset from Mux, so a webhook you never received doesn't strand the video. - Payload → Mux delete. Handled by the collection's
afterDeletehook, not the webhook. - Server-side ingest / seeding. The 6s limit doesn't apply; ingest waits for
readyand writes full metadata into the doc, so these play straight away whatever their length. - Playback of already-populated videos: the URLs compute on read from the stored playback id.
Server-side ingest
Besides the browser uploader, a mux-video can be created server-side from a local file or URL. This is handy for imports, migrations, and seeding. Pass a source; the beforeValidate hook uploads it to Mux, waits for the asset to be ready, fills in the metadata, and discards source.
import { ingestMuxVideo } from '@pro-laico/payload-mux'
await ingestMuxVideo(payload, { source: '/path/to/intro.mp4', title: 'Intro' })
// or a URL: ingestMuxVideo(payload, { source: 'https://example.com/intro.mp4', title: 'Intro' })
// playback policy comes from the plugin's `options.playbackPolicy`; pass { playbackPolicy: 'signed' } to override one video
// posterTimestamp: 12.5 picks the poster frame; omitted, the poster is the first frame
// collection: 'media' — only if you renamed the collection AND `payload` was built without the plugin; otherwise the marker's slug is usedLocal paths stream from disk, so ingesting a large file doesn't load it all into memory. Ingest is also what powers declarative seeding — see How it works.