Payload Plugins
Pluginspayload-mux

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.

For AI / LLMs: View Markdown

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.

EndpointWhat it does
POST /api/mux/uploadMint 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/webhookVerify 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 playbackOptions in on its own. Without it the video stays preparing until 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.deleted event.
  • autoCreateOnWebhook backfill. 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 afterDelete hook, not the webhook.
  • Server-side ingest / seeding. The 6s limit doesn't apply; ingest waits for ready and 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 used

Local 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.

On this page