openapi: 3.1.0
info:
  title: Omi Medical Speech-to-Text API
  version: 2026-07-27
  description: |
    Curated public contract for Omi Medical STT. Internal and administrative
    endpoints are not part of this contract.
  contact:
    name: Omi Health
    email: hello@omi.health
servers:
  - url: https://api.omi.health
    description: Production
security:
  - BearerAuth: []
tags:
  - name: Transcription
  - name: Long audio
paths:
  /v1/audio/transcriptions:
    post:
      tags: [Transcription]
      operationId: createTranscription
      summary: Upload audio for inline or asynchronous transcription
      description: |
        One direct-upload front door. Audio under 30.000 seconds without a
        webhook returns an OpenAI-compatible 200 response. Audio at or above
        30.000 seconds, or any request with webhook_url, returns an Omi
        transcription job (202). Files of 30 seconds or longer 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"
        "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"
        "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"
        "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
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, omi-medical-edge-1]
        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 uses the key default and then English.
          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"]'
        diarize:
          type: boolean
          default: false
        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. Maximum 1,000; auto/Arabic/Hindi maximum 30.
          example: '["Tinel","Hepcludex","tirzepatide"]'
        patterns:
          type: string
          description: Comma-separated request-only structured-token patterns. Invited preview; 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}
    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.
        duration: {type: number, format: float}
        model:
          type: string
          enum: [omi-medical-1, omi-medical-edge-1]
        request_id: {type: string}
        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}
        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}
            processing_ms:
              type: object
              properties:
                asr: {type: integer}
                diarization: {type: integer}
    Segment:
      type: object
      required: [id, speaker, 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
          const: per_utterance
        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: [enabled, mode, requested_terms, applied_terms, suspect_insertions, safety_fallback]
      properties:
        enabled: {type: boolean}
        mode:
          type: string
          enum: [direct, retrieval]
        requested_terms: {type: integer}
        applied_terms: {type: integer}
        suspect_insertions: {type: integer}
        safety_fallback: {type: boolean}
        language: {type: string}
    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
        language:
          type: string
          description: Explicit supported BCP-47 tag. Async auto is invite-only.
        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}
        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: 30
            max_audio_seconds:
              type: integer
              const: 1800
    Error:
      type: object
      properties:
        detail:
          oneOf:
            - {type: string}
            - {type: object}
        message: {type: string}
        code: {type: string}
  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"}
    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"}
x-websocket:
  url: wss://rt.omi.health/v1/realtime
  status: invited-preview
  schema: v1-preview
  documentation: https://console.omi.health/docs/realtime
