Skip to content

REST API Reference

The API is built on Django REST Framework and lives in the api/ package. All endpoints return JSON and require an authenticated session.

Source: api/viewsets.py, api/serializers.py, rota/urls.py.

Architecture

Aspect Detail
Framework Django REST Framework
Authentication SessionAuthentication -- cookie-based Django sessions
Permissions IsAuthenticated on all endpoints (global default)
Schema DRF Spectacular (OpenAPI 3.0) at /api/schema/
Throttling Global burst: 20/min (anonymous); per-view rates below
URL prefix /api/ (unversioned), /api/v1/, /api/v2/

Throttling Tiers

Tier Rate Used by
standard 100/hour CRUD ViewSets (clinicians, shifts, leave, alerts, working terms)
expensive_calculation 10/hour Statistics, forecast, workload, availability, bulk calculations
burst (default) 20/min All anonymous requests

Cross-cutting Concerns

All ViewSets inherit from APIRequestLoggingMixin (api/mixins.py), which logs:

  • Request method, path, user, and query params
  • Response status code and duration
  • Errors with stack traces
  • Slow requests exceeding 1.0 second threshold

Authentication

The API uses Django's built-in session authentication via django-allauth. There is no token or API key system.

Requirements for every request:

  1. A valid Django session cookie (obtained by logging in through /accounts/login/)
  2. A CSRF token header for unsafe methods (POST, PATCH, DELETE)
Concern Detail
Login URL /accounts/login/ (django-allauth)
CSRF header X-CSRFToken
CSRF cookie csrftoken
CSRF cookie SameSite Lax
Login redirect /

Include the CSRF token in the X-CSRFToken header for any write request:

X-CSRFToken: <value of csrftoken cookie>

API Versioning

Unversioned (/api/)

Deprecated — redirects to v1. Registered via DefaultRouter in rota/urls.py. Includes all resource ViewSets plus standalone endpoints for rota generation, rebalancing, statistics, and availability. Use /api/v1/ for new integrations.

v1 (/api/v1/)

Registered in api/v1/urls.py. Contains:

  • ClinicianViewSetV1 -- full CRUD plus stats and active actions (same shape as unversioned)
  • StatisticsViewSetV1 -- list, forecast, workload (wraps responses with version key)

v1 responses include a version field and slightly different envelope structures.

v2 (/api/v2/)

Placeholder. api/v2/urls.py registers no ViewSets yet. The v2 router and URLconf exist for forward compatibility.

Endpoints

Clinicians

Managed by ClinicianViewSet (router-registered at /api/clinicians/).

Method URL Description Auth
GET /api/clinicians/ List all clinicians. Supports ?search=<name> and ?ordering=<field>. Any user
POST /api/clinicians/ Create a clinician. Any user
GET /api/clinicians/{id}/ Retrieve a single clinician. Any user
PATCH /api/clinicians/{id}/ Partial update a clinician. Any user
PUT /api/clinicians/{id}/ Full update a clinician. Any user
DELETE /api/clinicians/{id}/ Delete a clinician. Any user
GET /api/clinicians/active/ List only clinicians where active=True. Any user
GET /api/clinicians/{id}/stats/ Return shift counts, deficiency, and leave balance for one clinician. Any user

Clinician serializer fields: id, name, active, created_at, updated_at.

Clinician stats response shape:

{
    "clinician_id": 1,
    "clinician_name": "Dr. Smith",
    "current_deficiency": 0.5,
    "completed_shifts": 12,
    "scheduled_shifts": 8,
    "total_shifts": 20,
    "leave_balance_remaining": 6.0,
    "as_of_date": "2024-06-01"
}

Working Terms

Managed by WorkingTermViewSet (router-registered at /api/working-terms/). Optionally nested under clinicians.

Method URL Description Auth
GET /api/working-terms/ List all working terms. Ordered by -start_date. Any user
POST /api/working-terms/ Create a working term. Any user
GET /api/working-terms/{id}/ Retrieve a working term. Any user
PATCH /api/working-terms/{id}/ Partial update. Any user
PUT /api/working-terms/{id}/ Full update. Any user
DELETE /api/working-terms/{id}/ Delete a working term. Any user
GET /api/clinicians/{pk}/working-terms/ List terms for a specific clinician (nested route). Any user

Serializer fields: id, clinician, clinician_name, type, start_date, end_date, percentage, fixed_working_days, fixed_half_days, cannot_work_days, must_work_days, annual_leave_entitlement, participates_in_duty, created_at, updated_at.

Shifts

Managed by ShiftViewSet (router-registered at /api/shifts/).

Method URL Description Auth
GET /api/shifts/ List shifts. Supports ?date_from= and ?date_to= filters. Ordered by date, clinician. Any user
POST /api/shifts/ Create a shift. Any user
GET /api/shifts/{id}/ Retrieve a shift. Any user
PATCH /api/shifts/{id}/ Partial update. Any user
PUT /api/shifts/{id}/ Full update. Any user
DELETE /api/shifts/{id}/ Delete a shift. Any user
PATCH /api/shifts/{id}/pin/ Pin a shift (sets is_pinned=True). Prevents automatic rebalancing. Any user
PATCH /api/shifts/{id}/unpin/ Unpin a shift (sets is_pinned=False). Allows automatic rebalancing. Any user

Serializer fields: id, clinician, clinician_name, date, type, duration, status, is_pinned, counts_as_worked, counts_toward_minimum, created_by, created_at, modified_by, modified_at.

Pin/unpin -- The pin and unpin actions set modified_by to the authenticated user's username.

Leave Requests

Managed by LeaveRequestViewSet (router-registered at /api/leave-requests/).

Method URL Description Auth
GET /api/leave-requests/ List all leave requests. Ordered by -requested_at. Any user
POST /api/leave-requests/ Create a leave request. Triggers LeaveRequestProcessor on save. Any user
GET /api/leave-requests/{id}/ Retrieve a leave request. Any user
PATCH /api/leave-requests/{id}/ Partial update. Any user
PUT /api/leave-requests/{id}/ Full update. Any user
DELETE /api/leave-requests/{id}/ Delete a leave request. Any user
GET /api/leave-requests/mine/ List leave requests for a specific clinician. Pass ?clinician_id=. Any user
POST /api/leave-requests/{id}/cancel/ Cancel an approved leave request. Delegates to LeaveRequestProcessor.cancel_request(). Any user

Serializer fields: id, clinician, clinician_name, type, start_date, end_date, status, requested_at, processed_at, processed_by, denial_reason, affected_shift_count.

Create side-effect: perform_create calls LeaveRequestProcessor.process_request() which validates against leave quota and updates shift records.

Alerts

Managed by AlertViewSet (router-registered at /api/alerts/).

Method URL Description Auth
GET /api/alerts/ List alerts. Returns active only by default. Pass ?active=false to include all. Ordered by -date, -created_at. Any user
POST /api/alerts/ Create an alert. Any user
GET /api/alerts/{id}/ Retrieve an alert. Any user
PATCH /api/alerts/{id}/ Partial update. Any user
PUT /api/alerts/{id}/ Full update. Any user
DELETE /api/alerts/{id}/ Delete an alert. Any user
POST /api/alerts/{id}/resolve/ Resolve an alert (status becomes RESOLVED). Any user
POST /api/alerts/{id}/dismiss/ Dismiss an alert (status becomes DISMISSED). Any user

Serializer fields: id, type, type_label, severity, date, message, details, status, created_at, resolved_at.

Default filtering: The get_queryset method filters to status='ACTIVE' unless ?active=false is passed.

Calculations

Managed by CalculationViewSet (router-registered at /api/calculations/).

Method URL Description Auth
GET /api/calculations/ List available calculation endpoints. Any user
POST /api/calculations/bulk-leave-quota/ Calculate leave quota for multiple clinicians in one request. Any user

Bulk leave quota request:

{
    "clinician_ids": [1, 2, 3]
}

Maximum batch size: 100 clinician IDs. All IDs must correspond to active clinicians.

Bulk leave quota response:

{
    "results": [
        {
            "clinician_id": 1,
            "clinician_name": "Dr. Smith",
            "has_entitlement": true,
            "entitlement": "8.00",
            "remaining": "5.50",
            "unit": "weeks",
            "adjustments": "0.00",
            "percentage_used": 31
        }
    ],
    "total_count": 3,
    "success_count": 3,
    "error_count": 0,
    "errors": []
}

Individual clinician errors are reported in the errors array without failing the entire batch.

Rota Generation

Standalone endpoint (not router-registered).

Method URL Description Auth
POST /api/rota/generate/ DEPRECATED -- returns 501 Not Implemented. Any user

This endpoint has been superseded by the phase-based rota generation system accessed through the admin interface (/admin/rota/) or the management command python manage.py generate_rota --phase <1|3|4|6|all>.

Rebalancing

Standalone endpoint (not router-registered).

Method URL Description Auth
POST /api/rebalance/ DEPRECATED -- returns 501 Not Implemented. Any user

The rebalancing service depended on the replaced RotaGenerator class. A future reimplementation is planned.

Statistics

Standalone endpoints (not router-registered).

Method URL Description Auth
GET /api/statistics/ List available statistics sub-endpoints. Any user
GET /api/statistics/forecast/ Staffing forecast for a date range. Any user
GET /api/statistics/workload/ Workload balance report for a reporting period. Any user

Forecast query parameters:

Param Required Default Description
start_date No Today ISO date (YYYY-MM-DD)
end_date No start_date + 4 weeks ISO date (YYYY-MM-DD)
weeks No 4 Number of weeks (used if end_date omitted)

Forecast response shape:

[
    {
        "date": "2024-06-03",
        "day_of_week": "Monday",
        "minimum_required": 2,
        "scheduled_count": 3,
        "pending_leave": 1,
        "status": "above"
    }
]

status values: "above", "at", "below" -- comparing scheduled_count against minimum_required.

Workload query parameters:

Param Required Default Description
reporting_period No "Period 1" Period name to report on
as_of_date No Today ISO date for the report snapshot

Workload response shape:

{
    "clinician_summaries": [...],
    "reporting_period": "Period 1"
}

Availability

Standalone endpoint (not router-registered).

Method URL Description Auth
GET /api/availability/ Availability heatmap (doctor count per day) for a date range. Any user

Query parameters:

Param Required Default Description
start_date No Today ISO date
end_date No start_date + 4 weeks ISO date

Response shape:

[
    {"date": "2024-06-03", "doctors_count": 3},
    {"date": "2024-06-04", "doctors_count": 2}
]

Calendar Data (Frontend)

This is a frontend-specific JSON endpoint (not DRF), registered in frontend/urls.py.

Method URL Description Auth
GET /api/calendar-data/ Calendar shifts, leave, and alerts grouped by date. Used by the rota calendar view. Any user

Query parameters: start_date (required), end_date (required), clinician_id (optional).

Response keys: shiftsByDate, ownShiftsByDate, today, alertsByDate (admin only), annualLeaveByDate, requestedOffByDate, coronersLeaveByDate, studyLeaveByDate, ownAnnualLeaveByDate.

Leave Booking (Frontend)

Frontend-specific JSON endpoints for the leave booking flow.

Method URL Description Auth
GET /leave-booking/calendar-data/ Capacity and availability data for the leave booking calendar. Any user
POST /leave-booking/submit/ Submit a leave booking request. Accepts JSON body with leave_type and dates array. Any user

v1 Endpoints

Method URL Description
GET /api/v1/clinicians/ List clinicians (v1: no pagination).
POST /api/v1/clinicians/ Create clinician.
GET /api/v1/clinicians/{id}/ Retrieve clinician.
GET /api/v1/clinicians/active/ Active clinicians only.
GET /api/v1/clinicians/{id}/stats/ Clinician statistics (same shape as unversioned).
GET /api/v1/statistics/ List available statistics endpoints.
GET /api/v1/statistics/forecast/ Staffing forecast (response wrapped in {version, start_date, end_date, results}).
GET /api/v1/statistics/workload/ Workload report (response wrapped with version key).

v1 forecast and workload responses include a "version": "v1" field in the response envelope. The unversioned equivalents do not.

Request/Response Formats

Content Types

  • Request body: application/json (DRF parsers)
  • Response body: application/json
  • CSRF token: X-CSRFToken header

Common Patterns

Listing endpoints return arrays directly (no pagination wrapper in the current configuration):

[
    {"id": 1, "name": "Dr. Smith", "active": true, ...},
    {"id": 2, "name": "Dr. Jones", "active": true, ...}
]

Detail endpoints return a single object:

{"id": 1, "name": "Dr. Smith", "active": true, "created_at": "...", "updated_at": "..."}

Creation returns the created object with HTTP 201:

{"id": 3, "name": "Dr. New", "active": true, "created_at": "...", "updated_at": "..."}

Error Handling

DRF's default exception handling applies. All errors return JSON.

Validation Errors (400)

{
    "field_name": ["This field is required."],
    "another_field": ["Invalid value."]
}

Not Authenticated (401)

{
    "detail": "Authentication credentials were not provided."
}

Not Found (404)

{
    "detail": "No Clinician matches the given query."
}

Throttled (429)

{
    "detail": "Request was throttled. Expected available in 3600 seconds."
}

Deprecated Endpoints (501)

The rota generation and rebalancing endpoints return 501 with:

{
    "error": "This endpoint has been deprecated. ..."
}

Standard HTTP Status Codes

Code Meaning
200 Success (read, update, action)
201 Created (POST to list endpoint)
204 No content (DELETE)
400 Validation error
401 Not authenticated
403 Permission denied
404 Not found
429 Throttled
500 Server error
501 Deprecated / not implemented