What is Clarity Clinic?
Clarity Clinic is a clinic appointment and booking system built as a monorepo. It comprises three apps: a Backend API (ASP.NET Core 8) that owns all business logic and data, a Patient app (React 19) for self-service booking, and a Staff dashboard (Angular 20) for admins, receptionists, and doctors.
The backend exposes a JSON/REST API secured with custom JWT authentication and role-based authorization,
featuring a 15-minute slot engine, race-safe booking, an HMAC-signed payment webhook, and
Accept-Language localization (English/Arabic). This page is the single reference for the whole
system — read it to understand the architecture and to call every endpoint.
Backend API :5180
Single source of truth behind both apps.
- Custom JWT auth + BCrypt
- Race-safe 15-min slot engine
- HMAC-signed payment webhook
- Swagger UI at
/swagger
Patient app :5173
Public self-service booking experience.
- Browse doctors & services
- Book and pay online
- View appointments & prescriptions
- AR / EN with full RTL
Staff dashboard :4321
Internal control room for the clinic.
- Admin — staff, services, reports
- Receptionist — desk & calendar
- Doctor — schedule & visits
- AR / EN RTL, light / dark
System architecture
Clarity Clinic is a classic three-tier system: two frontend clients talk to one backend API, which persists everything in SQL Server.
The backend is layered: Controllers → Services → EF Core DbContext. Controllers are thin HTTP adapters;
services hold business logic (slot engine, booking, payments); EF Core maps to SQL Server LocalDB (database ClinicDb).
The API uses custom JWT auth (BCrypt password hashing, no ASP.NET Identity UI) and role-based [Authorize] attributes.
Cross-cutting features include a global exception envelope, restrictive CORS (only the two known frontends), and JSON-file
localization keyed off the Accept-Language header.
:5173 and staff dashboard :4321. Clients:PatientApp and Clients:StaffApp each accept a comma-separated list of origins, so several dev origins can be allowed at once. The payment gateway calls POST /api/payments/webhook with an X-Signature header.
Tech stack
| Layer | App | Stack | URL |
|---|---|---|---|
| Backend API | backend | ASP.NET Core 8 (C#), EF Core, SQL Server LocalDB, custom JWT + BCrypt, JSON i18n | http://localhost:5180 |
| Patient app | patient-app | React 19 + Vite + TypeScript, TanStack Query, Axios, Zustand, react-i18next (AR/EN RTL) | http://localhost:5173 |
| Staff dashboard | staff-dashboard | Angular 20 (standalone components + signals), AR/EN RTL, light/dark | http://localhost:4321 |
| Database | — | SQL Server LocalDB, database ClinicDb (auto-migrated + seeded on backend boot) | — |
Swagger UI is served at http://localhost:5180/swagger.
Getting started
All three apps run locally on the ports above. Start the backend first — it auto-migrates and seeds the database on boot.
Building a client? The must-read companion is the Frontend integration notes (docs/FRONTEND.md).
Backend API
cd backend
dotnet run --project Clinic.Api --urls http://localhost:5180
Important — ports & CORS. The default launchSettings profile binds port 5010. To match the documented ports and let the staff dashboard pass CORS, run on 5180 and set the staff origin via env var.
# PowerShell $env:Clients__StaffApp = "http://localhost:4321" dotnet run --project Clinic.Api --urls http://localhost:5180 # bash Clients__StaffApp=http://localhost:4321 dotnet run --project Clinic.Api --urls http://localhost:5180
On boot the backend applies EF Core migrations and seeds data (see Seeded credentials) into SQL Server LocalDB (ClinicDb).
Reset the database (drops it; the next run re-migrates and re-seeds)
cd backend
dotnet ef database drop --force --project Clinic.Api
dotnet run --project Clinic.Api --urls http://localhost:5180
Patient app :5173
cd patient-app
npm install
npm run dev
Runs at http://localhost:5173. Reads VITE_API_URL (default http://localhost:5180).
Staff dashboard :4321
cd staff-dashboard
npm install
npm start -- --port 4321
Runs at http://localhost:4321. API base in src/app/core/config.ts → http://localhost:5180/api.
Smoke test
A scripted end-to-end check (35 assertions) against a running backend:
API_BASE=http://localhost:5180 node backend/smoke-test.mjs
Authentication
The API uses JWT Bearer tokens. Passwords are hashed with BCrypt; there is no ASP.NET Identity UI.
POST /api/auth/register— patient self-signup. Returns{ token, user }.POST /api/auth/login— any user. Returns{ token, user }.
Send the token on every authenticated request:
Authorization: Bearer <token>
Optionally set Accept-Language: en or Accept-Language: ar to localize response messages.
Status codes: 401 = missing or invalid token; 403 = authenticated but wrong role.
Response shape
Both register and login return { token, user }, where user is:
{
"id": "string",
"name": "string",
"email": "string",
"phone": "string",
"role": "Patient | Doctor | Receptionist | Admin",
"isActive": true,
"avatarUrl": "string | null"
}
Example — log in and capture the token
# Log in as the seeded patient and store the JWT in $TOKEN TOKEN=$(curl -s -X POST http://localhost:5180/api/auth/login \ -H "Content-Type: application/json" \ -H "Accept-Language: en" \ -d '{ "email": "patient@clinic.local", "password": "Pat#123" }' \ | python -c "import sys, json; print(json.load(sys.stdin)['token'])") echo "$TOKEN" # Use it curl -s http://localhost:5180/api/auth/me \ -H "Authorization: Bearer $TOKEN"
In the examples below, $TOKEN is the JWT obtained from login, and {{baseUrl}} = http://localhost:5180.
Roles & permissions
There are four roles (the UserRole enum): Patient, Doctor, Receptionist, Admin. Permissions are enforced
server-side via [Authorize(Roles = ...)]; the UI only hides controls. Anonymous endpoints (browse doctors/services, payment webhook) need no token.
| Capability | Patient | Doctor | Receptionist | Admin | Anonymous |
|---|---|---|---|---|---|
| Register (self-signup) | — | — | — | — | ✓ |
| Log in / view own profile / change password | ✓ | ✓ | ✓ | ✓ | — |
| Browse doctors & services, view open slots | ✓ | ✓ | ✓ | ✓ | ✓ |
| Book own appointment | ✓ | — | — | — | — |
| View own appointments / prescriptions / cancel own | ✓ | — | — | — | — |
| Day calendar (all appointments by date/doctor/status) | — | — | ✓ | ✓ | — |
| Walk-in booking / reschedule | — | — | ✓ | ✓ | — |
| Check-in (Arrived) / No-show / mark cash-paid | — | — | ✓ | ✓ | — |
| Manage services (create/update/delete) | — | — | ✓ | ✓ | — |
| Manage doctor availability / blocked dates | — | — | ✓ | ✓ | — |
| Create / search patients | — | — | ✓ | ✓ | — |
| Complete visit / write & edit prescription | — | ✓ | — | — | — |
| Doctor schedule / patient history | — | ✓ | — | — | — |
| Manage staff (create doctors/receptionists, activate) | — | — | — | ✓ | — |
| Reports | — | — | — | ✓ | — |
| Dashboard stats | — | — | ✓ | ✓ | — |
| Payment webhook / dev mock-pay | ✓* | ✓* | ✓* | ✓* | ✓ |
* Payment endpoints are AllowAnonymous (the gateway calls them), so any caller may reach them.
Appointment lifecycle
An appointment moves through the AppointmentStatus enum. The path depends on how it was booked.
- Online + paymentMethod
Online→PendingPayment; after payment →Confirmed. - InClinic +
Cash→Confirmedimmediately (the patient pays at the clinic; reception later marks it cash-paid). Confirmed→Arrived(reception checks the patient in) →Completed(the doctor completes the visit and writes diagnosis/prescription).ConfirmedorArrived→NoShow(reception).ConfirmedorPendingPayment→Cancelled(patient or reception). The slot is freed and can be rebooked.
Booking the same slot twice returns 409 Conflict. Double-booking is race-safe on the server.
The slot engine
Availability is computed on a 15-minute grid. Given a doctor, a date, and a service, the engine returns the start times at which an appointment of that service's duration fits without overlapping existing bookings.
- Slots are 15-minute increments (
"09:00", "09:15", "09:30", ...). - Duration-fit: a slot is only offered if the full service duration fits inside the doctor's working window for that day.
- Overlap-safe: slots that would collide with existing active appointments are excluded.
- The doctor's working days are Sunday–Thursday (per seed data); blocked dates and availability windows further constrain results.
Fetch open slots
curl -s "http://localhost:5180/api/doctors/1/slots?date=2026-06-21&serviceId=1" # → { "date": "2026-06-21", "slots": ["09:00","09:15","09:30", ...] }
Race-safe booking. Booking runs inside a DB transaction, and a filtered unique index on (DoctorId, Date, StartTime) (restricted to active statuses) guarantees that two concurrent bookings of the same slot cannot both succeed — the loser gets 409 Conflict. Because the index is filtered to active statuses, cancelling an appointment frees the slot for rebooking.
Payment flow
Two booking paths determine whether a payment is required.
- Online +
Onlinepayment → appointment is created asPendingPaymentand the booking response includes apaymentobject with acheckoutUrl. After payment is confirmed, the appointment becomesConfirmed. - InClinic +
Cash→ appointment is created asConfirmedimmediately,paymentisnull. Reception later marks it cash-paid viaPUT /api/appointments/{id}/cash-paid.
Two payment endpoints (both AllowAnonymous):
POST /api/payments/webhook— the real, production gateway callback. It is HMAC-signed: the gateway sends the signature in theX-Signatureheader, with body{ transactionRef, status }. The handler is idempotent.POST /api/payments/mock/pay— a dev-only helper that simulates the gateway: it signs the payload server-side and drives the same signed, idempotent webhook handler, marking the appointmentPaid→Confirmed. Body{ appointmentId, status? }(statusdefaults to"Paid"). Idempotent — calling it twice leaves the appointmentPaidonce. Returns{ "paid": <bool> }.
PaymentStatus enum: Pending, Paid, Failed, Refunded. PaymentMethod enum: Online, Cash.
Localization
User-facing API messages are localized from JSON resource files (Resources/en.json, Resources/ar.json)
resolved from the request's Accept-Language header. Send Accept-Language: en or Accept-Language: ar:
curl -s -X POST http://localhost:5180/api/auth/login \ -H "Content-Type: application/json" \ -H "Accept-Language: ar" \ -d '{ "email": "nope@x.com", "password": "wrong" }'
The frontends mirror these keys (react-i18next / Angular), and an interceptor sends Accept-Language so backend messages return in the active language.
Error format
Every error is returned with the matching HTTP status code and a consistent JSON envelope.
{
"error": "validation",
"message": "Human-readable, localized message.",
"details": ["optional", "field-level", "messages"]
}
| HTTP | error code | Meaning |
|---|---|---|
| 400 | validation | Invalid input / failed validation |
| 401 | unauthorized | Missing or invalid token |
| 403 | forbidden | Authenticated but wrong role |
| 404 | not_found | Resource does not exist |
| 409 | conflict | Slot already taken / state conflict |
| 500 | server_error | Unexpected error (details are not leaked) |
End-to-end flows (curl)
Four real journeys through the system. Set up the base URL once; each flow logs in to get its own token.
export baseUrl=http://localhost:5180
a Patient books online + pays
# 1) Log in as the patient TOKEN=$(curl -s -X POST $baseUrl/api/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "patient@clinic.local", "password": "Pat#123" }' \ | python -c "import sys,json;print(json.load(sys.stdin)['token'])") # 2) Find an open slot (doctor 1, service 1 = General Consultation) curl -s "$baseUrl/api/doctors/1/slots?date=2026-06-21&serviceId=1" # 3) Book ONLINE with ONLINE payment → PendingPayment, payment.checkoutUrl returned curl -s -X POST $baseUrl/api/appointments \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "doctorId": 1, "serviceId": 1, "date": "2026-06-21", "startTime": "09:00", "mode": "Online", "paymentMethod": "Online" }' # → 201 { "appointment": { "id": 10, "status": "PendingPayment", "meetingLink": "...", "endTime": "09:30", ... }, # "payment": { "id": 5, "checkoutUrl": "https://gateway/checkout/..." } } # 4) Simulate payment (dev) → appointment becomes Confirmed curl -s -X POST $baseUrl/api/payments/mock/pay \ -H "Content-Type: application/json" \ -d '{ "appointmentId": 10 }' # → { "paid": true } # 5) Confirm curl -s "$baseUrl/api/appointments/10" -H "Authorization: Bearer $TOKEN" # → appointment.status == "Confirmed"
b Patient books cash / in-clinic
TOKEN=$(curl -s -X POST $baseUrl/api/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "patient@clinic.local", "password": "Pat#123" }' \ | python -c "import sys,json;print(json.load(sys.stdin)['token'])") # Book IN-CLINIC with CASH → Confirmed immediately, payment is null curl -s -X POST $baseUrl/api/appointments \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "doctorId": 1, "serviceId": 2, "date": "2026-06-21", "startTime": "10:00", "mode": "InClinic", "paymentMethod": "Cash" }' # → 201 { "appointment": { "status": "Confirmed", ... }, "payment": null }
c Reception walk-in
RTOKEN=$(curl -s -X POST $baseUrl/api/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "reception@clinic.local", "password": "Recep#123" }' \ | python -c "import sys,json;print(json.load(sys.stdin)['token'])") # Walk-in for a brand-new patient (or pass "patientId" for an existing one) curl -s -X POST $baseUrl/api/appointments/walk-in \ -H "Authorization: Bearer $RTOKEN" \ -H "Content-Type: application/json" \ -d '{ "newPatient": { "name": "Sara Nabil", "phone": "+201000000000", "email": "sara@example.com" }, "doctorId": 1, "serviceId": 1, "date": "2026-06-21", "startTime": "11:00", "mode": "InClinic" }'
d Check-in → doctor completes visit + prescription → patient views it
# Reception logs in and checks the patient in (appt id 10, Confirmed → Arrived) RTOKEN=$(curl -s -X POST $baseUrl/api/auth/login -H "Content-Type: application/json" \ -d '{ "email": "reception@clinic.local", "password": "Recep#123" }' \ | python -c "import sys,json;print(json.load(sys.stdin)['token'])") curl -s -X PUT $baseUrl/api/appointments/10/arrived -H "Authorization: Bearer $RTOKEN" # Doctor logs in, records the visit (diagnosis + prescription), then completes DTOKEN=$(curl -s -X POST $baseUrl/api/auth/login -H "Content-Type: application/json" \ -d '{ "email": "doctor@clinic.local", "password": "Doc#123" }' \ | python -c "import sys,json;print(json.load(sys.stdin)['token'])") curl -s -X POST $baseUrl/api/appointments/10/visit \ -H "Authorization: Bearer $DTOKEN" \ -H "Content-Type: application/json" \ -d '{ "diagnosis": "Seasonal allergic rhinitis", "notes": "Advise antihistamines; review in 2 weeks.", "prescription": [ { "drug": "Loratadine", "dosage": "10mg once daily", "duration": "14 days" } ] }' curl -s -X PUT $baseUrl/api/appointments/10/complete -H "Authorization: Bearer $DTOKEN" # Patient logs in and views their prescriptions PTOKEN=$(curl -s -X POST $baseUrl/api/auth/login -H "Content-Type: application/json" \ -d '{ "email": "patient@clinic.local", "password": "Pat#123" }' \ | python -c "import sys,json;print(json.load(sys.stdin)['token'])") curl -s $baseUrl/api/me/prescriptions -H "Authorization: Bearer $PTOKEN"
Full API reference
{{baseUrl}} = http://localhost:5180. Authenticated calls send Authorization: Bearer $TOKEN.
All requests/responses are JSON. Add Accept-Language: en|ar to localize messages.
Auth
/api/auth5 endpointsPatient self-signup. Returns { token, user }.
Request body
{ "displayName": "New Patient", "email": "new@example.com", "phone": "+201111111111", "password": "Secret#1" }curl
curl -s -X POST {{baseUrl}}/api/auth/register \
-H "Content-Type: application/json" \
-d '{ "displayName": "New Patient", "email": "new@example.com", "phone": "+201111111111", "password": "Secret#1" }'Log in as any user → { token, user }.
Request body
{ "email": "patient@clinic.local", "password": "Pat#123" }curl
curl -s -X POST {{baseUrl}}/api/auth/login \
-H "Content-Type: application/json" \
-d '{ "email": "patient@clinic.local", "password": "Pat#123" }'Returns the current user's profile.
curl -s {{baseUrl}}/api/auth/me -H "Authorization: Bearer $TOKEN"Update own profile.
Request body
{ "displayName": "Ahmed S.", "phone": "+201234567890", "avatarUrl": null }curl
curl -s -X PUT {{baseUrl}}/api/auth/profile \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "displayName": "Ahmed S.", "phone": "+201234567890", "avatarUrl": null }'Change own password.
Request body
{ "current": "Pat#123", "new": "NewPass#1" }curl
curl -s -X PUT {{baseUrl}}/api/auth/password \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "current": "Pat#123", "new": "NewPass#1" }'Doctors & Slots
/api/doctors8 endpointsPaged, filterable doctor list → { items, total, page, pageSize }.
Query params
specialization,serviceId,availableOn,mode,search,page,pageSize
curl -s "{{baseUrl}}/api/doctors?search=layla&mode=Online&page=1&pageSize=20"Doctor details including services. Each doctor object also carries rating (decimal), reviewCount (int), and yearsExperience (int) — a doctor with no reviews returns rating: 0, reviewCount: 0. The same three fields appear on every doctor in the GET /api/doctors list.
curl -s {{baseUrl}}/api/doctors/1
# → {
"id": 1,
"name": "Dr. Layla Hassan",
"specialization": "General Medicine",
"rating": 4.8,
"reviewCount": 37,
"yearsExperience": 12,
"services": [ ... ]
}Open slots for a service on a date → { date, slots: ["09:00", ...] }.
Query params
daterequired —yyyy-MM-ddserviceIdrequired
curl -s "{{baseUrl}}/api/doctors/1/slots?date=2026-06-21&serviceId=1"Get the doctor's weekly availability.
curl -s {{baseUrl}}/api/doctors/1/availability -H "Authorization: Bearer $TOKEN"Replace the doctor's weekly availability with an array of windows.
Request body
[ { "dayOfWeek": 0, "startTime": "09:00", "endTime": "17:00" } ]curl
curl -s -X PUT {{baseUrl}}/api/doctors/1/availability \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '[ { "dayOfWeek": 0, "startTime": "09:00", "endTime": "17:00" } ]'List the doctor's blocked days.
curl -s "{{baseUrl}}/api/doctors/{id}/blocked-dates" -H "Authorization: Bearer $TOKEN"
Block a date for the doctor.
Request body
{ "date": "2026-06-25" }curl
curl -s -X POST {{baseUrl}}/api/doctors/1/blocked-dates \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "date": "2026-06-25" }'Unblock a previously blocked date (date in the path).
curl -s -X DELETE {{baseUrl}}/api/doctors/1/blocked-dates/2026-06-25 \
-H "Authorization: Bearer $TOKEN"Services
/api/services4 endpointsList all services.
curl -s {{baseUrl}}/api/servicesCreate a service.
Request body
{ "name": "Allergy Test", "durationMinutes": 30, "price": 450, "doctorId": 1 }curl
curl -s -X POST {{baseUrl}}/api/services \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "name": "Allergy Test", "durationMinutes": 30, "price": 450, "doctorId": 1 }'Update a service.
Request body
{ "name": "Allergy Test", "durationMinutes": 30, "price": 500 }curl
curl -s -X PUT {{baseUrl}}/api/services/1 \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "name": "Allergy Test", "durationMinutes": 30, "price": 500 }'Delete a service.
curl -s -X DELETE {{baseUrl}}/api/services/1 -H "Authorization: Bearer $TOKEN"Appointments — Patient
5 endpointsBooking response (201): { "appointment": { ... }, "payment": { "id", "checkoutUrl" } | null }. For Online + Online the appointment is PendingPayment, payment.checkoutUrl is present, and the appointment also carries meetingLink and endTime. For Cash, the appointment is Confirmed and payment is null.
Book an appointment → 201 booking response (409 if the slot is taken).
Request body
{ "doctorId": 1, "serviceId": 1, "date": "2026-06-21", "startTime": "09:00", "mode": "Online", "paymentMethod": "Online" }curl
curl -s -X POST {{baseUrl}}/api/appointments \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "doctorId":1, "serviceId":1, "date":"2026-06-21", "startTime":"09:00", "mode":"Online", "paymentMethod":"Online" }'List the patient's own appointments.
Query params
when—upcoming|paststatus
curl -s "{{baseUrl}}/api/me/appointments?when=upcoming" -H "Authorization: Bearer $TOKEN"
List the patient's own prescriptions.
curl -s {{baseUrl}}/api/me/prescriptions -H "Authorization: Bearer $TOKEN"Get one appointment. Returns the AppointmentDto directly (no wrapper).
curl -s {{baseUrl}}/api/appointments/10 -H "Authorization: Bearer $TOKEN"Cancel an appointment — frees the slot for rebooking.
curl -s -X PUT {{baseUrl}}/api/appointments/10/cancel -H "Authorization: Bearer $TOKEN"Appointments — Front desk
Admin · Receptionist6 endpointsDay calendar — all appointments for a date.
Query params
date,doctorId,status
curl -s "{{baseUrl}}/api/appointments?date=2026-06-21&doctorId=1&status=Confirmed" \ -H "Authorization: Bearer $TOKEN"
Walk-in booking. Pass patientId for an existing patient, or newPatient to create one. Returns the AppointmentDto directly.
Request body
{ "patientId": 3, // or "newPatient": { "name", "phone", "email" }
"doctorId": 1, "serviceId": 1, "date": "2026-06-21", "startTime": "11:00", "mode": "InClinic" }curl
curl -s -X POST {{baseUrl}}/api/appointments/walk-in \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "patientId": 3, "doctorId":1, "serviceId":1, "date":"2026-06-21", "startTime":"11:00", "mode":"InClinic" }'Move an appointment to another slot.
Request body
{ "date": "2026-06-22", "startTime": "10:00" }curl
curl -s -X PUT {{baseUrl}}/api/appointments/10/reschedule \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "date":"2026-06-22", "startTime":"10:00" }'Check the patient in (Confirmed → Arrived).
curl -s -X PUT {{baseUrl}}/api/appointments/10/arrived -H "Authorization: Bearer $TOKEN"Mark the appointment as a no-show.
curl -s -X PUT {{baseUrl}}/api/appointments/10/no-show -H "Authorization: Bearer $TOKEN"Record a cash payment for an in-clinic appointment.
curl -s -X PUT {{baseUrl}}/api/appointments/10/cash-paid -H "Authorization: Bearer $TOKEN"Appointments — Doctor
Doctor5 endpointsComplete the visit (→ Completed).
curl -s -X PUT {{baseUrl}}/api/appointments/10/complete -H "Authorization: Bearer $TOKEN"Record the visit: diagnosis, notes, and a prescription list.
Request body
{ "diagnosis": "...", "notes": "...",
"prescription": [ { "drug": "Loratadine", "dosage": "10mg daily", "duration": "14 days" } ] }curl
curl -s -X POST {{baseUrl}}/api/appointments/10/visit \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "diagnosis":"...", "notes":"...", "prescription":[{ "drug":"Loratadine","dosage":"10mg daily","duration":"14 days" }] }'Edit a visit (same-day only).
Request body
{ "diagnosis": "...", "notes": "...", "prescription": [ ... ] }curl
curl -s -X PUT {{baseUrl}}/api/visits/4 \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "diagnosis":"...", "notes":"...", "prescription":[ ... ] }'The doctor's schedule for a date.
Query params
date
curl -s "{{baseUrl}}/api/doctor/schedule?date=2026-06-21" -H "Authorization: Bearer $TOKEN"
A patient's visit history.
curl -s {{baseUrl}}/api/patients/3/history -H "Authorization: Bearer $TOKEN"Patients
/api/patients · Admin · Receptionist2 endpointsCreate a patient record.
Request body
{ "name": "Sara Nabil", "phone": "+201000000000", "email": "sara@example.com" }curl
curl -s -X POST {{baseUrl}}/api/patients \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "name":"Sara Nabil", "phone":"+201000000000", "email":"sara@example.com" }'Search patients.
Query params
search
curl -s "{{baseUrl}}/api/patients?search=ahmed" -H "Authorization: Bearer $TOKEN"
Admin
/api/admin · Admin only6 endpointsList all staff members.
curl -s {{baseUrl}}/api/admin/staff -H "Authorization: Bearer $TOKEN"Create a doctor → returns the doctor plus login credentials. Note: a new doctor has no services yet — create them via POST /api/services before booking.
Request body
{ "name": "Dr. Omar Khaled", "email": "omar@clinic.local", "phone": "+201222222222",
"password": "Doc#456", "specialization": "Dermatology", "bio": "...", "photoUrl": null,
"availability": [ { "dayOfWeek": 0, "startTime": "09:00", "endTime": "15:00" } ] }curl
curl -s -X POST {{baseUrl}}/api/admin/doctors \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "name":"Dr. Omar Khaled", "email":"omar@clinic.local", "phone":"+201222222222",
"password":"Doc#456", "specialization":"Dermatology", "bio":"...", "photoUrl":null,
"availability":[{ "dayOfWeek":0, "startTime":"09:00", "endTime":"15:00" }] }'Create a receptionist → returns the user plus login credentials.
Request body
{ "name": "Mona Adel", "email": "mona@clinic.local", "phone": "+201333333333", "password": "Recep#456" }curl
curl -s -X POST {{baseUrl}}/api/admin/receptionists \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "name":"Mona Adel", "email":"mona@clinic.local", "phone":"+201333333333", "password":"Recep#456" }'Update a staff member.
Request body
{ "name": "...", "phone": "...", "specialization": "...", "bio": "...", "photoUrl": null }curl
curl -s -X PUT {{baseUrl}}/api/admin/staff/5 \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "name":"Dr. Omar K.", "phone":"+201222222222", "specialization":"Dermatology", "bio":"...", "photoUrl":null }'Activate or deactivate a staff member.
Request body
{ "isActive": false }curl
curl -s -X PUT {{baseUrl}}/api/admin/staff/5/active \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "isActive": false }'Clinic reports.
curl -s {{baseUrl}}/api/admin/reports -H "Authorization: Bearer $TOKEN"Dashboard
Admin · Receptionist1 endpointDashboard statistics.
curl -s {{baseUrl}}/api/dashboard/stats -H "Authorization: Bearer $TOKEN"Payments
/api/payments · Anonymous2 endpointsProduction HMAC-signed, idempotent gateway callback → { received: true }. The gateway sends the signature in the X-Signature header.
Headers & body
# header: X-Signature: <hmac-of-body> { "transactionRef": "txn_abc123", "status": "Paid" }
curl
curl -s -X POST {{baseUrl}}/api/payments/webhook \
-H "Content-Type: application/json" \
-H "X-Signature: <hmac-of-body>" \
-d '{ "transactionRef": "txn_abc123", "status": "Paid" }'
# → { "received": true }Dev helper: simulates the gateway, marking the appointment Paid → Confirmed → { paid }. Idempotent.
Request body
{ "appointmentId": 10, "status": "Paid" } // status defaults to "Paid"curl
curl -s -X POST {{baseUrl}}/api/payments/mock/pay \
-H "Content-Type: application/json" \
-d '{ "appointmentId": 10 }'
# → { "paid": true }System
ops · not under /apiprobeLiveness/readiness probe — returns { "status": "healthy" } (200) when the database is reachable, else 503. Operational endpoint, not part of the 44 /api total.
curl -s {{baseUrl}}/health
# → { "status": "healthy" }Gotchas & contract notes
These are the non-obvious behaviours that trip people up. All verified against the live API.
- Only
POST /api/appointmentswraps its response. Booking returns{ appointment, payment }. Every other appointment endpoint —walk-in,GET /appointments/{id},cancel,reschedule,arrived,no-show,cash-paid,complete— returns theAppointmentDtodirectly (noappointment/paymentwrapper). Don't reach for.appointmenton those. - Online vs. cash booking differ:
mode: Online+paymentMethod: Online→201PendingPaymentwithpayment.checkoutUrland ameetingLink;mode: InClinic+paymentMethod: Cash→201Confirmedwithpayment: null(paid at the desk → reception callscash-paidlater). - Slots respect service duration, not just the 15-min grid. A slot is only offered if the whole service fits without overlap. Booking a 30-min service at
09:00removes both09:00and09:15from availability. Past times for today are filtered out; days off (Fri/Sat) and blocked dates return zero slots. - Always re-fetch slots after a booking, and handle
409("that slot was just taken") by re-fetching and asking the user to re-pick. Double-booking is race-safe on the server. - A newly admin-added doctor has no services.
POST /api/admin/doctorscreates the login + availability but not bookable services — create them viaPOST /api/servicesbefore that doctor can be booked. - Numeric fields accept a number or a numeric string.
serviceId: 1andserviceId: "1"both work (ASP.NET Web JSON defaults). Send real numbers from typed code. GET /api/doctorspages atpageSize=10by default. PasspageSize(andpage) if you need more than 10 per request. Filters available:search,specialization,serviceId,availableOn,mode.- Errors are a consistent envelope:
{ error, message, details }with the correct HTTP status —400validation,401no/invalid token,403wrong role,404not found,409conflict (slot taken or a business rule like deactivating a doctor who has future appointments). Surfacemessageto users.
Coverage: every endpoint, filter, and the cases above were exercised against the running API in an automated audit (e2e-test/api-audit.cjs, 72/72 checks passing).
Postman collection
A ready-to-import Postman collection is provided so you can exercise every endpoint without writing curl.
docs/postman/Clarity-Clinic.postman_collection.json
Import the collection (and its environment, if present), set the baseUrl variable to http://localhost:5180, log in to capture a token, and run the requests. Most authenticated requests reference the captured token via the Authorization: Bearer {{token}} header.
Seeded credentials
On boot the backend seeds these accounts (passwords are for local development only).
| Role | Password | Name | |
|---|---|---|---|
| Admin | admin@clinic.local | Admin#123 | — |
| Doctor | doctor@clinic.local | Doc#123 | Dr. Layla Hassan |
| Receptionist | reception@clinic.local | Recep#123 | Mona Adel |
| Patient | patient@clinic.local | Pat#123 | Ahmed Samir |
Seed data: one doctor (Dr. Layla Hassan) with two services — General Consultation (30 min, 300 EGP) and Follow-up (15 min, 150 EGP). Working days Sunday–Thursday.