{
  "openapi": "3.1.0",
  "info": {
    "title": "OpenTranscription API",
    "version": "1.0.0",
    "description": "REST API for the OpenTranscription platform. Programmatic access to transcription job creation, model catalog, benchmarks, and usage analytics.\n\n**Authentication:** Most operations require an API key issued via the dashboard at https://opentranscription.io/settings/keys. Public catalog operations (`/models`, `/benchmarks`) are unauthenticated.\n\n**Rate limits:** Tier-based, sliding window. See response headers.\n\n**Versioning:** See the [versioning policy](https://opentranscription.io/docs/versioning-policy) for our deprecation guarantees.\n\n**Explore the platform:**\n\n- [Model catalog](https://opentranscription.io/models) \u2014 every model this API can route to, with pricing and capabilities\n- [Accuracy leaderboard](https://opentranscription.io/ranker) \u2014 WER benchmarks on the open golden set behind the `/benchmarks` endpoints\n- [Playground](https://opentranscription.io/playground) \u2014 run the same models interactively before writing code\n- [Reliability & SLOs](https://opentranscription.io/docs/reliability) \u2014 uptime targets and error-budget policy\n- [Blog](https://opentranscription.io/blog) \u2014 engineering notes and model deep-dives\n- [Download this spec](https://opentranscription.io/openapi.json) \u2014 OpenAPI 3.1, the source of this reference\n\n**Clients:** [TypeScript SDK](https://www.npmjs.com/package/@opentranscription/sdk) (`npm install @opentranscription/sdk`) \u00b7 [Python SDK](https://pypi.org/project/opentranscription/) (`pip install opentranscription`) \u00b7 [CLI](https://www.npmjs.com/package/@opentranscription/cli) (`npm install -g @opentranscription/cli`)",
    "contact": {
      "name": "OpenTranscription Support",
      "url": "https://opentranscription.io/support"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://opentranscription.io/terms"
    }
  },
  "servers": [
    {
      "url": "https://opentranscription.io",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/v1/transcriptions": {
      "post": {
        "operationId": "createTranscription",
        "summary": "Create a transcription job",
        "description": "Create a new transcription job. **Required scope:** `transcriptions:write`.\n\nBefore calling this endpoint, first call `POST /api/v1/uploads` to obtain a signed URL, upload your audio file directly to that URL, and pass the returned `file_path` to this endpoint.\n\nThe job is enqueued asynchronously — poll `GET /api/v1/transcriptions/{id}` or set a `webhook_url` to receive completion notification.\n\n**Billing:** Credits are reserved upfront based on estimated file duration. Actual cost is committed when the job completes. If the job fails, reserved credits are released. Use `use_own_key: true` to route the job through your organization's provider API key (BYOK) — no credits are consumed.",
        "tags": ["Transcriptions"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTranscriptionRequest"
              },
              "examples": {
                "direct": {
                  "summary": "Direct model selection",
                  "value": {
                    "file_path": "a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio-2025-01-15.mp3",
                    "model": "openai/whisper-large-v3",
                    "language": "en",
                    "title": "Bob interview - intake call"
                  }
                },
                "routed": {
                  "summary": "Smart Router — cheapest model",
                  "value": {
                    "file_path": "a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio-2025-01-15.mp3",
                    "router": {
                      "strategy": "cheapest"
                    },
                    "language": "en",
                    "diarization": true
                  }
                },
                "routed_accuracy": {
                  "summary": "Smart Router — best accuracy for medical audio",
                  "value": {
                    "file_path": "a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio-2025-01-15.mp3",
                    "router": {
                      "strategy": "most_accurate",
                      "preset": "medical",
                      "ignore": ["openai"]
                    }
                  }
                },
                "virtual_id": {
                  "summary": "Virtual model ID shorthand",
                  "value": {
                    "file_path": "a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio-2025-01-15.mp3",
                    "model": "auto/fastest"
                  }
                },
                "chain": {
                  "summary": "User-defined fallback chain",
                  "value": {
                    "file_path": "a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio-2025-01-15.mp3",
                    "models": [
                      "deepgram/nova-3",
                      "assemblyai/best",
                      "openai/whisper-large-v3"
                    ],
                    "language": "en"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Job created and enqueued for processing.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranscriptionJobCreated"
                },
                "examples": {
                  "direct": {
                    "summary": "Direct model selection",
                    "value": {
                      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "status": "uploaded",
                      "model_id": "openai/whisper-large-v3",
                      "custom_model_id": null,
                      "language": "en",
                      "use_own_key": false,
                      "credits_reserved": 150,
                      "routed_from": null,
                      "fallback_source": null,
                      "created_at": "2025-01-15T10:30:00.000Z"
                    }
                  },
                  "routed": {
                    "summary": "Smart Router resolved model",
                    "value": {
                      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
                      "status": "uploaded",
                      "model_id": "deepgram/nova-3",
                      "custom_model_id": null,
                      "language": "en",
                      "use_own_key": false,
                      "credits_reserved": 80,
                      "routed_from": "auto/cheapest",
                      "fallback_source": "auto",
                      "created_at": "2025-01-15T10:31:00.000Z"
                    }
                  },
                  "user_chain": {
                    "summary": "User fallback chain (response returns chain primary)",
                    "value": {
                      "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
                      "status": "uploaded",
                      "model_id": "deepgram/nova-3",
                      "custom_model_id": null,
                      "language": "en",
                      "use_own_key": false,
                      "credits_reserved": 80,
                      "routed_from": null,
                      "fallback_source": "user_chain",
                      "created_at": "2025-01-15T10:32:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "No models match the routing constraints. Returned when using `router` and all candidate models are filtered out by language, feature, or provider constraints. The `suggestions` array lists near-miss models with the specific constraint that excluded them.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["no_models_match"]
                    },
                    "message": {
                      "type": "string"
                    },
                    "suggestions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "model_id": {
                            "type": "string"
                          },
                          "failed_constraint": {
                            "type": "string"
                          },
                          "detail": {
                            "type": "string"
                          }
                        },
                        "required": ["model_id", "failed_constraint", "detail"]
                      }
                    }
                  },
                  "required": ["error", "message", "suggestions"]
                },
                "example": {
                  "error": "no_models_match",
                  "message": "No active models match your routing constraints.",
                  "suggestions": [
                    {
                      "model_id": "deepgram/nova-3",
                      "failed_constraint": "features",
                      "detail": "Missing features: diarization"
                    },
                    {
                      "model_id": "openai/whisper-large-v3",
                      "failed_constraint": "language",
                      "detail": "Model does not support language: ja"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "get": {
        "operationId": "listTranscriptions",
        "summary": "List transcription jobs",
        "description": "List your organization's transcription jobs. **Required scope:** `transcriptions:read`.\n\nResults are paginated. Use `page` and `limit` to navigate. Filter by `status` to monitor in-progress jobs, or by `search` to find a specific file.",
        "tags": ["Transcriptions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/QueryLimit"
          },
          {
            "$ref": "#/components/parameters/QueryPage"
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by job status. Accepts a single status value or comma-separated list (e.g. `processing,uploaded`).",
            "required": false,
            "schema": {
              "type": "string",
              "example": "completed"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive substring search across `file_name` and `title`. Matches when either field contains the query. Filter metacharacters `, ( ) * \" \\` are rejected with 400.",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200,
              "example": "interview"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort direction by `created_at`. Defaults to `desc` (newest first).",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["asc", "desc"],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of jobs.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranscriptionJobList"
                },
                "example": {
                  "data": [
                    {
                      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "status": "completed",
                      "model_id": "openai/whisper-large-v3",
                      "model_name": "Whisper Large v3",
                      "provider": "OpenAI",
                      "language": "en",
                      "title": null,
                      "file_name": "audio-2025-01-15.mp3",
                      "file_size_bytes": 4200000,
                      "credits_reserved": 150,
                      "credits_used": 142,
                      "processing_time_ms": 8400,
                      "error": null,
                      "has_transcript": true,
                      "created_at": "2025-01-15T10:30:00.000Z",
                      "updated_at": "2025-01-15T10:30:42.000Z"
                    }
                  ],
                  "pagination": {
                    "page": 1,
                    "limit": 25,
                    "total": 1,
                    "totalPages": 1
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/transcriptions/{id}": {
      "get": {
        "operationId": "getTranscription",
        "summary": "Get a transcription job",
        "description": "Fetch the current status and, if completed, the full transcript for a single job. **Required scope:** `transcriptions:read`.\n\nWhen `status` is `completed`, the `transcript` field is populated. When the job is still processing, `transcript` is absent.",
        "tags": ["Transcriptions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/PathJobId"
          }
        ],
        "responses": {
          "200": {
            "description": "Job details, with transcript if available.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranscriptionJobDetail"
                },
                "example": {
                  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "status": "completed",
                  "model_id": "openai/whisper-large-v3",
                  "language": "en",
                  "title": null,
                  "file_name": "audio-2025-01-15.mp3",
                  "file_size_bytes": 4200000,
                  "credits_reserved": 150,
                  "credits_used": 142,
                  "processing_time_ms": 8400,
                  "error": null,
                  "webhook_url": null,
                  "metadata": {
                    "estimated_duration_seconds": 210
                  },
                  "created_at": "2025-01-15T10:30:00.000Z",
                  "updated_at": "2025-01-15T10:30:42.000Z",
                  "transcript": {
                    "text": "Hello, this is a sample transcription of the audio file.",
                    "language": "en",
                    "confidence": 0.97,
                    "segments": [
                      {
                        "id": 0,
                        "start": 0.0,
                        "end": 3.5,
                        "text": "Hello, this is a sample transcription of the audio file.",
                        "speaker": null
                      }
                    ],
                    "words": [
                      {
                        "text": "Hello",
                        "start": 0.0,
                        "end": 0.5,
                        "confidence": 0.99
                      }
                    ],
                    "word_timestamps": "available"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updateTranscription",
        "summary": "Rename a transcription job",
        "description": "Update editable job fields. Currently only `title` is editable. **Required scope:** `transcriptions:write`. Send `title: null` to clear a previously-set title (display will fall back to `file_name`).",
        "tags": ["Transcriptions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/PathJobId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateTranscriptionRequest"
              },
              "example": {
                "title": "Bob interview - intake call"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job updated. Returns the full job summary shape so clients can sync their cache without a follow-up GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranscriptionJobSummary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteTranscription",
        "summary": "Cancel or delete a transcription job",
        "description": "Cancel a pending or in-progress job, or delete a completed/failed job. **Required scope:** `transcriptions:write`.\n\nFor pending (`pending_upload`, `uploaded`) jobs the job is cancelled and its reservation is released back to your balance immediately.\n\nFor `processing` jobs the job is cancelled but the reservation is **not** returned immediately, because the transcription provider has already been billed for the work in flight. Credit reservations are reconciled later by an automated sweep once your organization has nothing running. Trial and free-tier allowances are **not** reconciled: cancelling a `processing` job consumes that run. Cancel before processing starts if you need the allowance back.\n\nThe response `message` tells you which happened.\n\nFor `completed` or `failed` jobs the job is marked deleted and stops appearing in `GET /api/v1/transcriptions`; a subsequent `GET /api/v1/transcriptions/{id}` returns 404. Its `status` is preserved, so a completed job is never relabelled as cancelled. The underlying record is retained because billing and usage history reference it.",
        "tags": ["Transcriptions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/PathJobId"
          }
        ],
        "responses": {
          "200": {
            "description": "Job cancelled or deleted.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranscriptionDeleteResponse"
                },
                "examples": {
                  "cancelled": {
                    "summary": "Job had not started yet — cancelled and the reservation returned",
                    "value": {
                      "message": "Job cancelled and reservation released",
                      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "status": "cancelled"
                    }
                  },
                  "cancelledWhileProcessing": {
                    "summary": "Job was already processing — cancelled, reservation reconciled later",
                    "value": {
                      "message": "Job cancelled",
                      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "status": "cancelled"
                    }
                  },
                  "deleted": {
                    "summary": "Job was completed — now hidden, record retained",
                    "value": {
                      "message": "Job deleted",
                      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/transcriptions/{id}/audio": {
      "get": {
        "operationId": "getTranscriptionAudio",
        "summary": "Download a transcription job's original audio",
        "description": "Redirects (302) to a short-lived signed URL (5-minute expiry) for the job's original uploaded audio file, served directly from storage. **Required scope:** `transcriptions:read`.\n\nFollow the redirect to fetch the audio bytes — this endpoint itself never returns audio. `Content-Disposition: attachment` is set so the download preserves the original filename.\n\nA mic (real-time) recording never has a stored audio object, so it answers 404 with `code: audio_not_available` rather than 410 — no retention window ever applied to it. A batch/file job whose audio has since been purged by retention answers 410 with `code: audio_not_retained`.",
        "tags": ["Transcriptions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/PathJobId"
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to a signed URL for the audio object. The signed URL expires after 5 minutes; request this endpoint again to get a fresh one.",
            "headers": {
              "Location": {
                "description": "Signed, time-limited URL to the audio file.",
                "schema": { "type": "string", "format": "uri" }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "The job does not exist, belongs to another organization, was deleted, or is a mic recording that never had a stored audio object (`code: audio_not_available`).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/ErrorResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "Machine-readable error code. `audio_not_available` when the job is a mic recording — never present for a plain not-found."
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "notFound": {
                    "summary": "Unknown, foreign-org, or deleted job",
                    "value": { "error": "Job not found" }
                  },
                  "micRecording": {
                    "summary": "Mic recording — no audio object ever existed",
                    "value": {
                      "error": "No audio was ever stored for this job",
                      "code": "audio_not_available"
                    }
                  }
                }
              }
            }
          },
          "410": {
            "description": "The job's audio was retained at completion but has since been purged by the organization's retention policy.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/ErrorResponse" },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "description": "`audio_not_retained` — the stored audio object has been deleted by retention."
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "Audio is no longer retained for this job",
                  "code": "audio_not_retained"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/uploads": {
      "post": {
        "operationId": "createUpload",
        "summary": "Create a signed upload URL for audio file upload",
        "description": "Generate a signed PUT URL for uploading an audio file directly to Supabase Storage. **Required scope:** `transcriptions:write`.\n\n**Three-step upload flow:**\n1. Call `POST /api/v1/uploads` with the file metadata to get a signed `upload_url` and a `file_path`.\n2. Upload the raw audio bytes directly to `upload_url` using an HTTP `PUT` request with the file as the request body.\n3. Pass the returned `file_path` to `POST /api/v1/transcriptions` to create the transcription job.\n\nThe signed URL expires in approximately 1–2 hours. Check the exact `expires_at` field for the precise expiration time. If you do not complete the upload before `expires_at`, request a new URL.\n\n**Audio only.** Uploads must be an audio file. To transcribe a video, extract its audio track first and upload that — e.g. `ffmpeg -i input.mp4 -vn -ac 1 -ar 16000 -c:a libmp3lame output.mp3`. Accepted audio types: mp3, wav, m4a, flac, ogg/opus, webm.",
        "tags": ["Transcriptions"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["file_name", "file_size", "mime_type"],
                "properties": {
                  "file_name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "Original filename including extension (e.g. `interview.mp3`). Used to derive the storage path extension and validate the file format."
                  },
                  "file_size": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 104857600,
                    "description": "File size in bytes. Must not exceed 100 MiB (104,857,600 bytes)."
                  },
                  "mime_type": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "MIME type of the audio file (e.g. `audio/mpeg`, `audio/wav`, `audio/ogg`). Used to validate the file format alongside the filename extension."
                  }
                }
              },
              "example": {
                "file_name": "interview-2025-01-15.mp3",
                "file_size": 3456789,
                "mime_type": "audio/mpeg"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed upload URL generated. PUT your audio bytes to `upload_url` before `expires_at`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["upload_url", "file_path", "expires_at"],
                  "properties": {
                    "upload_url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Short-lived signed PUT URL. Upload the raw audio file bytes to this URL using `PUT` with the file as the request body."
                    },
                    "file_path": {
                      "type": "string",
                      "description": "Storage path for the uploaded file (format: `<org_uuid>/<timestamp>_<id>.<ext>`). Pass this value as `file_path` when calling `POST /api/v1/transcriptions`."
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "ISO 8601 timestamp when the `upload_url` expires, extracted from the JWT embedded in the URL itself. Typically 1–2 hours from issuance. You must complete the upload before this time."
                    }
                  }
                },
                "example": {
                  "upload_url": "https://storage.supabase.co/storage/v1/object/upload/sign/audio/a1b2c3d4-e5f6-7890-abcd-ef1234567890/1736944200000_xK9mNpQrStUvWxYz.mp3?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
                  "file_path": "a1b2c3d4-e5f6-7890-abcd-ef1234567890/1736944200000_xK9mNpQrStUvWxYz.mp3",
                  "expires_at": "2025-01-15T12:10:00.000Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Get usage statistics",
        "description": "Usage stats for your organization aggregated by model and time period. **Required scope:** `usage:read`.\n\nReturns job counts, audio minutes processed, and credits spent. Useful for billing reconciliation and capacity planning.\n\n**Period granularity:** `7d` and `30d` group by calendar day (YYYY-MM-DD). `90d` and `365d` group by calendar month (YYYY-MM).",
        "tags": ["Usage"],
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "description": "Lookback window for the report.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["7d", "30d", "90d", "365d"],
              "default": "30d"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage statistics for the requested period.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageResponse"
                },
                "example": {
                  "total_jobs": 48,
                  "total_minutes": 312.75,
                  "total_credits_spent": 4420,
                  "period": "30d",
                  "by_model": [
                    {
                      "model_id": "openai/whisper-large-v3",
                      "model_name": "Whisper Large v3",
                      "provider_name": "OpenAI",
                      "job_count": 32,
                      "total_minutes": 218.5,
                      "total_credits": 3094
                    },
                    {
                      "model_id": "deepgram/nova-2",
                      "model_name": "Nova-2",
                      "provider_name": "Deepgram",
                      "job_count": 16,
                      "total_minutes": 94.25,
                      "total_credits": 1326
                    }
                  ],
                  "by_period": [
                    {
                      "date": "2025-01-15",
                      "job_count": 5,
                      "total_minutes": 34.2,
                      "total_credits": 484
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/models": {
      "get": {
        "operationId": "listModels",
        "summary": "List transcription models",
        "description": "Returns all active transcription models with pricing and capabilities. No authentication required.\n\nBy default returns the `ModelsData` shape (models + filter options + stats). Pass any marketplace query parameter (`mode`, `compliance`, `capabilities`, `badge`, `min_price`, `max_price`) to receive the richer `MarketplaceData` shape that includes per-model health metrics, badges, and benchmark breakdowns.\n\nFilter values are validated: an unrecognized `mode`, `compliance`, `capabilities`, or `badge` value, or a non-numeric `min_price`/`max_price`, returns `400` with a message naming the invalid value(s) and the allowed set. Comma-separated values tolerate surrounding whitespace.\n\nResponses are cached at the CDN layer for up to 1 hour (`Cache-Control: public, s-maxage=3600`).",
        "tags": ["Catalog"],
        "security": [],
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "description": "Filter by transcription mode. `both` includes models that support either.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["batch", "realtime", "both"]
            }
          },
          {
            "name": "compliance",
            "in": "query",
            "description": "Comma-separated compliance certifications to filter by (e.g. `hipaa,soc2`). All listed certifications must be met.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "hipaa"
            }
          },
          {
            "name": "capabilities",
            "in": "query",
            "description": "Comma-separated model capabilities to filter by (e.g. `diarization,custom_vocabulary`). A model must advertise **all** listed capabilities to match (AND semantics). Recognized keys: `diarization`, `custom_vocabulary`, `word_timestamps`, `code_switching`, `auto_detect`.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "diarization,custom_vocabulary"
            }
          },
          {
            "name": "badge",
            "in": "query",
            "description": "Comma-separated badge types to filter by. Any matching badge qualifies the model.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "best_value,fastest"
            }
          },
          {
            "name": "min_price",
            "in": "query",
            "description": "Minimum `cost_per_second` (inclusive).",
            "required": false,
            "schema": {
              "type": "number",
              "format": "float"
            }
          },
          {
            "name": "max_price",
            "in": "query",
            "description": "Maximum `cost_per_second` (inclusive).",
            "required": false,
            "schema": {
              "type": "number",
              "format": "float"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Model catalog. Shape varies by query parameters — see description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelsResponse"
                },
                "example": {
                  "data": {
                    "models": [
                      {
                        "id": "openai/whisper-large-v3",
                        "name": "whisper-large-v3",
                        "display_name": "Whisper Large v3",
                        "provider": {
                          "id": "prov-openai-uuid",
                          "name": "OpenAI"
                        },
                        "description": "OpenAI's most capable open-source speech-to-text model. Excellent accuracy across languages.",
                        "pricing": {
                          "cost_per_second": 0.0111,
                          "currency": "credits"
                        },
                        "performance": {
                          "avg_speed_factor": 12.5,
                          "avg_wer": 0.0412,
                          "avg_latency_ms": 680
                        },
                        "capabilities": {
                          "supported_languages": [
                            "en",
                            "es",
                            "fr",
                            "de",
                            "ja",
                            "zh"
                          ],
                          "max_file_size": 26214400,
                          "max_duration_seconds": null,
                          "supported_formats": [
                            "mp3",
                            "mp4",
                            "wav",
                            "flac",
                            "ogg",
                            "m4a"
                          ],
                          "features": ["diarization", "word_timestamps"]
                        },
                        "is_active": true,
                        "lifecycle": "active",
                        "successor_model_id": null,
                        "mode": "batch"
                      },
                      {
                        "id": "deepgram/nova-2",
                        "name": "nova-2",
                        "display_name": "Nova-2",
                        "provider": {
                          "id": "prov-deepgram-uuid",
                          "name": "Deepgram"
                        },
                        "description": "Deepgram's fastest production model with strong English accuracy.",
                        "pricing": {
                          "cost_per_second": 0.0083,
                          "currency": "credits"
                        },
                        "performance": {
                          "avg_speed_factor": 45.0,
                          "avg_wer": 0.0621,
                          "avg_latency_ms": 320
                        },
                        "capabilities": {
                          "supported_languages": ["en"],
                          "max_file_size": 2147483648,
                          "max_duration_seconds": null,
                          "supported_formats": [
                            "mp3",
                            "wav",
                            "flac",
                            "ogg",
                            "m4a",
                            "webm"
                          ],
                          "features": [
                            "diarization",
                            "word_timestamps",
                            "smart_format"
                          ]
                        },
                        "is_active": true,
                        "lifecycle": "active",
                        "successor_model_id": null,
                        "mode": "both"
                      }
                    ],
                    "filters": {
                      "languages": ["de", "en", "es", "fr", "ja", "zh"],
                      "providers": [
                        {
                          "id": "prov-deepgram-uuid",
                          "name": "Deepgram"
                        },
                        {
                          "id": "prov-openai-uuid",
                          "name": "OpenAI"
                        }
                      ],
                      "priceRange": {
                        "min": 0.0,
                        "max": 0.0278
                      }
                    },
                    "stats": {
                      "total_models": 2,
                      "total_languages": 6,
                      "avg_price": 0.0097
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/models/{modelId}": {
      "get": {
        "operationId": "getModel",
        "summary": "Get a single model",
        "description": "Returns details for one model by its `provider/model-name` id (e.g. `deepgram/nova-3`). No authentication required.\n\nReturns the same per-model entry found in the `models` array of GET /api/v1/models. Responds `404` if no active model matches the id.\n\nResponses are cached at the CDN layer for up to 1 hour (`Cache-Control: public, s-maxage=3600`).",
        "tags": ["Catalog"],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PathModelId"
          }
        ],
        "responses": {
          "200": {
            "description": "The requested model.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ModelEntry"
                    }
                  },
                  "required": ["data"]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/benchmarks": {
      "get": {
        "operationId": "listBenchmarks",
        "summary": "Get leaderboard",
        "description": "Returns the accuracy and performance leaderboard across all models. No authentication required.\n\nModels are scored and ranked by a composite metric (50% accuracy, 30% speed, 20% cost). Use `language`, `category`, and `accent` filters to see rankings for specific audio domains.\n\nSet `mode=realtime` to get the realtime-STT leaderboard instead — streaming models scored on accuracy, responsiveness (time to first word), and stability (flicker), with the same `language`/`category`/`accent` filters. Defaults to `mode=batch`.\n\nResponses are cached at the CDN layer for up to 1 hour.",
        "tags": ["Benchmarks"],
        "security": [],
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "description": "Which leaderboard to return. Defaults to `batch`.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["batch", "realtime"],
              "default": "batch"
            }
          },
          {
            "name": "language",
            "in": "query",
            "description": "ISO 639-1 language code to filter benchmark results (e.g. `en`, `es`).",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]+$",
              "example": "en"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Audio category filter (e.g. `medical`, `legal`, `conversational`).",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]+$",
              "example": "medical"
            }
          },
          {
            "name": "accent",
            "in": "query",
            "description": "Accent filter. Accepted values: `us`, `uk`, `indian`, `australian`, `african`.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["us", "uk", "indian", "australian", "african"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Leaderboard data. Shape depends on `mode`: `LeaderboardResponse` for `mode=batch` (default), `RealtimeLeaderboardResponse` for `mode=realtime`.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/LeaderboardResponse"
                    },
                    {
                      "$ref": "#/components/schemas/RealtimeLeaderboardResponse"
                    }
                  ]
                },
                "examples": {
                  "batch": {
                    "summary": "mode=batch (default)",
                    "value": {
                      "data": {
                        "leaderboard": [
                          {
                            "model_id": "openai/whisper-large-v3",
                            "model_name": "Whisper Large v3",
                            "provider_name": "OpenAI",
                            "description": "OpenAI's most capable open-source speech-to-text model.",
                            "avg_wer": 0.0412,
                            "avg_cer": 0.0198,
                            "avg_latency_ms": 680,
                            "avg_cost_per_second": 0.0111,
                            "benchmark_count": 24,
                            "overall_score": 87.4,
                            "last_run_at": "2025-01-14T03:00:00.000Z"
                          },
                          {
                            "model_id": "deepgram/nova-2",
                            "model_name": "Nova-2",
                            "provider_name": "Deepgram",
                            "description": "Deepgram's fastest production model.",
                            "avg_wer": 0.0621,
                            "avg_latency_ms": 320,
                            "avg_cost_per_second": 0.0083,
                            "benchmark_count": 18,
                            "overall_score": 82.1,
                            "last_run_at": "2025-01-14T03:00:00.000Z"
                          }
                        ],
                        "filters": {
                          "languages": ["en", "es", "fr"],
                          "categories": ["conversational", "legal", "medical"],
                          "accents": ["australian", "indian", "uk", "us"]
                        },
                        "total_benchmarks": 156,
                        "total_models": 8
                      }
                    }
                  },
                  "realtime": {
                    "summary": "mode=realtime",
                    "value": {
                      "data": {
                        "leaderboard": [
                          {
                            "model_id": "deepgram/flux",
                            "model_name": "Flux",
                            "provider_name": "Deepgram",
                            "p50_ttfw_ms": 180,
                            "p50_drain_ms": 90,
                            "avg_flicker": 0.03,
                            "avg_cadence": 4.2,
                            "avg_rtf": 1.01,
                            "avg_wer": 0.0512,
                            "benchmark_count": 12,
                            "realtime_score": 84.6,
                            "last_run_at": "2025-01-14T03:00:00.000Z"
                          }
                        ],
                        "filters": {
                          "languages": ["en"],
                          "categories": ["conversational"],
                          "accents": ["us"]
                        },
                        "total_benchmarks": 12,
                        "total_models": 1,
                        "last_run_at": "2025-01-14T03:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/benchmarks/export": {
      "get": {
        "operationId": "exportBenchmarks",
        "summary": "Download leaderboard as CSV",
        "description": "Returns the same leaderboard as `GET /api/v1/benchmarks`, serialized as a downloadable CSV file (`Content-Disposition: attachment`). No authentication required.\n\nColumns for `mode=batch` (default): `rank, model_id, model_name, provider_name, overall_score, avg_wer, avg_cer, avg_mer, avg_wil, avg_latency_ms, cost_credits_per_second, cost_usd_per_min, benchmark_count, last_run_at`. `cost_usd_per_min` is derived as `cost_credits_per_second × 0.6`.\n\nColumns for `mode=realtime`: `rank, model_id, model_name, provider_name, realtime_score, avg_wer, avg_cer, p50_ttfw_ms, p50_drain_ms, avg_flicker, avg_cadence, avg_rtf, benchmark_count, last_run_at`.\n\nThe `rank` column is 1-based and matches the ranked board order. The `language`, `category`, and `accent` filters behave exactly as on the JSON endpoint. Responses are cached at the CDN layer for up to 1 hour.",
        "tags": ["Benchmarks"],
        "security": [],
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "description": "Which leaderboard to export. Defaults to `batch`.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["batch", "realtime"],
              "default": "batch"
            }
          },
          {
            "name": "language",
            "in": "query",
            "description": "ISO 639-1 language code to filter benchmark results (e.g. `en`, `es`).",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]+$",
              "example": "en"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Audio category filter (e.g. `medical`, `legal`, `conversational`).",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]+$",
              "example": "medical"
            }
          },
          {
            "name": "accent",
            "in": "query",
            "description": "Accent filter. Accepted values: `us`, `uk`, `indian`, `australian`, `african`.",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["us", "uk", "indian", "australian", "african"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "CSV file of the leaderboard, ranked order preserved. The `Content-Disposition` header names the file (e.g. `opentranscription-batch-benchmarks-2026-07-23.csv`).",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "rank,model_id,model_name,provider_name,overall_score,avg_wer,avg_cer,avg_mer,avg_wil,avg_latency_ms,cost_credits_per_second,cost_usd_per_min,benchmark_count,last_run_at\r\n1,cartesia/ink-whisper,Ink-Whisper,Cartesia,84.6,0.172,0.09,0.15,0.2,773,0.003667,0.0022,35,2026-07-23T03:00:00.000Z"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/benchmarks/{modelId}": {
      "get": {
        "operationId": "getModelBenchmarks",
        "summary": "Get model benchmark detail",
        "description": "Detailed benchmark history, category breakdown, and accent breakdown for a single model. No authentication required.\n\nReturns `404` if the model has no **batch** benchmark data (model may exist but may not have been evaluated yet). The response's `realtime` field carries realtime-STT benchmark detail for that model and is `null` when the model has no realtime benchmark rows — it is independent of the 404 gate above, which is keyed on batch data only.",
        "tags": ["Benchmarks"],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PathModelId"
          }
        ],
        "responses": {
          "200": {
            "description": "Model benchmark detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelBenchmarkDetailResponse"
                },
                "example": {
                  "data": {
                    "model": {
                      "id": "openai/whisper-large-v3",
                      "name": "Whisper Large v3",
                      "description": "OpenAI's most capable open-source speech-to-text model.",
                      "provider_name": "OpenAI",
                      "cost_per_second": 0.0111
                    },
                    "summary": {
                      "avg_wer": 0.0412,
                      "avg_cer": 0.0198,
                      "avg_latency_ms": 680,
                      "benchmark_count": 24,
                      "overall_score": 87.4
                    },
                    "history": [
                      {
                        "run_at": "2025-01-07",
                        "avg_wer": 0.0421,
                        "avg_latency_ms": 695,
                        "benchmark_count": 8
                      },
                      {
                        "run_at": "2025-01-14",
                        "avg_wer": 0.0412,
                        "avg_latency_ms": 680,
                        "benchmark_count": 8
                      }
                    ],
                    "by_category": [
                      {
                        "category": "conversational",
                        "avg_wer": 0.0388,
                        "avg_latency_ms": 660,
                        "count": 8
                      },
                      {
                        "category": "medical",
                        "avg_wer": 0.0521,
                        "avg_latency_ms": 710,
                        "count": 8
                      }
                    ],
                    "by_accent": [
                      {
                        "accent": "us",
                        "avg_wer": 0.0355,
                        "count": 12
                      },
                      {
                        "accent": "uk",
                        "avg_wer": 0.0489,
                        "count": 6
                      }
                    ],
                    "realtime": null
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "webhooks": {
    "transcriptionCompleted": {
      "post": {
        "summary": "Transcription completed",
        "description": "Sent to your `webhook_url` when a job completes. Thin event — fetch the transcript with `GET /api/v1/transcriptions/{data.id}`. Verify the `X-OT-Signature` header: HMAC-SHA256 over `<t>.<raw_body>` with your signing secret (Settings → API keys), constant-time compare against any `v1`, reject if `|now - t| > 300s`. Dedupe on `id` (delivery is at-least-once). Return 2xx quickly; non-2xx is retried with exponential backoff up to 5 times.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_9f8c2a1b4d6e",
                "event": "transcription.completed",
                "created": "2025-01-15T10:30:42.000Z",
                "data": {
                  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "status": "completed",
                  "error_code": null
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your endpoint acknowledged receipt of the event. Return any 2xx status to confirm delivery — this is independent of the job outcome (a `transcription.failed` event is still ACKed with 2xx). A non-2xx response is retried up to 5 times with exponential backoff."
          }
        }
      }
    },
    "transcriptionFailed": {
      "post": {
        "summary": "Transcription failed",
        "description": "Sent to your `webhook_url` when a job fails. `data.status` is always `\"failed\"`. `data.error` and `data.error_code` are populated. Same `X-OT-Signature` verification as `transcriptionCompleted`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_3c7d9e2f1a5b",
                "event": "transcription.failed",
                "created": "2025-01-15T10:31:05.000Z",
                "data": {
                  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
                  "status": "failed",
                  "error": "Provider returned a non-retryable error.",
                  "error_code": "transcription_failed"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your endpoint acknowledged receipt of the event. Return any 2xx status to confirm delivery — this is independent of the job outcome (a `transcription.failed` event is still ACKed with 2xx). A non-2xx response is retried up to 5 times with exponential backoff."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ot_<32 chars>",
        "description": "API key issued via the OpenTranscription dashboard. Format: `ot_` prefix followed by 32 alphanumeric characters. Request with `Authorization: Bearer ot_...` header."
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Requests allowed per minute for your tier.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests remaining in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "Unix timestamp (seconds) when the rate limit window resets.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "parameters": {
      "PathJobId": {
        "name": "id",
        "in": "path",
        "description": "UUID of the transcription job.",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid",
          "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        }
      },
      "PathModelId": {
        "name": "modelId",
        "in": "path",
        "description": "Model ID in `provider/model-name` format (e.g. `openai/whisper-large-v3`).",
        "required": true,
        "schema": {
          "type": "string",
          "example": "openai/whisper-large-v3"
        }
      },
      "QueryLimit": {
        "name": "limit",
        "in": "query",
        "description": "Maximum number of results to return. Default: 25, max: 100.",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      },
      "QueryPage": {
        "name": "page",
        "in": "query",
        "description": "1-indexed page number.",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1
        }
      }
    },
    "schemas": {
      "WebhookEvent": {
        "type": "object",
        "description": "Signed webhook event delivered to a job's `webhook_url`. Thin by design — contains identifiers, not the transcript.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique event/delivery ID (`evt_…`). Dedupe key — stable across retries of the same delivery.",
            "example": "evt_9f8c2a1b4d6e"
          },
          "event": {
            "type": "string",
            "enum": ["transcription.completed", "transcription.failed"]
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Job ID. Use with GET /api/v1/transcriptions/{id}."
              },
              "status": {
                "type": "string",
                "enum": ["completed", "failed"]
              },
              "error": {
                "type": ["string", "null"],
                "description": "Present on failure only."
              },
              "error_code": {
                "type": ["string", "null"],
                "description": "Structured error identifier on failure; null otherwise."
              }
            },
            "required": ["id", "status"]
          }
        },
        "required": ["id", "event", "created", "data"]
      },
      "ErrorResponse": {
        "type": "object",
        "description": "Standard error response body.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "required": {
            "type": "string",
            "description": "Present on 403 scope errors — the required scope that was missing."
          },
          "details": {
            "type": "array",
            "description": "Present on validation errors — the raw Zod issues, each naming the offending path and what was expected.",
            "items": {
              "type": "object"
            }
          },
          "documentation_url": {
            "type": "string",
            "format": "uri",
            "description": "Present on 400 responses — where this request's contract is defined. Deep-links to the request schema when the endpoint has one (e.g. `#/components/schemas/CreateTranscriptionRequest`), otherwise points at this document."
          }
        },
        "required": ["error"]
      },
      "JobStatus": {
        "type": "string",
        "description": "Lifecycle state of a transcription job.",
        "enum": [
          "pending_upload",
          "uploaded",
          "processing",
          "completed",
          "failed",
          "cancelled"
        ]
      },
      "TranscriptionSegment": {
        "type": "object",
        "description": "A time-aligned segment of transcript text.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Zero-based segment index."
          },
          "start": {
            "type": "number",
            "format": "float",
            "description": "Segment start time in seconds."
          },
          "end": {
            "type": "number",
            "format": "float",
            "description": "Segment end time in seconds."
          },
          "text": {
            "type": "string",
            "description": "Transcribed text for this segment."
          },
          "speaker": {
            "type": ["string", "null"],
            "description": "Speaker label if diarization was enabled (e.g. `A`, `B`). Null otherwise."
          },
          "words": {
            "type": "array",
            "description": "Word-level timing for this segment. `[]` when the transcript's `word_timestamps` is not `available`.",
            "items": {
              "$ref": "#/components/schemas/TranscriptionWord"
            }
          }
        },
        "required": ["id", "start", "end", "text"]
      },
      "TranscriptionWord": {
        "type": "object",
        "description": "Word-level timing and confidence.",
        "properties": {
          "text": {
            "type": "string",
            "description": "The transcribed word."
          },
          "start": {
            "type": "number",
            "format": "float",
            "description": "Word start time in seconds."
          },
          "end": {
            "type": "number",
            "format": "float",
            "description": "Word end time in seconds."
          },
          "confidence": {
            "type": "number",
            "format": "float",
            "description": "Confidence score (0.0–1.0). Always present; some models fill a placeholder value."
          },
          "speaker": {
            "type": "string",
            "description": "Speaker label if diarization was enabled."
          }
        },
        "required": ["text", "start", "end", "confidence"]
      },
      "Transcript": {
        "type": "object",
        "description": "Completed transcript in the OpenTranscription Unified Schema (OTUS).",
        "properties": {
          "text": {
            "type": "string",
            "description": "Full transcript as a single string."
          },
          "language": {
            "type": ["string", "null"],
            "description": "Detected or specified ISO 639-1 language code."
          },
          "confidence": {
            "type": ["number", "null"],
            "format": "float",
            "description": "Overall confidence score (0.0–1.0), if provided by the model."
          },
          "segments": {
            "type": ["array", "null"],
            "description": "Time-aligned segments. Null if the model does not support segment-level output.",
            "items": {
              "$ref": "#/components/schemas/TranscriptionSegment"
            }
          },
          "words": {
            "type": ["array", "null"],
            "description": "Word-level timing. An array when `word_timestamps` is `available`; `null` when it is `unavailable` (the model returned no usable word timings) or `disabled` (the caller set `word_timestamps: false`).",
            "items": {
              "$ref": "#/components/schemas/TranscriptionWord"
            }
          },
          "word_timestamps": {
            "type": "string",
            "enum": ["available", "unavailable", "disabled"],
            "description": "Status of word-level timing for this transcript. `available`: `words` and `segments[].words` are populated. `unavailable`: the model returned no words, or every word had zero timing. `disabled`: the caller requested `word_timestamps: false` on the create call."
          }
        },
        "required": ["text"]
      },
      "CreateTranscriptionRequest": {
        "type": "object",
        "description": "Request body for creating a transcription job.\n\n**Model selection:** Provide exactly one of `model`, `models`, or `router`. Use `model` for a single direct model, `models` for a user-defined fallback chain (first is primary; subsequent entries are tried on retryable failure), or `router` for automatic strategy-based selection.\n\nVirtual model IDs (`auto/cheapest`, `auto/fastest`, `auto/best`) are also accepted in the `model` field as shorthand for router strategies.",
        "properties": {
          "file_path": {
            "type": "string",
            "description": "Audio file path in Supabase Storage. Format: `<org_uuid>/<filename>`. Obtain by uploading via the dashboard or the storage upload flow first.",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.+$",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890/interview-2025-01-15.mp3"
          },
          "model": {
            "type": "string",
            "description": "Model ID to use. See `GET /api/v1/models` for available models. Mutually exclusive with `models` and `router`.\n\nVirtual IDs are also accepted: `auto/cheapest` (lowest cost), `auto/fastest` (lowest latency), `auto/best` (lowest WER).",
            "example": "openai/whisper-large-v3"
          },
          "models": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 2,
            "maxItems": 5,
            "description": "Ordered fallback chain of concrete model IDs (no `auto/*` virtual IDs). The first entry is the primary; on retryable provider failure, subsequent entries are tried in order. Mutually exclusive with `model` and `router`. Chains are subject to your organization's routing preferences — if an org `blocked_providers` rule or `max_cost_per_minute_credits` cap excludes an entry, the job fails with `preferences_conflict` or `cost_cap_exceeded`.",
            "example": [
              "deepgram/nova-3",
              "assemblyai/best",
              "openai/whisper-large-v3"
            ]
          },
          "router": {
            "$ref": "#/components/schemas/RouterConfig"
          },
          "language": {
            "type": "string",
            "description": "ISO 639-1 language code (e.g. `en`, `es`). If omitted, the model will auto-detect. When using `router`, this also filters candidate models to those supporting the specified language.",
            "minLength": 2,
            "maxLength": 2,
            "example": "en"
          },
          "diarization": {
            "type": "boolean",
            "description": "Enable speaker diarization. `true` forces on, `false` forces off, omit to use the model's default — except on a model whose provider cannot return word timestamps and speaker labels together (`mistral/voxtral-mini-transcribe`), where an omitted value stays off unless `word_timestamps` is `false`. When using `router`, models that don't support diarization are excluded from candidates."
          },
          "word_timestamps": {
            "type": "boolean",
            "default": true,
            "description": "Enable word-level timestamps. Defaults to `true`. The resulting transcript's `word_timestamps` field reports one of three statuses: `available` (words were returned), `unavailable` (the model returned no usable word timings), or `disabled` (this was set to `false`). When using `router`, an explicit `true` excludes candidate models that don't support word timestamps."
          },
          "use_own_key": {
            "type": "boolean",
            "description": "Route this job through your organization's BYOK provider API key. Requires a key configured in settings. A 5% routing fee (of equivalent provider cost) is charged to platform credits; the first 100 minutes/month per org are free. A negative credit balance blocks new jobs.",
            "default": false
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "Public HTTPS URL to receive a POST notification when the job completes or fails. Events are signed (`X-OT-Signature`) and delivered at-least-once with retries — see the `webhooks` section. Thin payload; fetch the transcript via `GET /api/v1/transcriptions/{id}`.",
            "example": "https://your-app.example.com/webhooks/transcription"
          },
          "metadata": {
            "type": "object",
            "description": "Arbitrary key-value metadata stored with the job. Values can be any JSON type. `quality_warning` is a reserved key — the platform writes it (see `TranscriptionJobDetail.metadata` below) and a request supplying it is rejected with a 400.",
            "additionalProperties": true,
            "example": {
              "customer_id": "cust_12345",
              "source": "mobile-app"
            }
          },
          "duration": {
            "type": "number",
            "format": "float",
            "description": "Client-detected audio duration in seconds. Used for credit reservation estimation only — more accurate than the server-side file-size heuristic.",
            "exclusiveMinimum": 0
          },
          "custom_model_id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID of a user-uploaded fine-tuned model. Mutually exclusive with standard `model` routing."
          },
          "vocabulary_list_id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID of a saved vocabulary list to improve recognition of domain-specific terms."
          },
          "custom_words": {
            "type": "array",
            "description": "Inline custom vocabulary words for this transcription. Each word is 1-100 characters. Max 1000 entries.",
            "maxItems": 1000,
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "example": ["Kubernetes", "HIPAA", "NLP"]
          },
          "title": {
            "type": ["string", "null"],
            "minLength": 1,
            "maxLength": 200,
            "description": "Optional user-defined display name for this job. Shown in the UI list and detail page; falls back to `file_name` when null. Whitespace is trimmed; whitespace-only values are rejected."
          },
          "code_switching": {
            "type": "boolean",
            "description": "Enable code-switching detection for mixed-language audio (explicit opt-in only, not auto-enabled). When using `router`, models that don't support code-switching are excluded from candidates."
          },
          "code_switching_confidence_threshold": {
            "type": "number",
            "format": "float",
            "description": "Confidence threshold for code-switching detection (0.0-1.0). AssemblyAI only.",
            "minimum": 0.0,
            "maximum": 1.0
          },
          "audio_retention_days": {
            "type": ["integer", "null"],
            "minimum": 0,
            "description": "Override your organization's API audio-retention setting for this job. An integer deletes the stored audio after that many days; `0` deletes it on completion; `null` retains it indefinitely, overriding any organization-level limit. Omit the field to leave the organization setting in force — `null` is a value, not the default.",
            "example": 7
          }
        },
        "required": ["file_path"]
      },
      "UpdateTranscriptionRequest": {
        "type": "object",
        "description": "Partial update of an existing transcription job. All properties optional.",
        "properties": {
          "title": {
            "type": ["string", "null"],
            "minLength": 1,
            "maxLength": 200,
            "description": "Display name. Send `null` to clear."
          }
        }
      },
      "RouterConfig": {
        "type": "object",
        "description": "Smart Router configuration for automatic model selection. The router picks the optimal model based on the specified strategy, filtering candidates by language, features, and provider constraints.\n\nOn provider failure, the router automatically falls back to the next-best candidate (up to 2 fallbacks). The response includes `routed_from` indicating the strategy used and `model_id` showing which model actually processed the job.",
        "properties": {
          "strategy": {
            "type": "string",
            "enum": ["cheapest", "fastest", "most_accurate"],
            "description": "`cheapest` — lowest `cost_per_second`. `fastest` — lowest `p95_latency_ms` from live health data. `most_accurate` — lowest Word Error Rate from benchmark data (filterable by `preset`)."
          },
          "preset": {
            "type": "string",
            "enum": [
              "general",
              "medical",
              "legal",
              "technical",
              "conversational",
              "noisy"
            ],
            "description": "Audio domain preset for the `most_accurate` strategy. Selects which benchmark category to sort by. Defaults to `general`. Ignored for other strategies.",
            "default": "general"
          },
          "only": {
            "type": "array",
            "description": "Allowlist of model IDs or provider slugs. Only models matching an entry are considered. A provider slug (e.g. `deepgram`) matches all models from that provider. A full model ID (e.g. `deepgram/nova-3`) matches exactly.",
            "items": {
              "type": "string"
            },
            "example": ["deepgram", "assemblyai/best"]
          },
          "ignore": {
            "type": "array",
            "description": "Blocklist of model IDs or provider slugs. Models matching any entry are excluded from candidates.",
            "items": {
              "type": "string"
            },
            "example": ["openai"]
          },
          "allow_fallbacks": {
            "type": "boolean",
            "description": "Whether to automatically try the next-best model on provider failure. Defaults to `true`. Set to `false` to fail immediately if the selected model errors.",
            "default": true
          }
        },
        "required": ["strategy"]
      },
      "TranscriptionJobCreated": {
        "type": "object",
        "description": "Confirmation returned after a job is successfully created.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique job identifier. Use this to poll status."
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "model_id": {
            "type": "string",
            "description": "The model ID that will process this job. When using the router, this is the resolved model — not the virtual `auto/*` ID."
          },
          "custom_model_id": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "Custom model UUID if one was specified."
          },
          "language": {
            "type": ["string", "null"],
            "description": "Language code as specified in the request."
          },
          "use_own_key": {
            "type": "boolean",
            "description": "Whether BYOK routing is active for this job."
          },
          "credits_reserved": {
            "type": "integer",
            "description": "Credits reserved for this job. `0` for BYOK or free-model jobs."
          },
          "routed_from": {
            "type": ["string", "null"],
            "description": "Routing strategy that selected this model (e.g. `auto/cheapest`). `null` when the user specified a model directly via `model` or a user-defined `models` chain — for chain-based routing see `fallback_source` instead.",
            "example": "auto/cheapest"
          },
          "fallback_source": {
            "type": ["string", "null"],
            "enum": ["auto", "user_chain", null],
            "description": "Origin of the routing decision for this job. `auto` — Smart Router selected the model (via `router` or a virtual `auto/*` ID). `user_chain` — resolved from a user-supplied `models` fallback chain (returns the chain's primary). `null` — direct-model selection with no router or chain. Actual fallback events that occur during execution are recorded on the job record (`GET /transcriptions/{id}`) under `router_context.fallbacks_used`."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the job was created."
          }
        },
        "required": [
          "id",
          "status",
          "model_id",
          "use_own_key",
          "credits_reserved",
          "created_at"
        ]
      },
      "TranscriptionJobSummary": {
        "type": "object",
        "description": "Abridged job record returned in list operations.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "model_id": {
            "type": ["string", "null"],
            "description": "Model ID used for this job."
          },
          "model_name": {
            "type": ["string", "null"],
            "description": "Human-readable model display name."
          },
          "provider": {
            "type": ["string", "null"],
            "description": "Provider name (e.g. `OpenAI`, `Deepgram`)."
          },
          "language": {
            "type": ["string", "null"],
            "description": "Language code."
          },
          "title": {
            "type": ["string", "null"],
            "description": "User-defined display name. Null if not set — clients should fall back to `file_name` for display."
          },
          "file_name": {
            "type": "string",
            "description": "Original file name."
          },
          "file_size_bytes": {
            "type": ["integer", "null"],
            "description": "File size in bytes."
          },
          "credits_reserved": {
            "type": ["integer", "null"],
            "description": "Credits reserved at job creation."
          },
          "credits_used": {
            "type": ["integer", "null"],
            "description": "Credits actually consumed. Null until job completes."
          },
          "processing_time_ms": {
            "type": ["integer", "null"],
            "description": "Wall-clock processing time in milliseconds. Null until job completes."
          },
          "error": {
            "type": ["string", "null"],
            "description": "User-safe error message if the job failed. Sanitized; never contains internal details. For programmatic handling, prefer the structured error_code field."
          },
          "error_code": {
            "type": ["string", "null"],
            "description": "Structured machine-readable error identifier set when `status='failed'`. Null for successful jobs and legacy rows.\n\nFor programmatic handling, branch on this field rather than parsing the human-readable `error` text. New codes may be added over time — consumers should handle unknown values gracefully and fall back to displaying the `error` field.\n\n**Known codes:**\n\n| Code | Meaning | Recommended action |\n|------|---------|--------------------|\n| `service_unavailable` | Platform funding issue, transient | Retry after a few minutes |\n| `transcription_failed` | Provider returned a non-retryable error | Try a different model or audio file |\n| `model_not_found` | Selected model is unavailable or inactive | Choose another model |\n| `insufficient_credits` | Not enough credits for the operation | Top up the account |\n| `free_minutes_exhausted` | Free-tier minutes used up for the month | Upgrade or wait until next month |\n| `max_fallbacks_exceeded` | Router exhausted the fallback chain | Contact support |\n| `internal_error` | Infrastructure issue | Retry later; contact support if persistent |\n| `provider_access_blocked` | Provider blocked the request at the network level (VPN/proxy/IP reputation) | Disable any VPN/proxy or use a paid key |\n| `provider_auth_failed` | Provider rejected the credentials — invalid/expired key or insufficient permissions | Check the API key and its permissions |\n| `unsupported_audio` | The audio's format or length is not supported by the selected model | Try a different file or model |\n| `unsupported_language` | The requested language is not supported by the selected model | Select a supported language or model |\n| `provider_payment_required` | BYOK: your provider account is out of credit | Add funds or enable billing with the provider |"
          },
          "has_transcript": {
            "type": "boolean",
            "description": "Whether a completed transcript is available for this job."
          },
          "created_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "updated_at": {
            "type": ["string", "null"],
            "format": "date-time"
          }
        },
        "required": ["id", "status", "has_transcript"]
      },
      "PaginationMeta": {
        "type": "object",
        "description": "Pagination metadata.",
        "properties": {
          "page": {
            "type": "integer",
            "description": "Current page (1-indexed)."
          },
          "limit": {
            "type": "integer",
            "description": "Results per page."
          },
          "total": {
            "type": "integer",
            "description": "Total number of matching records."
          },
          "totalPages": {
            "type": "integer",
            "description": "Total number of pages."
          }
        },
        "required": ["page", "limit", "total", "totalPages"]
      },
      "TranscriptionJobList": {
        "type": "object",
        "description": "Paginated list of transcription job summaries.",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TranscriptionJobSummary"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        },
        "required": ["data", "pagination"]
      },
      "TranscriptionJobDetail": {
        "type": "object",
        "description": "Full job record, including transcript if the job has completed.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "model_id": {
            "type": ["string", "null"]
          },
          "language": {
            "type": ["string", "null"]
          },
          "title": {
            "type": ["string", "null"],
            "description": "User-defined display name. Null if not set — clients should fall back to `file_name` for display."
          },
          "file_name": {
            "type": "string"
          },
          "file_size_bytes": {
            "type": ["integer", "null"]
          },
          "credits_reserved": {
            "type": ["integer", "null"]
          },
          "credits_used": {
            "type": ["integer", "null"]
          },
          "processing_time_ms": {
            "type": ["integer", "null"]
          },
          "error": {
            "type": ["string", "null"],
            "description": "User-safe error message if the job failed. Sanitized; never contains internal details. For programmatic handling, prefer the structured error_code field."
          },
          "error_code": {
            "type": ["string", "null"],
            "description": "Structured machine-readable error identifier set when `status='failed'`. Null for successful jobs and legacy rows.\n\nFor programmatic handling, branch on this field rather than parsing the human-readable `error` text. New codes may be added over time — consumers should handle unknown values gracefully and fall back to displaying the `error` field.\n\n**Known codes:**\n\n| Code | Meaning | Recommended action |\n|------|---------|--------------------|\n| `service_unavailable` | Platform funding issue, transient | Retry after a few minutes |\n| `transcription_failed` | Provider returned a non-retryable error | Try a different model or audio file |\n| `model_not_found` | Selected model is unavailable or inactive | Choose another model |\n| `insufficient_credits` | Not enough credits for the operation | Top up the account |\n| `free_minutes_exhausted` | Free-tier minutes used up for the month | Upgrade or wait until next month |\n| `max_fallbacks_exceeded` | Router exhausted the fallback chain | Contact support |\n| `internal_error` | Infrastructure issue | Retry later; contact support if persistent |\n| `provider_access_blocked` | Provider blocked the request at the network level (VPN/proxy/IP reputation) | Disable any VPN/proxy or use a paid key |\n| `provider_auth_failed` | Provider rejected the credentials — invalid/expired key or insufficient permissions | Check the API key and its permissions |\n| `unsupported_audio` | The audio's format or length is not supported by the selected model | Try a different file or model |\n| `unsupported_language` | The requested language is not supported by the selected model | Select a supported language or model |\n| `provider_payment_required` | BYOK: your provider account is out of credit | Add funds or enable billing with the provider |"
          },
          "webhook_url": {
            "type": ["string", "null"],
            "format": "uri"
          },
          "metadata": {
            "type": ["object", "null"],
            "description": "Free-form job metadata. May include a `quality_warning` object (see below) when the transcript's plausibility gate flagged this result; absent otherwise. New keys may be added over time — consumers should ignore keys they don't recognize.\n\n**`quality_warning`** (present only when the result was flagged; the job still completes normally otherwise):\n\n| Field | Type | Meaning |\n|-------|------|--------|\n| `tier` | `\"empty\" \\| \"sparse\" \\| \"language_mismatch\"` | Which check flagged the result |\n| `billable` | `boolean` | `false` for `empty`/`sparse` (not charged, reservation released); `true` for `language_mismatch` (charged normally — the signal is weaker, so it warns without refusing the charge) |\n| `reasonCode` | `string \\| null` | One of `empty_transcript`, `low_word_density`, `language_undetermined`, `language_not_supported`, `low_confidence` |\n| `wordsPerMinute` | `number \\| null` | Set for the `sparse` tier |\n| `detectedLanguage` | `string \\| null` | Set for the `language_mismatch` tier (BCP-47 primary subtag, or `und`) |\n| `confidence` | `number \\| null` | Overall transcript confidence, when the `language_mismatch` tier used it as a signal |\n\nSee `docs/architecture/transcript-quality-gates.md` for the thresholds.",
            "additionalProperties": true
          },
          "created_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "updated_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "transcript": {
            "$ref": "#/components/schemas/Transcript",
            "description": "Present only when `status` is `completed`."
          }
        },
        "required": ["id", "status"]
      },
      "TranscriptionDeleteResponse": {
        "type": "object",
        "description": "Response after cancelling or deleting a job.",
        "properties": {
          "message": {
            "type": "string",
            "description": "Describes what happened (cancelled vs. deleted)."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "description": "Present when a pending/processing job was cancelled. Value is always `cancelled`."
          }
        },
        "required": ["message", "id"]
      },
      "UsageByModel": {
        "type": "object",
        "description": "Usage breakdown for a single model.",
        "properties": {
          "model_id": {
            "type": "string",
            "description": "Model ID."
          },
          "model_name": {
            "type": "string",
            "description": "Human-readable model name."
          },
          "provider_name": {
            "type": "string",
            "description": "Provider name."
          },
          "job_count": {
            "type": "integer",
            "description": "Number of jobs run with this model in the period."
          },
          "total_minutes": {
            "type": "number",
            "format": "float",
            "description": "Total audio minutes processed."
          },
          "total_credits": {
            "type": "number",
            "description": "Total credits consumed by this model. Fractional: credits are stored as numeric(16,6)."
          }
        },
        "required": [
          "model_id",
          "model_name",
          "provider_name",
          "job_count",
          "total_minutes",
          "total_credits"
        ]
      },
      "UsageByPeriod": {
        "type": "object",
        "description": "Usage for a single time bucket.",
        "properties": {
          "date": {
            "type": "string",
            "description": "Date bucket. `YYYY-MM-DD` for 7d/30d periods; `YYYY-MM` for 90d/365d periods.",
            "example": "2025-01-15"
          },
          "job_count": {
            "type": "integer"
          },
          "total_minutes": {
            "type": "number",
            "format": "float"
          },
          "total_credits": {
            "type": "number"
          }
        },
        "required": ["date", "job_count", "total_minutes", "total_credits"]
      },
      "UsageResponse": {
        "type": "object",
        "description": "Aggregated usage statistics.",
        "properties": {
          "total_jobs": {
            "type": "integer",
            "description": "Total jobs in the period."
          },
          "total_minutes": {
            "type": "number",
            "format": "float",
            "description": "Total audio minutes processed (rounded to 2 decimal places)."
          },
          "total_credits_spent": {
            "type": "number",
            "description": "Total credits spent in the period. Always equals the sum of `by_model[].total_credits` (and of `by_period[].total_credits`): each job counts its ledger charge (job charge or BYOK platform fee) when one exists; a BYOK job with no charge (inside the free tier) counts 0; any other job without a charge (trial) counts its `credits_used`."
          },
          "period": {
            "type": "string",
            "enum": ["7d", "30d", "90d", "365d"],
            "description": "The period this report covers."
          },
          "by_model": {
            "type": "array",
            "description": "Usage broken down by model, sorted by `job_count` descending.",
            "items": {
              "$ref": "#/components/schemas/UsageByModel"
            }
          },
          "by_period": {
            "type": "array",
            "description": "Usage broken down by time bucket, sorted by date ascending.",
            "items": {
              "$ref": "#/components/schemas/UsageByPeriod"
            }
          }
        },
        "required": [
          "total_jobs",
          "total_minutes",
          "total_credits_spent",
          "period",
          "by_model",
          "by_period"
        ]
      },
      "ModelProvider": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Provider UUID."
          },
          "name": {
            "type": "string",
            "description": "Provider display name (e.g. `OpenAI`, `Deepgram`)."
          }
        },
        "required": ["id", "name"]
      },
      "ModelPricing": {
        "type": "object",
        "properties": {
          "cost_per_second": {
            "type": "number",
            "format": "float",
            "description": "Cost in credits per second of audio. Multiply by 60 to get credits per minute."
          },
          "currency": {
            "type": "string",
            "enum": ["credits"],
            "description": "Always `credits`. 1 credit = $0.01 USD."
          }
        },
        "required": ["cost_per_second", "currency"]
      },
      "ModelPerformance": {
        "type": "object",
        "properties": {
          "avg_speed_factor": {
            "type": ["number", "null"],
            "format": "float",
            "description": "Ratio of audio duration to processing time (e.g. `12.5x` means 1 min of audio processes in ~5s)."
          },
          "avg_wer": {
            "type": "number",
            "format": "float",
            "description": "Average Word Error Rate across benchmark runs (0.0 = perfect). Only present if benchmarks exist."
          },
          "avg_cer": {
            "type": "number",
            "format": "float",
            "description": "Average Character Error Rate. Optional."
          },
          "avg_mer": {
            "type": "number",
            "format": "float",
            "description": "Average Match Error Rate. Optional."
          },
          "avg_wil": {
            "type": "number",
            "format": "float",
            "description": "Average Word Information Lost. Optional."
          },
          "avg_latency_ms": {
            "type": "integer",
            "description": "Average processing latency in milliseconds. Optional."
          }
        }
      },
      "ModelCapabilities": {
        "type": "object",
        "properties": {
          "supported_languages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "ISO 639-1 language codes supported by this model."
          },
          "max_file_size": {
            "type": ["integer", "null"],
            "description": "Maximum accepted file size in bytes. Null if unlimited. Enforced at job creation against the stored object's byte count."
          },
          "max_duration_seconds": {
            "type": ["integer", "null"],
            "description": "Maximum accepted audio duration in seconds. Null when the provider documents no duration limit. Enforced at job creation against the `duration` you declare on the request; omit `duration` and the check is skipped, but the job may then fail at the provider instead.",
            "examples": [900, 36000, null]
          },
          "supported_formats": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Accepted audio format extensions (e.g. `mp3`, `wav`, `flac`)."
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Enabled feature list (e.g. `diarization`, `word_timestamps`, `smart_format`)."
          }
        },
        "required": ["supported_languages", "supported_formats", "features"]
      },
      "ModelEntry": {
        "type": "object",
        "description": "A transcription model with pricing and capabilities.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique model identifier in `provider/model-name` format."
          },
          "name": {
            "type": "string",
            "description": "Internal model name slug."
          },
          "display_name": {
            "type": "string",
            "description": "Human-readable display name."
          },
          "provider": {
            "$ref": "#/components/schemas/ModelProvider"
          },
          "description": {
            "type": "string",
            "description": "Localized description of the model."
          },
          "pricing": {
            "$ref": "#/components/schemas/ModelPricing"
          },
          "performance": {
            "$ref": "#/components/schemas/ModelPerformance"
          },
          "capabilities": {
            "$ref": "#/components/schemas/ModelCapabilities"
          },
          "is_active": {
            "type": "boolean",
            "description": "Whether the model is servable at all. `true` for BOTH `lifecycle: active` and `lifecycle: deprecated` — only `retired` reads `false`."
          },
          "lifecycle": {
            "type": "string",
            "enum": ["active", "deprecated"],
            "description": "`active` = offered for new selections. `deprecated` = fully servable when named explicitly (this `model` id, or a `models[]` chain entry), but excluded from a NEW `auto/*` or `router`-config resolution. A `retired` model never appears in this catalog (it fails `is_active` before reaching this list)."
          },
          "successor_model_id": {
            "type": ["string", "null"],
            "description": "Recommended replacement when `lifecycle` is `deprecated`. `null` when no successor is recorded, or when the model is `active`."
          },
          "mode": {
            "type": "string",
            "enum": ["batch", "realtime", "both"],
            "description": "`batch` = file upload only; `realtime` = streaming only; `both` = supports either."
          }
        },
        "required": [
          "id",
          "name",
          "display_name",
          "provider",
          "description",
          "pricing",
          "performance",
          "capabilities",
          "is_active",
          "lifecycle",
          "successor_model_id",
          "mode"
        ]
      },
      "ModelFilters": {
        "type": "object",
        "properties": {
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Distinct language codes across all active models."
          },
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModelProvider"
            },
            "description": "Distinct providers with active models."
          },
          "priceRange": {
            "type": "object",
            "properties": {
              "min": {
                "type": "number",
                "format": "float"
              },
              "max": {
                "type": "number",
                "format": "float"
              }
            },
            "required": ["min", "max"]
          }
        },
        "required": ["languages", "providers", "priceRange"]
      },
      "ModelStats": {
        "type": "object",
        "properties": {
          "total_models": {
            "type": "integer"
          },
          "total_languages": {
            "type": "integer"
          },
          "avg_price": {
            "type": "number",
            "format": "float",
            "description": "Average `cost_per_second` across all active models."
          }
        },
        "required": ["total_models", "total_languages", "avg_price"]
      },
      "ModelsData": {
        "type": "object",
        "description": "Standard models catalog payload (no marketplace params).",
        "properties": {
          "models": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModelEntry"
            }
          },
          "filters": {
            "$ref": "#/components/schemas/ModelFilters"
          },
          "stats": {
            "$ref": "#/components/schemas/ModelStats"
          }
        },
        "required": ["models", "filters", "stats"]
      },
      "ModelsResponse": {
        "type": "object",
        "description": "Top-level wrapper for the models catalog.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ModelsData"
          }
        },
        "required": ["data"]
      },
      "LeaderboardEntry": {
        "type": "object",
        "description": "A single model's leaderboard position and aggregate benchmark metrics.",
        "properties": {
          "model_id": {
            "type": "string"
          },
          "model_name": {
            "type": "string"
          },
          "provider_name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "avg_wer": {
            "type": "number",
            "format": "float",
            "description": "Average Word Error Rate (0.0 = perfect)."
          },
          "avg_cer": {
            "type": "number",
            "format": "float",
            "description": "Average Character Error Rate. Optional."
          },
          "avg_mer": {
            "type": "number",
            "format": "float",
            "description": "Average Match Error Rate. Optional."
          },
          "avg_wil": {
            "type": "number",
            "format": "float",
            "description": "Average Word Information Lost. Optional."
          },
          "avg_latency_ms": {
            "type": "integer",
            "description": "Average processing latency in milliseconds."
          },
          "avg_cost_per_second": {
            "type": "number",
            "format": "float",
            "description": "Credits per second of audio."
          },
          "benchmark_count": {
            "type": "integer",
            "description": "Number of benchmark runs included in the averages."
          },
          "overall_score": {
            "type": "number",
            "format": "float",
            "description": "Composite score 0–100. Weights: 50% accuracy, 30% speed, 20% cost."
          },
          "last_run_at": {
            "type": "string",
            "format": "date-time",
            "description": "Timestamp of the most recent benchmark run for this model."
          }
        },
        "required": [
          "model_id",
          "model_name",
          "provider_name",
          "description",
          "avg_wer",
          "avg_latency_ms",
          "avg_cost_per_second",
          "benchmark_count",
          "overall_score",
          "last_run_at"
        ]
      },
      "LeaderboardFilters": {
        "type": "object",
        "properties": {
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "accents": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": ["languages", "categories", "accents"]
      },
      "LeaderboardData": {
        "type": "object",
        "properties": {
          "leaderboard": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LeaderboardEntry"
            }
          },
          "filters": {
            "$ref": "#/components/schemas/LeaderboardFilters"
          },
          "total_benchmarks": {
            "type": "integer",
            "description": "Total number of benchmark rows in the result set (before grouping)."
          },
          "total_models": {
            "type": "integer",
            "description": "Number of models represented in the leaderboard."
          }
        },
        "required": [
          "leaderboard",
          "filters",
          "total_benchmarks",
          "total_models"
        ]
      },
      "LeaderboardResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/LeaderboardData"
          }
        },
        "required": ["data"]
      },
      "BenchmarkModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "description": {
            "type": "string"
          },
          "provider_name": {
            "type": "string"
          },
          "cost_per_second": {
            "type": "number",
            "format": "float"
          }
        },
        "required": [
          "id",
          "name",
          "description",
          "provider_name",
          "cost_per_second"
        ]
      },
      "BenchmarkSummary": {
        "type": "object",
        "properties": {
          "avg_wer": {
            "type": "number",
            "format": "float"
          },
          "avg_cer": {
            "type": "number",
            "format": "float"
          },
          "avg_mer": {
            "type": "number",
            "format": "float"
          },
          "avg_wil": {
            "type": "number",
            "format": "float"
          },
          "avg_latency_ms": {
            "type": "integer"
          },
          "benchmark_count": {
            "type": "integer"
          },
          "overall_score": {
            "type": "number",
            "format": "float",
            "description": "Composite score 0–100."
          }
        },
        "required": [
          "avg_wer",
          "avg_latency_ms",
          "benchmark_count",
          "overall_score"
        ]
      },
      "BenchmarkHistoryEntry": {
        "type": "object",
        "properties": {
          "run_at": {
            "type": "string",
            "description": "Date of the benchmark run (YYYY-MM-DD).",
            "example": "2025-01-14"
          },
          "avg_wer": {
            "type": "number",
            "format": "float"
          },
          "avg_cer": {
            "type": "number",
            "format": "float"
          },
          "avg_mer": {
            "type": "number",
            "format": "float"
          },
          "avg_wil": {
            "type": "number",
            "format": "float"
          },
          "avg_latency_ms": {
            "type": "integer"
          },
          "benchmark_count": {
            "type": "integer"
          }
        },
        "required": ["run_at", "avg_wer", "avg_latency_ms", "benchmark_count"]
      },
      "BenchmarkCategoryEntry": {
        "type": "object",
        "properties": {
          "category": {
            "type": "string"
          },
          "avg_wer": {
            "type": "number",
            "format": "float"
          },
          "avg_cer": {
            "type": "number",
            "format": "float"
          },
          "avg_mer": {
            "type": "number",
            "format": "float"
          },
          "avg_wil": {
            "type": "number",
            "format": "float"
          },
          "avg_latency_ms": {
            "type": "integer"
          },
          "count": {
            "type": "integer"
          }
        },
        "required": ["category", "avg_wer", "avg_latency_ms", "count"]
      },
      "BenchmarkAccentEntry": {
        "type": "object",
        "properties": {
          "accent": {
            "type": "string"
          },
          "avg_wer": {
            "type": "number",
            "format": "float"
          },
          "count": {
            "type": "integer"
          }
        },
        "required": ["accent", "avg_wer", "count"]
      },
      "ModelBenchmarkDetail": {
        "type": "object",
        "description": "Detailed benchmark data for a single model.",
        "properties": {
          "model": {
            "$ref": "#/components/schemas/BenchmarkModel"
          },
          "summary": {
            "$ref": "#/components/schemas/BenchmarkSummary"
          },
          "history": {
            "type": "array",
            "description": "Per-date aggregate benchmark results, sorted ascending by date.",
            "items": {
              "$ref": "#/components/schemas/BenchmarkHistoryEntry"
            }
          },
          "by_category": {
            "type": "array",
            "description": "Benchmark breakdown by golden set category.",
            "items": {
              "$ref": "#/components/schemas/BenchmarkCategoryEntry"
            }
          },
          "by_accent": {
            "type": "array",
            "description": "Benchmark breakdown by accent.",
            "items": {
              "$ref": "#/components/schemas/BenchmarkAccentEntry"
            }
          },
          "realtime": {
            "description": "Realtime-STT benchmark detail for this model, or null if it has no realtime benchmark rows.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/RealtimeBenchmarkDetail"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": ["realtime"]
      },
      "ModelBenchmarkDetailResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ModelBenchmarkDetail"
          }
        },
        "required": ["data"]
      },
      "RealtimeLeaderboardEntry": {
        "type": "object",
        "description": "A single model's realtime-STT leaderboard position and aggregate streaming metrics.",
        "properties": {
          "model_id": {
            "type": "string"
          },
          "model_name": {
            "type": "string"
          },
          "provider_name": {
            "type": "string"
          },
          "p50_ttfw_ms": {
            "type": ["integer", "null"],
            "description": "P50 time to first output in milliseconds — the first word shown, whether an interim partial or a committed final (so commit-as-you-go models that emit no partials still report a value). Field named `ttfp` for historical reasons."
          },
          "p50_drain_ms": {
            "type": ["integer", "null"],
            "description": "P50 final-chunk drain time in milliseconds, after audio stops."
          },
          "avg_flicker": {
            "type": "number",
            "format": "float",
            "description": "Average partial-word revision rate (0.0 = no revisions)."
          },
          "avg_cadence": {
            "type": "number",
            "format": "float",
            "description": "Average partial emissions per second of audio. Descriptive, not scored."
          },
          "avg_rtf": {
            "type": "number",
            "format": "float",
            "description": "Average real-time factor. Reads ≈1.0 for a well-behaved model, since audio is streamed at 1× pace. Descriptive, not scored."
          },
          "avg_wer": {
            "type": "number",
            "format": "float",
            "description": "Average Word Error Rate (0.0 = perfect)."
          },
          "avg_cer": {
            "type": "number",
            "format": "float",
            "description": "Average Character Error Rate. Optional."
          },
          "benchmark_count": {
            "type": "integer",
            "description": "Number of realtime benchmark runs included in the averages."
          },
          "realtime_score": {
            "type": "number",
            "format": "float",
            "description": "Composite realtime score 0–100. Weights: 50% accuracy, 25% responsiveness (time to first word), 20% stability (flicker), 5% tail latency (drain)."
          },
          "last_run_at": {
            "type": "string",
            "format": "date-time",
            "description": "Timestamp of the most recent realtime benchmark run for this model."
          }
        },
        "required": [
          "model_id",
          "model_name",
          "provider_name",
          "p50_ttfw_ms",
          "p50_drain_ms",
          "avg_flicker",
          "avg_cadence",
          "avg_rtf",
          "avg_wer",
          "benchmark_count",
          "realtime_score",
          "last_run_at"
        ]
      },
      "RealtimeLeaderboardFilters": {
        "type": "object",
        "properties": {
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "accents": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": ["languages", "categories", "accents"]
      },
      "RealtimeLeaderboardData": {
        "type": "object",
        "properties": {
          "leaderboard": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RealtimeLeaderboardEntry"
            }
          },
          "filters": {
            "$ref": "#/components/schemas/RealtimeLeaderboardFilters"
          },
          "total_benchmarks": {
            "type": "integer",
            "description": "Total number of realtime benchmark rows in the result set (before grouping)."
          },
          "total_models": {
            "type": "integer",
            "description": "Number of models represented in the realtime leaderboard."
          },
          "last_run_at": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Timestamp of the most recent realtime benchmark run across all models."
          }
        },
        "required": [
          "leaderboard",
          "filters",
          "total_benchmarks",
          "total_models",
          "last_run_at"
        ]
      },
      "RealtimeLeaderboardResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/RealtimeLeaderboardData"
          }
        },
        "required": ["data"]
      },
      "RealtimeBenchmarkCategoryEntry": {
        "type": "object",
        "properties": {
          "category": {
            "type": "string"
          },
          "p50_ttfw_ms": {
            "type": ["integer", "null"]
          },
          "avg_wer": {
            "type": "number",
            "format": "float"
          },
          "avg_flicker": {
            "type": "number",
            "format": "float"
          }
        },
        "required": ["category", "p50_ttfw_ms", "avg_wer", "avg_flicker"]
      },
      "RealtimeBenchmarkSummary": {
        "type": "object",
        "properties": {
          "p50_ttfw_ms": {
            "type": ["integer", "null"]
          },
          "p50_drain_ms": {
            "type": ["integer", "null"]
          },
          "avg_flicker": {
            "type": "number",
            "format": "float"
          },
          "avg_cadence": {
            "type": "number",
            "format": "float"
          },
          "avg_rtf": {
            "type": "number",
            "format": "float"
          },
          "avg_wer": {
            "type": "number",
            "format": "float"
          },
          "avg_cer": {
            "type": "number",
            "format": "float"
          },
          "benchmark_count": {
            "type": "integer"
          },
          "realtime_score": {
            "type": "number",
            "format": "float"
          }
        },
        "required": [
          "p50_ttfw_ms",
          "p50_drain_ms",
          "avg_flicker",
          "avg_cadence",
          "avg_rtf",
          "avg_wer",
          "benchmark_count",
          "realtime_score"
        ]
      },
      "RealtimeBenchmarkDetail": {
        "type": "object",
        "description": "Detailed realtime-STT benchmark data for a single model: an overall roll-up plus a per-category breakdown.",
        "properties": {
          "model_id": {
            "type": "string"
          },
          "summary": {
            "$ref": "#/components/schemas/RealtimeBenchmarkSummary"
          },
          "by_category": {
            "type": "array",
            "description": "Realtime benchmark breakdown by golden set category.",
            "items": {
              "$ref": "#/components/schemas/RealtimeBenchmarkCategoryEntry"
            }
          },
          "last_run_at": {
            "type": ["string", "null"],
            "format": "date-time"
          }
        },
        "required": ["model_id", "summary", "by_category", "last_run_at"]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request body or query parameters failed validation, or the request body was not valid JSON.\n\nA request whose body is empty or malformed JSON returns `{ \"error\": \"Invalid JSON\" }`. A well-formed body that fails schema validation returns `{ \"error\": \"Validation error\", \"details\": [...] }`, where `details` carries the raw Zod issues.\n\nEvery 400 also carries a `documentation_url` pointing at the schema that defines the request, so a failing call tells you where its contract is written down.\n\n**Routing-preferences error codes** (when posting to `/api/v1/transcriptions` with `model`, `models`, or `router`):\n- `preferences_conflict` — every entry in the `models` chain is excluded by your org's `blocked_providers` allowlist/blocklist.\n- `cost_cap_exceeded` — every candidate exceeds the org's `max_cost_per_minute_credits` cap (BYOK and custom-model jobs bypass the cap).\n- `invalid_model_in_chain` — one or more `models[]` entries are not valid, active model IDs.\n- `org_config_invalid` — the org's stored routing preferences failed schema validation on read; request is failed closed. Contact support or an org admin to reset preferences.\n- `model_capacity_exceeded` — the audio exceeds the chosen model's documented capacity. The response carries `limit` (`max_file_size` or `max_duration_seconds`), `model_id`, `limit_value` in the same units `/v1/models` publishes for that field (BYTES for size, seconds for duration), and an English `detail`. Size is checked against the stored object; duration only against the `duration` you declared, so omitting `duration` skips that check and defers the failure to the provider. `auto/*` routes around an over-capacity model instead of failing.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "validationError": {
                "summary": "Body failed schema validation",
                "value": {
                  "error": "Validation error",
                  "details": [
                    {
                      "code": "invalid_type",
                      "path": ["file_path"],
                      "expected": "string",
                      "message": "Required"
                    }
                  ],
                  "documentation_url": "https://opentranscription.io/openapi.json#/components/schemas/CreateTranscriptionRequest"
                }
              },
              "invalidJson": {
                "summary": "Body was empty or not valid JSON",
                "value": {
                  "error": "Invalid JSON",
                  "documentation_url": "https://opentranscription.io/openapi.json"
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "Unauthorized"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Valid API key, but the key lacks the required scope.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "Insufficient scope",
              "required": "transcriptions:write"
            }
          }
        }
      },
      "NotFound": {
        "description": "The requested resource was not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "Job not found"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "Insufficient credits or free-minutes allowance exhausted — or the organization has a negative credit balance (outstanding BYOK fees).",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "description": "Machine-readable error code. `FREE_MINUTES_EXHAUSTED` when the monthly free allowance is used up. `NEGATIVE_BALANCE` when the organization has an outstanding BYOK fee overdraft."
                    },
                    "reset_at": {
                      "type": "string",
                      "description": "ISO 8601 date when free minutes reset. Present when `code` is `FREE_MINUTES_EXHAUSTED`."
                    }
                  }
                }
              ]
            },
            "example": {
              "error": "Insufficient credits. Please add credits to your account."
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests — rate limit exceeded.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "Too many requests"
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "Internal server error"
            }
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Transcriptions",
      "description": "Create and manage transcription jobs."
    },
    {
      "name": "Usage",
      "description": "Organization usage analytics."
    },
    {
      "name": "Catalog",
      "description": "Public model catalog and benchmark data. No authentication required."
    },
    {
      "name": "Benchmarks",
      "description": "Leaderboard and per-model benchmark detail."
    }
  ]
}
