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
| Field | Type | Required | Notes |
|---|---|---|---|
hand | "LEFT" | "RIGHT" | โ | |
overallScore | number | โ | 0โ100. |
wristScore | number | null | 0โ100. | |
fingerScore | number | null | 0โ100. | |
lightingQuality | "GOOD" | "FAIR" | "POOR" | null | ||
lightingMetrics | { meanY?, stdY? } | null | ||
wrist | object | null | See below. | |
joints | array | Per-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)
| Field | Type | Required | Notes |
|---|---|---|---|
hand | "LEFT" | "RIGHT" | โ | |
duration | number | โ | Seconds. |
overallScore / speedScore / consistencyScore | number | โ | 0โ100. |
touchPrecision | number | Preferred name; accuracyScore accepted as legacy. | |
cycleCount / totalTouches | number | โ | |
sequencesCompleted, timeTo10Sequences, averageFrequency, sequenceAccuracy | number | Paper-validated metrics (Kuchtaruk et al. 2024). | |
fingerScores | Record<string, { finger, accuracy, consistency, score, touchCount }> | ||
lightingQuality / lightingMetrics | see 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
| Field | Type | Required | Notes |
|---|---|---|---|
mode | "QUICK" | "DETAILED" | โ | |
hand | "LEFT" | "RIGHT" | "BOTH" | โ | |
painType | enum | See Clinical concepts. | |
timeOfDay | enum | ||
entries | array | { areaType, hand, digit?, location?, severity }, severity 0โ10. | |
medications | array | { medicationName, dose?, effectivenessRating? }. | |
strategies | string[] | e.g. ELEVATION, REST, ICE. | |
comments | string |
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)
| Field | Type | Notes |
|---|---|---|
hand | "LEFT" | "RIGHT" | "BOTH" | Required. |
morningStiffnessDuration | enum | NONE โฆ OVER_60_MIN. |
morningStiffnessSeverity | number | 0โ10. |
functionalLimitations | { task, difficulty }[] | |
jointSymptoms | { id, digit, location, tenderness, swelling, warmth, crepitus }[] | |
symptomSymmetry / symptomOnset | enum | |
familyHistoryArthritis | boolean | |
compositeScore, progressionLevel, patternType, romDeficitScore, painBurdenScore, functionalScore, stiffnessScore, affectedJointCount | number/enum | Computed scores. |
jointHealthMap | { digit, location, healthScore, romScore, painScore, hasSymptoms, flags }[] | |
clinicalFlags | string[] | |
recommendation | string | |
sourceRomAssessmentId, sourceDexterityAssessmentId, sourcePainSessionId | number | Optional 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.
| Endpoint | Upstream |
|---|---|
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
| Field | Type | Required | Notes |
|---|---|---|---|
type | "STAY_IN_TOUCH" | "DEMO" | Defaults to STAY_IN_TOUCH. | |
name | string | โ | |
email | string | โ | Valid email. |
company | string | ||
interest | "API" | "SDK" | "FHIR" | "RESEARCH" | "OTHER" | ||
message | string | Up to 4000 chars. | |
website | string | Honeypot โ leave empty. |
Response 201: { received: true, sent: boolean, message: string }.
Errors: 400 validation error (with field); 500 on failure.