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.

What's here today. Every endpoint listed on this page is implemented and gated by the same feature flags advertised on the pricing page. If you need a capability that isn't listed, it does not exist yet — please ask in the contact form before building against 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 flagStarterGrowthProfessionalEnterprise
public_api_read — all GET endpoints
public_api_writePOST/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.

StatusCodeWhen
401unauthenticatedMissing or invalid bearer token.
402feature_not_in_planEndpoint requires a plan flag the tenant doesn't have.
403forbiddenPolicy denies the action for this user.
403no_tenant_contextToken is not associated with a tenant.
404not_foundRecord missing, or belongs to a sibling tenant.
422validation_failedRequest body failed validation — see errors.
429rate_limitedTenant 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
NameTypeDescription
statusstringFilter by lifecycle status (active, onboarding, terminated, …).
per_pageintPage 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
NameTypeDescription
personnel_idintFilter to shifts assigned to one worker.
fromdate (YYYY-MM-DD)Lower bound on shift date.
todate (YYYY-MM-DD)Upper bound on shift date.
per_pageintPage 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
NameTypeDescription
site_idintFilter to one site.
statusstringLifecycle status (open, investigating, closed, …).
severity_minint 1-5Minimum 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
FieldTypeRequiredNotes
site_idintyesMust belong to this tenant.
incident_type_idintyesSystem-seeded or tenant-owned type.
occurred_atdatetimeyesISO-8601.
severityint 1-5yes1 = informational, 5 = critical.
narrativestringnoUp to 10,000 characters.
roster_shift_idintnoLink 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:

DateChange
2026-04-21Initial public documentation of v1 read + write surfaces.