Skip to main content
For long files (a 60-minute call recording takes a few minutes to recognise) the request does not have to stay open: send a webhook_url, get a job id at once, and receive the result on your server when it is ready. Works with the native API and with OpenAI model names (OpenAI SDK: extra_body), with every option (normalize, mode, diarize, gender_detection, hints, hint_context, speaker_lock, voice_isolation, response_format including the OpenAI formats). Live streaming is unchanged. Not combinable with stream=true. Maximum audio length: 4 hours.

1. Submit → 202

The answer comes as soon as the file is uploaded and decoded (a few seconds for an hour of audio):
Billing: the audio duration is counted once, when the job is accepted (this 202); status reads and the webhook are not counted. Errors in the request (unknown option, language, bad webhook_url or file_url, unreadable audio) come at once as 400, exactly like a synchronous request.

2. Webhook

POST to webhook_url, Content-Type: application/json:
  • result is the body the same synchronous request answers: the native JSON, or the OpenAI body for OpenAI model names ({"text", "usage", …}, verbose_json, diarized_json); text, srt and vtt come as a string. (A job decodes the ~28-second parts of a long file one at a time, so a few words of a long file can differ from a synchronous run, as they also can between two synchronous runs under different load.)
  • event says what finished: transcription.completed or transcription.failed (also in the X-Mihu-Event header). message is the same in one human-readable line. source is the audio: \{"type": "upload", "filename", "size_bytes"\} or \{"type": "file_url", "url", "filename", "size_bytes"\} (the URL without its query string, so pre-signed credentials are never echoed). request lists the options the job ran with, defaults filled in. Your webhook_metadata comes back as metadata.
  • Job ids are UUIDs.
  • On failure: "event": "transcription.failed", "status": "failed" and "error": {"status": 400, "message": "…"} instead of result.
  • Headers: X-Mihu-Webhook-Id (= job id, the same on every attempt: use it to ignore duplicates), X-Mihu-Event, X-Mihu-Timestamp (Unix seconds), X-Mihu-Attempt, and with a secret X-Mihu-Signature: sha256=<hex HMAC-SHA256(webhook_secret, "\{timestamp\}.\{raw body\}")>.
  • Answer with any 2xx within 10 seconds (store the body, process it later). Anything else, a timeout or a redirect counts as a failed attempt; it is retried after 1 s, 10 s, 1 min, 5 min and 30 min (6 attempts, ~36 minutes), then given up (the result can still be read with GET until it expires).
Verify the signature (reject old timestamps, compare in constant time):
Use the raw request body as received (before JSON parsing).

3. Status and result: GET /v1/audio/transcriptions/{id}

Same API key (tenant) as the request that created the job; any other key gets 404.
status: queued → processing → completed | failed. progress (0–1) while processing where it can be measured (model=umay), else null. Once completed the answer also has event, message and result (or error), and webhook.state shows the delivery: pending, delivered, failed. Polling instead of a webhook is fine (every 10–30 s).

Data retention

  • The audio is deleted as soon as recognition has finished (uploaded and downloaded files right after decoding; the decoded audio kept for a queued job is deleted when the job ends).
  • The job with its result is deleted 1 hour after completion; afterwards GET answers 404. Transcripts are not kept longer. Everything stays on the server of the region you call.

OpenAI SDK

The SDK expects a transcription in the answer, so read the 202 from the raw response as above. The webhook’s result is the OpenAI body of the chosen response_format. webhook_metadata may also be a dict in extra_body (the SDK sends it as webhook_metadata[key] fields; it comes back as an object with string values).

Downloading from a URL: file_url

Pre-signed storage links (S3, GCS, Azure) work; the link must stay valid until the 202 arrives. Without webhook_url the request waits for the result as usual.