Ship your first media job in minutes.
Upload once, request exactly the outputs you need, and receive a signed terminal webhook. The examples below match the live API contract used by production integrations.
Upload, submit, then wait for the webhook.
MediaRuntime is asynchronous. A successful submission returns QUEUED immediately; completed outputs arrive later through your signed webhook.
# 1. Ask MediaRuntime for a short-lived upload URL.
UPLOAD=$(curl -sS -X POST "https://mediaruntime.com/v1/upload-url" \
-H "X-API-Key: $MEDIARUNTIME_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename":"launch-trailer.mp4","content_type":"video/mp4"}')
UPLOAD_URL=$(printf '%s' "$UPLOAD" | jq -r .upload_url)
FILE_URI=$(printf '%s' "$UPLOAD" | jq -r .file_uri)
UPLOAD_CONTENT_TYPE=$(printf '%s' "$UPLOAD" | jq -r '.upload_headers["Content-Type"]')
UPLOAD_AUTH=$(printf '%s' "$UPLOAD" | jq -r '.upload_headers.Authorization // empty')
# 2. Upload the bytes. Send every returned upload header.
UPLOAD_ARGS=(-H "Content-Type: $UPLOAD_CONTENT_TYPE")
if [[ -n "$UPLOAD_AUTH" ]]; then
UPLOAD_ARGS+=(-H "Authorization: $UPLOAD_AUTH")
fi
curl -sS -X PUT "$UPLOAD_URL" \
"${UPLOAD_ARGS[@]}" \
--upload-file ./launch-trailer.mp4
# 3. Queue an asynchronous job using the returned file_uri.
curl -sS -X POST "https://mediaruntime.com/v1/transcode" \
-H "X-API-Key: $MEDIARUNTIME_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg file_uri "$FILE_URI" '{
file_url: $file_uri,
metadata: { asset_id: "asset_0426", media_type: "video" },
outputs: [{
type: "mp4",
preset: "mp4_720p_h264_aac",
poster_time_sec: 2
}]
}')"{
"job_id": "job_1320c28b72104811b075a26a99496cf6",
"status": "QUEUED",
"tier": "standard",
"msg": "accepted"
}Keep API keys on your server.
Create a key from Account → API Keys. The raw key is shown once and belongs in your secret manager, never in browser code, mobile binaries, logs, or source control.
Header
Send X-API-Key on every /v1 request.
Storage
Store MEDIARUNTIME_API_KEY in a server-side secret manager.
Rotation
Create a replacement, deploy it, verify traffic, then revoke the old key.
Make metadata do the integration work.
The production pattern used by wMedia is intentionally simple: submit an input, output recipes, and enough opaque metadata to reconnect the terminal event to your own database record.
{
"file_url": "gs://bucket/path/source.mp4",
"metadata": {
"producer": "my-api",
"entity_id": "video_01J8Y4",
"owner_id": "user_482",
"media_type": "video",
"trace_id": "req_f839"
},
"moderation": {
"enabled": true,
"mode": "report",
"checks": ["sexual", "violence", "dangerous"]
},
"watermark": { "enabled": true },
"outputs": [
{
"type": "mp4",
"preset": "mp4_720p_h264_aac",
"path_suffix": "web",
"poster_time_sec": 2,
"gif_preview": {
"enabled": true,
"width": 320,
"fps": 8,
"start_time": 2,
"duration": 2.5
}
},
{
"type": "image",
"preset": "image_multi_v1",
"path_suffix": "covers",
"images": [
{ "width": 1280, "height": 720, "mode": "fit", "format": "webp", "quality": 84 },
{ "width": 480, "height": 480, "mode": "cover", "format": "webp", "quality": 80 }
]
}
]
}| Field | Type | Notes |
|---|---|---|
| file_url | string | One gs:// or public HTTPS input. Use this or inputs, never both. |
| inputs | array | Batch fan-out for 1–25 inputs. Each may carry input_id and metadata. |
| outputs | array | 1–10 output recipes. Each requires type; preset is strongly recommended. |
| metadata | object | Up to 32 KiB of JSON. Persisted and echoed at meta.request_metadata. |
| moderation | object | Premium visual-media checks: sexual, violence, dangerous. |
| watermark | object | Premium visual-media overlay. The account must already have a PNG logo. |
Batch fan-out
Use a batch when every input needs the same outputs. Per-input metadata is merged into each child job; the parent job becomes your batch reference.
{
"inputs": [
{
"file_url": "https://cdn.example.com/a.mp4",
"input_id": "asset-a",
"metadata": { "position": 0 }
},
{
"file_url": "https://cdn.example.com/b.mp4",
"input_id": "asset-b",
"metadata": { "position": 1 }
}
],
"metadata": { "batch_id": "import_2026_08_09" },
"outputs": [{ "type": "mp4", "preset": "mp4_720p_h264_aac" }]
}Choose the artifact you want the engine to produce.
The type and preset form one executable recipe. Use the exact type shown below: a preset name alone does not change the output type, and mismatched pairs can be rejected or routed incorrectly.
{
"file_url": "https://cdn.example.com/landscape-interview.mp4",
"outputs": [{
"type": "social",
"preset": "social_vertical_blur"
}]
}social_vertical_blur creates a 1080×1920 H.264/AAC MP4. The source is scaled to fit without cropping; a blurred copy fills the 9:16 canvas behind it. Use it for Reels, TikTok, and Shorts. It is a Premium recipe because the vertical output is 1920 pixels high.Video files and posters
| Preset | Send | Job execution | Base tier |
|---|---|---|---|
| mp4_720p_h264_aac (type: mp4) | Video | 720p H.264/AAC MP4 with faststart for web playback. | Standard |
| mp4_ladder_v1 (type: mp4) | Video | Three MP4 renditions at 1080p, 720p, and 480p, plus a poster for each rendition. | Standard |
| transmux_mp4_fast (type: mp4) | Compatible audio/video | Copies existing streams into a faststart MP4 without re-encoding. If copy fails and fallback is enabled, the engine re-encodes with the 720p H.264 preset. | Standard |
| poster_frame_v1 (type: mp4) | Video | One 720p JPG captured at poster_time_sec. The request type is still mp4. | Standard |
| mp4_hevc_1080p (type: mp4) | Video | 1080p HEVC/H.265 + AAC MP4 with the hvc1 tag for Apple playback. | Premium |
| mp4_av1_smart (type: mp4) | Video | 1080p AV1 + Opus MP4 optimized for compression efficiency; encoding is CPU-intensive. | Premium |
| mov_prores_422 (type: mp4) | Video | ProRes 422 HQ + PCM MOV editing master. Preserves source dimensions and produces a large mezzanine file. | Premium |
Social video
| Preset | Send | Job execution | Base tier |
|---|---|---|---|
| social_vertical_blur (type: social) | Video | 1080×1920 H.264/AAC MP4. Fits the source over a blurred 9:16 background for Reels, TikTok, and Shorts. | Premium |
Animated GIF
| Preset | Send | Job execution | Base tier |
|---|---|---|---|
| gif_hq (type: gif) | Video | Animated GIF at 480px width and 15 fps using palette generation for better color quality. | Standard |
Frame extraction
| Preset | Send | Job execution | Base tier |
|---|---|---|---|
| extract_frames_1 (type: frames) | Video | Numbered JPG frame sequence sampled at 1 frame per second. | Standard |
| extract_frames_5 (type: frames) | Video | Numbered JPG frame sequence sampled at 5 frames per second. | Standard |
Streaming
| Preset | Send | Job execution | Base tier |
|---|---|---|---|
| hls_ladder_v1 (type: hls) | Video | HLS VOD package with 1080p and 720p H.264/AAC variants, master playlist, and 6-second segments. | Standard |
Audio
| Preset | Send | Job execution | Base tier |
|---|---|---|---|
| audio_copy_fast (type: audio) | Audio or video with audio | Copies the source audio stream without re-encoding. If copy fails and fallback is enabled, the engine writes 128 kbps AAC instead. | Standard |
| audio_aac_128k (type: audio) | Audio or video with audio | 128 kbps AAC in a faststart M4A file. | Standard |
| audio_mp3_128k (type: audio) | Audio or video with audio | 128 kbps MP3 file. | Standard |
| audio_opus_96k (type: audio) | Audio or video with audio | 96 kbps Opus file, well suited to speech delivery. | Standard |
| audio_loudnorm_aac_128k (type: audio) | Audio or video with audio | Normalizes toward -16 LUFS, then writes 128 kbps AAC. | Standard |
| audio_trim_silence_aac_128k (type: audio) | Audio or video with audio | Removes leading and trailing silence, then writes 128 kbps AAC. | Premium |
| audio_loudnorm_trim_aac_128k (type: audio) | Audio or video with audio | Trims boundary silence, normalizes toward -16 LUFS, then writes 128 kbps AAC. | Premium |
| audio_whisper_prep (type: audio) | Audio or video with audio | 16 kHz mono PCM WAV prepared for Whisper, ASR, or other speech pipelines. | Premium |
Image derivatives
| Preset | Send | Job execution | Base tier |
|---|---|---|---|
| image_multi_v1 (type: image) | Image | Creates the images array you request using fit, fill, cover, or contain. Tier depends on format, size, count, smart crop, and background removal. | Standard or Premium |
transmux_mp4_fast avoids a quality-changing encode when the source is MP4-compatible. If copy fails and fallback is enabled, the engine re-encodes with mp4_720p_h264_aac; use that preset directly when you require predictable output characteristics.Upload once. Produce the formats and sidecar artifacts your product needs.
The source extension does not select the output. The type and preset choose the executable recipe, so one uploaded video can become playback video, audio-only media, transcripts, GIF previews, posters, or frame sequences in the same asynchronous job.
| Source | Deliverable | Recipe |
|---|---|---|
| JPG, PNG, or WebP | JPG, PNG, WebP, or AVIF derivatives | image + image_multi_v1; choose images[].format, dimensions, mode, and quality. |
| Video | Web MP4, HLS, social video, or editing master | Choose the matching mp4, hls, or social preset. |
| Video | Animated GIF, poster, or JPG frame sequence | Use gif_hq, poster_frame_v1, extract_frames_1/5, or attach gif_preview to a video output. |
| Video or audio | M4A, MP3, Opus, or speech WAV | Choose the corresponding audio_* preset; video inputs have their audio stream extracted. |
| Video or audio speech | SRT, WebVTT, or both | Add subtitles to an audio or video output and choose srt, vtt, or both. |
{
"file_url": "gs://value-returned-by-upload-url",
"metadata": {
"media_type": "image",
"asset_id": "product-photo-0426"
},
"outputs": [{
"type": "image",
"preset": "image_multi_v1",
"path_suffix": "converted",
"images": [
{ "width": 1600, "height": 1200, "mode": "fit", "format": "jpg", "quality": 88 },
{ "width": 1600, "height": 1200, "mode": "fit", "format": "webp", "quality": 82 }
]
}]
}{
"file_url": "gs://value-returned-by-upload-url",
"outputs": [
{ "type": "mp4", "preset": "mp4_720p_h264_aac", "path_suffix": "web-video" },
{ "type": "audio", "preset": "audio_mp3_128k", "path_suffix": "audio-only" }
]
}{
"file_url": "gs://value-returned-by-upload-url",
"metadata": { "media_type": "video", "asset_id": "interview-0426" },
"outputs": [{
"type": "audio",
"preset": "audio_aac_128k",
"path_suffix": "audio-and-transcript",
"subtitles": {
"enabled": true,
"languages": ["auto"],
"format": "both",
"model": "ggml-base.bin",
"translate_to_english": false
}
}]
}{
"file_url": "gs://value-returned-by-upload-url",
"metadata": { "media_type": "video", "asset_id": "trailer-0426" },
"outputs": [{
"type": "mp4",
"preset": "mp4_720p_h264_aac",
"path_suffix": "web",
"poster_time_sec": 4,
"poster_format": "jpg",
"gif_preview": {
"enabled": true,
"width": 480,
"fps": 10,
"start_time": 4,
"duration": 3
}
}]
}{
"file_url": "gs://value-returned-by-upload-url",
"metadata": { "media_type": "video", "asset_id": "stream-0426" },
"outputs": [{
"type": "hls",
"preset": "hls_ladder_v1",
"path_suffix": "stream"
}]
}{
"file_url": "gs://value-returned-by-upload-url",
"metadata": { "media_type": "video", "asset_id": "clip-0426" },
"outputs": [
{ "type": "gif", "preset": "gif_hq", "path_suffix": "animated-preview" },
{ "type": "frames", "preset": "extract_frames_1", "path_suffix": "sampled-frames" }
]
}hls_ladder_v1 creates a master playlist, 1080p and 720p H.264/AAC variant playlists, and 6-second media segments. Use the reported master-playlist URL, or move the complete bundle together.gif_hq creates a full 480px, 15 fps primary GIF. gif_preview adds a shorter, explicitly timed GIF to another video output. Frame presets return numbered JPG sequences at one or five frames per second.Useful switches
audio_aac_128k returns M4A, audio_mp3_128k returns MP3, audio_opus_96k returns Opus, and audio_whisper_prep returns 16 kHz mono WAV. Set subtitles.format to srt, vtt, or both. For only one poster image, send type: mp4 with preset: poster_frame_v1 and the desired poster_time_sec.
Start with a preset; override only what matters.
Presets keep requests readable and give the engine a stable baseline. Add explicit rendition, subtitle, preview, codec, or bitrate options only when the product requires them.
{
"file_url": "https://cdn.example.com/source.jpg",
"outputs": [{
"type": "image",
"preset": "image_multi_v1",
"images": [
{ "width": 1200, "height": 630, "mode": "cover", "format": "webp", "quality": 84 },
{ "width": 320, "height": 320, "mode": "cover", "format": "webp", "quality": 78 }
]
}]
}{
"file_url": "https://cdn.example.com/interview.wav",
"outputs": [{
"type": "audio",
"preset": "audio_aac_128k",
"subtitles": {
"enabled": true,
"languages": ["auto"],
"format": "both",
"model": "ggml-base.bin"
}
}]
}{ "watermark": { "enabled": true } }; MediaRuntime resolves the server-owned logo.Attach a sampled visual-safety report to the job.
Moderation runs before transcoding for one image or video input. The current production contract is report-only: the report records evidence and decisions, but it does not block the transcode.
{
"file_url": "https://cdn.example.com/upload.mp4",
"metadata": { "media_type": "video", "asset_id": "asset_0426" },
"moderation": {
"enabled": true,
"mode": "report",
"checks": ["sexual", "violence", "dangerous"]
},
"outputs": [{
"type": "mp4",
"preset": "mp4_720p_h264_aac"
}]
}{
"moderation": {
"requested": {
"enabled": true,
"mode": "report",
"checks": ["sexual", "violence", "dangerous"],
"media_type": "video",
"phase": "phase1_video_report"
},
"result": {
"ok": true,
"media_type": "video",
"verdict": "review",
"flagged_checks": ["violence"],
"scores": {
"violence": { "yes": 0.82, "no": 0.18 }
},
"decisions": {
"violence": { "decision": "review", "raw_decision": "review" }
},
"evidence": {
"frames_sampled": 8,
"frames_flagged": [{
"frame_index": 3,
"timestamp_sec": 20,
"verdict": "review",
"flagged_checks": ["violence"]
}]
},
"video": {
"frame_interval_sec": 10,
"max_frames": 24
}
}
},
"meta": {
"moderation_result": {
"url": "https://storage.googleapis.com/.../moderation_result.json"
}
},
"usage": {
"breakdown": { "moderation_units": 120 }
}
}| Contract | Value | Notes |
|---|---|---|
| Plan | Premium | The API returns 403 unless the account is Premium or auto-upgrade is allowed. |
| Mode | report | This is the only accepted mode today. Do not send block. |
| Checks | sexual, violence, dangerous | Send one to three checks. Omitting checks selects all three. |
| Inputs | One image or video | Audio-only inputs, batches, and Sandbox moderation are rejected. |
| Video sampling | Fixed interval, bounded frames | The service chooses the interval and cap; read the actual values from result.video and result.evidence. |
| Decision | allow, review, or block signal | Report mode never blocks execution. Use result.verdict, decisions, flagged_checks, and evidence in your own policy. |
| Artifact | meta/moderation_result.json | Included in the output ZIP and exposed through meta.moderation_result.url when available. |
| Billing | Separate moderation units | Estimated and settled with the job at usage.breakdown.moderation_units. |
review as a signal for a human queue, publishing hold, or another policy you control. A completed report is not a promise that the media is safe, legal, or policy-compliant.Verify the raw bytes before trusting the event.
Terminal events are delivered at least once and ordering is not guaranteed. Verify HMAC-SHA256, reject stale timestamps, deduplicate event_id, acknowledge quickly, and move heavy work to a queue.
{
"event_id": "webhook_evt_job_1320c28b72104811b075a26a99496cf6",
"job_id": "job_1320c28b72104811b075a26a99496cf6",
"account_id": "acc_xxx",
"status": "COMPLETED",
"completedAt": "2026-08-09T02:41:23Z",
"billing": { "status": "PAID", "estimatedUnits": 31 },
"usage": { "units_total": 31, "breakdown": {} },
"delivery": {
"mode": "PULL",
"retentionDays": 7,
"expiresAt": "2026-08-16T02:41:23Z",
"bundle": {
"type": "zip",
"filename": "outputs.zip",
"download": {
"url": "https://mediaruntime.com/v1/jobs/job_1320c28b72104811b075a26a99496cf6/bundle?token=...",
"expiresAt": "2026-08-16T02:41:23Z"
}
}
},
"meta": {
"engine_result_url": "https://storage.googleapis.com/...",
"outputs_root_gs": "gs://.../jobs/acc_xxx/job_.../outputs",
"request_metadata": {
"producer": "my-api",
"entity_id": "video_01J8Y4",
"media_type": "video"
}
}
}delivery.bundle.download.url, or fetch meta.engine_result_url to enumerate individual output and artifact paths.import crypto from "node:crypto";
import express from "express";
const app = express();
// Register this route before any express.json() middleware.
app.post("/webhooks/mediaruntime", express.raw({ type: "application/json" }), (req, res) => {
const eventId = req.get("X-Transcoder-Id") || "";
const signatureHeader = req.get("X-Transcoder-Signature") || "";
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.trim().split("="))
);
const timestamp = parts.t || "";
const received = parts.v1 || "";
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!eventId || !timestamp || !received || ageSeconds > 300) {
return res.sendStatus(401);
}
const expected = crypto
.createHmac("sha256", process.env.MEDIARUNTIME_WEBHOOK_SECRET)
.update(timestamp + "." + eventId + ".")
.update(req.body) // raw Buffer; never JSON.stringify(req.body)
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(received, "utf8");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
// Enqueue work and deduplicate on event.event_id in your database.
console.log(event.event_id, event.job_id, event.status);
return res.sendStatus(200);
});Delivery rules
- Return any 2xx only after signature verification and durable enqueue/deduplication.
- Treat event_id as the idempotency key.
- Use meta.request_metadata to find your entity without a second lookup table.
- Download retained outputs before delivery.expiresAt.
- A FAILED or REJECTED event has error.code/message and no usable bundle.
Prepaid, pay as you go, and settled from actual usage.
Add a card, fund the wallet, and submit work without a recurring subscription. MediaRuntime reserves an estimate before execution and settles the final charge when the job reaches a terminal state.
| Plan | Public rate | Minimum top-up | Default auto-top-up |
|---|---|---|---|
| Standard Pay-As-You-Go | $0.02/min | $20.00 | $20.00 at $2.00 available |
| Premium Pay-As-You-Go | $0.05/min | $60.00 | $60.00 at $5.00 available |
billing and usage fields for reconciliation.Wallet rules
- Available credit equals wallet credit minus funds reserved for running jobs.
- Insufficient available credit returns HTTP 402 before execution.
- Auto-top-up is optional and requires a card on file.
- A Premium-only request returns 403 when upgrade is not permitted.
Retry transport failures, not invalid work.
Most API errors use a top-level detail field. It may be a string or a structured object, so log the entire response alongside your correlation ID—but never log the API key.
| Status | Meaning | What your integration should do |
|---|---|---|
| 400 | The request is logically invalid or the estimator rejected it. | Fix the request; do not retry unchanged. |
| 401 | The API key is invalid, expired, or revoked. | Correct or rotate the key; do not blindly retry. |
| 402 | The account, wallet, or billing preflight cannot cover the job. | Fund the wallet or resolve billing first. |
| 403 | The plan, role, or feature gate does not permit the request. | Change the plan/request; do not retry unchanged. |
| 413 | The HTTP request body exceeds 2 MiB. | Upload media separately and send URLs only. |
| 422 | The JSON does not match the API schema. | Correct the named field. |
| 429 | The account or key is rate limited. | Retry with exponential backoff and jitter. |
| 500/502/503 | A transient platform dependency failed or a lane is paused. | Retry safely with backoff; preserve your correlation metadata. |
{
"detail": "Insufficient wallet balance for this job"
}The small surface most integrations need.
All server-to-server endpoints below use X-API-Key. The tokenized bundle URL is the only exception because it carries its own short-lived, job-scoped credential.
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/upload-url | Create a 15-minute upload target in the input bucket. |
| POST | /v1/transcode | Queue a single-input or batch media job. |
| GET | /v1/jobs/{job_id}/bundle?token=... | Redeem the job-scoped token for a bundle; no API key is required. |
| POST | /v1/jobs/{job_id}/retry-webhook | Retry the terminal webhook for a job you own. |
| POST | /v1/account/watermark-logo/upload-url | Create an upload target for the account PNG logo. |
| POST | /v1/account/watermark-logo/confirm | Confirm the logo and its placement settings. |