{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/ffpipe-dev/json-schema/ffpipe-dev-probe-request-schema.json",
  "title": "ProbeRequest",
  "description": "EXACTLY ONE of `sourceUrl` or `upload: true` is required. Every option below is optional and every number/boolean must be a JSON number/boolean, not a string. Validation runs before any charge.",
  "x-generated": "2026-11-07",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/ffpipe-dev-openapi.json#/components/schemas/ProbeRequest",
  "type": "object",
  "additionalProperties": true,
  "properties": {
    "sourceUrl": {
      "$ref": "#/$defs/SourceUrl"
    },
    "upload": {
      "$ref": "#/$defs/UploadFlag"
    },
    "outputFormat": {
      "type": "string",
      "enum": [
        "json",
        "xml",
        "csv",
        "flat",
        "ini"
      ],
      "default": "json",
      "description": "Format of the delivered analysis. `json` (the default) is the only one that carries the normalized `ffpipe_summary` block; the others are the raw writer output, untouched. `flat` is delivered as .txt."
    },
    "sections": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "format",
          "streams",
          "chapters",
          "programs"
        ]
      },
      "default": [
        "format",
        "streams",
        "chapters"
      ],
      "description": "Which sections to report. Defaults to format + streams + chapters. Duplicates are collapsed; an unknown name is a 400 (never charged)."
    },
    "summary": {
      "type": "boolean",
      "default": true,
      "description": "Keep the normalized `ffpipe_summary` block (JSON only). Send false for byte-exact raw output. PREFER the summary: it answers coded-vs-display dimensions, rotation, fps as a number and HDR directly, which is where hand-rolled parsing of the raw sections usually goes wrong."
    },
    "selectStreams": {
      "type": "string",
      "maxLength": 256,
      "description": "Restrict the analysis to matching streams, e.g. `v:0` (first video stream) or `a` (all audio). A specifier that matches nothing still succeeds and says so."
    },
    "showFrames": {
      "type": "boolean",
      "default": false,
      "description": "Report per-FRAME detail. Large: unless you also send `readIntervals`, a bounded window is applied for you so the analysis cannot outgrow what we can deliver. Combine with `showEntries` to keep only the fields you need."
    },
    "showPackets": {
      "type": "boolean",
      "default": false,
      "description": "Report per-PACKET detail. Same bounded-window treatment as `showFrames`."
    },
    "showEntries": {
      "type": "string",
      "maxLength": 256,
      "description": "Keep only the named fields, e.g. `frame=pict_type,pts_time,key_frame` or `format=duration`. Section and field names only; a malformed expression is a 400 (never charged) rather than a job that cannot succeed."
    },
    "readIntervals": {
      "type": "string",
      "maxLength": 256,
      "description": "Limit the analysis to a window, e.g. `%+60` (the first 60 seconds), `30%+10` (10 seconds from 0:30) or `%+#500` (the first 500 packets). Applied automatically when frame/packet detail is requested without one. A malformed expression is a 400 (never charged)."
    },
    "countFrames": {
      "type": "boolean",
      "default": false,
      "description": "Count frames exactly, which requires decoding the whole file - slow on long sources. Leave it off unless an exact count is the point."
    },
    "countPackets": {
      "type": "boolean",
      "default": false,
      "description": "Count packets exactly. Same cost caveat as `countFrames`."
    }
  },
  "$defs": {
    "SourceUrl": {
      "type": "string",
      "format": "uri",
      "description": "https URL we fetch the input from. Rejected before any charge if it is not https, is an IP literal, or resolves to a localhost/.internal/.local name; redirects are followed (max 3) and every hop is re-checked. We fetch from datacenter IP ranges - hosts that block datacenter egress will fail with `ingest_failed`, so prefer a presigned URL from storage you control. Body size cap: 100 MiB by default, enforced while streaming."
    },
    "UploadFlag": {
      "type": "boolean",
      "const": true,
      "description": "Push intake: instead of a `sourceUrl`, reserve an upload window and PUT the raw bytes (<= 100 MiB, same cap) to the `uploadUrl` in the 202 before `uploadExpiresAt`. You are charged at the 202, before any bytes move: letting the window expire is a terminal `error` with no refund; a pipeline failure after your upload earns a `retryVoucher`."
    }
  }
}