{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/ffmpeg-micro-com/json-schema/ffmpeg-micro-com-transcode-schema.json",
  "title": "Transcode",
  "x-generated": "2026-10-30",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/ffmpeg-micro-com-openapi.yaml#/components/schemas/Transcode",
  "type": "object",
  "required": [
    "id",
    "status",
    "output_format",
    "created_at"
  ],
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "status": {
      "$ref": "#/$defs/TranscodeStatus"
    },
    "inputs": {
      "type": "array",
      "description": "Array of processed input metadata, populated at enqueue from the media probe.\n",
      "items": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "GCS URL of the (possibly re-hosted) input"
          },
          "filename": {
            "type": "string"
          },
          "duration": {
            "type": "number",
            "description": "Seconds; 0 for still images"
          },
          "format": {
            "type": "string"
          },
          "size_bytes": {
            "description": "File size in bytes, as a decimal string the way the media probe reports it (`\"991017\"`; `\"0\"` when the probe gave no size). Older jobs may carry an integer. Parse it as a number before doing arithmetic.\n",
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "string",
                "pattern": "^[0-9]+$"
              }
            ]
          },
          "downloadUrl": {
            "type": "string",
            "description": "Short-lived signed HTTPS URL for the input"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Per-input FFmpeg options as submitted"
          }
        }
      }
    },
    "outputs": {
      "type": "array",
      "description": "Output file records, populated on completion",
      "items": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "gs:// URL of the output file"
          },
          "filename": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer"
          },
          "format": {
            "type": "string"
          }
        }
      }
    },
    "output_format": {
      "type": "string",
      "description": "Output format as submitted — the value is stored verbatim, so a job created with `jpg` or `MP4` reads back exactly that way (validation is case/alias-insensitive; canonical values are mp4, webm, mov, avi, mp3, m4a, aac, wav, ogg, opus, flac, jpeg, png, gif, webp). Rows with `job_type: transcribe` carry `srt`.\n"
    },
    "job_type": {
      "type": "string",
      "enum": [
        "transcode",
        "transcribe"
      ],
      "description": "Discriminator — transcode and transcribe jobs share this resource shape and both appear in `GET /v1/transcodes` listings (there is no job_type filter). Transcribe rows additionally carry `media_url`.\n"
    },
    "billable_minutes": {
      "type": "number",
      "description": "Billable minutes, metered per second on input duration (a 19-second job bills 0.3167). Not an integer.\n"
    },
    "duration_seconds": {
      "type": "number",
      "description": "While the job is pending/processing: the billable input span (sum of input durations after trim options such as `-ss`/`-t`). Once completed: replaced by the measured duration of the OUTPUT file, which can differ from the input span (e.g. loops run longer). Billing always uses the input-side value.\n"
    },
    "processing_time_seconds": {
      "type": [
        "number",
        "null"
      ],
      "description": "Wall-clock processing time measured by the toolkit; null until completed"
    },
    "queue_position": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Jobs waiting in the job's toolkit lane at acceptance, this one included: the accepting worker's queue length right after this job was enqueued, so 1 means it was next in line. Set once at acceptance and never updated: a rough \"how far back was I\", not a live counter. Once `started_at` is set the job is no longer waiting, whatever this says. Null on jobs accepted before the field existed, or when the toolkit's accept response carried no queue length.\n"
    },
    "lane": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "short",
        "long",
        "heavy",
        null
      ],
      "description": "The processing lane the job runs in. Jobs with many large inputs (more than five 4K frames' worth of pixels in total) go to `heavy`, a service with more memory. Otherwise, inputs longer than two minutes (by probed length or requested output span) go to `long`; everything else is `short`. Each lane is a separate service, so nothing in one can delay a job in another. Null on jobs accepted before lanes existed.\n"
    },
    "started_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "When the toolkit began executing the job. Set as soon as encoding starts, so null means the job is still waiting in its lane and non-null with status `processing` means it is encoding now. If that start signal was missed, it is filled from the completion or failure report. `started_at - created_at` is submission-to-start and includes the gateway's own download and probe before dispatch, so it is an upper bound on queue wait, not the wait itself.\n"
    },
    "error_message": {
      "type": [
        "string",
        "null"
      ],
      "description": "Populated when status is `failed`, and also on user cancellation (`canceled` jobs carry \"Job canceled by user\") — do not treat non-null error_message alone as job failure.\n"
    },
    "error_category": {
      "type": "string",
      "enum": [
        "processing_limit",
        "file_size",
        "quota",
        "upload",
        "storage",
        "validation",
        "processing",
        "transient",
        "unknown"
      ],
      "description": "Machine classification derived from error_message (generated column). Only meaningful when status is `failed`; jobs with no error carry `unknown`, never null. `processing_limit` is reserved for a job stopped at a per-job processing ceiling; no deployed release emits it yet (the ceiling ships separately).\n"
    },
    "completed_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "Set on successful completion only — failed jobs keep this null"
    },
    "metadata": {
      "type": "object",
      "description": "Job metadata object. Caller-supplied metadata from the create request is echoed under `metadata.user` (JSON object, max 8 KB serialized). The object also carries internal keys written by the service (e.g. preset details, request origin, download token for the anonymous \"Try It\" flow) — treat keys other than `user` as opaque and unstable.\n",
      "properties": {
        "user": {
          "type": "object"
        }
      }
    },
    "ffmpeg_options": {
      "type": "object",
      "description": "Final FFmpeg options used (audit trail)",
      "properties": {
        "source": {
          "type": "string",
          "enum": [
            "preset",
            "raw_options",
            "virtual_options",
            "defaults"
          ],
          "description": "How the options were derived. `raw_options`: the caller's `options`, sanitized. `virtual_options`: the caller's `options` included a virtual option (for example `@text-overlay`) that the gateway expanded. `preset`: a named preset. `defaults`: neither options nor a preset was given.\n"
        },
        "options": {
          "type": "array",
          "description": "The normalized option list handed to FFmpeg, in order.",
          "items": {
            "type": "object",
            "required": [
              "option"
            ],
            "properties": {
              "option": {
                "type": "string",
                "description": "FFmpeg flag (e.g. `-c:v`, `-crf`, `-f`)"
              },
              "argument": {
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "number"
                  }
                ],
                "description": "Flag value. Omitted (never null) for flag-only options (`-an`, `-vn`, `-shortest`) and whenever the caller sent null, so a bare flag serializes without one.\n"
              }
            }
          }
        },
        "preset": {
          "type": [
            "object",
            "null"
          ]
        }
      }
    },
    "output_url": {
      "type": [
        "string",
        "null"
      ],
      "description": "Populated when status = completed"
    },
    "created_at": {
      "type": "string",
      "format": "date-time"
    },
    "updated_at": {
      "type": "string",
      "format": "date-time"
    },
    "user_id": {
      "type": "string",
      "description": "Internal user identifier resolved from API key"
    },
    "input_url": {
      "type": "string",
      "deprecated": true,
      "description": "[DEPRECATED] Use inputs array instead"
    }
  },
  "$defs": {
    "TranscodeStatus": {
      "type": "string",
      "enum": [
        "pending",
        "processing",
        "completed",
        "failed",
        "canceled"
      ],
      "description": "Job lifecycle: `pending` (created, awaiting pickup) → `processing` → `completed` | `failed`; a user cancellation sets `canceled` (US spelling). Note there is no `queued` status — new jobs start as `pending`.\n"
    }
  }
}