API reference
Every Studio HTTP endpoint — management + public.
OHM Studio's HTTP API has two surfaces:
- Management (JWT) — used by the Studio UI for CRUD on projects, APIs, keys.
- Public (API key) — used by your apps for extractions.
Base URL: https://<your-hospital-api>/api/studio/v1.
OHM is hospital-deployed — each hospital exposes its own API URL.
For evaluation, https://api.ohm.doctor is OHM's own demo hospital.
For production, ask the customer hospital for their URL.
Public endpoints
Auth:
Authorization: Bearer ohms_<live|test>_…
POST /extract/:apiSlug
// request
{ "text": "...", "inputs": { "patientAge": 35 }, "includeInsights": false }
// response
{ "data": { ... }, "apiSlug": "opd-clinic", "version": "2026-05-08T..." }POST /audio/transcribe
Multipart: file (audio), language? (string), speakerMode? ("doctor" | "doctor_patient").
{
"transcript": "Patient reports fever 3 days...",
"language": "en",
"durationSec": 1827,
"chunked": false,
"chunkCount": 1
}The transcript is always returned in English, regardless of the
spoken language. OHM's STT runs in translate mode, so a Tamil / Hindi
/ Telugu / Bengali / code-mixed consult comes back as clean English
text. The language field is the detected
source language (useful for analytics).
chunked is true and chunkCount is > 1 when the audio was longer
than ~55 min and the server split it across multiple STT submissions.
A sentence spanning a chunk boundary may be missing 2-3 words. Show
a small warning in your UI when you see this.
POST /audio/extract/:apiSlug
Multipart: file, language?, inputs? (JSON-stringified), speakerMode? ("doctor" | "doctor_patient").
{
"transcript": "...",
"language": "en",
"durationSec": 1827,
"chunked": false,
"chunkCount": 1,
"data": { ... },
"apiSlug": "opd-clinic"
}POST /audio/extract/:apiSlug/stream
Server-Sent Events. Same body as the one-shot variant; the response is
an text/event-stream of transcript → data → done chunks. Halves
perceived latency. SDK: ohm.audio.extractStream({ apiSlug, file }).
See Streaming for chunk shapes and the SDK wrapper.
POST /summarize
{
"text": "...",
"style": "patient" | "handover" | "executive" | "progress-note",
"maxLines": 5,
"language": "en"
}→ { "summary": "..." }
POST /insights/:apiSlug
{ "transcript": "...", "priorNoteSummary": "..." }→ { "insights": { ... } }
GET /apis
Discovery endpoint. Returns the published Studio APIs visible to the
caller. Accepts either an API key (Authorization: Bearer ohms_…,
project-scoped) or a Studio user JWT (organisation-wide).
Query params:
status—PUBLISHED(default),DRAFT,ARCHIVED, orALL
[
{ "slug": "opd-clinic", "name": "OPD clinic", "status": "PUBLISHED", "version": 4, "updatedAt": "..." },
{ "slug": "discharge", "name": "Discharge", "status": "PUBLISHED", "version": 1, "updatedAt": "..." }
]Powers ohm.apis.list() in the SDK and ohm-studio list / pull-all
in the CLI.
GET /apis/by-slug/:slug
Full detail for a single slug — published schema, system prompt,
inputs, latest version. Used by the CLI's pull command for codegen
and by ohm.apis.get(slug) in the SDK. Same dual-auth model as
GET /apis.
{
"slug": "opd-clinic",
"name": "OPD clinic",
"status": "PUBLISHED",
"version": 4,
"publishedSchema": { "sections": [ ... ] },
"publishedSystemPrompt": "...",
"publishedInputs": [ ... ],
"publishedAt": "...",
"updatedAt": "..."
}GET /invocations?patientHash=…
Patient-level audit search. Returns slim metadata for every extraction touching the given patient hash within the last N days across the caller's organisation. Transcripts and extracted JSON are never returned via this surface — only timing, tokens, recordedById, status.
Query parameters:
patientHash(required) — hex SHA-256 of the patient identifier the hospital uses (ABHA / MRN / IPD). The hospital hashes; OHM never sees raw PHI.sinceDays(optional, default 90) — search window in days.limit(optional, default 100, capped at 500) — max rows returned.
{
"patientHash": "a1b2c3...",
"sinceDays": 30,
"total": 12,
"invocations": [
{
"id": "inv_...",
"apiId": "api_...",
"endpoint": "audio.extract",
"status": "SUCCESS",
"latencyMs": 4200,
"audioSeconds": 12,
"totalTokens": 6018,
"recordedById": "nurse-iyer-001",
"createdAt": "2026-05-08T10:30:00Z"
}
]
}Powers ohm.invocations.searchByPatient(...) in the SDK. Compliance-
critical surface — the caller (your hospital backend / admin app) is
responsible for enforcing role checks before exposing it to end users.
Audit & idempotency fields
All extraction endpoints (/extract, /audio/extract,
/audio/extract/:slug/stream, /insights, /audio/transcribe) accept
the same audit / idempotency parameters:
| Field | Where | Purpose |
|---|---|---|
patientHash | Body | Opaque patient identifier hash. Stored on the invocation row for audit search. |
recordedById | Body | Verified clinician id from your session token. |
Idempotency-Key | HTTP header | De-dup retry-on-network-glitch. Same key in same org replays for 24 h. |
Idempotency replay returns the original response (when retained) or a
{ replayed: true, requestId } shell. Mobile apps SHOULD send a UUID
v4 as the key on every recording attempt to protect against duplicate
chart entries.
Per-API behaviour toggles
Configurable in Studio → API → Settings tab:
| Setting | Default | Effect |
|---|---|---|
retainPayloads | off | When on, every invocation row stores retainedInput + retainedOutput. Off = privacy-first. |
enforceClinicalFoundation | on | OHM Clinical Foundation Block prepended to every extraction. Disabling requires a logged reason. |
redactPHI | off | Patient identifiers (names after honorifics, ABHA/Aadhaar/phone/MRN) replaced with typed tokens before LLM call. Response carries phiTokenMap. |
Management endpoints
Auth:
Authorization: Bearer <jwt>. RequiresORG_ADMIN(orPLATFORM_ADMIN).
Projects
| Method | Path |
|---|---|
| GET | /projects |
| POST | /projects |
| GET | /projects/:id |
| PATCH | /projects/:id |
| DELETE | /projects/:id (soft-delete) |
APIs
| Method | Path |
|---|---|
| GET | /projects/:id/apis |
| POST | /projects/:id/apis |
| GET | /apis/:id |
| PATCH | /apis/:id |
| POST | /apis/:id/publish |
| DELETE | /apis/:id (archive) |
| GET | /apis/:id/versions |
| POST | /apis/:id/versions/:version/rollback |
| POST | /apis/:id/playground |
| GET | /apis/:id/invocations |
| GET | /apis/:id/usage |
POST /apis/:id/versions/:version/rollback restores the named version's
schema + prompt + inputs into the API's draft so the next Publish snaps
back. Audit-logged.
Keys
| Method | Path |
|---|---|
| GET | /projects/:id/keys |
| POST | /projects/:id/keys (plaintext shown ONCE) |
| PATCH | /keys/:id |
| DELETE | /keys/:id (revoke) |
Usage
| Method | Path |
|---|---|
| GET | /overview |
| GET | /projects/:id/usage |
AI assistant
| Method | Path |
|---|---|
| POST | /ai-assist |
| GET | /ai-assist/drafts/:apiId |
Body: { apiId, want: "fields" | "prompt" | "edge-cases" | "test-transcript" | "diagnose" | "chat", message, context? }.
Webhooks
| Method | Path |
|---|---|
| GET | /projects/:projectId/webhooks |
| POST | /projects/:projectId/webhooks |
| PATCH | /webhooks/:id |
| DELETE | /webhooks/:id |
POST body: { url, events: ["invocation.success" \| "invocation.failed" \| ...] }.
The plaintext signing secret is returned once on create — store it on
the receiving server. Outgoing requests are signed with HMAC-SHA256 in
the OHM-Signature: sha256=… header. See Scale & throughput
for the verifier snippet.
Saved test transcripts
Per-API regression fixtures used in the Studio Playground.
| Method | Path |
|---|---|
| GET | /apis/:id/test-transcripts |
| POST | /apis/:id/test-transcripts |
| PATCH | /test-transcripts/:id |
| DELETE | /test-transcripts/:id |
Error response shape
Every non-2xx response has the same JSON shape:
{
"error": "Human-readable message",
"code": "MACHINE_READABLE_ENUM",
"requestId": "req_01H..."
}requestId is the field to copy when contacting support — it lets us
look up your exact request in our logs.
The SDK packages parse this shape into typed exceptions. The full
catalogue (10 classes — OHMAuthError, OHMRateLimitError,
OHMValidationError, OHMNotFoundError, OHMTimeoutError,
OHMNetworkError, OHMQuotaExceededError, OHMServerError,
OHMConfigError, OHMAbortError) is documented at
/sdk/javascript#errors. Stable string codes
exported as OHM_ERROR_CODES survive across SDK versions — use them
for log analytics and alerting rules.
Status codes
| Code | Meaning | SDK class | When to retry? |
|---|---|---|---|
| 200 | Success | — | — |
| 400 | Malformed body / bad language code / unsupported file | OHMValidationError | No — fix the request |
| 401 | Missing / invalid / revoked / expired key, suspended org | OHMAuthError | No — re-mint or unsuspend |
| 403 | Key missing required scope, or live key blocked in mobile bundle | OHMAuthError | No — fix scope / use proxy pattern |
| 404 | API slug not found / not published / job purged | OHMNotFoundError (carries availableSlugs?) | No — publish in Studio first |
| 408 | Client-side request timeout | OHMTimeoutError | Yes — bump timeoutMs or retry |
| 413 | Audio file too large (default cap 100 MB) | OHMValidationError | No — split or downsample |
| 422 | Validation error — body missing required fields | OHMValidationError (carries fields[]) | No — fix per error.fields[] |
| 429 | Rate limit exceeded — retry-after header set | OHMRateLimitError (carries retryAfterSec) | Yes — back off retryAfterSec |
| 504 | Gateway / upstream timeout | OHMTimeoutError | Yes — give it another go |
| 5xx | Server error — transient | OHMServerError | Yes (with backoff), max 2 retries |
| (network) | DNS / TCP / TLS / dropped connection | OHMNetworkError | Yes — or queue locally on RN |
| (caller cancel) | signal.abort() fired | OHMAbortError | Never — caller cancelled |
Per-endpoint error notes
| Endpoint | Most likely failures |
|---|---|
POST /extract/:slug | 404 (slug typo / not published), 422 (text empty or schema-required input missing) |
POST /audio/transcribe | 400 (unsupported language code), 413 (file too large), 5xx (STT provider hiccup — retried automatically twice) |
POST /audio/extract/:slug | All of the above + 404 (apiSlug) |
POST /audio/extract/:slug/stream | Same as one-shot. Stream errors arrive as { type: "error", message, code } SSE frames, NOT thrown — listen for them in the iterator. |
POST /summarize | 422 (empty text or unknown style), 429 |
POST /insights/:slug | 404 (apiSlug or insights not enabled on that API), 422 (transcript empty) |