Developer documentation

Clarity Clinic

Complete API & project reference

Everything a developer needs in one browsable page: architecture, getting started, authentication, roles & permissions, core concepts, end-to-end flows, and the full reference for all 44 endpoints (plus a /health probe) — each with method, path, required role, request body, and a copyable curl example.

Base URL · http://localhost:5180 JSON / REST JWT Bearer auth AR / EN localization 44 endpoints
Project overview

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

ASP.NET Core 8 · EF Core · SQL Server

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

React 19 · Vite · TypeScript

Public self-service booking experience.

  • Browse doctors & services
  • Book and pay online
  • View appointments & prescriptions
  • AR / EN with full RTL

Staff dashboard :4321

Angular 20 · Standalone · Signals

Internal control room for the clinic.

  • Admin — staff, services, reports
  • Receptionist — desk & calendar
  • Doctor — schedule & visits
  • AR / EN RTL, light / dark
How it fits together

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.

Patient App
React 19 · Vite
:5173
Staff Dashboard
Angular 20
:4321
Backend API
ASP.NET Core 8
:5180
Controllers→ Services→ EF Core
SQL Server LocalDB
database
ClinicDb
CORS allows requests only from the patient app :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

LayerAppStackURL
Backend APIbackendASP.NET Core 8 (C#), EF Core, SQL Server LocalDB, custom JWT + BCrypt, JSON i18nhttp://localhost:5180
Patient apppatient-appReact 19 + Vite + TypeScript, TanStack Query, Axios, Zustand, react-i18next (AR/EN RTL)http://localhost:5173
Staff dashboardstaff-dashboardAngular 20 (standalone components + signals), AR/EN RTL, light/darkhttp://localhost:4321
DatabaseSQL Server LocalDB, database ClinicDb (auto-migrated + seeded on backend boot)

Swagger UI is served at http://localhost:5180/swagger.

Run it locally

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.tshttp://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
Concepts

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.

Who can do what

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 PatientDoctorReceptionistAdminAnonymous
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.

Core concepts · state machine

Appointment lifecycle

An appointment moves through the AppointmentStatus enum. The path depends on how it was booked.

  • Online + paymentMethod OnlinePendingPayment; after payment → Confirmed.
  • InClinic + CashConfirmed immediately (the patient pays at the clinic; reception later marks it cash-paid).
  • ConfirmedArrived (reception checks the patient in) → Completed (the doctor completes the visit and writes diagnosis/prescription).
  • Confirmed or ArrivedNoShow (reception).
  • Confirmed or PendingPaymentCancelled (patient or reception). The slot is freed and can be rebooked.
PendingPayment Confirmed — starts here for Online + Online pay
▲ Book InClinic + Cash → Confirmed directly
Confirmed Arrived Completed
Confirmed/Arrived NoShow
Confirmed/PendingPayment Cancelled → slot freed & rebookable

Booking the same slot twice returns 409 Conflict. Double-booking is race-safe on the server.

Core concepts

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.

Core concepts

Payment flow

Two booking paths determine whether a payment is required.

  • Online + Online payment → appointment is created as PendingPayment and the booking response includes a payment object with a checkoutUrl. After payment is confirmed, the appointment becomes Confirmed.
  • InClinic + Cash → appointment is created as Confirmed immediately, payment is null. Reception later marks it cash-paid via PUT /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 the X-Signature header, 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 appointment PaidConfirmed. Body { appointmentId, status? } (status defaults to "Paid"). Idempotent — calling it twice leaves the appointment Paid once. Returns { "paid": <bool> }.

PaymentStatus enum: Pending, Paid, Failed, Refunded. PaymentMethod enum: Online, Cash.

Core concepts

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.

Core concepts

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"]
}
HTTPerror codeMeaning
400validationInvalid input / failed validation
401unauthorizedMissing or invalid token
403forbiddenAuthenticated but wrong role
404not_foundResource does not exist
409conflictSlot already taken / state conflict
500server_errorUnexpected error (details are not leaked)
Step by step

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"
REST reference

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 endpoints
POST/api/auth/register Anonymous

Patient 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" }'
POST/api/auth/login Anonymous

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" }'
GET/api/auth/me Authenticated

Returns the current user's profile.

curl -s {{baseUrl}}/api/auth/me -H "Authorization: Bearer $TOKEN"
PUT/api/auth/profile Authenticated

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 }'
PUT/api/auth/password Authenticated

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 endpoints
GET/api/doctorsAnonymous

Paged, 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"
GET/api/doctors/{id}Anonymous

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": [ ... ]
}
GET/api/doctors/{id}/slotsAnonymous

Open slots for a service on a date → { date, slots: ["09:00", ...] }.

Query params

  • daterequiredyyyy-MM-dd
  • serviceIdrequired
curl -s "{{baseUrl}}/api/doctors/1/slots?date=2026-06-21&serviceId=1"
GET/api/doctors/{id}/availabilityAdmin · Receptionist

Get the doctor's weekly availability.

curl -s {{baseUrl}}/api/doctors/1/availability -H "Authorization: Bearer $TOKEN"
PUT/api/doctors/{id}/availabilityAdmin · Receptionist

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" } ]'
GET/api/doctors/{id}/blocked-datesAdmin · Receptionist

List the doctor's blocked days.

curl -s "{{baseUrl}}/api/doctors/{id}/blocked-dates" -H "Authorization: Bearer $TOKEN"
POST/api/doctors/{id}/blocked-datesAdmin · Receptionist

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" }'
DELETE/api/doctors/{id}/blocked-dates/{date}Admin · Receptionist

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 endpoints
GET/api/servicesAnonymous

List all services.

curl -s {{baseUrl}}/api/services
POST/api/servicesAdmin · Receptionist

Create 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 }'
PUT/api/services/{id}Admin · Receptionist

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/api/services/{id}Admin · Receptionist

Delete a service.

curl -s -X DELETE {{baseUrl}}/api/services/1 -H "Authorization: Bearer $TOKEN"

Appointments — Patient

5 endpoints

Booking 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.

POST/api/appointmentsPatient

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" }'
GET/api/me/appointmentsPatient

List the patient's own appointments.

Query params

  • whenupcoming | past
  • status
curl -s "{{baseUrl}}/api/me/appointments?when=upcoming" -H "Authorization: Bearer $TOKEN"
GET/api/me/prescriptionsPatient

List the patient's own prescriptions.

curl -s {{baseUrl}}/api/me/prescriptions -H "Authorization: Bearer $TOKEN"
GET/api/appointments/{id}Patient owner or staff

Get one appointment. Returns the AppointmentDto directly (no wrapper).

curl -s {{baseUrl}}/api/appointments/10 -H "Authorization: Bearer $TOKEN"
PUT/api/appointments/{id}/cancelPatient owner or staff

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 endpoints
GET/api/appointmentsAdmin · Receptionist

Day 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"
POST/api/appointments/walk-inAdmin · Receptionist

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" }'
PUT/api/appointments/{id}/rescheduleAdmin · Receptionist

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" }'
PUT/api/appointments/{id}/arrivedAdmin · Receptionist

Check the patient in (Confirmed → Arrived).

curl -s -X PUT {{baseUrl}}/api/appointments/10/arrived -H "Authorization: Bearer $TOKEN"
PUT/api/appointments/{id}/no-showAdmin · Receptionist

Mark the appointment as a no-show.

curl -s -X PUT {{baseUrl}}/api/appointments/10/no-show -H "Authorization: Bearer $TOKEN"
PUT/api/appointments/{id}/cash-paidAdmin · Receptionist

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 endpoints
PUT/api/appointments/{id}/completeDoctor

Complete the visit (→ Completed).

curl -s -X PUT {{baseUrl}}/api/appointments/10/complete -H "Authorization: Bearer $TOKEN"
POST/api/appointments/{id}/visitDoctor

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" }] }'
PUT/api/visits/{id}Doctor

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":[ ... ] }'
GET/api/doctor/scheduleDoctor

The doctor's schedule for a date.

Query params

  • date
curl -s "{{baseUrl}}/api/doctor/schedule?date=2026-06-21" -H "Authorization: Bearer $TOKEN"
GET/api/patients/{id}/historyDoctor

A patient's visit history.

curl -s {{baseUrl}}/api/patients/3/history -H "Authorization: Bearer $TOKEN"

Patients

/api/patients · Admin · Receptionist2 endpoints
POST/api/patientsAdmin · Receptionist

Create 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" }'
GET/api/patientsAdmin · Receptionist

Search patients.

Query params

  • search
curl -s "{{baseUrl}}/api/patients?search=ahmed" -H "Authorization: Bearer $TOKEN"

Admin

/api/admin · Admin only6 endpoints
GET/api/admin/staffAdmin

List all staff members.

curl -s {{baseUrl}}/api/admin/staff -H "Authorization: Bearer $TOKEN"
POST/api/admin/doctorsAdmin

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" }] }'
POST/api/admin/receptionistsAdmin

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" }'
PUT/api/admin/staff/{id}Admin

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 }'
PUT/api/admin/staff/{id}/activeAdmin

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 }'
GET/api/admin/reportsAdmin

Clinic reports.

curl -s {{baseUrl}}/api/admin/reports -H "Authorization: Bearer $TOKEN"

Dashboard

Admin · Receptionist1 endpoint
GET/api/dashboard/statsAdmin · Receptionist

Dashboard statistics.

curl -s {{baseUrl}}/api/dashboard/stats -H "Authorization: Bearer $TOKEN"

Payments

/api/payments · Anonymous2 endpoints
POST/api/payments/webhookAnonymous

Production 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 }
POST/api/payments/mock/payAnonymous

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 /apiprobe
GET/healthAnonymous

Liveness/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" }
Read before coding

Gotchas & contract notes

These are the non-obvious behaviours that trip people up. All verified against the live API.

  1. Only POST /api/appointments wraps its response. Booking returns { appointment, payment }. Every other appointment endpointwalk-in, GET /appointments/{id}, cancel, reschedule, arrived, no-show, cash-paid, complete — returns the AppointmentDto directly (no appointment/payment wrapper). Don't reach for .appointment on those.
  2. Online vs. cash booking differ: mode: Online + paymentMethod: Online201 PendingPayment with payment.checkoutUrl and a meetingLink; mode: InClinic + paymentMethod: Cash201 Confirmed with payment: null (paid at the desk → reception calls cash-paid later).
  3. 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:00 removes both 09:00 and 09:15 from availability. Past times for today are filtered out; days off (Fri/Sat) and blocked dates return zero slots.
  4. 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.
  5. A newly admin-added doctor has no services. POST /api/admin/doctors creates the login + availability but not bookable services — create them via POST /api/services before that doctor can be booked.
  6. Numeric fields accept a number or a numeric string. serviceId: 1 and serviceId: "1" both work (ASP.NET Web JSON defaults). Send real numbers from typed code.
  7. GET /api/doctors pages at pageSize=10 by default. Pass pageSize (and page) if you need more than 10 per request. Filters available: search, specialization, serviceId, availableOn, mode.
  8. Errors are a consistent envelope: { error, message, details } with the correct HTTP status — 400 validation, 401 no/invalid token, 403 wrong role, 404 not found, 409 conflict (slot taken or a business rule like deactivating a doctor who has future appointments). Surface message to 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).

Tooling

Postman collection

A ready-to-import Postman collection is provided so you can exercise every endpoint without writing curl.

Collection
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.

Local development

Seeded credentials

On boot the backend seeds these accounts (passwords are for local development only).

RoleEmailPasswordName
Adminadmin@clinic.localAdmin#123
Doctordoctor@clinic.localDoc#123Dr. Layla Hassan
Receptionistreception@clinic.localRecep#123Mona Adel
Patientpatient@clinic.localPat#123Ahmed 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.