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"
}
]
}Knock Integration
The Patient Graph integrates with Knock to deliver intelligent notifications across 6 key workflows. Notifications are triggered based on patient events and health data changes.
Notification Workflows
1. Daily Dose Reminders
Triggered by protocol schedules and user preferences.
const triggerDoseReminder = async (userId: string, protocol: Protocol) => {
await knock.workflows.trigger('daily-dose-reminder', {
recipients: [{ id: userId }],
data: {
compound_name: protocol.compound,
dosage: protocol.dosage,
route: protocol.route,
protocol_name: protocol.name,
protocol_id: protocol.id,
},
schedule: {
at: protocol.reminderTime,
timezone: protocol.timezone,
},
});
};2. Supply Low Alerts
Triggered when supply calculations indicate low inventory.
const triggerSupplyAlert = async (userId: string, supply: Supply, calculation: SupplyCalculation) => {
await knock.workflows.trigger('supply-low-alert', {
recipients: [{ id: userId }],
data: {
compound_name: supply.compoundName,
current_amount: supply.currentAmount,
unit: supply.unit,
days_remaining: calculation.daysRemaining,
supply_id: supply.id,
reorder_url: `https://loopbiolabs.com/products/${supply.compoundName.toLowerCase()}`,
},
});
};3. Lab Results Available
Triggered when new lab results are processed and analyzed.
const notifyLabResults = async (userId: string, labResult: LabResult) => {
await knock.workflows.trigger('lab-results-ready', {
recipients: [{ id: userId }],
data: {
lab_date: labResult.labDate,
provider: labResult.provider,
lab_id: labResult.id,
biomarker_count: labResult.biomarkers.length,
has_concerns: labResult.biomarkers.some(b => b.status === 'high' || b.status === 'low'),
},
});
};4. Adherence Milestones
Triggered when users reach protocol adherence goals.
const celebrateMilestone = async (userId: string, milestone: AdherenceMilestone) => {
await knock.workflows.trigger('adherence-milestone', {
recipients: [{ id: userId }],
data: {
milestone_type: milestone.type,
milestone_days: milestone.days,
protocol_name: milestone.protocolName,
protocol_id: milestone.protocolId,
achievement_date: milestone.achievedAt,
},
});
};5. Protocol Updates
Triggered when clinicians modify patient protocols.
const notifyProtocolUpdate = async (userId: string, protocol: Protocol, clinician: Clinician) => {
await knock.workflows.trigger('protocol-updated', {
recipients: [{ id: userId }],
data: {
protocol_name: protocol.name,
protocol_id: protocol.id,
clinician_name: clinician.name,
clinician_id: clinician.id,
update_summary: protocol.updateSummary,
updated_at: protocol.updatedAt,
},
});
};6. Weekly Check-in Reminders
Scheduled based on user preferences for health check-ins.
const scheduleWeeklyCheckin = async (userId: string, preferences: NotificationPreferences) => {
await knock.workflows.trigger('weekly-checkin', {
recipients: [{ id: userId }],
schedule: {
at: preferences.weeklyCheckin.time,
timezone: preferences.timezone,
frequency: 'weekly',
day: preferences.weeklyCheckin.day,
},
});
};User Preference Management
curl -X PUT "https://patient-graph.loop.health/notification-preferences" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"workflows": {
"daily-dose-reminder": { "enabled": true },
"supply-low-alert": { "enabled": true },
"lab-results-ready": { "enabled": true },
"adherence-milestone": { "enabled": true },
"protocol-updated": { "enabled": true },
"weekly-checkin": { "enabled": false }
},
"quiet_hours": {
"enabled": true,
"start": "22:00",
"end": "08:00"
}
}'Rimo Integration
The Patient Graph serves as the central hub for Rimo Health telehealth integration, handling both SSO authentication and webhook events from the Rimo platform.
Rimo Webhook Handler
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)
Supported Events: 16 event types across treatment, consultation, order, and payment lifecycles.
Event Processing Flow
Treatment Lifecycle Events
Treatment Created/Updated
const handleTreatmentEvent = async (event: RimoWebhookEvent) => {
const { treatmentId, customerId, status } = event.data;
// Update or create treatment in Patient Graph
await upsertTreatment({
id: treatmentId,
customerId,
rimoTreatmentId: treatmentId,
status,
updatedAt: new Date(event.timestamp),
});
// Trigger appropriate notification
if (status === 'approved') {
await knock.workflows.trigger('treatment-approved', {
recipients: [{ id: customerId }],
data: {
treatment_id: treatmentId,
clinician_name: event.data.clinicianName,
},
});
}
// Update BFF routes for real-time UI updates
await updateBFFRoutes(customerId, 'treatments', treatmentId);
};Order and Prescription Events
const handleOrderEvent = async (event: RimoWebhookEvent) => {
const { orderId, treatmentId, customerId } = event.data;
switch (event.type) {
case 'order.created':
await createPrescription({
rimoOrderId: orderId,
treatmentId,
customerId,
medications: event.data.medications,
status: 'pending',
});
break;
case 'order.shipped':
await updatePrescription(orderId, {
status: 'shipped',
trackingNumber: event.data.trackingNumber,
carrier: event.data.carrier,
estimatedDelivery: event.data.estimatedDelivery,
});
// Notify patient of shipment
await knock.workflows.trigger('prescription-shipped', {
recipients: [{ id: customerId }],
data: {
order_id: orderId,
tracking_number: event.data.trackingNumber,
carrier: event.data.carrier,
estimated_delivery: event.data.estimatedDelivery,
},
});
break;
case 'order.delivered':
await updatePrescription(orderId, {
status: 'delivered',
deliveredAt: event.data.deliveredAt,
});
// Start protocol if this is first delivery
await checkAndStartProtocol(customerId, treatmentId);
break;
}
};BFF Route Integration
The Patient Graph updates BFF (Backend for Frontend) routes to provide real-time data to the Loop Health web application:
const updateBFFRoutes = async (customerId: string, dataType: string, resourceId: string) => {
try {
// Invalidate relevant cache keys
await redis.del(`bff:${customerId}:${dataType}`);
await redis.del(`bff:${customerId}:dashboard`);
// Trigger real-time update via WebSocket
await notifyBFFUpdate(customerId, {
type: dataType,
resourceId,
timestamp: new Date().toISOString(),
});
// Update search indexes if needed
if (dataType === 'treatments' || dataType === 'prescriptions') {
await updateSearchIndex(customerId, dataType, resourceId);
}
} catch (error) {
console.error('Failed to update BFF routes:', error);
}
};Webhook Health Check
curl https://patient-graph.loop.health/webhooks/rimo/healthResponse:
{
"status": "ok",
"uptime": 86400,
"metrics": {
"processed": {
"treatment.approved": 45,
"order.shipped": 23,
"charge.captured": 67
},
"failed": {
"treatment.created": 2
},
"avgProcessingTimes": {
"treatment.approved": 150,
"order.shipped": 89
}
},
"failureQueue": {
"pending": 0,
"maxRetries": 3
}
}