Output reports
status: "completed" says the encode finished. It does not say the file is the one you asked for.
An encoder exits zero on plenty of files that are not what was promised: an audio-only source packaged as video, a source truncated at 60% of its length, an 8-bit file still wearing HDR tags. The output report closes that gap. Finished outputs carry report once the worker that produced them measures it; older outputs have no report. It holds the facts read back from the produced file, plus a verdict on whether those facts match the output spec the job was created with.
{
"id": "out_m1n2o3p4q5",
"object": "job_output",
"status": "completed",
"report": {
"container": "mp4",
"video": {
"codec": "h264",
"profile": "high",
"level": "4.0",
"pix_fmt": "yuv420p",
"width": 1920,
"height": 1080,
"frame_rate": 29.97,
"bitrate_kbps": 4800,
"color": {
"primaries": "bt709",
"transfer": "bt709",
"matrix": "bt709",
"range": "tv"
},
"hdr_format": "none"
},
"audio": [
{
"codec": "aac",
"channels": 2,
"sample_rate_hz": 48000,
"bitrate_kbps": 128,
"language": "eng"
}
],
"duration_seconds": 763.1,
"verdict": {
"matches_request": true,
"mismatches": []
},
"checked_at": "2026-09-14T10:55:08Z"
}
}The facts are measured on the machine that produced the file. The verdict is computed by the API, from the job’s own spec — so one job yields one verdict no matter which build encoded it, and an encoder cannot report itself correct.
When a report appears
report is present once the output has been measured, and omitted otherwise. Two things carry no report: an output that never produced a file, and an output produced before this field existed.
Absence means “not measured”. It never means “nothing wrong”. Check that report is present before reading verdict, and treat a missing report as unknown rather than as a pass.
A failed output carries its report too, and that is the case where it matters most: an output rejected for not being what was asked for is exactly the one where you need to see what it was instead. A canceled output carries none — it was interrupted rather than finished, so there is no produced file to describe.
Reports ride JobOutput, so they appear everywhere an output does:
| Surface | Carries the report |
|---|---|
JobService.GetJob, JobService.WatchJob | Yes |
JobService.ListJobs | Yes |
output.ready, output.failed webhooks | Yes, on the output object itself |
job.succeeded, job.failed, job.canceled webhooks | Yes, inside outputs[] |
| The job detail page in the dashboard | Yes, as an “Output report” panel per output |
No webhook event was added for this. The field rides the events you already receive, so an existing webhook integration picks it up without changing anything.
Attributes
| Attribute | Type | Description |
|---|---|---|
container | string | Delivery format of the output, lowercase: mp4, mov, webm, mkv, hls, dash or adaptive. The same vocabulary as the request’s type. It names what was delivered, not what a demuxer would call an intermediate file. It is the one field here that is not a measurement — see What container is, and is not. |
video | object | null | The produced video stream. Absent when the file carries no video. See The video stream. |
audio | array | The produced audio streams, in the order the container carries them. Empty for an output encoded without audio. See Audio streams. |
duration_seconds | number | null | Measured playback duration of the produced file. Read from the file itself, which is what lets it catch an encode that stopped early. |
verdict | object | Whether the file matches what was asked for, and where it does not. See The verdict. |
checked_at | string | ISO 8601 timestamp of when the produced file was measured. |
Which rendition the facts describe
For a single-file output, the file itself.
For a multi-rendition output — an ABR ladder, HLS, DASH, adaptive — the facts describe the highest-resolution rendition. That is the same rendition width, height and average_bitrate_kbps already report, and the one the verdict is judged against. Per-rendition detail stays in variant_results.
Measurements are taken on the encoded rendition before packaging, which is why container states the delivery format rather than whatever a probe would call an individual segment.
What container is, and is not
Every other field in a report is read off the produced bytes. container is not: it states the output’s delivery format, which is the thing the request already named.
That makes it the one field the verdict cannot really police. A container mismatch is in the vocabulary below for completeness, but in practice a report that echoes the requested type always matches. The real check that a file is in the container it was promised runs on the encoder host, before the file is delivered, and it fails the output outright rather than describing it. container is here so a report is self-describing on its own, not as that check’s second opinion.
The video stream
| Attribute | Type | Description |
|---|---|---|
codec | string | Codec the stream is encoded with, lowercase: h264, hevc, vp9, av1. Note hevc — that is the decoder name for the codec a request calls h265. |
profile | string | null | Encoded profile, lowercase and without spaces, e.g. high, main, main10, baseline. Absent when the codec has no profile concept or the encoder reported none. |
level | string | null | Encoded level as the codec expresses it, e.g. 4.0, 5.1. |
pix_fmt | string | null | Pixel format, e.g. yuv420p (8-bit) or yuv420p10le (10-bit). This is how bit depth is read back: a 10-bit output that came out 8-bit says so here. |
width | integer | Encoded frame width in pixels, as stored. |
height | integer | Encoded frame height in pixels, as stored. |
frame_rate | number | null | Measured average frame rate, e.g. 29.97, 30, 59.94. |
bitrate_kbps | integer | null | Measured video bitrate. Absent when the container carries no per-stream bitrate. |
color | object | null | Colour signalling carried by the stream. See Colour signalling. |
hdr_format | string | HDR format the stream signals. See What hdr_format can say. |
Colour signalling
Each value is the lowercase name the format assigns it. Absent means the stream carries no value for it.
| Attribute | Type | Description |
|---|---|---|
primaries | string | null | Colour primaries, e.g. bt709 (SDR), bt2020 (wide gamut). |
transfer | string | null | Transfer characteristics, e.g. bt709 (SDR), smpte2084 (PQ / HDR10), arib-std-b67 (HLG). |
matrix | string | null | Matrix coefficients, e.g. bt709, bt2020nc. |
range | string | null | Signal range: tv (limited) or pc (full). |
Audio streams
| Attribute | Type | Description |
|---|---|---|
codec | string | Codec the stream is encoded with, lowercase, e.g. aac, opus, mp3, flac. |
channels | integer | null | Channel count: 1 for mono, 2 for stereo, 6 for 5.1. |
sample_rate_hz | integer | null | Sampling rate in hertz, e.g. 48000. |
bitrate_kbps | integer | null | Measured audio bitrate. Absent when the container carries no per-stream bitrate. |
language | string | null | Language tag carried by the stream, e.g. eng, spa. Absent when the stream is untagged. |
Why the values are strings and not enums
container, codec, profile, level, pix_fmt, the colour fields and hdr_format are lowercase strings rather than fixed enums. Those vocabularies belong to the media formats, not to us: a pixel format or profile we have not catalogued yet has to be reportable the day it appears, rather than wait for a new enum value to travel through three SDKs.
Two consequences worth planning for:
- Compare with
==against the spellings documented above, and treat an unrecognised value as something new, not as an error. containeris the closed one. It is the delivery format of the output, from the same list as the request’stype.
What hdr_format can say
Today hdr_format resolves to exactly one of none, hdr10 or hlg. Nothing else is ever emitted.
It is derived from the colour signalling above, so it is a reading of the file rather than an echo of the request: an SDR source that arrived wearing HDR tags, and whose tags we rewrote, reports none.
hdr10_plus and dolby_vision are reserved spellings — documented so you can switch on them ahead of time, and not emitted. Separating HDR10+ from static HDR10, or finding a Dolby Vision RPU, needs per-frame side data the report does not carry. Until that ships, an HDR10+ or Dolby Vision output reads as hdr10 here, which is the static layer it also genuinely signals. Do not read hdr10 as proof the dynamic layer is absent.
The verdict
| Attribute | Type | Description |
|---|---|---|
matches_request | boolean | True when every property we can check matched the request. Properties the request left open cannot make this false. |
mismatches | array | Every property that did not match, one entry each. Empty when matches_request is true, and never empty when it is false. |
A file that is not what was asked for:
{
"verdict": {
"matches_request": false,
"mismatches": [
{ "field": "video.codec", "expected": "h264", "actual": "hevc" },
{ "field": "video.resolution", "expected": "1080p", "actual": "720p" }
]
}
}Mismatches
| Attribute | Type | Description |
|---|---|---|
field | string | Which property differs, from the fixed list below. Branch on this, not on the values beside it. |
expected | string | What the request asked for, as a short value, e.g. h264, 1920, high, present. Where the request allows several answers, this is the alternatives joined with a vertical bar — see below. |
actual | string | What the produced file carries instead, in the same vocabulary as expected, e.g. hevc, 1280, main, absent. |
field comes from a fixed vocabulary. These are the only values it takes:
| Value | Raised when |
|---|---|
container | The delivery format differs from the requested type. An adaptive output accepts adaptive, hls and dash, since it delivers both manifests from shared segments. In practice this does not fire — see What container is, and is not. |
video.present | The file carries no video stream. |
video.codec | The video codec differs. An ABR ladder matches any codec it declares. |
video.profile | The request named a profile and the file carries a different one. Matched as a substring, since encoders name profiles more fully than requests do. |
video.pix_fmt | The request pinned a bit depth — H.265 main10, or HDR kept or forced — and the file does not carry it. |
video.resolution | The request named a resolution tier and the file landed in a different one. Carries tier names, e.g. 1080p. |
video.width, video.height | The request named exact pixel dimensions and the file differs. Carries pixel counts. |
video.frame_rate | The request named a frame rate and the file is more than 0.5 fps away from it. |
video.color.transfer, video.color.primaries | HDR was requested without tone mapping, and the file’s colour signalling does not carry it. |
audio.present | disable_audio was set and the file carries audio anyway. |
duration_seconds | The file is shorter than the length agreed at probe time, by more than the tolerance below. |
When several answers are acceptable
A multi-codec ABR ladder declares more than one codec, so more than one answer satisfies it. expected then carries the alternatives joined by a vertical bar, in the same short vocabulary as a single value:
{
"field": "video.codec",
"expected": "h264|hevc",
"actual": "av1"
}Split on the bar before comparing, or just render expected as-is.
What the verdict deliberately does not check
The verdict only asserts what your request actually pins. A property you left open cannot produce a mismatch, because telling you a correct file is broken is worse than saying nothing about it.
- Audio codec, channels and sample rate. A request names none of them — an audio track config carries language, label, source track and a default flag. They are reported as facts, with nothing to compare them against.
- Absent audio on an output that did not disable it. The source may legitimately have carried no sound.
- Colour on an SDR request. The encoder carries the source’s signalling through, and the source is yours.
- An output longer than promised. A clipped encode starts on a keyframe and can legally run a little long. Only a shortfall is a truncation.
- Exact pixels when a tier was requested. Scaling preserves the source’s aspect ratio, so a 4:3 source at
1080pis legitimately 1440 pixels wide.
The duration tolerance
A shortfall is a mismatch when it exceeds the larger of one second and 2% of the promised length. The fixed second absorbs container rounding and the keyframe-aligned seek of a clipped encode; the fraction keeps the allowance proportionate on a long source.
The promise is the length agreed at probe time — your input’s duration, or the clip window for a clipped job. It is never the output’s own measured duration, which would compare the file with itself.
What a report never carries
No encoder command lines, no tool output, no stack traces, no storage paths, no raw probe dumps. Every value is a short identifier read off the media itself, and anything that does not look like one is dropped rather than passed through.
The rule is a character allow-list — lowercase letters, digits, and ., -, _, at most 32 characters — rather than a length cap. A length cap is not a guarantee: the file did not match is twenty-two characters and would sail through a generous one straight onto a customer-facing field. Excluding the space is what makes a sentence impossible.
Related
- The Output object — every field an output carries,
reportamong them - Output specifications — what the verdict compares the file against
- Webhooks — the events the report rides on
- HDR — what the colour signalling means