Core workflow

Asynchronous transcription

Use direct upload for short clips and OpenAI compatibility. For long recordings, create a job first and upload directly to signed storage for an immediate job handle, fewer proxy hops, and safer retries.

Jobs stay in their region

Async jobs work on every host. Audio, jobs and results are stored in the region that processes the request: EU on api.eu.omi.health, US on api.us.omi.health, and the routed region on api.omi.health (nearest healthy region). Always follow the returned poll_url and result URLs. Regions & endpoints
POSThttps://api.omi.health/v1/audio/transcriptions

Direct upload: 200 inline or 202 job

POSThttps://api.omi.health/v1/jobs

Create upload slot

POST/v1/jobs/{job_id}/complete

Freeze upload and enqueue

GET/v1/jobs/{job_id}

Poll status and result

The direct-upload front door

Send the same multipart request to /v1/audio/transcriptions for files up to exactly 100,000,000 bytes. Memory-resident audio up to 60.000 seconds without a callback returns an OpenAI-compatible 200 response. Eligible English Medical recordings up to 15 minutes, including compressed input, can also return 200. Other long requests—or any request with webhook_url—return a 202 job envelope with Location and Retry-After: 5. Handle both response types. For job delivery, the direct endpoint has to receive, validate, and durably store the complete request body before it can return the 202 job envelope.

The 15-minute inline path also applies to ordinary requests without a privacy toggle or header. Sync delivery and zero retention are separate: ordinary requests can fall back to stored jobs when inline capacity is busy; requests requiring X-Omi-Retention: nonefail instead. The 60-second and 15-minute limits measure audio duration, not HTTP request time. See the delivery comparison and supported options.

One job accepts up to 2 hours of audio (the job envelope's max_audio_seconds field is the authoritative live value). Long recordings are segmented and reassembled server-side — send the whole consultation as one file and one job; you never need to split audio yourself.

Prefer presigned upload for long recordings

The direct endpoint is convenient, but it cannot return a job ID while the client is still uploading the body. For recordings at or above 60 seconds when durable job delivery is desired—especially remote clients or files measured in tens of megabytes—use the job flow below. Eligible recordings up to 15 minutes can instead use extended inline delivery. The job ID arrives before the upload, and the bytes travel directly to EU object storage. The same path supports files up to 1 GiB. It stores the upload and does not support zero-retention mode.

1. Create a job

curl https://api.omi.health/v1/jobs \
  -H "Authorization: Bearer $OMI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "omi-medical-1",
    "filename": "consultation.flac",
    "content_type": "audio/flac",
    "content_length_bytes": 1234567,
    "language": "en",
    "vocabulary": ["Hepcludex", "Tinel"],
    "diarize": true,
    "max_speakers": 2
  }'

The response contains a presigned POST form in upload, plus owner-scoped complete_url and poll_url. Send every returned upload field exactly as provided.

To evaluate the open English model through the same durable job workflow, use this request instead:

curl https://api.omi.health/v1/jobs \
  -H "Authorization: Bearer $OMI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "omi-medical-edge-1",
    "filename": "consultation.m4a",
    "content_type": "audio/mp4",
    "content_length_bytes": 1234567,
    "language": "en",
    "diarize": true,
    "max_speakers": 2,
    "response_format": "verbose_json"
  }'

Hosted Edge is a focused English route

omi-medical-edge-1 returns segments and word timestamps and accepts optional speaker labels. It does not accept vocabulary, Patterns, language detection, or non-default profiles. Use the flagship for those production features and realtime.

2. Upload, complete, and poll

# Install once: python -m pip install requests
import os, time, requests
from urllib.parse import urlparse

api = "https://api.omi.health"
headers = {"Authorization": f"Bearer {os.environ['OMI_API_KEY']}"}
audio_path = "consultation.flac"
size = os.path.getsize(audio_path)

# 1. Reserve an owner-scoped upload.
job = requests.post(
    f"{api}/v1/jobs",
    headers=headers,
    json={
        "model": "omi-medical-1",
        "filename": "consultation.flac",
        "content_type": "audio/flac",
        "content_length_bytes": size,
        "language": "en",
    },
).json()

# 2. Upload directly to signed storage.
with open(audio_path, "rb") as audio:
    upload = requests.post(
        job["upload"]["url"],
        data=job["upload"]["fields"],
        files={"file": ("consultation.flac", audio, "audio/flac")},
    )
upload.raise_for_status()

# 3. Freeze the upload and enqueue transcription.
requests.post(job["complete_url"], headers=headers).raise_for_status()

# 4. Long-poll server-side. This returns as soon as the job changes state,
#    so there is no blind client sleep after completion. Results up to 4 MiB
#    are returned inline, avoiding a second API round trip.
while True:
    state_response = requests.get(
        job["poll_url"],
        headers=headers,
        params={"wait": 20, "include_result": "true"},
        timeout=30,
    )
    state_response.raise_for_status()
    state = state_response.json()
    partial = (state.get("transcription") or {}).get("partial_result_url")
    if state["status"] == "running" and partial:
        preview = requests.get(partial, headers=headers).json()
        print("provisional:", preview["text"][:80], "...")  # may revise
    if state["status"] == "succeeded":
        result_envelope = state["result"]
        result = result_envelope.get("content")
        if result is None:
            result_url = result_envelope["download_url"]
            result_host = (urlparse(result_url).hostname or "").lower()
            result_headers = headers if result_host in {
                "api.omi.health", "api.eu.omi.health"
            } else {}
            result_response = requests.get(result_url, headers=result_headers)
            result_response.raise_for_status()
            result = result_response.json()
        if (result.get("decoration") or {}).get("status") == "processing":
            time.sleep(0.25)  # text is final; speaker labels are still attaching
            continue
        print(result["text"])
        break
    if state["status"] == "failed":
        raise RuntimeError(state["error"]["message"])

The state machine is awaiting_upload → accepted → running → succeeded, with failed as the terminal error state. Use bounded server-side long polling with ?wait=20&include_result=true. It returns when state changes and prevents a completed job from sitting unseen behind a client sleep.

Text-first delivery on diarized jobs

Jobs reach succeeded as soon as the transcript text is final. If you requested diarize, speaker labels and word timestamps may still be attaching for a short window after that: the first result you download can carry decoration: {"status": "processing", "stale_after": …} and word_timing_available: false instead of the speaker fields. The transcript text in that result is already final and safe to use.

  • Treat succeeded as “text is final,” and decoration.status as the speaker-label lifecycle. When enrichment lands — typically well under a minute — the stored result is replaced in place; download the result again (each poll may mint a fresh URL) to receive the decorated transcript.
  • If decoration.status is still processing, re-download on your normal poll cadence until it is gone or until the stale_after timestamp passes.
  • On the rare enrichment failure the job stays succeeded and decoration resolves to {"status": "unavailable", "review_required": true} — the transcript is delivered, speaker labels are not.
  • Webhook consumers: the callback fires once, at succeeded. If your workflow needs speaker labels, fetch the result on receipt and apply the same re-download rule when decoration is still processing; there is no second callback on enrichment.
  • Jobs without diarize are unaffected — their first result is complete.

Long jobs may additionally expose provisional text while still running: the status envelope can carry transcription: {"status": "processing", "partial_result_url": …, "result_revision": …, "completed_audio_seconds": …, "provisional": true}. The partial artifact covers roughly the first minutes of audio, may be revised, and is replaced entirely by the final result at succeeded. Pollers that ignore the block keep today's behavior exactly; never treat a partial as the final transcript.

Job request fields

FieldDefaultNotes
modelomi-medical-1omi-medical-1 or the English-only omi-medical-edge-1 evaluation route.
filenamerequiredMust match the declared content type.
content_typerequiredSupported audio MIME type.
content_length_bytesrequiredValidated before an upload slot is created.
languagekey default, then enExplicit tag or automatic per-utterance routing; available on every plan.
vocabularynoneArray of up to 1,000 terms; lists above 50 select the 50 most relevant terms.
patternsnoneRequest-scoped; available on every plan.
profiledefaultOmit or send default. Historical standard and turbo inputs are deprecated aliases with identical behavior.
diarizefalseAttach speaker labels and word timestamps to the final transcript.
max_speakers4Integer 1–4.
webhook_urlnoneHTTPS callback on port 443; requires a separate signing secret.

Retention and result access

  • Uploaded audio is deleted when processing completes or the job terminally fails — audio never waits out a retention window.
  • Result artifacts are retained until your configured expiry: default 24 hours, configurable from 1 to 72 hours in the console. Owner-scoped job metadata and expiry tombstones may remain for up to 72 hours so expired jobs return a stable response. Jobs and results are stored in the region that processed the request (see Regions & endpoints). Jobs with a webhook_url have an effective 9-hour minimum so the final delivery retry can still be picked up.
  • After saving an async transcript, call DELETE /v1/jobs/{job_id}/result. A 204 confirms removal; 202 means retrieval is disabled and physical cleanup is pending. GET returns 410 afterward. Downloading does not delete a result.
  • Zero-retention batch delivery is available on every plan via X-Omi-Retention: none or the console setting: English Medical, explicit language=en, dictionary=false, json/text/verbose_json, no speakers/custom hints, up to 15 minutes and 32 MiB. It returns on the same connection and never creates a stored job. Unsupported options return 422; busy admission returns 503. Content-free usage and audit metadata remain.
  • Uploads and results are owner-scoped; another API key cannot poll your job.
  • Request include_result=true to receive terminal JSON up to 4 MiB in result.content. Larger results retain result.download_url.
  • Send the same API-key Authorization header when downloading an Omi API result URL. Storage-backed result links expire after 15 minutes.
  • Verbose long-job results expose start/end in seconds like inline responses. Legacy start_ms/end_ms fields remain during migration.
  • Each successful poll may mint a fresh result URL while the result is retained.
  • After the configured expiry, polling returns result: {"expired": true}. An already-expired signed URL returns the storage provider’s native expiry response.
  • Do not reuse a presigned form or upload a different file into an existing job.

Automatic language detection on long audio

Async language: "auto" is available on every plan when the detector is healthy. Explicit-language jobs are available on every plan as well.

Omitted language uses bounded detection

Omi detects the dominant language from the first speech-bearing 30-second window, then transcribes the original full recording with the selected language. The completed job declareslanguage_source: "detected". Explicitlanguage: "auto" enables per-utterance routing and optional language hints.

Idempotent job creation

Send an Idempotency-Key of 1–255 characters when creating an async job. Keys are scoped to the API credential and retained for 24 hours. Repeating the same request returns the original job; reusing the key for different audio or options returns idempotency_conflict. On an inline 200 request, the header is accepted and ignored.