# Appointment API > **Prefix:** `/api/v1/appointment*` --- ## GET `/api/v1/appointment-slots` Get all appointment slots (available and booked) for a doctor on a specific date. **Permission:** `PUBLIC` ### Query Parameters | Param | Type | Required | Description | |-------|------|----------|-------------| | `doctor_uuid` | string (UUID) | ✅ | Doctor UUID | | `date` | string | ✅ | Date in `Y-m-d` format (e.g. `2024-06-15`) | ### Response `200` ```json { "success": true, "data": { "doctor_uuid": "550e8400-...", "date": "2024-06-15", "sessions": [ { "start_time": "09:00", "end_time": "13:00", "slots": [ { "start": 1718438400, "end": 1718439600, "start_time": "09:00", "end_time": "09:20", "location_id": null, "is_available": true }, { "start": 1718439600, "end": 1718440800, "start_time": "09:20", "end_time": "09:40", "location_id": null, "is_available": false } ] }, { "start_time": "15:00", "end_time": "17:00", "slots": [...] } ] } } ``` > Returns **all** slots grouped by work shift. `is_available: false` means the slot is either already booked (active pending/confirmed appointment) **or** its start time has already passed (for today's date). Session boundaries match the doctor's `WeeklySchedule` or date override config. > > Returns an **empty** `sessions` array when the date is a holiday, a closed date override, in the past, beyond the doctor's booking window, or when online booking is disabled (see `meta` in `appointment-settings.md`). ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_NOT_FOUND_001` | 404 | Doctor not found | | `ERR_VALIDATION_001` | 422 | Missing or invalid date/doctor_uuid | --- ## GET `/api/v1/appointment-settings/month-availability/{doctorUuid}` Which days of a month are bookable — used by the public calendar to grey out unavailable days. **Permission:** `PUBLIC` ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `doctorUuid` | string (UUID) | Doctor UUID | ### Query Parameters | Param | Type | Required | Description | |-------|------|----------|-------------| | `year` | integer | ✅ | **Gregorian** year (e.g. `2026`) | | `month` | integer | ✅ | Gregorian month `1`–`12` | > Input is Gregorian. A Jalali (Shamsi) front-end must convert the displayed month to the Gregorian month(s) it spans before calling. ### Response `200` ```json { "success": true, "data": { "year": 2026, "month": 6, "disabled_dates": ["2026-06-01", "2026-06-17", "2026-06-26"], "enabled_dates": ["2026-06-15", "2026-06-16", "2026-06-18"], "online_booking_enabled": true, "booking_window": { "value": 1, "unit": "month" } } } ``` | Field | Type | Description | |-------|------|-------------| | `disabled_dates` | string[] | `Y-m-d` days with no bookable slot (holiday / closed override / non-working / past / out-of-window) | | `enabled_dates` | string[] | `Y-m-d` days with at least one slot | | `online_booking_enabled` | boolean | Doctor's online-booking flag | | `booking_window` | object | `{ value, unit }` — `unit` is `week` or `month` | ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_VALIDATION_002` | 404 | Doctor not found | | `ERR_VALIDATION_001` | 422 | Invalid year/month | --- ## POST `/api/v1/appointment` Book an appointment slot. **Permission:** `AUTH` — any authenticated user ### Request Body (`application/json`) ```json { "doctor_uuid": "550e8400-e29b-41d4-a716-446655440000", "slot_start": 1718438400, "slot_end": 1718439600, "for_self": false, "patient_name": "علی احمدی", "patient_mobile": "09120000000", "patient_national_code": "0012345678", "patient_gender": "male", "patient_reason": "چکاپ", "note": "لطفاً سریع ویزیت شوم" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `doctor_uuid` | string (UUID) | ✅ | Doctor UUID | | `slot_start` | integer | ✅ | Slot start (Unix timestamp) | | `slot_end` | integer | ✅ | Slot end (Unix timestamp) | | `for_self` | boolean | ❌ | `true` (default) = patient is the logged-in payer; `false` = booking for someone else | | `patient_name` | string | ⚠️ | Required when `for_self=false`; otherwise filled from the payer's profile | | `patient_mobile` | string | ⚠️ | Required when `for_self=false`; otherwise the payer's mobile | | `patient_national_code` | string | ❌ | Patient national code (only when for another person) | | `patient_gender` | string | ❌ | `male` / `female` | | `patient_reason` | string | ❌ | Reason for visit | | `note` | string | ❌ | Patient note | > **Payer vs patient:** the authenticated user (`user`) is always the payer; the `patient_*` fields describe who the visit is for and are stored separately. **Temporary lock:** the slot is held by the new `pending` booking for **15 minutes** (`expires_at = created_at + 900`). If payment is not completed in time, the booking is moved to `expired` and the slot is freed (see `app:cancel-expired-appointments`). An expired pending booking no longer blocks the slot even before the cron runs. ### Response `201` ```json { "success": true, "data": { "uuid": "appt-uuid-...", "doctor": { "uuid": "...", "name": "دکتر علی احمدی" }, "user": { "uuid": "...", "mobile": "..." }, "slot_start": 1718438400, "slot_end": 1718439600, "status": "pending", "note": "...", "expires_at": 1718438100, "patient_name": "علی احمدی", "patient_mobile": "09120000000", "patient_national_code": "0012345678", "patient_gender": "male", "patient_reason": "چکاپ", "version": 1, "created_at": 1717000000 } } ``` **Appointment Status Values:** | Value | Description | |-------|-------------| | `pending` | Awaiting payment | | `confirmed` | Paid and confirmed | | `cancelled` | Cancelled | | `completed` | Visit completed | | `no_show` | Patient did not show | ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_VALIDATION_002` | 404 | Doctor not found | | `ERR_CONFLICT_001` | 409 | Slot already booked (incl. concurrent booking — the booking is atomic) | | `ERR_VALIDATION_001` | 422 | Invalid slot times, past slot, or missing patient name/mobile when `for_self=false` | --- ## GET `/api/v1/appointment/{uuid}` Get appointment detail. **Permission:** `AUTH` — must be the patient, the doctor, or `ROLE_ADMIN` ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `uuid` | string (UUID) | Appointment UUID | ### Response `200` ```json { "success": true, "data": { "uuid": "appt-uuid-...", "doctor": { "uuid": "...", "name": "دکتر علی احمدی", "specialties": [ { "uuid": "...", "name": "اورولوژی عمومی" } ] }, "address": { "uuid": "...", "name": "مطب دکتر علی احمدی", "address": "یزد، خیابان ...", "telephone": "035...", "map": { "latitude": "31.8", "longitude": "54.3" }, "city": { "id": "132", "name": "یزد" }, "province": { "id": "100", "name": "یزد" } }, "user": { "uuid": "...", "mobile": "09..." }, "slot_start": 1718438400, "slot_end": 1718439600, "status": "confirmed", "note": "...", "patient_name": "...", "patient_mobile": "...", "created_at": 1717000000 } } ``` > `doctor.specialties` آرایه (ممکن است خالی)؛ `address` اولین آدرس پزشک است (ممکن است `null` اگر پزشک آدرسی ندارد). `address.map.latitude/longitude` رشته یا `null`. تاریخ‌ها Unix. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_FORBIDDEN_001` | 403 | Not the patient/doctor/admin | | `ERR_NOT_FOUND_001` | 404 | Appointment not found | --- ## GET `/api/v1/appointments/doctor/{doctorUuid}` Get all appointments for a specific doctor. **Permission:** `AUTH` — must be the doctor, their secretary, or `ROLE_ADMIN` ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `doctorUuid` | string (UUID) | Doctor UUID | ### Query Parameters | Param | Type | Required | Description | |-------|------|----------|-------------| | `status` | string | ❌ | Filter: `pending`, `confirmed`, `cancelled`, `completed`, `no_show` | ### Response `200` ```json { "success": true, "data": [ { "uuid": "...", "user": { "uuid": "...", "real_name": "..." }, "slot_start": 1718438400, "slot_end": 1718439600, "status": "confirmed", "price": 500000 } ] } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_FORBIDDEN_001` | 403 | Not authorized to view this doctor's appointments | | `ERR_NOT_FOUND_001` | 404 | Doctor not found | --- ## GET `/api/v1/appointments/user` Get all appointments for the authenticated user. **Permission:** `AUTH` ### Query Parameters | Param | Type | Required | Description | |-------|------|----------|-------------| | `status` | string | ❌ | Filter by status | ### Response `200` ```json { "success": true, "data": [ { "uuid": "...", "doctor": { "uuid": "...", "title": "دکتر علی احمدی" }, "slot_start": 1718438400, "slot_end": 1718439600, "status": "confirmed", "price": 500000 } ] } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | --- ## PATCH `/api/v1/appointment/{uuid}/status` Change appointment status. **Permission:** `AUTH` — patient can cancel; doctor/secretary can confirm/complete/no_show; admin can do all ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `uuid` | string (UUID) | Appointment UUID | ### Request Body ```json { "status": "cancelled", "version": 3 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `status` | string | ✅ | New status value | | `version` | integer | ❌ | Optimistic lock version (prevents double-submit) | **Allowed Transitions by Role:** | Actor | Allowed transitions | |-------|---------------------| | Patient | `pending → cancelled` | | Doctor / Secretary | `pending → confirmed`, `confirmed → completed`, `confirmed → no_show` | | Admin | Any transition | ### Response `200` Updated appointment object. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_FORBIDDEN_001` | 403 | Not authorized for this transition | | `ERR_NOT_FOUND_001` | 404 | Appointment not found | | `ERR_CONFLICT_001` | 409 | Version mismatch (optimistic lock) | | `ERR_VALIDATION_001` | 422 | Invalid status value | --- ## POST `/api/v1/my/appointment` Create a new appointment for a patient. Used by doctor/clinic/secretary to book appointments on behalf of patients. If no user exists with the given mobile, a new user account is created automatically. **Auth:** `IS_AUTHENTICATED_FULLY` — Roles: `ROLE_DOCTOR`, `ROLE_CLINIC`, `ROLE_SECRETARY`, `ROLE_ADMIN` ### Request Body ```json { "doctor_uuid": "doctor-uuid", "slot_start": 1718438400, "slot_end": 1718439600, "patient_mobile": "09123456789", "patient_name": "علی محمدی", "note": "optional note" } ``` > اگر کاربری با این شماره موبایل وجود نداشته باشد، یک کاربر جدید با نقش `ROLE_USER` ساخته می‌شود. ### Response `201` ```json { "success": true, "data": { "uuid": "appt-uuid", "slot_start": 1718438400, "slot_end": 1718439600, "status": "pending" } } ``` ### Error Responses | Code | HTTP | Description | |------|------|-------------| | `FORBIDDEN` | 403 | Role not allowed | | `VALIDATION` | 422 | Missing required fields | | `DOCTOR_NOT_FOUND` | 404 | Doctor UUID not found | | `SLOT_TAKEN` | 409 | Slot already booked | --- ## GET /api/v1/my/appointments Role-aware paginated list of appointments. Returns only what the authenticated user is authorized to see. **Auth:** `IS_AUTHENTICATED_FULLY` (any role) **Role behavior:** | Role | Scope | |------|-------| | `ROLE_ADMIN` | All appointments | | `ROLE_CLINIC` | Appointments for doctors in this clinic | | `ROLE_DOCTOR` | Appointments for this doctor | | `ROLE_SECRETARY` | Appointments for the linked doctor (empty if `appointments.view` permission is false) | | (plain patient `ROLE_USER`) | The patient's own appointments (`a.user = current user`) | ### Query Parameters | Param | Type | Default | Description | |-------|------|---------|-------------| | `page` | int | 1 | Page number | | `limit` | int | 15 | Items per page (max 100) | | `search` | string | — | Search by mobile, real name, or doctor name | | `status` | string | — | Filter by appointment status | | `date` | string | — | Filter by date in `Y-m-d` format | ### Response `200` ```json { "success": true, "data": [ { "uuid": "string", "patient_name": "string", "patient_mobile": "string", "doctor_name": "string", "clinic_name": "string | null", "appointment_date": "2026-07-25", "appointment_time": "14:30", "slot_start": 1700000000, "status": "reserved", "amount": 0, "created_at": "ISO 8601 string" } ], "meta": { "totalRecords": 8000, "totalPages": 533, "currentPage": 1 } }