Feature guide

Webhooks

Receive signed, transcript-free completion events for asynchronous transcription jobs. Fetch the result through authenticated polling after verification.

Create a separate signing secret first

Webhook signing secrets are not API keys. Create one from the console, copy it during the one-time reveal, and store it server-side. A request with webhook_url fails before job creation when the submitting API key has no active signing secret.

Submit a callback

curl https://api.omi.health/v1/audio/transcriptions \
  -H "Authorization: Bearer $OMI_API_KEY" \
  -H "Idempotency-Key: consultation-2026-07-27-001" \
  -F [email protected] \
  -F model=omi-medical-1 \
  -F language=en \
  -F webhook_url=https://example.com/omi/events

Adding webhook_url always makes the direct upload asynchronous, even for a clip under 30 seconds. The same field is available on the presigned POST /v1/jobs path for files above the 100,000,000-byte direct cap.

Event contract

{
  "id": "evt_…",
  "type": "transcription.job.succeeded",
  "created": "2026-07-27T01:02:03Z",
  "data": {
    "job_id": "job_…",
    "status": "succeeded",
    "poll_url": "https://api.omi.health/v1/jobs/job_…"
  }
}

Events never contain transcript text, filenames, result URLs, or raw decoder errors. Authenticate data.poll_url with the API key that created the job.

HeaderMeaning
webhook-idStable event id; use it for deduplication.
webhook-timestampUnix timestamp for this delivery attempt.
webhook-signaturev1 Base64 HMAC-SHA256 signature.

Verify before parsing

import base64, hashlib, hmac, time

def verify_omi_webhook(raw_body: bytes, headers: dict, secret: str):
    event_id = headers["webhook-id"]
    timestamp = int(headers["webhook-timestamp"])
    if abs(time.time() - timestamp) > 300:
        raise ValueError("stale webhook")

    key = base64.b64decode(secret.removeprefix("whsec_"), validate=True)
    signed = (
        event_id.encode() + b"." + str(timestamp).encode() + b"." + raw_body
    )
    expected = "v1," + base64.b64encode(
        hmac.new(key, signed, hashlib.sha256).digest()
    ).decode()
    if not hmac.compare_digest(expected, headers["webhook-signature"]):
        raise ValueError("bad signature")

    # Persist webhook-id before handling. Repeated ids are duplicate delivery.
    return event_id
  • Verify against the raw request bytes, not re-serialized JSON.
  • Reject timestamps outside a ±5-minute window.
  • Deduplicate by webhook-id.
  • Return any 2xx only after your durable receiver accepts the event.

Delivery and rotation

Delivery is at least once. Omi attempts immediately, then after 1 minute, 5 minutes, 30 minutes, 2 hours, and 8 hours. Requests time out after 10 seconds and redirects are not followed.

Rotation creates a new active secret and preserves the previous version for a 24-hour overlap. In-flight events remain pinned to the version selected when their job was accepted.

Destination rules

Callback URLs must use HTTPS on port 443, resolve to public internet addresses, contain no user information or fragment, and be no longer than 2,048 characters. Omi re-resolves and validates the host for every attempt and never follows redirects.