Patient Graph API
The Patient Graph API is the clinical data backbone of Loop Health. It provides CRUD operations for patient profiles, lab results, protocols, events, treatments, prescriptions, and conversation history — all protected by RBAC and audit logging.
Base URLs:
- Production:
https://patient-graph.loop.health - Development:
http://localhost:3000
Authentication: Clerk JWT bearer token required on all endpoints (except webhooks).
Rate Limit: 100 requests per 60 seconds per user.
Profiles
List Profiles
curl -X GET "https://patient-graph.loop.health/profiles?limit=20&offset=0" \
-H "Authorization: Bearer $CLERK_JWT"Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
email | string | No | Filter by email address |
subscriptionTier | string | No | Filter by tier |
tag | string | No | Filter by tag |
limit | number | No | Max results (default: 20, max: 100) |
offset | number | No | Offset for pagination (default: 0) |
RBAC: profile:read. Customers see only their own profile.
Response:
{
"success": true,
"data": [
{
"id": "prof_abc123",
"externalId": "user_clerk_123",
"email": "patient@example.com",
"firstName": "Jane",
"lastName": "Doe",
"dateOfBirth": "1990-01-15",
"biologicalSex": "female",
"tags": ["beta-tester"],
"metadata": {},
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-06-15T12:00:00Z"
}
],
"meta": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}Get Profile
curl -X GET "https://patient-graph.loop.health/profiles/:id" \
-H "Authorization: Bearer $CLERK_JWT"RBAC: profile:read. Customers can only access their own profile.
Create Profile
curl -X POST "https://patient-graph.loop.health/profiles" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"externalId": "user_clerk_456",
"email": "newpatient@example.com",
"firstName": "John",
"lastName": "Smith",
"dateOfBirth": "1985-03-20",
"biologicalSex": "male",
"tags": [],
"metadata": {}
}'RBAC: profile:write
Response: 201 Created with Location header.
Update Profile
curl -X PATCH "https://patient-graph.loop.health/profiles/:id" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jonathan",
"tags": ["vip"]
}'RBAC: profile:write. Customers can only update their own profile.
Delete Profile
curl -X DELETE "https://patient-graph.loop.health/profiles/:id" \
-H "Authorization: Bearer $CLERK_JWT"RBAC: profile:delete (admin/staff only).
Lab Results
List Labs
curl -X GET "https://patient-graph.loop.health/labs?customerId=prof_abc123&limit=10" \
-H "Authorization: Bearer $CLERK_JWT"Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID to filter by |
status | string | No | Filter by status (pending, reviewed, archived) |
provider | string | No | Filter by lab provider |
fromDate | string | No | ISO date — labs after this date |
toDate | string | No | ISO date — labs before this date |
limit | number | No | Max results (default: 20, max: 100) |
offset | number | No | Offset for pagination (default: 0) |
RBAC: lab_results:read. Customers see only their own labs.
Response:
{
"success": true,
"data": [
{
"id": "lab_xyz789",
"customerId": "prof_abc123",
"labDate": "2024-06-01",
"provider": "Quest Diagnostics",
"status": "reviewed",
"biomarkers": [
{
"code": "testosterone-total",
"name": "Total Testosterone",
"value": 650,
"unit": "ng/dL",
"referenceRange": { "low": 300, "high": 1000 },
"status": "normal"
}
],
"fileUrl": "https://...",
"notes": "Annual panel",
"createdAt": "2024-06-02T10:00:00Z"
}
],
"meta": { "page": 1, "limit": 10, "total": 3, "totalPages": 1 }
}Get Lab Result
curl -X GET "https://patient-graph.loop.health/labs/:id" \
-H "Authorization: Bearer $CLERK_JWT"Create Lab Result
curl -X POST "https://patient-graph.loop.health/labs" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"customerId": "prof_abc123",
"labDate": "2024-06-01",
"provider": "Quest Diagnostics",
"biomarkers": [
{
"code": "testosterone-total",
"name": "Total Testosterone",
"value": 650,
"unit": "ng/dL"
}
],
"status": "pending",
"notes": "Annual bloodwork"
}'RBAC: lab_results:write
Delete Lab Result
curl -X DELETE "https://patient-graph.loop.health/labs/:id" \
-H "Authorization: Bearer $CLERK_JWT"RBAC: lab_results:delete (admin/staff only).
Protocols
List Protocols
curl -X GET "https://patient-graph.loop.health/protocols?customerId=prof_abc123" \
-H "Authorization: Bearer $CLERK_JWT"Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID to filter by |
status | string | No | Filter by status (active, paused, completed, cancelled) |
limit | number | No | Max results (default: 20, max: 100) |
offset | number | No | Offset for pagination (default: 0) |
RBAC: protocols:read. Customers see only their own protocols.
Response:
{
"success": true,
"data": [
{
"id": "proto_def456",
"customerId": "prof_abc123",
"title": "BPC-157 Recovery Protocol",
"description": "8-week recovery protocol",
"status": "active",
"startDate": "2024-06-01",
"endDate": "2024-07-27",
"items": [
{
"compound": "BPC-157",
"dosage": "250mcg",
"frequency": "2x daily",
"route": "subcutaneous"
}
],
"prescriberId": "dr_smith",
"notes": "Start low, titrate up",
"createdAt": "2024-06-01T00:00:00Z"
}
]
}Create Protocol
curl -X POST "https://patient-graph.loop.health/protocols" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"customerId": "prof_abc123",
"title": "BPC-157 Recovery Protocol",
"description": "8-week recovery protocol",
"status": "active",
"startDate": "2024-06-01",
"endDate": "2024-07-27",
"items": [
{
"compound": "BPC-157",
"dosage": "250mcg",
"frequency": "2x daily",
"route": "subcutaneous"
}
],
"prescriberId": "dr_smith"
}'Update Protocol
curl -X PATCH "https://patient-graph.loop.health/protocols/:id" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{ "status": "paused", "notes": "Paused for travel" }'Delete Protocol
curl -X DELETE "https://patient-graph.loop.health/protocols/:id" \
-H "Authorization: Bearer $CLERK_JWT"Patient Events
List Events
curl -X GET "https://patient-graph.loop.health/events?customerId=prof_abc123&type=lab_parsed" \
-H "Authorization: Bearer $CLERK_JWT"Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID |
type | string | No | Event type filter |
source | string | No | Event source filter |
fromDate | string | No | ISO date start |
toDate | string | No | ISO date end |
limit | number | No | Max results (default: 20, max: 100) |
offset | number | No | Offset (default: 0) |
Event Types: lab_parsed, protocol_started, protocol_completed, note_added, check_in, wearable_sync, treatment_approved, prescription_shipped
RBAC: events:read. Customers see only their own events.
Create Event
curl -X POST "https://patient-graph.loop.health/events" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"customerId": "prof_abc123",
"type": "note_added",
"description": "Patient reported improved sleep",
"data": { "category": "wellness", "severity": "info" },
"source": "luna-ai"
}'Treatments
List Treatments
curl -X GET "https://patient-graph.loop.health/treatments?customerId=prof_abc123" \
-H "Authorization: Bearer $CLERK_JWT"Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID |
status | string | No | Treatment status |
rimoTreatmentId | string | No | Rimo treatment ID |
limit | number | No | Max results (default: 20) |
offset | number | No | Offset (default: 0) |
Create Treatment
curl -X POST "https://patient-graph.loop.health/treatments" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"customerId": "prof_abc123",
"rimoTreatmentId": "rimo_treat_123",
"offeringId": "offering_456",
"offeringName": "TRT Protocol",
"status": "pending"
}'Update Treatment
curl -X PATCH "https://patient-graph.loop.health/treatments/:id" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{ "status": "approved", "approvedAt": "2024-06-15T12:00:00Z" }'Prescriptions
List Prescriptions
curl -X GET "https://patient-graph.loop.health/prescriptions?customerId=prof_abc123" \
-H "Authorization: Bearer $CLERK_JWT"Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID |
treatmentId | string | No | Filter by treatment |
status | string | No | Prescription status |
rimoOrderId | string | No | Rimo order ID |
limit | number | No | Max results (default: 20) |
offset | number | No | Offset (default: 0) |
Create Prescription
curl -X POST "https://patient-graph.loop.health/prescriptions" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"customerId": "prof_abc123",
"treatmentId": "treat_789",
"rimoOrderId": "rimo_ord_456",
"medicationName": "BPC-157",
"dosage": "5mg/vial",
"quantity": 2,
"refills": 3,
"status": "pending"
}'Conversation History
List Conversations
curl -X GET "https://patient-graph.loop.health/conversation-history?customerId=prof_abc123&channel=luna&limit=50" \
-H "Authorization: Bearer $CLERK_JWT"Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID |
channel | string | No | Channel filter (luna, support, voice) |
sessionId | string | No | Session ID filter |
role | string | No | Message role filter (user, assistant, system) |
fromDate | string | No | ISO date start |
toDate | string | No | ISO date end |
limit | number | No | Max results (default: 50, max: 500) |
offset | number | No | Offset (default: 0) |
Create Conversation Entry
curl -X POST "https://patient-graph.loop.health/conversation-history" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"customerId": "prof_abc123",
"channel": "luna",
"role": "user",
"content": "What are my latest lab results?",
"sessionId": "sess_abc123",
"metadata": {}
}'Audit Logs
List Audit Logs
curl -X GET "https://patient-graph.loop.health/audit-logs?limit=50" \
-H "Authorization: Bearer $CLERK_JWT"Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
actorId | string | No | Filter by actor (user who performed action) |
customerId | string | No | Filter by patient whose data was accessed |
action | string | No | Filter by action (read, write, delete, export) |
resource | string | No | Filter by resource type |
outcome | string | No | Filter by outcome (allowed, denied) |
fromDate | string | No | ISO date start |
toDate | string | No | ISO date end |
limit | number | No | Max results (default: 20) |
offset | number | No | Offset (default: 0) |
RBAC: audit_log:read — admin (full) and staff (read-only). Not accessible to customers or support.
Response:
{
"success": true,
"data": [
{
"id": "audit_123",
"actorId": "user_clerk_789",
"customerId": "prof_abc123",
"role": "staff",
"action": "read",
"resource": "lab_results",
"outcome": "allowed",
"metadata": {},
"createdAt": "2024-06-15T12:00:00Z"
}
]
}Rimo Webhooks
Receive Webhook
POST /webhooks/rimoAuthentication: HMAC-SHA256 signature verification (no Clerk JWT).
Headers:
X-Rimo-Signature— HMAC-SHA256 hex digestX-Rimo-Timestamp— Unix timestamp (must be within 5 minutes)
Events:
treatment.created— New treatment created in Rimotreatment.approved— Treatment approved by clinicianorder.transmitted— Prescription sent to pharmacyorder.shipped— Order shipped to patientcharge.captured— Payment captured
Idempotency: Events are deduplicated by event ID (in-memory).
Webhook Health Check
curl https://patient-graph.loop.health/webhooks/rimo/healthReturns { "status": "ok" }.