# PX360 External API Documentation Base URL: `/api/v1/external/` ## Authentication All requests require an `X-API-Key` header. ``` X-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: ```python 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:** ```json { "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:** ```json { "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= ``` | Parameter | Required | Description | |---|---|---| | `hospital` | Yes | Hospital name (case-insensitive) | | `location_type` | No | Filter by location type (`OP`, `IP`, `ER`, `GENERAL`) | **Response:** ```json { "count": 27, "results": [ { "id": "uuid", "name": "Emergency", "name_ar": "", "code": "emergency" } ] } ``` ### List Departments ``` GET /api/v1/external/departments/?hospital= ``` | Parameter | Required | Description | |---|---|---| | `hospital` | Yes | Hospital name (case-insensitive) | **Response:** ```json { "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=&department= ``` | Parameter | Required | Description | |---|---|---| | `hospital` | Yes | Hospital name (case-insensitive) | | `department` | Yes | Department name (case-insensitive) | **Response:** ```json { "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:** ```json { "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):** ```json { "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// ``` **Response:** ```json { "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//satisfaction/ ``` **Request Body:** | Field | Required | Type | Description | |---|---|---|---| | `satisfaction` | Yes | string | One of: `satisfied`, `neutral`, `dissatisfied`, `no_response` | **Example:** ```json { "satisfaction": "satisfied" } ``` **Response (200):** ```json { "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):** ```json { "success": true, "reference_number": "INQ-20260609-124293", "status": "open" } ``` ### List / Retrieve Inquiries ``` GET /api/v1/external/inquiries/list/ GET /api/v1/external/inquiries// ``` Same pagination and date/status filters as complaints. **Retrieve Response:** ```json { "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):** ```json { "success": true, "reference_number": "OBS-O8TCIC", "status": "new" } ``` ### List / Retrieve Observations ``` GET /api/v1/external/observations/list/ GET /api/v1/external/observations// ``` **Retrieve Response:** ```json { "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):** ```json { "success": true, "reference_number": "APR-202607-0001", "status": "draft" } ``` ### List / Retrieve Appreciations ``` GET /api/v1/external/appreciations/list/ GET /api/v1/external/appreciations// ``` **Retrieve Response:** ```json { "id": "uuid", "reference_number": "APR-202607-0001", "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):** ```json { "success": true, "reference_number": "SGT-202607-0001", "status": "submitted" } ``` ### List / Retrieve Suggestions ``` GET /api/v1/external/suggestions/list/ GET /api/v1/external/suggestions// ``` **Retrieve Response:** ```json { "id": "uuid", "reference_number": "SGT-202607-0001", "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` should identify the doctor (existing staff matched by employee ID, license number, or name; otherwise a new Staff record is auto-created — see below). ``` POST /api/v1/external/doctor-ratings/ ``` **Request Body:** | Field | Required | Type | Description | |---|---|---|---| | `hospital` | Yes | string | Hospital name (case-insensitive) | | `doctor_id` | Yes | string | Doctor's employee ID | | `rating` | Yes | integer | Rating from 1 to 5 | | `doctor_name` | Conditional | string | Doctor name. Required when `doctor_id` does not match an existing staff record (see auto-creation below). Otherwise 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`) | **Staff lookup & auto-creation:** When a rating is submitted, the server looks up the doctor by (in order) `employee_id`, `license_number`, then `doctor_name` (exact / contains / first+last name split). If no existing staff is matched, a new Staff record is **auto-created** with `staff_type=physician`, `job_title="Physician"`, `employee_id=doctor_id`, and the provided name. In that case `doctor_name` is required; omitting it returns `400`. The auto-creation is logged as an `external_staff_auto_created` audit event. **Example:** ```json { "hospital": "Al Nuzha", "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):** ```json { "success": true, "rating_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "rating": 4 } ``` **Error Responses:** | HTTP Status | Condition | |---|---| | `400` | `doctor_id` matches no existing staff AND `doctor_name` is missing (cannot auto-create) | | `400` | `rating` outside 1-5 range | | `400` | `hospital` not found | | `403` | Hospital does not match API key scope | **Error Example (cannot auto-create without a name):** ```json { "doctor_name": [ "Staff not found for this doctor_id; provide doctor_name to auto-create the staff record." ] } ``` ### List Doctor Ratings Returns a paginated list of doctor ratings, scoped to the API key's hospital. ``` GET /api/v1/external/doctor-ratings/list/ ``` **Query Parameters (all optional):** | Parameter | Description | |---|---| | `doctor_id` | Filter by HIS employee ID (exact match) | | `rating` | Filter by rating value 1-5 (exact match) | | `rating_from` | Rating-date lower bound (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`) | | `rating_to` | Rating-date upper bound (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`) | | `created_from` / `created_to` | Created-at date range | | `updated_from` / `updated_to` | Updated-at date range | | `page` | Page number (default `1`) | | `page_size` | Page size (default `20`, max `100`) | **Response (200):** ```json { "count": 42, "page": 1, "page_size": 20, "results": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "staff_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "hospital_name": "Al Nuzha", "source": "his_api", "source_reference": "", "doctor_id": "10738", "doctor_name": "OMAYMAH YAQOUB ELAMEIAN", "doctor_name_raw": "10738 - OMAYMAH YAQOUB ELAMEIAN", "doctor_name_display": "OMAYMAH YAQOUB ELAMEIAN", "department_name": "Internal Medicine", "patient_uhid": "UHID12345", "patient_name": "Ahmed Ali", "patient_type": "OP", "admit_date": null, "discharge_date": null, "rating": 4, "feedback": "Great doctor, very caring and attentive.", "rating_date": "2026-06-10T00:00:00+03:00", "is_aggregated": false, "aggregated_at": null, "created_at": "2026-06-10T14:22:11+03:00", "updated_at": "2026-06-10T14:22:11+03:00" } ] } ``` ### Retrieve a Doctor Rating Retrieve a single rating by its UUID (returned in the `rating_id` field on creation). ``` GET /api/v1/external/doctor-ratings// ``` **Response (200):** Same object shape as a single item from the list endpoint. **Error Responses:** | HTTP Status | Condition | |---|---| | `404` | Rating does not exist, or is outside the API key's hospital scope | | `403` | API key does not have access to `doctor_ratings` | --- ## 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_from` starts at 00:00, `created_to` extends to 23:59:59 - Full datetime values (`YYYY-MM-DDTHH:MM:SS`) are also accepted ### Pagination All list endpoints return paginated results: ```json { "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-YYYYMM-NNNN` | | Suggestion | `SGT-` | `SGT-YYYYMM-NNNN` |