From ae7af290c8a81ddca5576aa045c7ce2b562890c0 Mon Sep 17 00:00:00 2001 From: Dagger Date: Fri, 28 Aug 2026 13:23:12 +0000 Subject: [PATCH 1/2] docs(ehr): add EHR developer API guide and SDK examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ehr-api.md: comprehensive developer guide (auth, errors, webhooks, rate limits, pagination, FHIR CapabilityStatement) - sdk-examples/typescript/: 4 files (auth, patients, observations, webhooks) - sdk-examples/python/: 4 files (auth, patients, observations, webhooks) Refs: PIX-4408 (F2.6 — Open API + Developer Documentation) --- developers/ehr-api.md | 634 ++++++++++++++++++ developers/sdk-examples/python/auth.py | 57 ++ .../sdk-examples/python/observations.py | 165 +++++ developers/sdk-examples/python/patients.py | 137 ++++ developers/sdk-examples/python/webhooks.py | 200 ++++++ developers/sdk-examples/typescript/auth.ts | 66 ++ .../sdk-examples/typescript/observations.ts | 162 +++++ .../sdk-examples/typescript/patients.ts | 144 ++++ .../sdk-examples/typescript/webhooks.ts | 210 ++++++ 9 files changed, 1775 insertions(+) create mode 100644 developers/ehr-api.md create mode 100644 developers/sdk-examples/python/auth.py create mode 100644 developers/sdk-examples/python/observations.py create mode 100644 developers/sdk-examples/python/patients.py create mode 100644 developers/sdk-examples/python/webhooks.py create mode 100644 developers/sdk-examples/typescript/auth.ts create mode 100644 developers/sdk-examples/typescript/observations.ts create mode 100644 developers/sdk-examples/typescript/patients.ts create mode 100644 developers/sdk-examples/typescript/webhooks.ts diff --git a/developers/ehr-api.md b/developers/ehr-api.md new file mode 100644 index 0000000..926ccc9 --- /dev/null +++ b/developers/ehr-api.md @@ -0,0 +1,634 @@ +# Pixelated EHR API Developer Guide + +The Pixelated EHR API provides programmatic access to electronic health +records, clinical encounters, observations, notes, analytics, and outcome +measures. The API follows FHIR R4 resource conventions and uses a consistent +JSON response envelope across all endpoints. + +## Base URL + +``` +https://api.pixelated.com/api/ehr/v1 +``` + +All paths in this guide are relative to this base URL unless noted otherwise. + +## Authentication + +Every request requires an API key sent in the `X-API-Key` header. + +``` +X-API-Key: pk_live_abc123... +``` + +API keys are SHA-256 hashed at rest. The plaintext key is shown only once when +created. Store it securely. If a key is compromised, revoke it immediately +through the key management endpoint. + +### Scopes + +| Scope | Description | +|-------|-------------| +| `read` | Read access to EHR resources (patients, encounters, observations, notes, analytics, outcomes) | +| `write` | Create and update EHR resources | +| `admin` | Full access including key management, configuration, and administrative endpoints | + +A key's scope determines which endpoints it can call. A `read`-scoped key +cannot create or modify resources. Endpoints that require `write` or `admin` +scope return `403 forbidden` when called with a `read`-only key. + +### Key Management + +Manage API keys at: + +``` +https://api.pixelated.com/api/developer/api-keys/ +``` + +Supported operations: + +- `GET /api/developer/api-keys/` - List your API keys +- `POST /api/developer/api-keys/` - Create a new API key +- `DELETE /api/developer/api-keys/{id}` - Revoke a key + +When you create a key, the response includes the plaintext key value. This is +the only time the full key is visible. Subsequent list responses show only the +key prefix and metadata. + +## Response Envelope + +### Success + +Single-resource responses return a `data` field: + +```json +{ + "data": { + "id": "patient-001", + "resourceType": "Patient", + "name": [{ "family": "Smith", "given": ["Jane"] }], + "active": true + } +} +``` + +List responses include a `pagination` object: + +```json +{ + "data": [ + { "id": "patient-001", "resourceType": "Patient" }, + { "id": "patient-002", "resourceType": "Patient" } + ], + "pagination": { + "limit": 20, + "offset": 0, + "total": 142 + } +} +``` + +### Error + +All errors use a consistent envelope with an `error` object containing a +machine-readable `code` and a human-readable `message`: + +```json +{ + "error": { + "code": "validation_failed", + "message": "patientId is required." + } +} +``` + +### HTTP Status Codes + +| Status | Meaning | +|--------|---------| +| 200 | Success (GET, PUT) | +| 201 | Created (POST) | +| 400 | Validation failed or malformed request | +| 401 | Missing or invalid API key | +| 403 | API key lacks required scope or permission | +| 404 | Resource not found | +| 429 | Rate limit exceeded | +| 500 | Internal server error | + +## Error Codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `validation_failed` | 400 | Request body or query parameters failed validation | +| `unauthorized` | 401 | API key missing, expired, or invalid | +| `forbidden` | 403 | API key scope or clinical role lacks permission for this operation | +| `not_found` | 404 | The requested resource does not exist | +| `rate_limited` | 429 | Too many requests in the current window | +| `internal_error` | 500 | Unexpected server error | + +Handle errors by checking the `code` field, not by parsing the `message`. The +message text may change between API versions, but the code is stable. + +## Pagination + +List endpoints accept `limit` and `offset` query parameters. + +| Parameter | Default | Maximum | +|-----------|---------|---------| +| `limit` | 20 | 100 | +| `offset` | 0 | None | + +Example request: + +``` +GET /patients?limit=50&offset=100 +``` + +Example response: + +```json +{ + "data": [/* up to 50 patient objects */], + "pagination": { + "limit": 50, + "offset": 100, + "total": 142 + } +} +``` + +The `pagination.total` field is included when the server can compute the total +count. Some endpoints may omit `total` for performance reasons. When `total` +is absent, continue paginating until `data` is empty. + +## Rate Limits + +API keys have configurable rate limits measured in requests per minute. The +default limit varies by scope: `read` keys get a higher default, `write` keys +a moderate default, and `admin` keys a lower default (since they access +sensitive endpoints). + +Rate limit information is returned in response headers: + +| Header | Description | +|--------|-------------| +| `X-RateLimit-Limit` | Maximum requests per minute for this key | +| `X-RateLimit-Remaining` | Requests remaining in the current window | +| `X-RateLimit-Reset` | Unix timestamp when the window resets | + +When you exceed the rate limit, the API returns `429` with an error code of +`rate_limited`. The response also includes a `Retry-After` header (seconds) +indicating when to retry. + +``` +HTTP/1.1 429 Too Many Requests +X-RateLimit-Limit: 60 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 1696234680 +Retry-After: 30 +Content-Type: application/json + +{ + "error": { + "code": "rate_limited", + "message": "Rate limit exceeded. Retry after 30 seconds." + } +} +``` + +Implement exponential backoff with jitter when retrying rate-limited requests. + +## Webhooks + +The EHR API sends webhook events when clinical resources change. Configure a +webhook endpoint through the developer dashboard to receive these events. + +### Event Types + +| Event | Description | +|-------|-------------| +| `patient.created` | A new patient was created | +| `patient.updated` | A patient record was updated | +| `encounter.created` | A new encounter was created | +| `encounter.updated` | An encounter was updated | +| `observation.created` | A new observation was recorded | +| `observation.updated` | An observation was updated | +| `note.created` | A clinical note was created | +| `note.signed` | A clinical note was signed by a clinician | + +### Webhook Payload + +```json +{ + "eventType": "patient.created", + "timestamp": "2024-01-15T10:30:00Z", + "resourceType": "Patient", + "resourceId": "patient-001", + "tenantId": "tenant-abc", + "data": { + "id": "patient-001", + "resourceType": "Patient", + "name": [{ "family": "Smith", "given": ["Jane"] }], + "active": true + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `eventType` | string | The event type (see table above) | +| `timestamp` | string | ISO 8601 timestamp of the event | +| `resourceType` | string | FHIR resource type (Patient, Encounter, Observation, etc.) | +| `resourceId` | string | ID of the affected resource | +| `tenantId` | string | Tenant ID for multi-tenant isolation | +| `data` | object | The full resource payload at the time of the event | + +### Signature Verification + +Every webhook delivery includes an HMAC-SHA256 signature in the +`X-Webhook-Signature` header. The signature is computed over the raw request +body using your webhook signing secret. + +To verify: + +1. Read the raw request body (before any JSON parsing). +2. Compute `HMAC-SHA256(raw_body, webhook_secret)` and encode as hex. +3. Compare the result to the `X-Webhook-Signature` header value using a + constant-time comparison. + +If the signature does not match, reject the delivery. Do not process +unsigned or mismatched webhooks. + +### Delivery and Retries + +The API expects your endpoint to respond with `200` within 10 seconds. If your +endpoint returns a non-2xx status or times out, the API retries delivery with +exponential backoff: 1 minute, 5 minutes, 15 minutes, 1 hour, 6 hours. After +the final retry, the delivery is marked as failed and logged. + +## FHIR Compatibility + +EHR API endpoints mirror FHIR R4 resource structures. Patient, Encounter, and +Observation resources follow FHIR R4 conventions for field names, data types, +and search parameters. + +A FHIR R4 CapabilityStatement is available at: + +``` +GET /api/fhir/r4/metadata +``` + +This endpoint returns the full FHIR CapabilityStatement describing supported +resources, interactions, search parameters, and operations. Use it to +validate FHIR compatibility before integrating. + +Not all FHIR search parameters are supported on every endpoint. The query +parameters documented in the Endpoints section below are the authoritative +list for each route. + +## Endpoints + +### Patients + +#### GET /patients + +Search patients by name query or active status. + +**Query Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `q` | string | Search query (matches patient name) | +| `active` | string | Filter by active status. Pass `false` to include inactive patients. Defaults to active-only. | +| `limit` | integer | Page size (default 20, max 100) | +| `offset` | integer | Page offset (default 0) | + +**Response:** `200` with paginated patient list. + +``` +GET /patients?q=Smith&limit=20&offset=0 +``` + +#### POST /patients + +Create a new patient. + +**Request Body:** + +```json +{ + "name": [ + { + "family": "Smith", + "given": ["Jane", "Marie"] + } + ], + "birthDate": "1985-03-15", + "gender": "female", + "telecom": [ + { "system": "phone", "value": "555-0100" } + ], + "address": [ + { "line": ["123 Main St"], "city": "Springfield", "state": "IL", "postalCode": "62701" } + ], + "active": true +} +``` + +**Response:** `201` with created patient resource. + +#### GET /patients/{id} + +Read a single patient by ID. + +**Response:** `200` with patient resource, or `404` if not found. + +#### PUT /patients/{id} + +Update an existing patient. Send the full patient resource in the body. + +**Response:** `200` with updated patient resource, or `404` if not found. + +#### DELETE /patients/{id} + +Delete a patient. This is a soft delete. The patient record is marked inactive +and excluded from default search results. + +**Response:** `200` with confirmation, or `404` if not found. + +### Encounters + +#### GET /encounters + +Search encounters. At least one filter is required. + +**Query Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `patient` | string | Patient ID | +| `status` | string | Encounter status (e.g., `planned`, `arrived`, `in-progress`, `finished`) | +| `practitioner` | string | Practitioner ID | +| `start` | string | ISO timestamp for date range start | +| `end` | string | ISO timestamp for date range end | +| `limit` | integer | Page size (default 20, max 100) | +| `offset` | integer | Page offset (default 0) | + +**Response:** `200` with paginated encounter list. + +``` +GET /encounters?patient=patient-001&status=finished&limit=20 +``` + +#### POST /encounters + +Create a new encounter from a FHIR Encounter resource. + +**Request Body:** FHIR Encounter resource. + +**Response:** `201` with created encounter. + +#### GET /encounters/{id} + +Read a single encounter by ID. + +**Response:** `200` with encounter resource, or `404`. + +#### PUT /encounters/{id} + +Update an existing encounter. + +**Response:** `200` with updated encounter, or `404`. + +#### DELETE /encounters/{id} + +Delete an encounter. + +**Response:** `200` with confirmation, or `404`. + +### Observations + +#### GET /observations + +Search observations. At least one filter is required. + +**Query Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `patient` | string | Patient ID | +| `encounter` | string | Encounter ID | +| `code` | string | LOINC code (e.g., `44261-6` for PHQ-9) | +| `status` | string | Observation status (e.g., `final`, `preliminary`) | +| `start` | string | ISO timestamp for date range start (requires `patient`) | +| `end` | string | ISO timestamp for date range end (requires `patient`) | +| `limit` | integer | Page size (default 20, max 100) | +| `offset` | integer | Page offset (default 0) | + +**Response:** `200` with paginated observation list. + +``` +GET /observations?patient=patient-001&code=44261-6&limit=50 +``` + +#### POST /observations + +Create a new observation (e.g., PHQ-9 or GAD-7 scores). + +**Request Body:** FHIR Observation resource. + +**Response:** `201` with created observation. + +#### GET /observations/{id} + +Read a single observation by ID. + +**Response:** `200` with observation resource, or `404`. + +#### PUT /observations/{id} + +Update an existing observation. + +**Response:** `200` with updated observation, or `404`. + +#### DELETE /observations/{id} + +Delete an observation. + +**Response:** `200` with confirmation, or `404`. + +### Clinical Notes + +#### GET /notes + +List clinical note templates, optionally filtered by modality. + +**Query Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `modality` | string | Filter templates by modality (e.g., `individual`, `group`, `family`) | + +**Response:** `200` with note template list. + +#### POST /notes + +Create a clinical note (DocumentReference) from a template. + +**Request Body:** + +```json +{ + "templateId": "initial-intake", + "patientId": "patient-001", + "encounterId": "encounter-001", + "values": { + "chief_complaint": "Patient reports persistent anxiety", + "mental_status": "Alert and oriented" + } +} +``` + +**Response:** `201` with created DocumentReference. + +#### POST /notes/{id}/sign + +Sign a clinical note. This is the compliance gate for AI-drafted notes. Only +manual, individual sign-off is accepted. Batch or automated signing is +rejected. + +**Request Body:** + +```json +{ + "note": { "id": "note-001", "resourceType": "DocumentReference" }, + "patient_id": "patient-001", + "encounter_id": "encounter-001", + "signer_ref": "Practitioner/practitioner-001" +} +``` + +**Response:** `200` with signed note, or `400` if the note is not a registered +AI draft or the sign request is automated. + +### Analytics + +#### GET /analytics + +Retrieve dashboard metrics for a specified dashboard type. + +**Query Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `type` | string | Required. One of: `practice`, `outcomes`, `utilization`, `billing`, `compliance` | +| `startDate` | string | ISO date string for time range start | +| `endDate` | string | ISO date string for time range end | +| `provider` | string | Filter by provider ID | +| `location` | string | Filter by location ID | +| `payer` | string | Filter by payer ID | + +**Response:** `200` with dashboard metrics. + +``` +GET /analytics?type=outcomes&startDate=2024-01-01&endDate=2024-03-31 +``` + +#### GET /analytics/saved-views + +List saved analytics views for the current user. + +**Response:** `200` with saved view list. + +#### POST /analytics/saved-views + +Create a saved analytics view. + +**Request Body:** + +```json +{ + "name": "Q1 Outcomes Dashboard", + "type": "outcomes", + "filters": { + "timeRange": { "start": "2024-01-01", "end": "2024-03-31" } + } +} +``` + +**Response:** `201` with created saved view. + +#### POST /analytics/export/pdf + +Export analytics data as a PDF document. + +**Request Body:** + +```json +{ + "type": "outcomes", + "startDate": "2024-01-01", + "endDate": "2024-03-31", + "format": "pdf" +} +``` + +**Response:** `200` with PDF binary content (`Content-Type: application/pdf`). + +### Outcomes + +#### GET /outcomes + +List available outcome measures (PHQ-9, GAD-7, OQ-45) with metadata. + +**Response:** `200` with measure list. + +#### GET /outcomes/alerts + +List outcome alerts. Alerts are triggered when significant change is detected +in outcome measure scores. + +**Response:** `200` with alert list. + +#### GET /outcomes/config + +Get the outcome configuration for the current tenant. Includes threshold +settings, alert rules, and measure enablement. + +**Response:** `200` with outcome configuration. + +#### PUT /outcomes/config + +Update the outcome configuration. + +**Request Body:** Updated configuration object. + +**Response:** `200` with updated configuration. + +#### GET /outcomes/trending + +Get trending outcome data. Returns time-series data for outcome measures +across the tenant. + +**Query Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `measure` | string | Filter by measure type (e.g., `phq-9`, `gad-7`, `oq-45`) | +| `startDate` | string | ISO date string for range start | +| `endDate` | string | ISO date string for range end | + +**Response:** `200` with trending data. + +## Sandbox + +A sandbox tenant with pre-seeded test data is available for development and +testing. The sandbox includes sample patients, encounters, observations, and +outcome measures. + +Contact support at `support@pixelated.com` to request sandbox access. Once +provisioned, you receive a sandbox API key and can make requests against the +same base URL. Sandbox data is reset periodically. + +Sandbox API keys have `read` and `write` scopes but cannot access `admin` +endpoints. Rate limits in the sandbox are lower than production. diff --git a/developers/sdk-examples/python/auth.py b/developers/sdk-examples/python/auth.py new file mode 100644 index 0000000..a50a2a0 --- /dev/null +++ b/developers/sdk-examples/python/auth.py @@ -0,0 +1,57 @@ +"""Python SDK Example: Authentication + +Shows how to initialize the Pixelated client with API key auth, +configure the base URL, and verify connectivity. + +Install: pip install pixelated_sdk +""" + +import os + +from pixelated_sdk import ApiClient + +# --------------------------------------------------------------------------- +# Basic client initialization +# --------------------------------------------------------------------------- + +# The client sends the API key in the X-API-Key header on every request. +# Set the key via an environment variable. Never hardcode it in source. + +client = ApiClient( + api_key=os.environ["PIXELATED_API_KEY"], + base_url="https://api.pixelated.com", +) + +# --------------------------------------------------------------------------- +# Sandbox client +# --------------------------------------------------------------------------- + +# Use the sandbox for development. The base URL is the same, but the +# API key routes requests to the sandbox tenant. + +sandbox_client = ApiClient( + api_key=os.environ["PIXELATED_SANDBOX_KEY"], + base_url="https://api.pixelated.com", +) + +# --------------------------------------------------------------------------- +# Verify connectivity +# --------------------------------------------------------------------------- + +# If the key is invalid, the SDK raises an ApiException with status 401 +# and code "unauthorized". If the key lacks scope, you get 403 "forbidden". + + +def verify_connection() -> None: + """Call a lightweight endpoint to confirm the key works.""" + try: + patients = client.patients.list(limit=1) + total = patients.pagination.total + print(f"Connection OK. Patient count: {total}") + except Exception as err: + print(f"Connection failed: {err}") + raise + + +if __name__ == "__main__": + verify_connection() diff --git a/developers/sdk-examples/python/observations.py b/developers/sdk-examples/python/observations.py new file mode 100644 index 0000000..6389792 --- /dev/null +++ b/developers/sdk-examples/python/observations.py @@ -0,0 +1,165 @@ +"""Python SDK Example: Observations + +Read, search, and create clinical observations through the EHR API. +Observations include outcome measure scores like PHQ-9 and GAD-7. +""" + +import os +from datetime import datetime, timezone + +from pixelated_sdk import ApiClient + +client = ApiClient( + api_key=os.environ["PIXELATED_API_KEY"], + base_url="https://api.pixelated.com", +) + + +# --------------------------------------------------------------------------- +# Search observations by patient +# --------------------------------------------------------------------------- + +# At least one filter is required: patient, encounter, code, or status. +# Combine patient with start/end to filter by date range. + + +def search_by_patient(patient_id: str) -> None: + result = client.observations.list(patient=patient_id, limit=50, offset=0) + + print(f"Found {result.pagination.total} observations") + for obs in result.data: + display = "Unknown" + if obs.code and obs.code.coding: + display = obs.code.coding[0].display or "Unknown" + value = obs.valueQuantity.value if obs.valueQuantity else "N/A" + print(f" {obs.id}: {display} = {value}") + + +# --------------------------------------------------------------------------- +# Search observations by LOINC code +# --------------------------------------------------------------------------- + +# Use the `code` parameter to filter by LOINC code. For example, +# `44261-6` is the PHQ-9 total score. + + +def search_by_code(patient_id: str, loinc_code: str) -> None: + result = client.observations.list( + patient=patient_id, code=loinc_code, limit=20 + ) + + print(f"Found {len(result.data)} observations for code {loinc_code}") + for obs in result.data: + value = obs.valueQuantity.value if obs.valueQuantity else "N/A" + unit = obs.valueQuantity.unit if obs.valueQuantity else "" + print(f" {obs.id}: value={value} {unit}") + + +# --------------------------------------------------------------------------- +# Search observations by date range +# --------------------------------------------------------------------------- + +# Pass `patient` along with `start` and `end` (ISO timestamps) to +# filter observations within a specific time window. + + +def search_by_date_range( + patient_id: str, start_date: str, end_date: str +) -> None: + result = client.observations.list( + patient=patient_id, start=start_date, end=end_date, limit=100 + ) + + print( + f"Found {len(result.data)} observations " + f"between {start_date} and {end_date}" + ) + + +# --------------------------------------------------------------------------- +# Read a single observation by ID +# --------------------------------------------------------------------------- + + +def read_observation(observation_id: str) -> None: + try: + obs = client.observations.read(observation_id) + print(f"Observation: {obs.data.id}, status={obs.data.status}") + except Exception as err: + if "not_found" in str(err): + print(f"Observation {observation_id} not found") + return + raise + + +# --------------------------------------------------------------------------- +# Create a PHQ-9 observation +# --------------------------------------------------------------------------- + +# Send a FHIR R4 Observation resource. The example below records a +# PHQ-9 total score of 12 (moderate depression). + + +def create_phq9_observation(patient_id: str) -> None: + created = client.observations.create( + resourceType="Observation", + status="final", + subject={"reference": f"Patient/{patient_id}"}, + code={ + "coding": [ + { + "system": "http://loinc.org", + "code": "44261-6", + "display": "PHQ-9 total score", + } + ] + }, + valueQuantity={ + "value": 12, + "unit": "score", + "system": "http://unitsofmeasure.org", + "code": "{score}", + }, + effectiveDateTime=datetime.now(timezone.utc).isoformat(), + ) + + print(f"Created observation with ID: {created.data.id}") + + +# --------------------------------------------------------------------------- +# Create a GAD-7 observation +# --------------------------------------------------------------------------- + +# LOINC code 70274-6 is the GAD-7 total score. + + +def create_gad7_observation(patient_id: str) -> None: + created = client.observations.create( + resourceType="Observation", + status="final", + subject={"reference": f"Patient/{patient_id}"}, + code={ + "coding": [ + { + "system": "http://loinc.org", + "code": "70274-6", + "display": "GAD-7 total score", + } + ] + }, + valueQuantity={ + "value": 8, + "unit": "score", + "system": "http://unitsofmeasure.org", + "code": "{score}", + }, + effectiveDateTime=datetime.now(timezone.utc).isoformat(), + ) + + print(f"Created GAD-7 observation: {created.data.id}") + + +if __name__ == "__main__": + patient_id = "patient-001" + search_by_patient(patient_id) + create_phq9_observation(patient_id) diff --git a/developers/sdk-examples/python/patients.py b/developers/sdk-examples/python/patients.py new file mode 100644 index 0000000..d669dc3 --- /dev/null +++ b/developers/sdk-examples/python/patients.py @@ -0,0 +1,137 @@ +"""Python SDK Example: Patients + +Read, search, create, and update patients through the EHR API. +""" + +import os + +from pixelated_sdk import ApiClient + +client = ApiClient( + api_key=os.environ["PIXELATED_API_KEY"], + base_url="https://api.pixelated.com", +) + + +# --------------------------------------------------------------------------- +# Search patients by name +# --------------------------------------------------------------------------- + +# The `q` parameter matches against patient family and given names. +# Results are paginated. Use `limit` and `offset` to page through. + + +def search_patients() -> None: + result = client.patients.list(q="Smith", active="true", limit=20, offset=0) + + print(f"Found {result.pagination.total} patients") + for patient in result.data: + name = patient.name[0] if patient.name else None + given = " ".join(name.given) if name and name.given else "" + family = name.family if name else "" + print(f" {patient.id}: {given} {family}") + + +# --------------------------------------------------------------------------- +# Read a single patient by ID +# --------------------------------------------------------------------------- + +# Returns the full FHIR Patient resource. Raises a not_found error +# if the patient does not exist. + + +def read_patient(patient_id: str) -> None: + try: + patient = client.patients.read(patient_id) + print(f"Patient: {patient.data.id}, active={patient.data.active}") + except Exception as err: + if "not_found" in str(err): + print(f"Patient {patient_id} not found") + return + raise + + +# --------------------------------------------------------------------------- +# Create a new patient +# --------------------------------------------------------------------------- + +# Send a FHIR R4 Patient resource in the body. The response includes +# the created patient with a server-assigned ID. + + +def create_patient() -> None: + new_patient = client.patients.create( + name=[{"family": "Doe", "given": ["John"]}], + birthDate="1990-06-15", + gender="male", + telecom=[ + {"system": "phone", "value": "555-0199"}, + {"system": "email", "value": "john.doe@example.com"}, + ], + address=[ + { + "line": ["456 Oak Ave"], + "city": "Portland", + "state": "OR", + "postalCode": "97201", + } + ], + active=True, + ) + + print(f"Created patient with ID: {new_patient.data.id}") + + +# --------------------------------------------------------------------------- +# Update an existing patient +# --------------------------------------------------------------------------- + +# Send the full patient resource. Partial updates are not supported. +# The response returns the updated resource. + + +def update_patient(patient_id: str) -> None: + updated = client.patients.update( + patient_id, + name=[{"family": "Doe", "given": ["John", "Michael"]}], + birthDate="1990-06-15", + gender="male", + active=True, + ) + + print(f"Updated patient: {updated.data.id}") + + +# --------------------------------------------------------------------------- +# Paginate through all active patients +# --------------------------------------------------------------------------- + +# Loop until the data array is empty. Stop if `total` is available +# and the offset exceeds it. + + +def list_all_active_patients() -> None: + limit = 100 + offset = 0 + total = float("inf") + all_ids: list[str] = [] + + while offset < total: + page = client.patients.list(active="true", limit=limit, offset=offset) + + for patient in page.data: + all_ids.append(patient.id) + + if page.pagination.total is not None: + total = page.pagination.total + + if len(page.data) < limit: + break + offset += limit + + print(f"Total active patients: {len(all_ids)}") + + +if __name__ == "__main__": + search_patients() + create_patient() diff --git a/developers/sdk-examples/python/webhooks.py b/developers/sdk-examples/python/webhooks.py new file mode 100644 index 0000000..1f6d30f --- /dev/null +++ b/developers/sdk-examples/python/webhooks.py @@ -0,0 +1,200 @@ +"""Python SDK Example: Webhook Handling + +Verify webhook signatures and process EHR events in a Flask, FastAPI, +or similar HTTP server. + +Webhooks are delivered as POST requests with a JSON body. Every +delivery includes an HMAC-SHA256 signature in the X-Webhook-Signature +header. Verify the signature before processing the payload. +""" + +import hashlib +import hmac +import json +import os +from typing import Any + +# --------------------------------------------------------------------------- +# Signature verification +# --------------------------------------------------------------------------- + + +def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool: + """Verify the HMAC-SHA256 signature of a webhook delivery. + + Args: + raw_body: The raw request body bytes (before JSON parsing). + signature: The value of the X-Webhook-Signature header. + secret: Your webhook signing secret. + + Returns: + True if the signature matches, False otherwise. + """ + expected = hmac.new( + secret.encode("utf-8"), raw_body, hashlib.sha256 + ).hexdigest() + + # Use hmac.compare_digest to prevent timing attacks + return hmac.compare_digest(expected, signature) + + +# --------------------------------------------------------------------------- +# Event types +# --------------------------------------------------------------------------- + +WEBHOOK_EVENTS = { + "patient.created", + "patient.updated", + "encounter.created", + "encounter.updated", + "observation.created", + "observation.updated", + "note.created", + "note.signed", +} + + +# --------------------------------------------------------------------------- +# Webhook handler +# --------------------------------------------------------------------------- + + +def handle_webhook( + raw_body: bytes, + headers: dict[str, str], + secret: str, +) -> tuple[int, dict[str, Any]]: + """Process an incoming webhook delivery. + + Args: + raw_body: The raw request body bytes. + headers: Request headers (lowercase keys). + secret: Your webhook signing secret. + + Returns: + A tuple of (status_code, response_body). + """ + # 1. Extract the signature header + signature = headers.get("x-webhook-signature") + if not signature: + return 401, {"error": "Missing X-Webhook-Signature header"} + + # 2. Verify the signature + if not verify_webhook_signature(raw_body, signature, secret): + return 401, {"error": "Invalid webhook signature"} + + # 3. Parse the payload + try: + payload = json.loads(raw_body.decode("utf-8")) + except (json.JSONDecodeError, UnicodeDecodeError): + return 400, {"error": "Invalid JSON payload"} + + event_type = payload.get("eventType", "") + resource_id = payload.get("resourceId", "") + tenant_id = payload.get("tenantId", "") + + # 4. Dispatch to the appropriate handler + try: + if event_type == "patient.created": + on_patient_created(payload) + elif event_type == "patient.updated": + on_patient_updated(payload) + elif event_type == "encounter.created": + on_encounter_created(payload) + elif event_type == "observation.created": + on_observation_created(payload) + elif event_type == "note.signed": + on_note_signed(payload) + else: + print(f"Unhandled event type: {event_type}") + except Exception as err: + # Log the error but still return 500 to trigger a retry. + # The API retries on non-2xx responses with exponential backoff. + print(f"Webhook handler error: {err}") + return 500, {"error": "Handler failed"} + + return 200, {"received": True} + + +# --------------------------------------------------------------------------- +# Event handlers +# --------------------------------------------------------------------------- + + +def on_patient_created(payload: dict[str, Any]) -> None: + resource_id = payload.get("resourceId", "") + tenant_id = payload.get("tenantId", "") + print(f"Patient created: {resource_id} in tenant {tenant_id}") + # Sync the patient to your system, trigger onboarding, etc. + + +def on_patient_updated(payload: dict[str, Any]) -> None: + resource_id = payload.get("resourceId", "") + print(f"Patient updated: {resource_id}") + # Update your local cache of the patient record. + + +def on_encounter_created(payload: dict[str, Any]) -> None: + resource_id = payload.get("resourceId", "") + print(f"Encounter created: {resource_id}") + # Notify the care team, update scheduling, etc. + + +def on_observation_created(payload: dict[str, Any]) -> None: + resource_id = payload.get("resourceId", "") + print(f"Observation created: {resource_id}") + # Check for alert thresholds, update dashboards, etc. + + +def on_note_signed(payload: dict[str, Any]) -> None: + resource_id = payload.get("resourceId", "") + print(f"Note signed: {resource_id}") + # Archive the signed note, update compliance records, etc. + + +# --------------------------------------------------------------------------- +# Flask integration example (commented out, requires flask) +# --------------------------------------------------------------------------- + +# from flask import Flask, request, Response +# +# app = Flask(__name__) +# WEBHOOK_SECRET = os.environ["PIXELATED_WEBHOOK_SECRET"] +# +# +# @app.route("/webhooks/pixelated", methods=["POST"]) +# def webhook() -> Response: +# raw_body = request.get_data() +# headers = {k.lower(): v for k, v in request.headers.items()} +# status, body = handle_webhook(raw_body, headers, WEBHOOK_SECRET) +# return Response( +# json.dumps(body), +# status=status, +# content_type="application/json", +# ) +# +# +# if __name__ == "__main__": +# app.run(port=3000) + + +# --------------------------------------------------------------------------- +# FastAPI integration example (commented out, requires fastapi) +# --------------------------------------------------------------------------- + +# from fastapi import FastAPI, Request, Response +# +# app = FastAPI() +# WEBHOOK_SECRET = os.environ["PIXELATED_WEBHOOK_SECRET"] +# +# +# @app.post("/webhooks/pixelated") +# async def webhook(request: Request) -> Response: +# raw_body = await request.body() +# headers = {k.lower(): v for k, v in request.headers.items()} +# status, body = handle_webhook(raw_body, headers, WEBHOOK_SECRET) +# return Response( +# content=json.dumps(body), +# status_code=status, +# media_type="application/json", +# ) diff --git a/developers/sdk-examples/typescript/auth.ts b/developers/sdk-examples/typescript/auth.ts new file mode 100644 index 0000000..df0314b --- /dev/null +++ b/developers/sdk-examples/typescript/auth.ts @@ -0,0 +1,66 @@ +/** + * TypeScript SDK Example: Authentication + * + * Shows how to initialize the Pixelated client with API key auth, + * configure the base URL, and verify connectivity. + * + * Install: pnpm add @pixelated/sdk-typescript + */ + +import { createPixelatedClient } from '@pixelated/sdk-typescript'; + +/** + * Basic client initialization. + * + * The client sends the API key in the X-API-Key header on every request. + * Set the key via an environment variable. Never hardcode it in source. + */ +const client = createPixelatedClient({ + apiKey: process.env.PIXELATED_API_KEY!, + baseUrl: 'https://api.pixelated.com', +}); + +/** + * Client with custom timeout and retry configuration. + * + * The SDK retries on 429 responses and network errors by default. + * Override the defaults when you need tighter or looser bounds. + */ +const clientWithRetry = createPixelatedClient({ + apiKey: process.env.PIXELATED_API_KEY!, + baseUrl: 'https://api.pixelated.com', + timeout: 10_000, + maxRetries: 5, + retryDelay: 500, +}); + +/** + * Client targeting the sandbox environment. + * + * Use the sandbox for development. The base URL is the same, but the + * API key routes requests to the sandbox tenant. + */ +const sandboxClient = createPixelatedClient({ + apiKey: process.env.PIXELATED_SANDBOX_KEY!, + baseUrl: 'https://api.pixelated.com', +}); + +/** + * Verify connectivity by calling a lightweight endpoint. + * + * If the key is invalid, the SDK throws an error with status 401 and + * code "unauthorized". If the key lacks scope, you get 403 "forbidden". + */ +async function verifyConnection(): Promise { + try { + const patients = await client.patients.list({ limit: 1 }); + console.log('Connection OK. Patient count:', patients.pagination.total); + } catch (err) { + if (err instanceof Error) { + console.error('Connection failed:', err.message); + } + throw err; + } +} + +verifyConnection(); diff --git a/developers/sdk-examples/typescript/observations.ts b/developers/sdk-examples/typescript/observations.ts new file mode 100644 index 0000000..82e8b03 --- /dev/null +++ b/developers/sdk-examples/typescript/observations.ts @@ -0,0 +1,162 @@ +/** + * TypeScript SDK Example: Observations + * + * Read, search, and create clinical observations through the EHR API. + * Observations include outcome measure scores like PHQ-9 and GAD-7. + */ + +import { createPixelatedClient } from '@pixelated/sdk-typescript'; + +const client = createPixelatedClient({ + apiKey: process.env.PIXELATED_API_KEY!, + baseUrl: 'https://api.pixelated.com', +}); + +/** + * Search observations by patient. + * + * At least one filter is required: patient, encounter, code, or status. + * Combine patient with start/end to filter by date range. + */ +async function searchByPatient(patientId: string): Promise { + const result = await client.observations.list({ + patient: patientId, + limit: 50, + offset: 0, + }); + + console.log(`Found ${result.pagination.total} observations`); + for (const obs of result.data) { + console.log( + ` ${obs.id}: ${obs.code?.coding?.[0]?.display ?? 'Unknown'} = ${obs.valueQuantity?.value ?? 'N/A'}`, + ); + } +} + +/** + * Search observations by LOINC code. + * + * Use the `code` parameter to filter by LOINC code. For example, + * `44261-6` is the PHQ-9 total score. + */ +async function searchByCode(patientId: string, loincCode: string): Promise { + const result = await client.observations.list({ + patient: patientId, + code: loincCode, + limit: 20, + }); + + console.log(`Found ${result.data.length} observations for code ${loincCode}`); + for (const obs of result.data) { + console.log( + ` ${obs.id}: value=${obs.valueQuantity?.value} ${obs.valueQuantity?.unit ?? ''}`, + ); + } +} + +/** + * Search observations by date range. + * + * Pass `patient` along with `start` and `end` (ISO timestamps) to + * filter observations within a specific time window. + */ +async function searchByDateRange( + patientId: string, + startDate: string, + endDate: string, +): Promise { + const result = await client.observations.list({ + patient: patientId, + start: startDate, + end: endDate, + limit: 100, + }); + + console.log( + `Found ${result.data.length} observations between ${startDate} and ${endDate}`, + ); +} + +/** + * Read a single observation by ID. + */ +async function readObservation(observationId: string): Promise { + try { + const obs = await client.observations.read(observationId); + console.log('Observation:', obs.data.id, obs.data.status); + } catch (err) { + if (err instanceof Error && err.message.includes('not_found')) { + console.log(`Observation ${observationId} not found`); + return; + } + throw err; + } +} + +/** + * Create a new observation. + * + * Send a FHIR R4 Observation resource. The example below records a + * PHQ-9 total score of 12 (moderate depression). + */ +async function createPhq9Observation(patientId: string): Promise { + const created = await client.observations.create({ + resourceType: 'Observation', + status: 'final', + subject: { reference: `Patient/${patientId}` }, + code: { + coding: [ + { + system: 'http://loinc.org', + code: '44261-6', + display: 'PHQ-9 total score', + }, + ], + }, + valueQuantity: { + value: 12, + unit: 'score', + system: 'http://unitsofmeasure.org', + code: '{score}', + }, + effectiveDateTime: new Date().toISOString(), + }); + + console.log('Created observation with ID:', created.data.id); +} + +/** + * Create a GAD-7 observation. + * + * LOINC code 70274-6 is the GAD-7 total score. + */ +async function createGad7Observation(patientId: string): Promise { + const created = await client.observations.create({ + resourceType: 'Observation', + status: 'final', + subject: { reference: `Patient/${patientId}` }, + code: { + coding: [ + { + system: 'http://loinc.org', + code: '70274-6', + display: 'GAD-7 total score', + }, + ], + }, + valueQuantity: { + value: 8, + unit: 'score', + system: 'http://unitsofmeasure.org', + code: '{score}', + }, + effectiveDateTime: new Date().toISOString(), + }); + + console.log('Created GAD-7 observation:', created.data.id); +} + +// Run examples +const patientId = 'patient-001'; +searchByPatient(patientId); +createPhq9Observation(patientId); diff --git a/developers/sdk-examples/typescript/patients.ts b/developers/sdk-examples/typescript/patients.ts new file mode 100644 index 0000000..2cd09c3 --- /dev/null +++ b/developers/sdk-examples/typescript/patients.ts @@ -0,0 +1,144 @@ +/** + * TypeScript SDK Example: Patients + * + * Read, search, create, and update patients through the EHR API. + */ + +import { createPixelatedClient } from '@pixelated/sdk-typescript'; + +const client = createPixelatedClient({ + apiKey: process.env.PIXELATED_API_KEY!, + baseUrl: 'https://api.pixelated.com', +}); + +/** + * Search patients by name. + * + * The `q` parameter matches against patient family and given names. + * Results are paginated. Use `limit` and `offset` to page through. + */ +async function searchPatients(): Promise { + const result = await client.patients.list({ + q: 'Smith', + active: 'true', + limit: 20, + offset: 0, + }); + + console.log(`Found ${result.pagination.total} patients`); + for (const patient of result.data) { + const name = patient.name?.[0]; + console.log(` ${patient.id}: ${name?.given?.join(' ')} ${name?.family}`); + } +} + +/** + * Read a single patient by ID. + * + * Returns the full FHIR Patient resource. Throws a not_found error + * if the patient does not exist. + */ +async function readPatient(patientId: string): Promise { + try { + const patient = await client.patients.read(patientId); + console.log('Patient:', patient.data.id, patient.data.active); + } catch (err) { + if (err instanceof Error && err.message.includes('not_found')) { + console.log(`Patient ${patientId} not found`); + return; + } + throw err; + } +} + +/** + * Create a new patient. + * + * Send a FHIR R4 Patient resource in the body. The response includes + * the created patient with a server-assigned ID. + */ +async function createPatient(): Promise { + const newPatient = await client.patients.create({ + name: [ + { + family: 'Doe', + given: ['John'], + }, + ], + birthDate: '1990-06-15', + gender: 'male', + telecom: [ + { system: 'phone', value: '555-0199' }, + { system: 'email', value: 'john.doe@example.com' }, + ], + address: [ + { + line: ['456 Oak Ave'], + city: 'Portland', + state: 'OR', + postalCode: '97201', + }, + ], + active: true, + }); + + console.log('Created patient with ID:', newPatient.data.id); +} + +/** + * Update an existing patient. + * + * Send the full patient resource. Partial updates are not supported. + * The response returns the updated resource. + */ +async function updatePatient(patientId: string): Promise { + const updated = await client.patients.update(patientId, { + name: [ + { + family: 'Doe', + given: ['John', 'Michael'], + }, + ], + birthDate: '1990-06-15', + gender: 'male', + active: true, + }); + + console.log('Updated patient:', updated.data.id); +} + +/** + * Paginate through all active patients. + * + * Loop until the data array is empty. Stop if `total` is available + * and the offset exceeds it. + */ +async function listAllActivePatients(): Promise { + const limit = 100; + let offset = 0; + let total = Infinity; + const allIds: string[] = []; + + while (offset < total) { + const page = await client.patients.list({ + active: 'true', + limit, + offset, + }); + + for (const patient of page.data) { + allIds.push(patient.id); + } + + if (page.pagination.total !== undefined) { + total = page.pagination.total; + } + + if (page.data.length < limit) break; + offset += limit; + } + + console.log(`Total active patients: ${allIds.length}`); +} + +searchPatients().then(() => createPatient()); diff --git a/developers/sdk-examples/typescript/webhooks.ts b/developers/sdk-examples/typescript/webhooks.ts new file mode 100644 index 0000000..251b2d4 --- /dev/null +++ b/developers/sdk-examples/typescript/webhooks.ts @@ -0,0 +1,210 @@ +/** + * TypeScript SDK Example: Webhook Handling + * + * Verify webhook signatures and process EHR events in an Express + * or similar HTTP server. + * + * Webhooks are delivered as POST requests with a JSON body. Every + * delivery includes an HMAC-SHA256 signature in the X-Webhook-Signature + * header. Verify the signature before processing the payload. + */ + +import crypto from 'node:crypto'; + +/** + * Verify the HMAC-SHA256 signature of a webhook delivery. + * + * @param rawBody - The raw request body as a Buffer (before JSON parsing) + * @param signature - The value of the X-Webhook-Signature header + * @param secret - Your webhook signing secret + * @returns true if the signature matches, false otherwise + */ +function verifyWebhookSignature( + rawBody: Buffer, + signature: string, + secret: string, +): boolean { + const expected = crypto + .createHmac('sha256', secret) + .update(rawBody) + .digest('hex'); + + // Use timingSafeEqual to prevent timing attacks + if (expected.length !== signature.length) { + return false; + } + + return crypto.timingSafeEqual( + Buffer.from(expected), + Buffer.from(signature), + ); +} + +/** + * Webhook event types and their descriptions. + */ +const WEBHOOK_EVENTS = { + patient_created: 'patient.created', + patient_updated: 'patient.updated', + encounter_created: 'encounter.created', + encounter_updated: 'encounter.updated', + observation_created: 'observation.created', + observation_updated: 'observation.updated', + note_created: 'note.created', + note_signed: 'note.signed', +} as const; + +type WebhookEvent = (typeof WEBHOOK_EVENTS)[keyof typeof WEBHOOK_EVENTS]; + +/** + * Parsed webhook payload. + */ +interface WebhookPayload { + eventType: WebhookEvent; + timestamp: string; + resourceType: string; + resourceId: string; + tenantId: string; + data: Record; +} + +/** + * Express-style webhook handler. + * + * Mount this on your server at the URL configured in the developer + * dashboard. The handler verifies the signature, parses the payload, + * dispatches to the appropriate handler, and returns 200. + * + * Important: use `express.raw()` to capture the raw body before + * JSON parsing. If you use `express.json()` first, the signature + * verification will fail because the body has been re-serialized. + * + * @example + * import express from 'express'; + * const app = express(); + * app.post('/webhooks/pixelated', express.raw({ type: '*/*' }), handleWebhook); + */ +async function handleWebhook( + rawBody: Buffer, + headers: Record, + secret: string, +): Promise<{ status: number; body: Record }> { + // 1. Extract the signature header + const signature = headers['x-webhook-signature']; + if (typeof signature !== 'string') { + return { + status: 401, + body: { error: 'Missing X-Webhook-Signature header' }, + }; + } + + // 2. Verify the signature + if (!verifyWebhookSignature(rawBody, signature, secret)) { + return { + status: 401, + body: { error: 'Invalid webhook signature' }, + }; + } + + // 3. Parse the payload + let payload: WebhookPayload; + try { + payload = JSON.parse(rawBody.toString('utf-8')) as WebhookPayload; + } catch { + return { + status: 400, + body: { error: 'Invalid JSON payload' }, + }; + } + + // 4. Dispatch to the appropriate handler + try { + switch (payload.eventType) { + case WEBHOOK_EVENTS.patient_created: + await onPatientCreated(payload); + break; + case WEBHOOK_EVENTS.patient_updated: + await onPatientUpdated(payload); + break; + case WEBHOOK_EVENTS.encounter_created: + await onEncounterCreated(payload); + break; + case WEBHOOK_EVENTS.observation_created: + await onObservationCreated(payload); + break; + case WEBHOOK_EVENTS.note_signed: + await onNoteSigned(payload); + break; + default: + console.log(`Unhandled event type: ${payload.eventType}`); + } + } catch (err) { + // Log the error but still return 200 to prevent retries. + // The API retries on non-2xx responses. If your handler fails + // transiently, return 500 to trigger a retry. + console.error('Webhook handler error:', err); + return { + status: 500, + body: { error: 'Handler failed' }, + }; + } + + return { status: 200, body: { received: true } }; +} + +// --------------------------------------------------------------------------- +// Event handlers +// --------------------------------------------------------------------------- + +async function onPatientCreated(payload: WebhookPayload): Promise { + console.log(`Patient created: ${payload.resourceId} in tenant ${payload.tenantId}`); + // Sync the patient to your system, trigger onboarding, etc. +} + +async function onPatientUpdated(payload: WebhookPayload): Promise { + console.log(`Patient updated: ${payload.resourceId}`); + // Update your local cache of the patient record. +} + +async function onEncounterCreated(payload: WebhookPayload): Promise { + console.log(`Encounter created: ${payload.resourceId}`); + // Notify the care team, update scheduling, etc. +} + +async function onObservationCreated(payload: WebhookPayload): Promise { + console.log(`Observation created: ${payload.resourceId}`); + // Check for alert thresholds, update dashboards, etc. +} + +async function onNoteSigned(payload: WebhookPayload): Promise { + console.log(`Note signed: ${payload.resourceId}`); + // Archive the signed note, update compliance records, etc. +} + +// --------------------------------------------------------------------------- +// Express integration example (commented out, requires express) +// --------------------------------------------------------------------------- + +/* +import express from 'express'; + +const app = express(); +const WEBHOOK_SECRET = process.env.PIXELATED_WEBHOOK_SECRET!; + +app.post( + '/webhooks/pixelated', + express.raw({ type: '*/*' }), + async (req, res) => { + const result = await handleWebhook( + req.body as Buffer, + req.headers, + WEBHOOK_SECRET, + ); + res.status(result.status).json(result.body); + }, +); + +app.listen(3000, () => { + console.log('Webhook server listening on port 3000'); +}); +*/ From 1a9c45c08161c214ddcbfa36ff9d3dc0e40b735b Mon Sep 17 00:00:00 2001 From: Dagger Date: Fri, 28 Aug 2026 21:08:23 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20Update=20developer=20guide=20HTTP=20?= =?UTF-8?q?methods=20to=20match=20actual=20API=20(PUT=E2=86=92PATCH,=20rem?= =?UTF-8?q?ove=20false=20DELETEs,=20PUT=E2=86=92POST=20for=20outcomes/conf?= =?UTF-8?q?ig)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- developers/ehr-api.md | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/developers/ehr-api.md b/developers/ehr-api.md index 926ccc9..802378e 100644 --- a/developers/ehr-api.md +++ b/developers/ehr-api.md @@ -343,9 +343,9 @@ Read a single patient by ID. **Response:** `200` with patient resource, or `404` if not found. -#### PUT /patients/{id} +#### PATCH /patients/{id} -Update an existing patient. Send the full patient resource in the body. +Partially update an existing patient. Send only the fields to update. **Response:** `200` with updated patient resource, or `404` if not found. @@ -394,18 +394,12 @@ Read a single encounter by ID. **Response:** `200` with encounter resource, or `404`. -#### PUT /encounters/{id} +#### PATCH /encounters/{id} -Update an existing encounter. +Partially update an existing encounter. **Response:** `200` with updated encounter, or `404`. -#### DELETE /encounters/{id} - -Delete an encounter. - -**Response:** `200` with confirmation, or `404`. - ### Observations #### GET /observations @@ -445,18 +439,12 @@ Read a single observation by ID. **Response:** `200` with observation resource, or `404`. -#### PUT /observations/{id} +#### PATCH /observations/{id} -Update an existing observation. +Partially update an existing observation. **Response:** `200` with updated observation, or `404`. -#### DELETE /observations/{id} - -Delete an observation. - -**Response:** `200` with confirmation, or `404`. - ### Clinical Notes #### GET /notes @@ -597,7 +585,7 @@ settings, alert rules, and measure enablement. **Response:** `200` with outcome configuration. -#### PUT /outcomes/config +#### POST /outcomes/config Update the outcome configuration.