Health Patient Portal: Delegated Agent API

Base URL https://portal.mohithgajjela.com/agent/v1. Machine-readable spec: openapi.json. FHIR R4 read view: /fhir/r4/metadata, SMART discovery: /.well-known/smart-configuration.

Authentication

The patient creates a grant under Account settings > Linked apps & agents and chooses scopes and a confirmation policy. Send the token as Authorization: Bearer <token>. Scopes: record:read appointments:read appointments:book appointments:cancel checkin:complete messages:read messages:send refills:request results:read medications:read billing:read billing:pay questionnaires:submit vitals:write profile:read.

Confirmation protocol

Confirmation protocol for irreversible actions (booking, cancelling or rescheduling an appointment, accepting a wait-list offer, sending or replying to a message, requesting refills, paying, signing a document, submitting a questionnaire): when the grant's confirm_policy is "confirm-irreversible" (the default), the first call returns HTTP 202 {status:"confirmation_required", confirmation_token, expires_in:300, summary, action, payload_hash}. Read the summary to the patient, get their yes, then repeat the identical request with the header X-Confirmation-Token: <confirmation_token>. Tokens are single-use, expire in 5 minutes, and are bound to the exact action + payload (409 on mismatch, reuse or expiry). Grants with confirm_policy "pre-authorized" skip the challenge. Every call is written to the patient's access log with actor=agent, on_behalf_of=patient and the grant id, and appears under "Who's accessed my record".

POST /agent/v1/appointments/appt_123/cancel  {"reason":"Schedule conflict"}
→ 202 {"status":"confirmation_required","confirmation_token":"cnf_…","summary":"Cancel the Annual physical with Dr. Anita Okafor on …","expires_in":300}
POST /agent/v1/appointments/appt_123/cancel  {"reason":"Schedule conflict"}   X-Confirmation-Token: cnf_…
→ 200 {"status":"cancelled", …}

Suggested conversation flow

  1. GET /context once at the start of a call.
  2. Read as needed (/record/summary, /results?new=true, /messages?unread=true).
  3. For actions: gather parameters (/visit-types, /slots, /pharmacies), call the write endpoint, read the summary from the 202 to the patient, confirm, repeat with the token.
  4. Everything you do is visible to the patient under "Who's accessed my record" and in the message center as "(via <grant label>)".
MethodPathDescriptionScope
GET/contextCompact briefing to open a conversation: patient, next appointments, eCheck-In availability, unread messages, new results, open refills, questionnaires due, preventive care due, balance due, wait-list offers, scheduling tickets, org info, server time.profile:read
GET/profileDemographics, contact info, emergency contact, pharmacies on file, coverage.profile:read
GET/record/summaryProblems, allergies, active medications, immunizations, last 10 results with flags, latest vitals, care team.record:read
GET/appointmentsList appointments.
Query: status upcoming (default) | past
appointments:read
GET/appointments/{id}Appointment details.appointments:read
GET/appointments/{id}/instructionsInstructions, address, arrival guidance, cancel policy.appointments:read
GET/visit-typesBookable visit types (duration, modality, direct vs request).appointments:read
GET/providersSchedulable providers, care-team members first.
Query: visit_type filter by visit type id
appointments:read
GET/slotsFree slots grouped by day with friendly labels.
Query: visit_type required · provider_id optional · from ISO datetime · to ISO datetime (default from + 14 days) · modality in-person | video · part_of_day morning | afternoon
appointments:read
POST/appointments confirmBook a free slot.
Body: visit_type_id string · slot_id string · reason_text string? · comments string? · scheduling_ticket_id string? (links a provider order)
appointments:book
POST/appointments/requestRequest an appointment for request-only visit types.
Body: visit_type_id string · provider_id string? · preferred_days string? · preferred_times string? · comments string?
appointments:book
POST/appointments/{id}/cancel confirmCancel an appointment (late cancellations are flagged).
Body: reason string
appointments:cancel
POST/appointments/{id}/reschedule confirmMove an appointment to another free slot.
Body: slot_id string
appointments:book + appointments:cancel
POST/appointments/{id}/waitlistJoin the wait list for an earlier time.appointments:book
POST/waitlist/{id}/accept confirmAccept an earlier-time offer.appointments:book
GET/appointments/{id}/checkineCheck-In steps with the data to verify (personal, insurance, copay, documents, medications, allergies, health issues, questionnaires).checkin:complete
POST/appointments/{id}/checkin/step/{key} confirmComplete one step. Review steps: {confirmed:true, changes:[{op:add|remove|update, target_id?, payload}]} (changes become pending change requests for staff review). payment: {pay_later:true} or {method_id} (saved method; confirmation required). documents: {consent_id, signature_name} (confirmation required). questionnaires: {assignment_id, answers}.checkin:complete
POST/appointments/{id}/checkin/completeFinish eCheck-In once no step is pending.checkin:complete
GET/messagesConversation list.
Query: folder conversations (default) | appointment | automated | archived · unread true
messages:read
GET/messages/{thread_id}Thread with all messages.messages:read
POST/messages/{thread_id}/readMark a thread read.messages:read
POST/messages confirmSend a new message on the patient's behalf (sender is labelled "<patient> (via <grant>)").
Body: kind medical-advice | refill | billing | customer-service | scheduling · recipient_practitioner_id string? (care team member) · recipient_pool nurse-line | billing | customer-service | scheduling (default by kind) · subject string · body string · about_type string? · about_id string?
messages:send
POST/messages/{thread_id}/reply confirmReply in a conversation.
Body: body string
messages:send
GET/resultsReleased test results.
Query: new true (unviewed only)
results:read
GET/results/{id}Result detail with components, flags, provider comment. Does not mark the result viewed unless mark_viewed=true.
Query: mark_viewed true
results:read
GET/results/{id}/trend/{loinc}All released values of one component over time.results:read
GET/medicationsMedication list with refill eligibility and renewal flags.medications:read
GET/pharmaciesPharmacies (patient's on file first).medications:read
GET/refillsRefill requests with status history.medications:read
POST/refills confirmRequest refills/renewals. Controlled substances and patient-reported meds are rejected.
Body: items [{medication_request_id, comments?}] · delivery_method pickup | mail · pharmacy_id string · pickup_at ISO datetime?
refills:request
GET/billing/summaryBalances per account, payment plans, saved payment methods, recent payments.billing:read
GET/billing/statementsStatements.billing:read
POST/billing/pay confirmPay toward an account using a saved payment method.
Body: account_id string · amount number · method_id string (saved method)
billing:pay
GET/questionnairesAssigned questionnaires.questionnaires:submit
GET/questionnaires/{assignment_id}Questionnaire items and any saved answers.questionnaires:submit
POST/questionnaires/{assignment_id} confirmSave (submit:false) or submit answers keyed by linkId.
Body: answers { [linkId]: value } · submit boolean (default true)
questionnaires:submit
GET/vitalsVitals and patient-entered readings.
Query: flowsheet blood_pressure | weight | blood_glucose | steps | heart_rate | temperature | spo2
record:read
POST/vitalsRecord a patient-entered reading (flagged pending clinician review).
Body: flowsheet blood_pressure | weight | blood_glucose | steps | heart_rate | temperature | spo2 · value number (non-BP) · systolic number (BP) · diastolic number (BP) · effective_at ISO datetime? · note string?
vitals:write
GET/auditThis grant's own access-log entries.
Query: limit number (default 50)
profile:read
GET/openapi.jsonThis document.public
GET/docsHuman-readable docs.public

Sandbox environment with fictional data. Every call is audited.