Skip to Content
API ReferencePatient Graph API

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:

ParamTypeRequiredDescription
emailstringNoFilter by email address
subscriptionTierstringNoFilter by tier
tagstringNoFilter by tag
limitnumberNoMax results (default: 20, max: 100)
offsetnumberNoOffset 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:

ParamTypeRequiredDescription
customerIdstringYesCustomer ID to filter by
statusstringNoFilter by status (pending, reviewed, archived)
providerstringNoFilter by lab provider
fromDatestringNoISO date — labs after this date
toDatestringNoISO date — labs before this date
limitnumberNoMax results (default: 20, max: 100)
offsetnumberNoOffset 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:

ParamTypeRequiredDescription
customerIdstringYesCustomer ID to filter by
statusstringNoFilter by status (active, paused, completed, cancelled)
limitnumberNoMax results (default: 20, max: 100)
offsetnumberNoOffset 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:

ParamTypeRequiredDescription
customerIdstringYesCustomer ID
typestringNoEvent type filter
sourcestringNoEvent source filter
fromDatestringNoISO date start
toDatestringNoISO date end
limitnumberNoMax results (default: 20, max: 100)
offsetnumberNoOffset (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:

ParamTypeRequiredDescription
customerIdstringYesCustomer ID
statusstringNoTreatment status
rimoTreatmentIdstringNoRimo treatment ID
limitnumberNoMax results (default: 20)
offsetnumberNoOffset (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:

ParamTypeRequiredDescription
customerIdstringYesCustomer ID
treatmentIdstringNoFilter by treatment
statusstringNoPrescription status
rimoOrderIdstringNoRimo order ID
limitnumberNoMax results (default: 20)
offsetnumberNoOffset (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:

ParamTypeRequiredDescription
customerIdstringYesCustomer ID
channelstringNoChannel filter (luna, support, voice)
sessionIdstringNoSession ID filter
rolestringNoMessage role filter (user, assistant, system)
fromDatestringNoISO date start
toDatestringNoISO date end
limitnumberNoMax results (default: 50, max: 500)
offsetnumberNoOffset (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:

ParamTypeRequiredDescription
actorIdstringNoFilter by actor (user who performed action)
customerIdstringNoFilter by patient whose data was accessed
actionstringNoFilter by action (read, write, delete, export)
resourcestringNoFilter by resource type
outcomestringNoFilter by outcome (allowed, denied)
fromDatestringNoISO date start
toDatestringNoISO date end
limitnumberNoMax results (default: 20)
offsetnumberNoOffset (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/rimo

Authentication: HMAC-SHA256 signature verification (no Clerk JWT).

Headers:

  • X-Rimo-Signature — HMAC-SHA256 hex digest
  • X-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/health

Response:

{ "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 } }