Base URL: http://localhost:8580 (direct) or http://localhost:8180/api (via nginx)
All API responses follow the FHIR R4 OperationOutcome format for errors:
{
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "invalid",
"diagnostics": "Human-readable error description"
}]
}- Authentication
- FHIR Resources
- Consent Management
- Clinical
- Documents
- Search
- Admin
- Notifications
- Research
- System
All protected endpoints require the Authorization: Bearer <token> header.
POST /auth/register/physician
Creates a physician account in pending status. Requires admin approval before login.
Request:
{
"email": "dr.sharma@example.com",
"password": "SecurePass123!",
"fullName": "Dr. Anil Sharma",
"phone": "+91-9876543210",
"mciNumber": "MCI-12345",
"specialization": "Cardiology",
"organizationId": "uuid-optional"
}Response (201):
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"message": "Physician registration submitted. Your account is pending admin approval."
}Password requirements: minimum 8 characters, must include uppercase, lowercase, digit, and special character. Cannot contain the email prefix. Common passwords are blocked.
POST /auth/register/patient
Creates a patient account in active status. Also creates a FHIR Patient resource.
Request:
{
"email": "meera.patel@example.com",
"password": "SecurePass123!",
"fullName": "Meera Patel",
"dateOfBirth": "1990-06-15",
"gender": "female",
"phone": "+91-9876543210",
"preferredLanguage": "en"
}Response (201):
{
"userId": "550e8400-e29b-41d4-a716-446655440001",
"fhirPatientId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "active",
"message": "Patient registration successful."
}POST /auth/login
Request:
{
"email": "dr.sharma@example.com",
"password": "SecurePass123!"
}Response — no MFA (200):
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 7200,
"role": "physician",
"requiresMFASetup": true
}Response — MFA required (200):
{
"accessToken": "eyJ...(partial token)...",
"expiresIn": 7200,
"role": "physician",
"requiresTOTP": true
}When requiresTOTP is true, the accessToken is a partial token. Use it to call the TOTP verification endpoint.
Rate limits: 5 failed attempts per email per 15 minutes. 10 failed attempts per IP per 15 minutes.
POST /auth/login/verify-totp
Authorization: Bearer <partial-token>
Request:
{
"code": "123456"
}Response (200):
{
"accessToken": "eyJ...(full token)...",
"refreshToken": "eyJ...",
"expiresIn": 7200,
"role": "physician"
}Lockout: 5 failed TOTP attempts triggers a 30-minute lockout.
POST /auth/refresh
Rotates the refresh token. The old token is revoked. If a revoked token is reused, all tokens for that user are revoked (suspected theft).
Request:
{
"refreshToken": "eyJ..."
}Response (200):
{
"accessToken": "eyJ...(new)...",
"refreshToken": "eyJ...(new)...",
"expiresIn": 7200,
"role": "physician"
}POST /auth/logout
Authorization: Bearer <token>
Request (optional):
{
"refreshToken": "eyJ..."
}Response: 204 No Content
GET /auth/me
Authorization: Bearer <token>
Response (200):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "dr.sharma@example.com",
"fullName": "Dr. Anil Sharma",
"phone": "+91-9876543210",
"role": "physician",
"status": "active",
"totpEnabled": true,
"specialization": "Cardiology",
"mciNumber": "MCI-12345",
"createdAt": "2026-01-15T10:30:00Z",
"lastLoginAt": "2026-03-12T06:00:00Z"
}POST /auth/password/change
Authorization: Bearer <token>
Request:
{
"oldPassword": "CurrentPass123!",
"newPassword": "NewSecurePass456!"
}Response (200):
{
"message": "Password changed successfully"
}All refresh tokens are revoked after a password change.
POST /auth/totp/setup
Authorization: Bearer <token>
Available to physicians and admins only.
Response (200):
{
"secret": "JBSWY3DPEHPK3PXP",
"qrCode": "data:image/png;base64,..."
}POST /auth/totp/verify-setup
Authorization: Bearer <token>
Request:
{
"code": "123456"
}Response (200):
{
"backupCodes": ["abc123", "def456", "ghi789", "..."],
"message": "TOTP has been enabled. Save your backup codes securely — they will not be shown again."
}All FHIR endpoints are under /fhir/R4/{ResourceType} and require authentication.
| Resource | Create | Read | Update | Delete | Search | History |
|---|---|---|---|---|---|---|
| Patient | Physician | All | Physician | Admin | All | All |
| Practitioner | Admin | All | Admin | Admin | All | All |
| Organization | Admin | All | Admin | Admin | All | All |
| Encounter | Physician | All | Physician | Admin | All | All |
| Condition | Physician | All | Physician | Admin | All | All |
| MedicationRequest | Physician | All | Physician | Admin | All | All |
| Observation | Physician | All | Physician | Admin | All | All |
| DiagnosticReport | Physician | All | Physician | Admin | All | All |
| AllergyIntolerance | Physician | All | Physician | Admin | All | All |
| Immunization | Physician | All | Physician | Admin | All | All |
POST /fhir/R4/{ResourceType}
Authorization: Bearer <token>
Content-Type: application/json
Request body: A valid FHIR R4 resource JSON. The id field is auto-generated if not provided.
Response (201): The created resource with server-assigned id and meta.versionId.
GET /fhir/R4/{ResourceType}/{id}
Authorization: Bearer <token>
Consent enforcement: Physicians can only read resources for patients who have granted them consent. Patients can only read their own resources.
Response (200): The FHIR resource JSON.
PUT /fhir/R4/{ResourceType}/{id}
Authorization: Bearer <token>
Content-Type: application/json
Response (200): The updated resource with incremented meta.versionId.
DELETE /fhir/R4/{ResourceType}/{id}
Authorization: Bearer <token>
Soft delete. The resource is marked as deleted but retained for audit purposes.
Response: 204 No Content
GET /fhir/R4/{ResourceType}?param=value
Authorization: Bearer <token>
Returns a FHIR Bundle of matching resources.
Common parameters (all resources):
| Parameter | Type | Description |
|---|---|---|
_count |
integer | Results per page (default: 20, max: 100) |
_offset |
integer | Pagination offset |
patient |
reference | Filter by patient (e.g., Patient/uuid) |
status |
string | Filter by status |
Resource-specific search parameters:
| Resource | Parameters |
|---|---|
| Patient | name, gender, birthdate |
| Practitioner | name, specialty |
| Organization | name, type |
| Encounter | patient, status, class, date (prefix: ge, le, gt, lt) |
| Condition | patient, clinical-status, category, code |
| MedicationRequest | patient, status, intent, code |
| Observation | patient, code, category, date (prefix support), status |
| DiagnosticReport | patient, code, category, status, date (prefix support) |
| AllergyIntolerance | patient, clinical-status, criticality |
| Immunization | patient, status, vaccine-code, date (prefix support) |
Response (200):
{
"resourceType": "Bundle",
"type": "searchset",
"total": 42,
"entry": [
{
"resource": { "resourceType": "Observation", "id": "...", "..." : "..." }
}
]
}GET /fhir/R4/{ResourceType}/{id}/_history
GET /fhir/R4/{ResourceType}/{id}/_history/{versionId}
Returns the version history of a resource, or a specific version.
Patient Timeline:
GET /fhir/R4/Patient/{id}/$timeline?_count=50&_offset=0
Returns a chronological bundle of all resources for a patient (Encounters, Conditions, Observations, etc.).
Lab Trends:
GET /fhir/R4/Observation/$lab-trends?patient=Patient/{id}&code={loinc-code}
Returns historical lab values for trend analysis.
All consent endpoints are under /consent and require authentication.
POST /consent/grant
Authorization: Bearer <token>
Role: patient or admin
Request:
{
"providerId": "physician-user-uuid",
"scope": ["Observation", "MedicationRequest", "Condition"],
"expiresAt": "2027-01-01T00:00:00Z",
"purpose": "treatment",
"notes": "Cardiology follow-up"
}If scope is omitted, defaults to ["*"] (all resource types). Creates a pending consent that the physician must accept.
Response (201): The created consent record.
PUT /consent/{consentId}/accept
Authorization: Bearer <token>
Role: physician or admin
Only the physician named in the consent can accept it.
Response (200):
{
"message": "Consent accepted"
}PUT /consent/{consentId}/decline
Authorization: Bearer <token>
Role: physician or admin
Request:
{
"reason": "Patient not under my care"
}Response (200):
{
"message": "Consent declined"
}DELETE /consent/{consentId}/revoke
Authorization: Bearer <token>
Role: patient, physician, or admin
Idempotent — revoking an already-revoked consent returns success.
Response (200):
{
"message": "Consent revoked"
}GET /consent/my-grants
Authorization: Bearer <token>
Role: patient or admin
Returns all consent records for the authenticated patient.
GET /consent/my-patients
Authorization: Bearer <token>
Role: physician or admin
Returns all patients who have granted active consent to the authenticated physician.
GET /consent/pending-requests
Authorization: Bearer <token>
Role: physician or admin
Returns pending consent requests awaiting the physician's acceptance.
POST /consent/break-glass
Authorization: Bearer <token>
Role: physician or admin
Creates temporary 24-hour full-access consent for emergency situations.
Request:
{
"patientId": "fhir-patient-uuid",
"reason": "Patient unconscious in emergency room, need immediate access to medical history and allergies for treatment",
"clinicalContext": "Emergency department"
}Constraints:
- Reason must be at least 20 characters
- Maximum 3 break-glass accesses per physician per 24 hours
- Patient is notified by email
- Fully audited
Response (201): The created emergency consent record.
GET /consent/access-log
Authorization: Bearer <token>
Role: patient or admin
Returns the audit trail of who accessed the patient's data.
POST /clinical/drug-check
Authorization: Bearer <token>
Role: physician
Checks a new medication against the patient's current prescriptions and allergies.
Request:
{
"patientId": "fhir-patient-uuid",
"rxnormCode": "161",
"medicationName": "Aspirin"
}Response (200):
{
"newMedication": { "rxnormCode": "161", "name": "Aspirin" },
"interactions": [
{
"drugA": { "rxnormCode": "161", "name": "Aspirin" },
"drugB": { "rxnormCode": "11289", "name": "Warfarin" },
"severity": "major",
"description": "Increased risk of bleeding",
"mechanism": "Additive antiplatelet effects",
"management": "Monitor INR closely",
"source": "openfda",
"cached": false
}
],
"allergyConflicts": [],
"highestSeverity": "major",
"hasContraindication": false,
"checkComplete": true
}Severity levels: contraindicated > major > moderate > minor > unknown > none
If the check finds a contraindicated interaction, MedicationRequest creation is blocked until the physician acknowledges the risk.
POST /clinical/drug-check/acknowledge
Authorization: Bearer <token>
Role: physician
Acknowledges a contraindicated interaction to allow prescription creation.
GET /clinical/drug-check/history/{patientId}
Authorization: Bearer <token>
Role: physician
Returns previous drug interaction check results for a patient.
POST /documents/upload
Authorization: Bearer <token>
Content-Type: multipart/form-data
Form fields:
file— The document file (PDF, PNG, JPG, JPEG)patientId— FHIR Patient ID
Uploads a lab report for asynchronous processing. The document goes through:
- OCR text extraction (Tesseract)
- LLM-based structured data extraction (Gemini)
- LOINC code mapping
- FHIR Observation resource creation
Response (202):
{
"jobId": "job-uuid",
"status": "pending",
"estimatedProcessingTime": "30-60 seconds"
}GET /documents/jobs/{jobId}
Authorization: Bearer <token>
Response (200):
{
"jobId": "job-uuid",
"status": "completed",
"observationsCreated": 12,
"loincMapped": 10,
"ocrConfidence": 0.87,
"llmProvider": "gemini",
"fhirReportId": "report-uuid",
"uploadedAt": "2026-03-12T06:00:00Z",
"completedAt": "2026-03-12T06:00:45Z"
}Job statuses: pending → processing → completed | failed | needs-manual-review
GET /documents/jobs?patientId={fhirPatientId}
Authorization: Bearer <token>
DELETE /documents/jobs/{jobId}
Authorization: Bearer <token>
GET /search?q={query}&patient={patientId}
Authorization: Bearer <token>
Full-text search across all FHIR resources using Elasticsearch. Physicians must provide a patient parameter. Patients are automatically scoped to their own data.
Response (200): FHIR Bundle of matching resources.
All admin endpoints require the admin role.
GET /admin/users # List all users
GET /admin/users/{userId} # Get user details
PUT /admin/users/{userId}/role # Update user role
POST /admin/physicians/{userId}/approve # Approve pending physician
POST /admin/physicians/{userId}/suspend # Suspend a physician
POST /admin/physicians/{userId}/reinstate # Reinstate suspended physician
POST /admin/researchers/invite # Invite a researcher
GET /doctors # List active physicians (all roles)
GET /admin/audit-logs # All audit logs
GET /admin/audit-logs/patient/{id} # Logs for a specific patient
GET /admin/audit-logs/actor/{id} # Logs for a specific actor
GET /admin/audit-logs/break-glass # Break-glass events only
GET /admin/stats # Dashboard statistics
GET /admin/system/health # Detailed system health
POST /admin/search/reindex # Trigger Elasticsearch reindex
POST /admin/tasks/cleanup-tokens # Trigger expired token cleanup
GET /notifications/preferences # Get notification preferences
PUT /notifications/preferences # Update preferences
POST /notifications/fcm-token # Register push notification token
DELETE /notifications/fcm-token # Revoke push notification token
POST /research/export # Request de-identified data export
GET /research/export/{exportId} # Get export status
GET /research/exports # List all exports
DELETE /research/export/{exportId} # Delete export (admin only)
GET /health
Returns 200 OK if the API server is running.
GET /ready
Returns 200 OK only if all dependencies (PostgreSQL, Redis, Elasticsearch, MinIO) are reachable. Returns 503 Service Unavailable if any dependency is down.
GET /metrics
Prometheus-compatible metrics endpoint.
| HTTP Status | Meaning |
|---|---|
| 400 | Invalid request (missing fields, validation failure) |
| 401 | Invalid credentials or expired token |
| 403 | Forbidden (wrong role, no consent, account suspended) |
| 404 | Resource not found |
| 409 | Conflict (duplicate email, TOTP already enabled) |
| 429 | Rate limit exceeded (includes Retry-After header) |
| 500 | Internal server error |