OHMOHM Studio

API reference

Every Studio HTTP endpoint — management + public.

View as Markdown

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 transcriptdatadone 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:

  • statusPUBLISHED (default), DRAFT, ARCHIVED, or ALL
[
  { "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:

FieldWherePurpose
patientHashBodyOpaque patient identifier hash. Stored on the invocation row for audit search.
recordedByIdBodyVerified clinician id from your session token.
Idempotency-KeyHTTP headerDe-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:

SettingDefaultEffect
retainPayloadsoffWhen on, every invocation row stores retainedInput + retainedOutput. Off = privacy-first.
enforceClinicalFoundationonOHM Clinical Foundation Block prepended to every extraction. Disabling requires a logged reason.
redactPHIoffPatient identifiers (names after honorifics, ABHA/Aadhaar/phone/MRN) replaced with typed tokens before LLM call. Response carries phiTokenMap.

Management endpoints

Auth: Authorization: Bearer <jwt>. Requires ORG_ADMIN (or PLATFORM_ADMIN).

Projects

MethodPath
GET/projects
POST/projects
GET/projects/:id
PATCH/projects/:id
DELETE/projects/:id (soft-delete)

APIs

MethodPath
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

MethodPath
GET/projects/:id/keys
POST/projects/:id/keys (plaintext shown ONCE)
PATCH/keys/:id
DELETE/keys/:id (revoke)

Usage

MethodPath
GET/overview
GET/projects/:id/usage

AI assistant

MethodPath
POST/ai-assist
GET/ai-assist/drafts/:apiId

Body: { apiId, want: "fields" | "prompt" | "edge-cases" | "test-transcript" | "diagnose" | "chat", message, context? }.

Webhooks

MethodPath
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.

MethodPath
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

CodeMeaningSDK classWhen to retry?
200Success
400Malformed body / bad language code / unsupported fileOHMValidationErrorNo — fix the request
401Missing / invalid / revoked / expired key, suspended orgOHMAuthErrorNo — re-mint or unsuspend
403Key missing required scope, or live key blocked in mobile bundleOHMAuthErrorNo — fix scope / use proxy pattern
404API slug not found / not published / job purgedOHMNotFoundError (carries availableSlugs?)No — publish in Studio first
408Client-side request timeoutOHMTimeoutErrorYes — bump timeoutMs or retry
413Audio file too large (default cap 100 MB)OHMValidationErrorNo — split or downsample
422Validation error — body missing required fieldsOHMValidationError (carries fields[])No — fix per error.fields[]
429Rate limit exceeded — retry-after header setOHMRateLimitError (carries retryAfterSec)Yes — back off retryAfterSec
504Gateway / upstream timeoutOHMTimeoutErrorYes — give it another go
5xxServer error — transientOHMServerErrorYes (with backoff), max 2 retries
(network)DNS / TCP / TLS / dropped connectionOHMNetworkErrorYes — or queue locally on RN
(caller cancel)signal.abort() firedOHMAbortErrorNever — caller cancelled

Per-endpoint error notes

EndpointMost likely failures
POST /extract/:slug404 (slug typo / not published), 422 (text empty or schema-required input missing)
POST /audio/transcribe400 (unsupported language code), 413 (file too large), 5xx (STT provider hiccup — retried automatically twice)
POST /audio/extract/:slugAll of the above + 404 (apiSlug)
POST /audio/extract/:slug/streamSame as one-shot. Stream errors arrive as { type: "error", message, code } SSE frames, NOT thrown — listen for them in the iterator.
POST /summarize422 (empty text or unknown style), 429
POST /insights/:slug404 (apiSlug or insights not enabled on that API), 422 (transcript empty)