Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001): - new ReferenceSequence model + generate_reference() helper (apps/core) - Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save() - prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only) - removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources - migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012 - unit tests (format, sanitization, monthly reset, 40-thread concurrency) QA audit: - isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env) - feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion) - reports/feedback-modules-qa-report.md Also bundles accumulated in-progress work across complaints, observations, organizations, templates, and other modules.
16 KiB
PX360 External API Documentation
Base URL: /api/v1/external/
Authentication
All requests require an X-API-Key header.
X-API-Key: <your-api-key>
API keys are managed through the Django admin panel. Each key can be scoped to:
- A specific hospital (restricts data access to that hospital)
- Specific entities (
complaints,inquiries,observations,appreciations,suggestions,doctor_ratings) - Empty
allowed_entities= access to all entities
Creating an API Key
Via Django admin or programmatically:
from apps.integrations.models import ExternalAPIKey
api_key, raw_key = ExternalAPIKey.create_key(
name="Partner Integration",
hospital=hospital_object, # Optional: scope to a hospital
allowed_entities=["complaints", "inquiries"], # Optional: empty = all
rate_limit=60, # Requests per minute
description="Integration with partner system",
)
# raw_key is shown ONCE - store it securely
Error Responses
| HTTP Status | Meaning |
|---|---|
401 |
Missing or invalid API key |
403 |
API key does not have access to the requested entity, or hospital scope mismatch |
404 |
Requested resource not found |
400 |
Validation error (missing/invalid fields) |
Lookup Endpoints
These endpoints provide dropdown/reference data for building forms.
List Hospitals
GET /api/v1/external/hospitals/
Response:
{
"count": 3,
"results": [
{ "id": "uuid", "name": "Al Nuzha", "code": "HH-N" },
{ "id": "uuid", "name": "Al Olya", "code": "HH-A" },
{ "id": "uuid", "name": "Al Suwaidi", "code": "HH-S" }
]
}
List Location Types
GET /api/v1/external/location-types/
Response:
{
"count": 4,
"results": [
{ "value": "OP", "label": "Outpatient", "label_ar": "خارجي" },
{ "value": "IP", "label": "Inpatient", "label_ar": "تنويم" },
{ "value": "ER", "label": "Emergency", "label_ar": "طوارئ" },
{ "value": "GENERAL", "label": "General", "label_ar": "عام" }
]
}
List Areas
GET /api/v1/external/areas/?hospital=<hospital_name>
| Parameter | Required | Description |
|---|---|---|
hospital |
Yes | Hospital name (case-insensitive) |
location_type |
No | Filter by location type (OP, IP, ER, GENERAL) |
Response:
{
"count": 27,
"results": [
{ "id": "uuid", "name": "Emergency", "name_ar": "", "code": "emergency" }
]
}
List Departments
GET /api/v1/external/departments/?hospital=<hospital_name>
| Parameter | Required | Description |
|---|---|---|
hospital |
Yes | Hospital name (case-insensitive) |
Response:
{
"count": 37,
"results": [
{ "id": "uuid", "name": "Critical Care Department", "name_en": "Critical Care Department", "name_ar": "", "code": "hh_n_critical_care_department" }
]
}
List Sections
GET /api/v1/external/sections/?hospital=<hospital_name>&department=<department_name>
| Parameter | Required | Description |
|---|---|---|
hospital |
Yes | Hospital name (case-insensitive) |
department |
Yes | Department name (case-insensitive) |
Response:
{
"count": 3,
"results": [
{ "id": "uuid", "name": "ICU", "name_ar": "", "code": "hh_n_critical_care_department__icu" }
]
}
Complaints
Create a Complaint
POST /api/v1/external/complaints/
Request Body:
| Field | Required | Type | Description |
|---|---|---|---|
hospital |
Yes | string | Hospital name (case-insensitive) |
title |
Yes | string | Complaint title (max 500 chars) |
description |
Yes | string | Detailed complaint description |
contact_name |
Yes | string | Reporter's name (max 200 chars) |
contact_phone |
Yes | string | Reporter's phone (max 20 chars) |
contact_email |
No | string | Reporter's email |
relation_to_patient |
No | string | One of: patient, relative, friend, other |
patient_name |
No | string | Patient name |
national_id |
No | string | National ID number |
incident_date |
No | date | Date of incident (YYYY-MM-DD) |
expected_result |
No | string | Expected resolution |
location_type |
No | string | One of: OP, IP, ER, GENERAL |
area |
No | string | Area name (resolved by hospital) |
department |
No | string | Department name (resolved by hospital) |
section |
No | string | Section name (requires department) |
Example:
{
"hospital": "Al Nuzha",
"title": "Long wait time in ER",
"description": "I waited 3 hours in the emergency room without being seen.",
"contact_name": "John Doe",
"contact_phone": "+966501234567",
"contact_email": "john@example.com",
"location_type": "ER",
"area": "Emergency",
"department": "Critical Care Department",
"section": "ICU"
}
Response (201):
{
"success": true,
"reference_number": "CMP-20260609-167152",
"status": "open"
}
List Complaints
GET /api/v1/external/complaints/list/
| Parameter | Required | Description |
|---|---|---|
page |
No | Page number (default: 1) |
page_size |
No | Items per page (default: 20, max: 100) |
created_from |
No | Filter by created date from (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) |
created_to |
No | Filter by created date to (date-only values auto-extend to end of day) |
updated_from |
No | Filter by updated date from |
updated_to |
No | Filter by updated date to |
status |
No | Filter by status value |
Retrieve a Complaint
GET /api/v1/external/complaints/<reference_number>/
Response:
{
"id": "uuid",
"reference_number": "CMP-20260609-167152",
"title": "Long wait time in ER",
"description": "I waited 3 hours...",
"status": "open",
"severity": "medium",
"priority": "medium",
"contact_name": "John Doe",
"contact_phone": "+966501234567",
"contact_email": "john@example.com",
"relation_to_patient": "",
"patient_name": "",
"incident_date": null,
"expected_result": "",
"resolution": "",
"satisfaction": "",
"hospital_name": "Al Nuzha",
"created_at": "2026-06-09T13:38:03.808618+03:00",
"updated_at": "2026-06-09T13:38:03.808683+03:00"
}
Resolution & Satisfaction Fields
| Field | Description |
|---|---|
resolution |
Free-text description of the resolution taken (empty if unresolved) |
satisfaction |
Patient satisfaction: satisfied, neutral, dissatisfied, no_response, or empty |
Set Patient Satisfaction
Sets the patient satisfaction level for a complaint. A resolution must be recorded first — returns 400 if the complaint has no resolution.
PATCH /api/v1/external/complaints/<reference_number>/satisfaction/
Request Body:
| Field | Required | Type | Description |
|---|---|---|---|
satisfaction |
Yes | string | One of: satisfied, neutral, dissatisfied, no_response |
Example:
{
"satisfaction": "satisfied"
}
Response (200):
{
"success": true,
"reference_number": "CMP-20260609-167152",
"satisfaction": "satisfied"
}
Inquiries
Create an Inquiry
POST /api/v1/external/inquiries/
Request Body:
| Field | Required | Type | Description |
|---|---|---|---|
hospital |
Yes | string | Hospital name |
subject |
Yes | string | Inquiry subject (max 500 chars) |
message |
Yes | string | Inquiry message |
contact_name |
Yes | string | Contact name |
contact_phone |
Yes | string | Contact phone |
contact_email |
No | string | Contact email |
category |
No | string | One of: appointment, billing, medical_records, general, other |
location_type |
No | string | One of: OP, IP, ER, GENERAL |
area |
No | string | Area name |
department |
No | string | Department name |
section |
No | string | Section name (requires department) |
Response (201):
{
"success": true,
"reference_number": "INQ-20260609-124293",
"status": "open"
}
List / Retrieve Inquiries
GET /api/v1/external/inquiries/list/
GET /api/v1/external/inquiries/<reference_number>/
Same pagination and date/status filters as complaints.
Retrieve Response:
{
"id": "uuid",
"reference_number": "INQ-20260609-124293",
"subject": "Appointment rescheduling",
"message": "I need to reschedule my cardiology appointment.",
"category": "appointment",
"status": "open",
"contact_name": "Jane Smith",
"contact_phone": "+966509876543",
"contact_email": "jane@example.com",
"hospital_name": "Al Nuzha",
"created_at": "2026-06-09T13:36:22.730962+03:00",
"updated_at": "2026-06-09T13:36:22.731020+03:00"
}
Observations
Create an Observation
POST /api/v1/external/observations/
Request Body:
| Field | Required | Type | Description |
|---|---|---|---|
hospital |
Yes | string | Hospital name |
description |
Yes | string | Observation description |
title |
No | string | Title (max 300 chars) |
severity |
No | string | One of: low, medium, high, critical (default: medium) |
category |
No | UUID | Observation category UUID |
location_text |
No | string | Free-text location description |
incident_datetime |
No | datetime | When the incident occurred |
contact_name |
No | string | Reporter name (anonymous supported) |
contact_phone |
No | string | Reporter phone |
contact_email |
No | string | Reporter email |
reporter_staff_id |
No | string | Staff ID of reporter |
patient_file_number |
No | string | Patient file number |
Response (201):
{
"success": true,
"reference_number": "OBS-O8TCIC",
"status": "new"
}
List / Retrieve Observations
GET /api/v1/external/observations/list/
GET /api/v1/external/observations/<tracking_code>/
Retrieve Response:
{
"id": "uuid",
"reference_number": "OBS-O8TCIC",
"title": "",
"description": "Wet floor near the main entrance, no warning sign posted.",
"severity": "medium",
"status": "new",
"location_text": "Main entrance lobby",
"incident_datetime": "2026-06-09T13:36:23.542046+03:00",
"contact_name": "Anonymous Reporter",
"contact_phone": "",
"contact_email": "",
"reporter_staff_id": "",
"patient_file_number": "",
"hospital_name": "Al Nuzha",
"category_name": null,
"created_at": "2026-06-09T13:36:23.540024+03:00",
"updated_at": "2026-06-09T13:36:23.540079+03:00"
}
Appreciations
Create an Appreciation
POST /api/v1/external/appreciations/
Request Body:
| Field | Required | Type | Description |
|---|---|---|---|
hospital |
Yes | string | Hospital name |
message |
Yes | string | Appreciation message |
contact_name |
Yes | string | Submitter name |
contact_phone |
Yes | string | Submitter phone |
Response (201):
{
"success": true,
"reference_number": "APR-20260609-239270",
"status": "draft"
}
List / Retrieve Appreciations
GET /api/v1/external/appreciations/list/
GET /api/v1/external/appreciations/<uuid>/
Retrieve Response:
{
"id": "uuid",
"reference_number": "APR-20260609-239270",
"message_en": "Dr. Ahmed was incredibly kind and thorough.",
"status": "draft",
"hospital_name": "Al Nuzha",
"is_anonymous": false,
"created_at": "2026-06-09T13:36:23.788028+03:00",
"updated_at": "2026-06-09T13:36:23.788077+03:00"
}
Suggestions
Create a Suggestion
POST /api/v1/external/suggestions/
Request Body:
| Field | Required | Type | Description |
|---|---|---|---|
hospital |
Yes | string | Hospital name |
message |
Yes | string | Suggestion message |
contact_name |
Yes | string | Submitter name |
contact_phone |
Yes | string | Submitter phone |
title |
No | string | Title (auto-generated from message if omitted) |
category |
No | string | One of: clinical_care, staff_service, facility, communication, appointment, billing, food_service, cleanliness, technology, general, other |
rating |
No | integer | Rating 1-5 |
Response (201):
{
"success": true,
"reference_number": "SG-20260609-316316",
"status": "submitted"
}
List / Retrieve Suggestions
GET /api/v1/external/suggestions/list/
GET /api/v1/external/suggestions/<uuid>/
Retrieve Response:
{
"id": "uuid",
"reference_number": "SG-20260609-316316",
"title": "Digital queue system",
"message": "Please implement a digital queue management system.",
"category": "technology",
"rating": null,
"status": "submitted",
"feedback_type": "suggestion",
"sentiment": "neutral",
"contact_name": "Suggestor",
"contact_phone": "+966504445566",
"hospital_name": "Al Nuzha",
"created_at": "2026-06-09T13:36:24.051998+03:00",
"updated_at": "2026-06-09T13:36:24.052047+03:00"
}
Doctor Ratings
Submit a Doctor Rating
Submit a patient rating for a doctor. The doctor_id must be an existing employee ID in the Staff table for the given hospital.
POST /api/v1/external/doctor-ratings/
Request Body:
| Field | Required | Type | Description |
|---|---|---|---|
hospital_id |
Yes | integer | Hospital ID |
doctor_id |
Yes | string | Doctor's employee ID (must exist in the hospital's Staff records) |
rating |
Yes | integer | Rating from 1 to 5 |
doctor_name |
No | string | Doctor name (stored for reference) |
feedback |
No | string | Patient feedback text |
rating_date |
No | date | Date of rating (YYYY-MM-DD, defaults to current date) |
patient_uhid |
No | string | Patient unique health ID |
patient_name |
No | string | Patient name |
patient_type |
No | string | One of: IP, OP, ER, DC |
department_name |
No | string | Department name |
admit_date |
No | date | Admission date (YYYY-MM-DD) |
discharge_date |
No | date | Discharge date (YYYY-MM-DD) |
Example:
{
"hospital_id": 1,
"doctor_id": "10738",
"doctor_name": "Dr. Omaymah Yaqoub",
"rating": 4,
"feedback": "Great doctor, very caring and attentive.",
"rating_date": "2026-06-10",
"patient_uhid": "UHID12345",
"patient_name": "Ahmed Ali",
"patient_type": "OP",
"department_name": "Internal Medicine"
}
Response (201):
{
"success": true,
"rating_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"staff_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"doctor_name": "OMAYMAH YAQOUB ELAMEIAN",
"rating": 4
}
Error Responses:
| HTTP Status | Condition |
|---|---|
400 |
doctor_id not found in the hospital's Staff records |
400 |
rating outside 1-5 range |
400 |
hospital_id not found |
403 |
Hospital does not match API key scope |
Error Example (doctor not found):
{
"doctor_id": [
"Doctor with employee ID '99999' not found at this hospital."
]
}
Common Patterns
Location Hierarchy
Locations follow a cascading hierarchy. Use lookup endpoints to populate dependent dropdowns:
1. GET /hospitals/ → pick hospital
2. GET /location-types/ → pick location type
3. GET /areas/?hospital=X → pick area (filtered by hospital)
4. GET /departments/?hospital=X → pick department
5. GET /sections/?hospital=X&department=Y → pick section
Date Filtering
All list endpoints support date range filtering:
GET /complaints/list/?created_from=2026-01-01&created_to=2026-06-30
GET /complaints/list/?updated_from=2026-06-01T08:00:00&updated_to=2026-06-09T17:00:00
- Date-only values (
YYYY-MM-DD):created_fromstarts at 00:00,created_toextends to 23:59:59 - Full datetime values (
YYYY-MM-DDTHH:MM:SS) are also accepted
Pagination
All list endpoints return paginated results:
{
"count": 150,
"page": 1,
"page_size": 20,
"results": [...]
}
| Parameter | Default | Max |
|---|---|---|
page |
1 | - |
page_size |
20 | 100 |
Reference Number Formats
| Entity | Prefix | Format |
|---|---|---|
| Complaint | CMP- |
CMP-YYYYMMDD-XXXXXX |
| Inquiry | INQ- |
INQ-YYYYMMDD-XXXXXX |
| Observation | OBS- |
OBS-XXXXXX (auto-generated by model) |
| Appreciation | APR- |
APR-YYYYMMDD-XXXXXX |
| Suggestion | SG- |
SG-YYYYMMDD-XXXXXX |