openapi: 3.1.0
info:
  title: Omi Medical Speech-to-Text API
  version: 2026-08-16
  description: |
    Curated public contract for Omi Medical STT. Internal and administrative
    endpoints are not part of this contract.

    Every error response uses the envelope
    {"error": {"code": "...", "message": "..."}} — see the Error schema.
    All processing runs in the EU (eu-central-1).
  contact:
    name: Omi Health
    email: hello@omi.health
servers:
  - url: https://api.omi.health
    description: Production
security:
  - BearerAuth: []
tags:
  - name: Transcription
  - name: Long audio
  - name: Realtime
paths:
  /v1/capabilities:
    get:
      tags: [Transcription]
      operationId: getCapabilities
      summary: Read the live model, language, and feature capability matrix
      security: []
      responses:
        "200":
          description: Live public capabilities
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /v1/audio/transcriptions:
    post:
      tags: [Transcription]
      operationId: createTranscription
      summary: Upload audio for inline or asynchronous transcription
      description: |
        One direct-upload front door. Audio up to 60.000 seconds without a
        webhook returns an OpenAI-compatible 200 response. Audio above
        60.000 seconds, or any request with webhook_url, returns an Omi
        transcription job (202). Files longer than 60 seconds use the
        asynchronous-optimized pipeline. The exact direct-upload cap is
        100,000,000 bytes; larger files use the presigned /v1/jobs path.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/TranscriptionRequest"
      responses:
        "200":
          description: Transcription completed
          headers:
            x-request-id:
              schema: {type: string}
              description: Include this identifier in support requests.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/VerboseTranscription"
                  - $ref: "#/components/schemas/MinimalTranscription"
            text/plain:
              schema: {type: string}
        "202":
          description: Transcription job accepted
          headers:
            Location:
              schema: {type: string}
            Retry-After:
              schema: {type: integer, const: 5}
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AcceptedJob"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/TooLarge"
        "422":
          $ref: "#/components/responses/Unprocessable"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/jobs:
    post:
      tags: [Long audio]
      operationId: createLongAudioJob
      summary: Create a long-audio upload slot
      description: |
        Returns an owner-scoped presigned POST form. Upload the exact file,
        call complete_url, then poll poll_url.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateJobRequest"
      responses:
        "201":
          description: Upload slot created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedJob"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/TooLarge"
        "422":
          $ref: "#/components/responses/Unprocessable"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/jobs/{job_id}/complete:
    post:
      tags: [Long audio]
      operationId: completeLongAudioUpload
      summary: Freeze an upload and enqueue transcription
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "202":
          description: Job accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: Upload is not present or cannot be frozen.
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/jobs/{job_id}:
    get:
      tags: [Long audio]
      operationId: getLongAudioJob
      summary: Poll a long-audio job
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "200":
          description: Current job state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Long audio]
      operationId: cancelLongAudioJob
      summary: Reserved cancellation surface
      description: Cancellation is not supported in v1.
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "405":
          description: cancellation_not_supported
  /v1/stream:
    get:
      tags: [Realtime]
      operationId: openRealtimeStream
      summary: Open a realtime transcription session
      description: |
        Upgrade to WebSocket, then send a JSON start message containing the API
        key followed by binary PCM16 audio at 16 kHz mono and a JSON stop
        message. Realtime supports the eight advertised language families on
        every plan. It shares the monthly 25-hour allowance and is $0.45 per
        audio-hour afterward on Pay as you go. Live speaker activity is opt-in
        and admitted separately from text transcription. Builder supports 2
        live sessions and 1 speaker room per project; Pay as you go supports
        10 and 2. When a pool is full, excess starts fail before audio with
        close code 1013 and TOO_MANY_STREAMS or SPEAKER_CAPACITY_FULL.
      security: []
      responses:
        "101":
          description: Switching Protocols
        "402":
          description: Monthly allowance exhausted and billing is blocked.
        "429":
          $ref: "#/components/responses/RateLimited"
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Omi API key
  parameters:
    JobId:
      name: job_id
      in: path
      required: true
      schema: {type: string}
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Tenant-scoped key for asynchronous job creation, retained for 24
        hours. Accepted and ignored when the direct request resolves inline.
      schema:
        type: string
        minLength: 1
        maxLength: 255
  schemas:
    TranscriptionRequest:
      type: object
      required: [file, model]
      properties:
        file:
          type: string
          format: binary
          description: WAV, MP3, M4A/MP4, WebM, OGG, FLAC, or AAC; no more than exactly 100,000,000 bytes.
        model:
          type: string
          enum: [omi-medical-1]
          description: The flagship hosted API model.
        response_format:
          type: string
          enum: [verbose_json, json, text, diarized_json]
          default: diarized_json
        language:
          type: string
          description: Supported BCP-47 tag or auto. Omission performs bounded dominant-language detection; use auto for deliberate per-utterance routing.
          examples: [en, en-AU, es-MX, pt-BR, auto]
        language_hints:
          type: string
          description: JSON array string containing 1–8 candidate tags. Valid only with language=auto.
          example: '["en","es-MX","pt-BR"]'
        profile:
          type: string
          enum: [default, standard, turbo]
          default: default
          description: |
            Authoritative processing profile for omi-medical-1. The same
            default behavior is used at every duration; 60 seconds changes
            response delivery only.
            `standard` and `turbo` are deprecated compatibility aliases for
            `default` and behave identically. Alias responses carry deprecation
            and warning headers.
        diarize:
          type: boolean
          default: false
        max_speakers:
          type: integer
          minimum: 1
          maximum: 4
          default: 4
          description: Upper speaker-count hint used only when diarize=true.
        dictionary:
          type: boolean
          default: true
          description: Set false to suppress stored terms for this request.
        vocabulary:
          type: string
          description: JSON array string of exact expected terms for omi-medical-1. Maximum 1,000 terms and 96 characters per term; up to 50 apply directly and larger lists use retrieval. Prefer 5–30 focused terms.
          example: '["Tinel","Hepcludex","tirzepatide"]'
        patterns:
          type: string
          description: Comma-separated request-only structured-token patterns, available on every plan; maximum 32.
        webhook_url:
          type: string
          format: uri
          maxLength: 2048
          description: HTTPS callback on port 443. Forces asynchronous dispatch and requires a separately created signing secret.
    MinimalTranscription:
      type: object
      required: [text]
      properties:
        text: {type: string}
        language_source:
          type: string
          enum: [requested, default, detected]
        warnings:
          type: array
          description: Present with code language_defaulted only when a stored/server default was used instead of detection.
          items:
            $ref: "#/components/schemas/PublicWarning"
    PublicWarning:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          enum: [language_defaulted]
        effective_language:
          type: string
        message:
          type: string
    VerboseTranscription:
      type: object
      required: [text, language, segments]
      properties:
        text: {type: string}
        language:
          type: string
          description: Effective language; mixed for multilingual auto and und for silence.
        language_source:
          type: string
          enum: [requested, default, detected]
          description: Whether the effective language was requested, defaulted from the account/server, or detected by the language detector.
        warnings:
          type: array
          description: Additive structured notices. Present with code language_defaulted only when a stored/server default was used instead of detection.
          items:
            $ref: "#/components/schemas/PublicWarning"
        duration: {type: number, format: float}
        model:
          type: string
          enum: [omi-medical-1]
        request_id: {type: string}
        profile:
          type: string
          enum: [default]
          description: Effective processing profile; the flagship route has one default.
        segments:
          type: array
          items:
            $ref: "#/components/schemas/Segment"
        words:
          type: array
          items:
            $ref: "#/components/schemas/Word"
        speakers:
          type: array
          items:
            type: object
            required: [speaker]
            properties:
              speaker: {type: string}
        speaker_ids:
          type: array
          description: Additive string-only speaker list on asynchronous artifacts during contract migration.
          items: {type: string}
        language_detection:
          $ref: "#/components/schemas/LanguageDetection"
        vocabulary:
          $ref: "#/components/schemas/VocabularyAudit"
        patterns:
          type: object
          additionalProperties: true
        metadata:
          type: object
          properties:
            api_version: {type: string}
            runtime: {type: string, const: omi-runtime}
            model_build:
              type: string
              description: >-
                The exact model build that served this request (e.g.
                "omi-medical-1@2026-07"). Pin against it to detect a
                model change. Available to accounts with a card on file;
                absent on older deployments.
            processing_ms:
              type: object
              properties:
                asr: {type: integer}
                diarization: {type: integer}
    Segment:
      type: object
      required: [id, text, start, end]
      properties:
        id: {type: string}
        speaker: {type: string}
        text: {type: string}
        start: {type: number, format: float}
        end: {type: number, format: float}
        confidence:
          type: [number, "null"]
        language: {type: string}
    Word:
      type: object
      required: [word, start, end]
      properties:
        word: {type: string}
        start: {type: number, format: float}
        end: {type: number, format: float}
        speaker: {type: string}
    LanguageDetection:
      type: object
      required: [mode, candidates, languages, utterances, fallbacks]
      properties:
        mode:
          type: string
          enum: [dominant_probe, per_utterance]
          description: dominant_probe for omitted language; per_utterance for explicit auto or a mixed probe.
        candidates:
          type: array
          items: {type: string}
        requested_candidates:
          type: array
          items: {type: string}
        languages:
          type: array
          items: {type: string}
        utterances: {type: integer}
        fallbacks:
          type: integer
          const: 0
    VocabularyAudit:
      type: object
      required: [schema, enabled, requested_terms, accepted_terms, prompted_terms, applied_terms, applied, dropped, dropped_reason, suspect_insertions, safety_fallback, chunk_count, fallback_chunks, language]
      properties:
        schema:
          type: string
          enum: [omi-vocabulary-audit-v3]
        enabled: {type: boolean}
        requested_terms: {type: integer}
        accepted_terms:
          type: integer
          description: Request terms admitted by validation and selection.
        accepted:
          type: array
          items: {type: string}
        prompted_terms:
          type: integer
          description: Accepted terms supplied to at least one context-assisted decode.
        prompted:
          type: array
          items: {type: string}
        applied_terms:
          type: integer
          deprecated: true
          description: Deprecated outcome alias of delivered_terms. It never describes prompt coverage.
        applied:
          type: array
          deprecated: true
          description: Deprecated outcome alias of delivered.
          items: {type: string}
        dropped:
          type: array
          items: {type: string}
        dropped_reason:
          type: [string, "null"]
          enum: [not_selected, safety_fallback, null]
        suspect_insertions: {type: integer}
        safety_fallback: {type: boolean}
        chunk_count: {type: integer, minimum: 1}
        fallback_chunks: {type: integer, minimum: 0}
        applied_chunks: {type: integer, minimum: 0}
        prompted_chunks: {type: integer, minimum: 0}
        prompt_status:
          type: string
          enum: [none, partial, complete]
          description: Whether accepted terms reached none, some, or all decode chunks.
        fully_prompted: {type: boolean}
        fully_prompted_terms: {type: integer, minimum: 0}
        partially_prompted_terms: {type: integer, minimum: 0}
        partially_prompted:
          type: array
          items: {type: string}
        coverage_summary: {type: string}
        application_status:
          type: string
          enum: [none, partial, complete]
          description: Delivered requested-term outcome; alias of delivery_status.
        delivery_status:
          type: string
          enum: [none, partial, complete, unknown]
          description: Whether none, some, or all requested terms appear in the delivered transcript.
        observed_terms:
          type: integer
          description: Requested terms found as normalized word phrases in the delivered transcript.
        observed:
          type: array
          items: {type: string}
        missing_after_decode:
          type: array
          description: Applied terms not found literally in the delivered transcript.
          items: {type: string}
        delivered_terms:
          type: integer
          description: Requested terms literally present as normalized phrases in the delivered transcript.
        delivered:
          type: array
          items: {type: string}
        unresolved_terms:
          type: integer
          description: Requested terms not found in the delivered transcript.
        unresolved:
          type: array
          items: {type: string}
        corrected_terms:
          type: integer
          description: Distinct caller-owned canonical terms introduced or surface-normalized by the conservative final correction pass.
        corrected:
          type: array
          items: {type: string}
        corrections_applied:
          type: integer
          description: Number of conservative surface correction operations.
        result_summary: {type: string}
        language: {type: string}
        numeric_arbitration: {type: string}
        numeric_spans: {type: integer}
        patterns_requested: {type: integer}
        patterns_applied: {type: integer}
        pattern_evidence:
          type: array
          items: {type: object, additionalProperties: true}
    CreateJobRequest:
      type: object
      required: [filename, content_type, content_length_bytes]
      properties:
        model:
          type: string
          const: omi-medical-1
          default: omi-medical-1
        filename:
          type: string
          maxLength: 255
        content_type:
          type: string
          maxLength: 100
        content_length_bytes:
          type: integer
          minimum: 1
          maximum: 1073741824
        response_format:
          type: string
          enum: [text, json, verbose_json, diarized_json]
          default: json
        profile:
          type: string
          enum: [default, standard, turbo]
          default: default
          description: |
            Authoritative processing profile. The same default behavior is
            used at every duration. `standard` and `turbo` are deprecated
            compatibility aliases for `default` and behave identically. Alias
            responses carry deprecation and warning headers.
        language:
          type: string
          description: Explicit supported BCP-47 tag. Omission or per-utterance automatic routing is available on every plan.
        language_hints:
          type: array
          minItems: 1
          maxItems: 8
          uniqueItems: true
          items: {type: string}
        vocabulary:
          type: array
          maxItems: 1000
          items:
            type: string
            maxLength: 96
        patterns:
          type: string
          maxLength: 2048
        diarize:
          type: boolean
          default: false
        max_speakers:
          type: integer
          minimum: 1
          maximum: 4
          default: 4
        webhook_url:
          type: string
          format: uri
          maxLength: 2048
          description: HTTPS callback on port 443; requires a separately created signing secret.
    Job:
      type: object
      required: [id, status, created_at, updated_at, model]
      properties:
        id: {type: string}
        status:
          type: string
          enum: [awaiting_upload, accepted, running, succeeded, failed]
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
        model:
          type: string
          const: omi-medical-1
        result:
          type: object
          properties:
            download_url: {type: string, format: uri}
            expires_in_seconds: {type: integer}
            content_type: {type: string}
            expired: {type: boolean}
        error:
          type: object
          properties:
            message: {type: string}
            code:
              type: string
              enum: [audio_too_long, audio_undecodable, internal_error, job_expired]
    AcceptedJob:
      type: object
      required: [id, object, status, created_at, model, poll_url, webhook]
      properties:
        id: {type: string}
        object: {type: string, const: transcription.job}
        status:
          type: string
          enum: [accepted, running, succeeded, failed]
        created_at: {type: string, format: date-time}
        model: {type: string, const: omi-medical-1}
        poll_url: {type: string, format: uri}
        transcription:
          type: object
          description: |
            Optional incremental-delivery block, present only while a long
            job is still running and a provisional partial exists. The
            partial may be revised and is entirely replaced by the final
            result at `succeeded`; pollers that ignore it keep the previous
            behavior exactly.
          properties:
            status: {type: string, const: processing}
            partial_result_url: {type: string, format: uri}
            result_revision: {type: integer}
            completed_audio_seconds: {type: number}
            provisional: {type: boolean, const: true}
        webhook:
          type: object
          required: [requested]
          properties:
            requested: {type: boolean}
    CreatedJob:
      allOf:
        - $ref: "#/components/schemas/Job"
        - type: object
          required: [upload, complete_url, poll_url, min_audio_seconds, max_audio_seconds]
          properties:
            upload:
              type: object
              required: [method, url, fields, expires_in_seconds]
              properties:
                method:
                  type: string
                  const: POST
                url: {type: string, format: uri}
                fields:
                  type: object
                  additionalProperties: {type: string}
                expires_in_seconds: {type: integer}
            complete_url: {type: string, format: uri}
            poll_url: {type: string, format: uri}
            min_audio_seconds:
              type: integer
              const: 1
              description: |
                Minimum accepted audio duration in seconds. The value in this
                job envelope is authoritative for the live deployment; treat
                this constant as documentation only.
            max_audio_seconds:
              type: integer
              const: 7200
              description: |
                Maximum accepted audio duration in seconds (2 hours). The value
                in this job envelope is authoritative for the live deployment;
                treat this constant as documentation only.
    Error:
      type: object
      required: [error]
      additionalProperties: false
      description: |
        Every error response from every route uses exactly this envelope and
        nothing else:

            {"error": {"code": "<snake_case>", "message": "<safe text>"}}

        No other top-level keys are returned, and no transcript text, request
        content, or internal detail is ever reflected in `message`. Branch on
        `error.code`, never on message text. Codes come from a closed ratified
        registry: a code that is not registered is never emitted.
      properties:
        error:
          type: object
          required: [code, message]
          additionalProperties: false
          properties:
            code:
              type: string
              description: |
                Stable machine-readable code from the ratified registry.
                Frequently seen on public routes: `invalid_request` (400/422),
                `unauthorized` (401), `billing_blocked` (402),
                `entitlement_denied` (403), `not_found` (404),
                `upload_too_large` (413), `rate_limited` (429),
                `capacity_unavailable` (503), `internal_error` (500).
              enum:
                - audio_too_long
                - audio_undecodable
                - billing_blocked
                - cancellation_not_supported
                - capacity_unavailable
                - conflict
                - diarization_unavailable
                - entitlement_denied
                - forbidden
                - format_unsupported_async
                - gone
                - idempotency_conflict
                - idempotency_in_progress
                - internal_error
                - invalid_request
                - job_expired
                - method_not_allowed
                - model_unknown
                - not_found
                - profile_unavailable_for_duration
                - queue_full
                - rate_limited
                - result_expired
                - service_unavailable
                - storage_unavailable
                - tenant_spool_exceeded
                - tenant_upload_concurrency_exceeded
                - timeout
                - unauthorized
                - unsafe_short_transcript
                - unsupported_media_type
                - upload_timeout
                - upload_too_large
                - upload_too_slow
                - webhook_secret_missing
                - webhook_url_invalid
            message:
              type: string
              description: Short, safe, human-readable explanation. Not stable; do not parse.
  responses:
    BadRequest:
      description: Malformed or unsupported request.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    PaymentRequired:
      description: |
        Ingestion is paused for billing reasons — the monthly free allowance
        and any promotional grant are used up, a spend cap was reached, or a
        payment failed (`billing_blocked`). The key is otherwise valid, and
        result retrieval keeps working. Add or fix a card in the console; you
        are never silently billed.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    Forbidden:
      description: |
        The key is valid but is not entitled to something the request asked
        for (`entitlement_denied`) — for example Patterns, automatic language
        detection or another entitlement-gated feature. Do not retry
        unchanged.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    NotFound:
      description: Resource not found for this API identity.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    TooLarge:
      description: File or audio duration exceeds this route’s limit.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    Unprocessable:
      description: Invalid language, hints, vocabulary, pattern, or feature combination.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    RateLimited:
      description: Rate, queue, or capacity limit. Honor Retry-After.
      headers:
        Retry-After:
          schema: {type: integer}
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    Unavailable:
      description: Temporary backend, storage, detector, or processing-capacity issue. Honor Retry-After.
      headers:
        Retry-After:
          schema: {type: integer}
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
