ShiftHub REST API v1
Base URL: https://shifthub.co.za/api/v1
The ShiftHub REST API lets you read and (on Professional and above) write the operational data in your workspace — personnel, sites, shifts, attendance events and incidents. The API is tenant-scoped: every request is answered inside the workspace your token belongs to, and you cannot see or write data outside it.
Authentication
The API authenticates with a personal access token issued via Laravel Sanctum. A tenant admin can mint a token from the admin panel (Settings → API tokens, available on Growth tier and above). Present the token on each request in the Authorization header:
Authorization: Bearer <your-token>
Accept: application/json
Tokens are tenant-scoped: the token inherits the tenant of the user that created it. You cannot target a different workspace with the same token.
Plan gating
API access is controlled by two plan feature flags:
| Feature flag | Starter | Growth | Professional | Enterprise |
|---|---|---|---|---|
public_api_read — all GET endpoints | — | ✓ | ✓ | ✓ |
public_api_write — POST/PATCH/DELETE | — | — | ✓ | ✓ |
A call to an endpoint whose flag is not enabled on the caller's plan returns 402 Payment Required with code: feature_not_in_plan.
Rate limits
The v1 surface is limited to 1,000 requests per hour per tenant. Unauthenticated requests fall back to 60/hour per IP. When you exceed the bucket you receive 429 Too Many Requests. Standard rate-limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After) are returned on every response.
Pagination
Collection endpoints return a Laravel-style paginator. Pass ?per_page=50 (max 100) to control page size and ?page=2 to navigate.
{
"data": [ ... ],
"links": { "first": "...", "last": "...", "prev": null, "next": "..." },
"meta": { "current_page": 1, "per_page": 25, "total": 142, "last_page": 6 }
}
Errors
Errors use standard HTTP status codes. The response body always contains code and message; validation errors additionally carry an errors bag.
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or invalid bearer token. |
| 402 | feature_not_in_plan | Endpoint requires a plan flag the tenant doesn't have. |
| 403 | forbidden | Policy denies the action for this user. |
| 403 | no_tenant_context | Token is not associated with a tenant. |
| 404 | not_found | Record missing, or belongs to a sibling tenant. |
| 422 | validation_failed | Request body failed validation — see errors. |
| 429 | rate_limited | Tenant over its 1,000/hour budget. |
Personnel
Read-only endpoints for the workforce register. Requires public_api_read.
GET /api/v1/personnel
Paginated list of personnel in the workspace.
Query parameters
| Name | Type | Description |
|---|---|---|
status | string | Filter by lifecycle status (active, onboarding, terminated, …). |
per_page | int | Page size, default 25, max 100. |
Example
curl -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
"https://shifthub.co.za/api/v1/personnel?status=active&per_page=50"
GET /api/v1/personnel/{id}
Fetch a single personnel record by ID. Encrypted columns (ID number, bank details) and medical notes are deliberately omitted from this resource.
Sites
The sites your workspace operates at. Requires public_api_read.
GET /api/v1/sites
Paginated list of sites, ordered by name.
GET /api/v1/sites/{id}
Fetch a single site with its geofence centre, radius, client pointer and lifecycle status.
Shifts
Rostered shifts — one row per (site, post, worker, day). Requires public_api_read.
GET /api/v1/shifts
Paginated list of roster shifts, sorted by date then planned start.
Query parameters
| Name | Type | Description |
|---|---|---|
personnel_id | int | Filter to shifts assigned to one worker. |
from | date (YYYY-MM-DD) | Lower bound on shift date. |
to | date (YYYY-MM-DD) | Upper bound on shift date. |
per_page | int | Page size, default 50, max 100. |
Attendance
Clock-in / clock-out events tied to a shift. Requires public_api_read.
GET /api/v1/attendance
Paginated feed of attendance events, sorted by occurrence time (descending). Each entry links back to a personnel_id, site_id, and (when applicable) roster_shift_id.
Incidents
Read: public_api_read. Create/update: public_api_write (Professional+).
GET /api/v1/incidents
Paginated list of incidents, ordered by occurred_at descending.
Query parameters
| Name | Type | Description |
|---|---|---|
site_id | int | Filter to one site. |
status | string | Lifecycle status (open, investigating, closed, …). |
severity_min | int 1-5 | Minimum severity tier. |
GET /api/v1/incidents/{id}
Full detail for a single incident including narrative and reporter pointer.
POST /api/v1/incidents
Open a new incident. Tenant is inferred from the token; it cannot be set in the body.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
site_id | int | yes | Must belong to this tenant. |
incident_type_id | int | yes | System-seeded or tenant-owned type. |
occurred_at | datetime | yes | ISO-8601. |
severity | int 1-5 | yes | 1 = informational, 5 = critical. |
narrative | string | no | Up to 10,000 characters. |
roster_shift_id | int | no | Link to the shift during which the incident occurred. |
PATCH /api/v1/incidents/{id}
Update status, severity or narrative of an existing incident.
Webhooks
Requires webhooks on the plan (Professional+). A tenant admin configures webhooks under Settings → Webhooks in the admin panel. Each delivery is signed with a shared secret:
POST {your-endpoint}
Content-Type: application/json
X-ShiftHub-Event: incident.created
X-ShiftHub-Signature: sha256=<hmac>
X-ShiftHub-Delivery: <uuid>
Current event catalogue: incident.created, incident.updated, shift.published, attendance.clocked_in, attendance.clocked_out, panic.raised. Failed deliveries are retried with exponential backoff (5 attempts over ~24 hours) and then parked for manual replay from the admin panel.
POPIA endpoints
Tenant admins can export or anonymize a personnel record's data to satisfy POPIA (Protection of Personal Information Act) access/erasure rights. These endpoints authenticate with the regular session or a token belonging to the tenant admin:
GET /api/personnel/{personnel}/data-export
Returns a JSON dump of every record associated with this personnel row — profile, qualifications, assignments, attendance, incidents. Use to fulfil a data-access request.
POST /api/personnel/{personnel}/erasure-request
Opens an erasure / anonymisation request. The admin panel shows pending requests under People → POPIA requests. See also the POPIA policy.
Changelog
We keep public API v1 deprecations to the same cadence as minor releases (30 days' notice in the admin panel before any breaking change). Changes so far:
| Date | Change |
|---|---|
| 2026-04-21 | Initial public documentation of v1 read + write surfaces. |