Skip to main content

API reference

Complete endpoint reference for the Digits Clinical API. All paths are relative to the base URL, which includes the /api prefix (e.g. https://api.digitshealth.com/api).

Auth legend: ๐Ÿ”“ none ยท ๐Ÿ”‘ required ยท ๐Ÿ‘ค optional ยท ๐Ÿ›ก๏ธ role-restricted.


Health

GET /health ๐Ÿ”“

Liveness and version probe.

Response 200

{ "status": "ok", "timestamp": "2026-06-07T12:00:00.000Z", "version": "0.1.0" }

Assessments

Mounted at /api/assessments. Submissions use optional auth: with a token the record links to your patient profile; without one it is anonymous (90-day expiry). Every submission mints a shareable 6-character code.

POST /assessments/rom ๐Ÿ‘ค

Save a Range-of-Motion assessment with optional wrist and per-joint detail.

Body

FieldTypeRequiredNotes
hand"LEFT" | "RIGHT"โœ…
overallScorenumberโœ…0โ€“100.
wristScorenumber | null0โ€“100.
fingerScorenumber | null0โ€“100.
lightingQuality"GOOD" | "FAIR" | "POOR" | null
lightingMetrics{ meanY?, stdY? } | null
wristobject | nullSee below.
jointsarrayPer-joint results.

wrist: unsigned_rom_deg, max_extension_deg, min_extension_deg, avg_extension_deg, max_flexion_deg, min_flexion_deg, avg_flexion_deg, frames_total, frames_valid, percent_valid_frames (all optional numbers; snake_case from the camera pipeline).

joints[]: { id, finger, jointType, minAngle, maxAngle, avgAngle, rom, normalRange: [min, max], flexibilityScore, frameCount }.

Response 201

{ "id": 1, "code": "XHE7JR", "formattedCode": "XHE-7JR", "overallScore": 82, "wristScore": 78, "fingerScore": 86 }

Errors: 400 if hand is missing; 500 on persistence failure.


POST /assessments/dexterity ๐Ÿ‘ค

Save a dexterity (finger-tapping) assessment with per-finger scores.

Body (key fields)

FieldTypeRequiredNotes
hand"LEFT" | "RIGHT"โœ…
durationnumberโœ…Seconds.
overallScore / speedScore / consistencyScorenumberโœ…0โ€“100.
touchPrecisionnumberPreferred name; accuracyScore accepted as legacy.
cycleCount / totalTouchesnumberโœ…
sequencesCompleted, timeTo10Sequences, averageFrequency, sequenceAccuracynumberPaper-validated metrics (Kuchtaruk et al. 2024).
fingerScoresRecord<string, { finger, accuracy, consistency, score, touchCount }>
lightingQuality / lightingMetricssee ROM

Response 201

{ "id": 5, "code": "K9M2PQ", "formattedCode": "K9M-2PQ", "overallScore": 74, "cycleCount": 18, "totalTouches": 142 }

POST /assessments/pain-v2 ๐Ÿ‘ค

Save a pain assessment (hand-diagram entries, medications, strategies).

Body

FieldTypeRequiredNotes
mode"QUICK" | "DETAILED"โœ…
hand"LEFT" | "RIGHT" | "BOTH"โœ…
painTypeenumSee Clinical concepts.
timeOfDayenum
entriesarray{ areaType, hand, digit?, location?, severity }, severity 0โ€“10.
medicationsarray{ medicationName, dose?, effectivenessRating? }.
strategiesstring[]e.g. ELEVATION, REST, ICE.
commentsstring

Response 201

{ "id": 9, "code": "P4T8RX", "formattedCode": "P4T-8RX", "dateCreated": "2026-06-07T...", "isComplete": true, "maxSeverity": 6, "entryCount": 3 }

Errors: 400 if hand or mode is missing.


POST /assessments/pain ๐Ÿ”“ (legacy)

Single-rating pain capture, not persisted to Prisma (returns an in-memory echo). Prefer pain-v2.

Body: { painRating (0โ€“10, required), isTakingMedication, isElevating, isResting, isTakingAcetaminophen?, isTakingIbuprofen?, isTakingNarcotics?, otherMedications?, comments? }.

Response 201: the submitted object plus id (UUID), dateCreated, isComplete. Errors: 400 if painRating is missing or out of 0โ€“10.


POST /assessments/arthritis ๐Ÿ‘ค

Save an arthritis evaluation. Scores are computed client-side and submitted; the API persists the questionnaire, joint symptoms, functional limitations, and the score breakdown.

Body (key fields)

FieldTypeNotes
hand"LEFT" | "RIGHT" | "BOTH"Required.
morningStiffnessDurationenumNONE โ€ฆ OVER_60_MIN.
morningStiffnessSeveritynumber0โ€“10.
functionalLimitations{ task, difficulty }[]
jointSymptoms{ id, digit, location, tenderness, swelling, warmth, crepitus }[]
symptomSymmetry / symptomOnsetenum
familyHistoryArthritisboolean
compositeScore, progressionLevel, patternType, romDeficitScore, painBurdenScore, functionalScore, stiffnessScore, affectedJointCountnumber/enumComputed scores.
jointHealthMap{ digit, location, healthScore, romScore, painScore, hasSymptoms, flags }[]
clinicalFlagsstring[]
recommendationstring
sourceRomAssessmentId, sourceDexterityAssessmentId, sourcePainSessionIdnumberOptional links to source assessments.

Response 201

{ "id": 3, "code": "ARTH42", "formattedCode": "ART-H42", "compositeScore": 71, "progressionLevel": 2, "patternType": "OA_PATTERN" }

See Clinical concepts for enum values and score meanings.


GET /assessments/:id/results ๐Ÿ”“

Fetch a saved assessment with full child detail. :id accepts the numeric record id or the 6-character code (dash optional).

Response 200

{
  "id": 12,
  "code": "XHE7JR",
  "formattedCode": "XHE-7JR",
  "assessmentType": "ROM",
  "hand": "RIGHT",
  "dateCreated": "2026-06-07T...",
  "resultSnapshot": { "overallScore": 82, "wristScore": 78, "...": "..." },
  "results": { "handResults": [ { "jointResults": [ ... ] } ] }
}

results shape depends on assessmentType (ROM โ†’ hand/joint results; FINGER_TAPPING โ†’ finger results; PAIN โ†’ entries/medications/strategies; ARTHRITIS โ†’ questionnaire + joint-health entries). null when no detail row is linked. Errors: 404 if no record matches.


Lookup

Mounted at /api/lookup. The public, code-centric surface for sharing and claiming assessments.

GET /lookup/:code ๐Ÿ”“

Public retrieval by code. Increments the record's view count.

Response 200

{
  "code": "XHE7JR", "formattedCode": "XHE-7JR",
  "assessmentType": "ROM", "hand": "RIGHT",
  "dateCreated": "2026-06-07T...", "resultSnapshot": { "...": "..." },
  "isAnonymous": true, "viewCount": 4
}

Errors: 400 invalid code format; 404 not found or expired.

POST /lookup/:code/claim ๐Ÿ”‘

Attach an anonymous record to the authenticated account (removes its expiry).

Response 200: { "message": "Assessment claimed successfully.", "code": "XHE-7JR" }. Errors: 400 invalid code; 404 not found (or already claimed).

POST /lookup/:code/email ๐Ÿ”“

Email an assessment's results to an address. Persists the email on the record so it can be claimed later.

Body: { "email": "person@example.com" }. Response 200: { success, sent, message, code }. Errors: 400 invalid code/email; 404 not found; 500 send failure.

POST /lookup/anonymous ๐Ÿ‘ค

Create a bare assessment record and mint a code. Links to the caller if a token is supplied.

Body: { assessmentType (required), hand?, resultSnapshot?, email? }. Response 200: { code, formattedCode, message }.


Patients

Mounted at /api/patients. All routes require authentication. List and profile routes additionally require the care_provider or admin role.

GET /patients/me/scores ๐Ÿ”‘

Score history (chart data points) for the authenticated user.

Query: hand (LEFT/RIGHT/BOTH), limit (default 90, max 365), offset. Response 200:

{ "scores": [ { "date": "...", "overallScore": 82, "romScore": 82, "dexterityScore": null, "painScore": null } ] }

GET /patients/me/assessments ๐Ÿ”‘

Assessment history for the authenticated user, with normalized type (FINGER_TAPPINGโ†’DEXTERITY), a detail summary string, and a mapped results object.

Query: type, limit (default 50, max 200), offset. Response 200: { assessments: [...], total }.

GET /patients ๐Ÿ›ก๏ธ care_provider/admin

List patients with latest score, trend, and attention flag.

Query: search, trend (improving/declining/stable), attention (true/false), sort (name/score/recent), limit (default 50, max 200), offset. Response 200:

{ "patients": [ { "id": 7, "name": "Jane Doe", "hand": "RIGHT", "injury": "...", "lastAssessment": "...", "score": 78, "trend": "improving", "needsAttention": false, "assessmentCount": 12 } ], "total": 1 }

GET /patients/:id ๐Ÿ›ก๏ธ care_provider/admin

Full patient profile (demographics, injury, surgery date, latest scores, active treatments). Errors: 404 if not a patient contact.

GET /patients/:id/assessments ๐Ÿ”‘

Assessment history for a specific patient (includes resultSnapshot). Query: type, limit (max 200), offset.

GET /patients/:id/journal ๐Ÿ”‘

Journal entries for a patient. Query: limit (max 200), offset. Response 200: { journal: [{ date, type, hand, pain, function, summary }], total }.

GET /patients/:id/scores ๐Ÿ”‘

Same shape as /me/scores for a specific patient. Query: hand, limit (max 365), offset.

GET /patients/:id/narrative-summary ๐Ÿ”‘

AI-generated clinical progression summary over recent journals + assessments. Falls back to a deterministic template when the LLM is unavailable. Response 200: { "summary": "...", "source": "ai" | "template" }.


Journal

Mounted at /api/journal. (Currently no auth guard; expects a patientId.)

POST /journal

Create a journal entry. Body: { patientId, entryType, transcript } required, plus optional affectedHand, painRating, functionRating, aiSummary, romAssessmentId, dexterityAssessmentId, painScaleAssessmentId, orchestratorSessionId. entryType โˆˆ PRE_ASSESSMENT, POST_ASSESSMENT, AD_HOC_CHECK_IN, INITIAL_INTAKE. Response 201: the created entry. Errors: 400 if required fields are missing.

GET /journal

List entries. Query: patientId (required; returns empty set otherwise), entryType, limit (default 20), offset. Response: { entries, total }.

GET /journal/:id

Single entry, or 404.

GET /journal/narrative-context/:patientId

Returns { "context": "..." } โ€” a prompt-ready context string built from recent entries.


Reports

Mounted at /api/reports. Bundles multiple assessments (by code) into a shareable report.

POST /reports ๐Ÿ‘ค

Create a report. Body: { title, assessmentCodes: string[] } (1โ€“20 codes). Response 200: { code, title, createdAt, assessmentCount }. Errors: 400 if title/codes missing or > 20 codes.

GET /reports/:code ๐Ÿ”“

Fetch a report (increments view count). Response 200: { code, title, createdAt, patientName, assessments[], aiExplanation }. Errors: 404.

GET /reports/:code/pdf ๐Ÿ”“

Download the report as a PDF. Response 200: application/pdf bytes, Content-Disposition: attachment. With the SDK: reports.downloadPdf(code) โ†’ ArrayBuffer.

POST /reports/:code/email ๐Ÿ”“

Email the report (PDF attached). Body: { to }. Response 200: { success, sent, message }. Errors: 400 invalid email; 404.

POST /reports/:code/explain ๐Ÿ”“

Generate (and persist) an AI explanation of the report. Falls back to a template if the LLM is unavailable. Response 200: { explanation, source: "ai" | "fallback" }.


Biomechanics

Mounted at /api/biomechanics. These endpoints proxy landmark frame payloads to the Python analysis service (PYTHON_API_URL) and return its JSON verbatim. Upstream failures surface as 502.

EndpointUpstream
POST /biomechanics/wrist/deviation/v1/wrist/deviation
POST /biomechanics/wrist/deviation/neutral-pose/v1/wrist/deviation/neutral-pose
POST /biomechanics/wrist/extension-flexion/v1/wrist/extensionflexion
POST /biomechanics/wrist/extension-flexion/neutral-pose/v1/wrist/extensionflexion/neutral-pose
POST /biomechanics/wrist/rotation/v1/wrist/rotation
POST /biomechanics/wrist/rotation/neutral-pose/v1/wrist/rotation/neutral-pose
POST /biomechanics/thumb/abduction/v1/thumb/abduction
POST /biomechanics/thumb/circumduction/v1/thumb/circumduction
POST /biomechanics/thumb/extension-flexion/v1/thumb/extensionflexion

The request and response bodies are owned by the Python service; the API and SDK treat them as opaque JSON.


AI / TTS

Mounted at /api/ai.

POST /ai/tts ๐Ÿ”“

Synthesize speech (Google Cloud Neural2 TTS). Body: { text }. Response 200: { "audio": "<base64>", "mimeType": "audio/L16;rate=24000" }, or 204 if no audio was generated. Errors: 400 empty text; 500 synthesis failure.

GET /ai/ws ๐Ÿ”“ (WebSocket)

The conversational orchestrator. Not a REST endpoint โ€” open a WebSocket and exchange the ClientMessage / ServerMessage JSON frames defined in @digits/shared (types/voice.ts). State machine: IDLE โ†’ GREETING โ†’ NAVIGATION โ†’ CHECK_IN โ†’ ASSESSMENT_SETUP โ†’ ASSESSMENT_ACTIVE โ†’ ASSESSMENT_REVIEW โ†’ REFLECTION. Client STT transcripts arrive as ui_event messages; the server emits transcript, ai_response, audio_chunk, action, coaching, state_change, and error. Not covered by the REST SDK.


Billing

Mounted at /api/billing. Stripe-backed. Tiers: Core ($1.99/mo) and Plus ($6.99/mo). Returns 503 when Stripe is not configured.

POST /billing/checkout ๐Ÿ”‘

Create a Stripe Checkout session. Body: { plan: "core" | "plus" } (default plus). Response 200: { url } (redirect the user there). Errors: 400 invalid plan, or 400 with code ALREADY_SUBSCRIBED / USE_PORTAL_TO_UPGRADE; 502 Stripe error.

POST /billing/checkout/verify ๐Ÿ”‘

Verify a returned Checkout session and sync the subscription. Body: { sessionId }. Response 200: { tier, status, currentPeriodEnd, cancelAtPeriodEnd }. Errors: 400 missing/invalid session; 403 session belongs to another user; 404.

POST /billing/portal ๐Ÿ”‘

Open the Stripe customer portal. Response 200: { url }. Errors: 400 no billing account.

GET /billing/subscription ๐Ÿ”‘

Current subscription. Response 200: { tier, status, currentPeriodEnd, cancelAtPeriodEnd } (tier/status are null when inactive).

POST /billing/webhook ๐Ÿ”“ (Stripe signature)

Server-to-server Stripe webhook. Verified with STRIPE_WEBHOOK_SECRET against the stripe-signature header (raw body). See Webhooks. Not part of the SDK.


Leads

Mounted at /api/leads. Public lead capture for the marketing site.

POST /leads/developer ๐Ÿ”“

Capture a developer/partner sign-up (stay-in-touch or demo request). Emails the team (LEADS_EMAIL, falling back to SMTP_FROM); when SMTP is unconfigured the lead is logged server-side instead of lost. Includes a honeypot (website) that silently absorbs bots.

Body

FieldTypeRequiredNotes
type"STAY_IN_TOUCH" | "DEMO"Defaults to STAY_IN_TOUCH.
namestringโœ…
emailstringโœ…Valid email.
companystring
interest"API" | "SDK" | "FHIR" | "RESEARCH" | "OTHER"
messagestringUp to 4000 chars.
websitestringHoneypot โ€” leave empty.

Response 201: { received: true, sent: boolean, message: string }. Errors: 400 validation error (with field); 500 on failure.