@amigo-ai/scribe-typescript-sdk
v0.14.0
Published
Framework-agnostic TypeScript SDK for the Amigo Scribe streaming service
Readme
@amigo-ai/scribe-typescript-sdk
Framework-agnostic TypeScript SDK for the Amigo Scribe streaming service.
Developer Guide · API Reference · Contributing · Security
Use this package for provider Scribe sessions, audio capture, transcription, and note workflows. Scribe has separate service hosts and credentials from the general Platform and Classic APIs. Obtain workspace access, an authorized clinician, and a visit type with a configured note template before making requests.
This release ships the CRUD REST client for the full session lifecycle and its per-session artifacts:
Session lifecycle:
createSession—POST /v1/{workspace_id}/sessionsallocate—POST /v1/{workspace_id}/sessions/{session_id}/allocate→{ host, expires_at }listSessions—GET /v1/{workspace_id}/sessions(cursor-paginated:limit,continuation_token)getSession—GET /v1/{workspace_id}/sessions/{session_id}updateSession—PATCH /v1/{workspace_id}/sessions/{session_id}(update supported appointment, metadata, patient-name, and visit-template fields; mode is fixed at creation)endSession—POST /v1/{workspace_id}/sessions/{session_id}/end(guarded →in-review)cancelSession—POST /v1/{workspace_id}/sessions/{session_id}/cancel(guarded →cancelled)getTranscript—GET /v1/{workspace_id}/sessions/{session_id}/transcript
Appointments (read-only; each carries a nested session object):
listAppointments—GET /v1/{workspace_id}/appointments(cursor-paginated:limit,continuation_token)getAppointment—GET /v1/{workspace_id}/appointments/{appointment_id}
Artifacts (note / summary / checklist / codes):
getNote—GET /v1/{workspace_id}/sessions/{session_id}/notegenerateNote—POST /v1/{workspace_id}/sessions/{session_id}/note(pass anote_typeNoteTemplate;amd-*templates emit theStructuredNoteenvelope,structuredpopulated /bodynull)putNote—PUT /v1/{workspace_id}/sessions/{session_id}/note— versioned autosave; send the fullStructuredNoteenvelope instructured+base_version(complete-document replacement).bodyis deprecated: abodywrite is rejected with422 deprecated_field. Stalebase_version→409 version_conflict, post-finalize →409 invalid_session_statefinalizeNote—POST /v1/{workspace_id}/sessions/{session_id}/note/finalize(send the reviewed note'sbase_version; stale versions conflict and missing required template fields fail validation)getSummary—GET /v1/{workspace_id}/sessions/{session_id}/summarygenerateSummary—POST /v1/{workspace_id}/sessions/{session_id}/summarygetChecklist—GET /v1/{workspace_id}/sessions/{session_id}/checklistgetCodes—GET /v1/{workspace_id}/sessions/{session_id}/codesgenerateCodes—POST /v1/{workspace_id}/sessions/{session_id}/codes
Assist (in-visit + post-visit AI helpers):
getActions—GET /v1/{workspace_id}/sessions/{session_id}/actions(reload-safe poller)generateActions—POST /v1/{workspace_id}/sessions/{session_id}/actions(async202/200; auto-enqueued when the note job succeeds)regenerateSection—POST /v1/{workspace_id}/sessions/{session_id}/note/regenerate-section({ section_id, instructions?, base_version }→202; stalebase_version→409 version_conflict, post-finalize →409 invalid_session_state)autoCheckChecklist—POST /v1/{workspace_id}/sessions/{session_id}/checklist/auto-check→200 { matches }(persistssource='auto'state; coexists with manual toggles)askSession—POST /v1/{workspace_id}/sessions/{session_id}/ask— a header-authenticated SSE streaming Q&A helper (an async generator ofdelta {text}frames then a terminaldone {generation_id}; notEventSource, not persisted as an artifact)
It also ships the recording-independent streaming WebSocket API client
(ScribeStreamClient) — the WS transport/protocol layer that attaches with a
short-lived attach ticket, streams caller-supplied PCM16 bytes, receives
transcript frames, and handles control frames, app-level keepalive, and
resumable reconnect. It has no audio-capture code (no getUserMedia,
AudioContext, or mic): a caller feeds it PCM16 via sendAudio.
The browser recording layer (ScribeRecorder + the AudioCapture pipeline)
drives the mic into that client — see Browser recording.
Plus the pure transcript core: the wire contract (CLIENT_FRAME,
SERVER_MESSAGE, CLOSE_CODE), normalizeTurn, the transcriptReducer (keyed
on the server-owned ordinal), and buildWsUrl.
The package is browser-first (esbuild platform: 'neutral', lib = ES2022+DOM,
no Node-only imports) but the REST client runs anywhere fetch exists.
Scribe sessions follow REST-create → REST-allocate → WS-attach: create a session, allocate a streaming host, then attach a WebSocket to
host. After the session ends, read the transcript and the generated artifacts (note, summary, checklist, codes) via the REST client covered here.
For the browser vs. server split-trust model there are two higher-level clients:
ScribeServerClient(backend) — holds the M2M secret; mints per-clinician provider tokens (act-as-by-email), does create/allocate, and mints WS-only attach tickets. ExposesallocateandmintAttachTicketseparately plus aprepareConnectionhelper. Server-side only.ScribeStreamClient(browser) — given a host + attach ticket, manages the WebSocket audio connection.
Integrating as an external customer? See the end-to-end customer integration guide — the split-trust model, copy-pasteable backend + browser examples, security, hosts, and troubleshooting.
Install
npm i @amigo-ai/scribe-typescript-sdkRequires Node.js ≥ 20 (or any modern browser / runtime with fetch +
WebSocket). The package is ESM-only — use import, not require.
Usage
import { ScribeClient, ServiceUnavailableError } from '@amigo-ai/scribe-typescript-sdk'
const scribe = new ScribeClient({
// Scribe API (CRUD) host — production. (Staging: https://scribe-staging.platform.amigo.ai)
baseUrl: 'https://scribe.platform.amigo.ai',
// A provider JWT carrying `scribe:sessions:write` (+ `scribe:sessions:read_own`
// for transcripts). Pass a string, or a (possibly async) supplier to refresh
// a short-lived token per request.
token: async () => getFreshProviderJwt(),
workspaceId: 'ws_123',
})
// 1. Create a session
const session = await scribe.createSession({
external_id: 'appointment-42',
visit_type: 'medical', // use the canonical visit type configured for this workspace
metadata: { clinic: 'north' },
})
// 2. Allocate a streaming host
try {
const { host, expires_at } = await scribe.allocate(session.id)
// attach your WebSocket to `wss://${host}/agent/stream/connect?session_id=...`
} catch (err) {
if (err instanceof ServiceUnavailableError) {
// Fleet at capacity / cooldown — retry after err.retryAfterSeconds
}
}
// 3. Read the persisted transcript (after the session is finalized)
const { segments } = await scribe.getTranscript(session.id)Server client (backend)
ScribeServerClient is the confidential, backend half: it holds the M2M
credentials and encapsulates the two mints + CRUD. Never import it into a
browser bundle.
import { ScribeServerClient } from '@amigo-ai/scribe-typescript-sdk'
const server = new ScribeServerClient({
identityBaseUrl: 'https://api.platform.amigo.ai', // identity /token (mints)
scribeBaseUrl: 'https://scribe.platform.amigo.ai', // Scribe CRUD
workspaceId: 'ws_123',
clientId: process.env.AMIGO_M2M_CLIENT_ID!,
clientSecret: process.env.AMIGO_M2M_CLIENT_SECRET!, // server-side only
})
// `clinicianEmail` comes from YOUR authenticated app session (never the browser).
const session = await server.createSession(clinicianEmail, {
external_id: 'appointment-42',
visit_type: 'medical', // requires a matching configured note template
})
// Encapsulating helper: one allocate + one ticket mint → the browser-safe bundle.
const { host, ticket } = await server.prepareConnection(clinicianEmail, session.id)
// ...or the pieces separately:
const allocation = await server.allocate(clinicianEmail, session.id) // { host, expiresAt }
const attach = await server.mintAttachTicket(clinicianEmail, session.id) // { ticket, expiresAt }
// Reads (transcript / note / summary / checklist / codes) as the clinician:
const { segments } = await server.scribe(clinicianEmail).getTranscript(session.id)A 400 invalid_target (BadRequestError with errorCode === 'invalid_target')
means the clinician has no active Scribe grant in the workspace.
Streaming WebSocket client (browser)
ScribeStreamClient owns the worker WebSocket: it attaches with an attach
ticket, streams caller-supplied PCM16, and handles keepalive + resumable
reconnect. Auth is a purpose-built, WS-only attach ticket (aud scribe-streaming,
scope scribe:streams:connect, ~5-min TTL) — not a raw provider JWT. The SDK
never holds a provider credential or mints tickets; it resolves a host + ticket
from your backend via one of three seams (re-invoked on every reconnect):
import { ScribeStreamClient } from '@amigo-ai/scribe-typescript-sdk'
const client = new ScribeStreamClient({
sessionId: session.id,
// PREFERRED: one call to your backend route returning { host, ticket }
// (backed by ScribeServerClient.prepareConnection).
connectionProvider: async sessionId => fetchConnectionFromYourBackend(sessionId),
onTurn: segment => render(segment), // normalized SttTranscriptSegment (server ordinal)
onStateChange: state => console.log(state),
onReconnect: () => console.log('resumed'),
onError: err => console.error(err),
})
await client.connect() // resolves connection setup; observe onStateChange for streaming
// Send only once onStateChange reports streaming (or use ScribeRecorder below).
// client.sendAudio(pcm16) accepts ArrayBuffer | Uint8Array; you own capture.
client.pause()
client.resume()
client.end() // finalize + clean close (1000)Instead of connectionProvider you can pass the split allocateProvider +
ticketProvider seams, or static host + ticket for a one-shot (not
reconnect-safe) connection.
Reconnect is automatic on 1012 / 1006 (never on 1000 / 4009 / 4001):
the client re-resolves a fresh host + ticket, reconnects the same session, sends
resume_from{acked_offset_bytes}, and resends only the unacked ring-buffer
audio. Pair normalizeTurn + transcriptReducer (or your own store) to render
by the server-owned ordinal.
Browser recording
ScribeRecorder is the browser-facing surface: it owns the Web Audio /
getUserMedia PCM16 capture pipeline and drives a ScribeStreamClient for you.
It adds no wire-protocol logic — reconnect, ring buffer, keepalive, and
attach-ticket auth all live in ScribeStreamClient; the attach-ticket seams are
passed straight through. start() connects + opens the mic and pipes each PCM16
chunk to sendAudio; pause/resume/end drive both capture and the client.
Browser-only (needs getUserMedia/AudioContext).
import { ScribeRecorder } from '@amigo-ai/scribe-typescript-sdk'
const recorder = new ScribeRecorder({
sessionId: session.id,
ticketProvider: async sessionId => ({ ticket: await mintAttachTicket(sessionId) }),
allocateProvider: async sessionId => allocateHost(sessionId), // → { host, expiresAt }
audioCaptureMode: 'mic', // or 'mic+system' (adds tab/window audio via getDisplayMedia)
onTurn: segment => render(segment),
onStateChange: ({ state, streamState, micPermission, capturing }) => update(state),
onReconnect: () => console.log('resumed'),
onError: err => console.error(err),
})
await recorder.start() // connect + open mic; pipes PCM16 → client.sendAudio
recorder.pause() // stops the mic; the WS stays open
await recorder.resume() // re-opens the mic
recorder.end() // stop the mic + finalize + clean closeOn reconnect the mic keeps running — the client re-allocates, re-mints a ticket,
and resends unacked audio while the recorder keeps feeding new chunks. The
recorder state matrix is idle → recording ⇄ paused → ended (plus failed,
e.g. the mic-permission-denied path, which also surfaces via onError).
Lower-level building blocks are exported too: floatToPcm16, STT_SAMPLE_RATE,
AudioCapture, and the captureStreamsForMode / createPcmCapturePipeline /
stopCapturedStreams helpers.
Configuration
| Option | Type | Notes |
| ---------------- | ------------------------------------------- | -------------------------------------------------------- |
| baseUrl | string | Base URL of the Scribe/platform API. |
| token | string \| () => string \| Promise<string> | Bearer token or supplier. Needs scribe:sessions:write. |
| workspaceId | string | Scopes every request; can be overridden per call. |
| fetch | FetchLike? | Injectable transport (defaults to global fetch). |
| defaultHeaders | Record<string,string>? | Extra headers merged into every request. |
Errors
Non-2xx responses throw typed errors (all extend ScribeError):
BadRequestError (400), AuthenticationError (401), PermissionError (403),
NotFoundError (404), ConflictError (409), ValidationError (422),
RateLimitError (429), ServerError (500), ServiceUnavailableError (503,
with retryAfterSeconds). Transport failures throw NetworkError.
Machine-readable domain error codes are surfaced on ScribeError.errorCode.
Structured-note codes: version_conflict / invalid_session_state (409),
deprecated_field (a body write on the deprecated compatibility field),
unsupported_note_template (unknown/mismatched pinned template),
note_template_required (session create/patch with no resolvable visit_type),
validation_error (structured document failed template validation, per-field
details), and finalize_validation_failed (finalize with a required
AMD/template field missing) — all 422. Async structured generation that fails
validation after a bounded repair leaves the note generation_status === 'failed'
with an error (ErrorDetail) describing the structured_generation_invalid
failure.
Development
npm ci
npm run openapi:sync # refresh openapi/scribe.json from production (network)
npm run generate:schema # regenerate src/generated/openapi.ts from openapi/scribe.json
npm run build # esbuild (ESM) + tsc (.d.ts) + NodeNext .js-extension fixup
npm run lint
npm test # unit tests (mocked transport)
npm run test:e2e # gated E2E — see tests/e2e/README.md (skips without creds)
npm run validate # publint + attw + npm pack --dry-run (pre-publish checks)Packaging. The SDK is ESM-only ("type": "module"): a single ESM entry
point (dist/index.mjs) built with esbuild plus tsc for the .d.ts types,
exported through the exports map (types/import). There is no CommonJS build.
tsc emits declarations with extensionless relative imports, which Node's
NodeNext ESM resolver rejects; scripts/fix-dts-extensions.mjs rewrites those to
explicit .js specifiers post-build so @arethetypeswrong/cli (attw) resolves
the types cleanly. Validate with npm run validate (publint + attw + npm pack
--dry-run).
Wire types. The REST wire types in src/types.ts are derived from the Scribe
service's production OpenAPI document
(https://scribe.platform.amigo.ai/v1/openapi.json, vendored at
openapi/scribe.json) via openapi-typescript.
To refresh types after the API schema changes:
npm run openapi:sync # re-fetch the spec into openapi/scribe.json
npm run generate:schema # regenerate src/generated/openapi.ts from that snapshotBoth openapi/scribe.json and the generated src/generated/openapi.ts are
committed so the build needs no network. CI runs generate:schema and fails the
PR if src/generated/openapi.ts is out of sync with the vendored spec (the
"Verify generated API types are committed" step). openapi:sync accepts
--url <https url> or --spec <local file> overrides; the default source is the
production URL. The generated module is types-only — nothing is added to the
runtime bundle.
Versioning & releases
The SDK follows semver. It is published to npm as
@amigo-ai/scribe-typescript-sdk with
npm provenance.
Releases are automated directly by .github/workflows/release.yml, without a
third-party release action:
- Merge source-code changes under
src/tomainusing Conventional Commit titles.fix:produces a patch release,feat:a minor release, and a breaking-change marker (feat!:or aBREAKING CHANGE:footer) a major release. Do not change thepackage.jsonversion in a source PR; CI enforces that the release workflow remains the sole owner of version bumps. - The workflow examines commit subjects and bodies since the latest
vX.Y.Ztag, calculates the next version, and creates a tag-only commit containing the correspondingpackage.json/package-lock.jsonversion bump. Changes outsidesrc/do not trigger the automated release flow. - The workflow pushes the annotated tag and creates a GitHub Release whose
generated notes cover the commits since the previous release tag. It then
re-validates the package
(
build → typecheck → test → publint + attw + npm pack) and runsnpm publish --provenance --access publicautomatically.
The workflow can still be dispatched manually from a vX.Y.Z tag as a recovery
path. The tag must match the version in package.json.
Publishing uses npm trusted publishing
(OIDC) — there is no npm token. The workflow authenticates with its
id-token: write OIDC token, which also produces the provenance attestation.
Before the first publish can succeed, two one-time setup steps are required (no repo secrets):
- Configure a trusted publisher for
@amigo-ai/scribe-typescript-sdkon npmjs.com (Package → Settings → Trusted Publisher) pointing at this repository (amigo-ai/scribe-typescript-sdk) and workflow (.github/workflows/release.yml). This is the account/ops action that replaces provisioning a token. - The repository must be public for OIDC trusted publishing + provenance.
Pre-publish validation can be run locally at any time:
npm run build
npm run validate # publint + attw (are-the-types-wrong) + npm pack --dry-runLicense
MIT
