Skip to content

REST API v1 reference

For admins

Keystone exposes a versioned REST API at /api/v1 for mobile apps, partner integrations and trust-built tooling. Every endpoint is JSON in / JSON out, tenant-scoped, and audit-logged the same way the in-app routes are.

This page is the canonical reference. The same endpoints back the official Keystone mobile app, so anything documented here is supported and won’t change without a deprecation window.

https://<your-trust>.example.org/api/v1/

Version is in the path. v1 is stable; breaking changes ship a new version. Non-breaking additions land in v1 and are announced in the release notes.

Bearer token, Laravel Sanctum personal access token. Get one of:

  1. POST /api/v1/auth/local — email + password (rate-limited 5/min).
  2. POST /api/v1/auth/oauth/exchange — OAuth code exchange for trusts using SSO (Microsoft Entra ID, Google Workspace).
  3. POST /api/v1/auth/mfa/challenge — required when the account has MFA enrolled; complete the flow before the bearer token is issued.

All three return a JSON payload containing the access_token (Sanctum personal access token) and a refresh_token. Pass the access token as Authorization: Bearer <token> on every request after that.

To rotate the access token without re-prompting the user:

POST /api/v1/auth/refresh
{ "refresh_token": "..." }

To revoke the current token cleanly:

POST /api/v1/auth/logout
Authorization: Bearer <token>

Every authenticated endpoint runs in exactly one workspace (one trust / tenant). The client picks via:

X-Workspace-Id: <workspace-uuid>

If omitted, the workspace defaults to the user’s first workspace membership. A mismatch — token’s user not a member of the requested workspace — returns 403.

This isolation is enforced by EnsureApiWorkspaceContext middleware, not the controllers; you cannot accidentally read or write across trusts.

  • JSON for request bodies and responses. Content-Type: application/json.
  • UUIDs for all resource IDs ([0-9a-f-]{36}).
  • ISO 8601 timestamps in UTC.
  • Pagination: index endpoints support ?page=<n> and ?per_page=<n> (max 100). Responses include data, meta.total, meta.per_page, meta.current_page.
  • Errors follow Laravel’s standard JSON shape:
    • 422 for validation failures, with errors: { field: [...] }.
    • 401 for missing / expired token.
    • 403 for permission or workspace-scope failures.
    • 404 for resources not in this workspace.
    • 429 for rate-limited routes (auth + write throttles).
  • Idempotency: state-changing endpoints are idempotent when called with identical bodies within the same logical transition (e.g. setting a ticket status to its current value is a no-op).

The API uses the same permission model as the web app — every endpoint enumerates the permission(s) it requires below. Pass a token whose user holds the permission in the workspace; otherwise the gate returns 403. Admin users pass every gate via Gate::before (the super-admin escape hatch).

MethodPathNotes
GET/configPublic app-config bootstrap (auth providers, branding). Used by login screens to render.
POST/auth/localEmail + password sign-in. Returns access + refresh tokens. Rate-limited 5/min.
POST/auth/refreshExchange a refresh token for a new access token. Rate-limited 30/min.
POST/auth/mfa/challengeSubmit an MFA code (TOTP or WebAuthn). Rate-limited 5/min.
POST/auth/oauth/exchangeOAuth code → token exchange. Rate-limited 10/min.
POST/auth/logoutRevoke the bearer token. Requires auth:sanctum.
MethodPathPermissionNotes
GET/meThe signed-in user + workspace + permissions. Use to bootstrap a client session.
MethodPathPermissionNotes
POST/devicesRegister an Expo push token for the signed-in user.
DELETE/devices/{deviceId}Unregister on sign-out.
MethodPathPermissionNotes
GET/me/notification-preferencesPer-channel opt-ins (email, push).
PUT/me/notification-preferencesUpdate.
MethodPathPermissionNotes
GET/ticketshelpdesk::agentIndex, filter via query string.
GET/tickets/{ticket}helpdesk::agentShow one ticket with relations.
POST/tickets— (any authed user)Create a ticket on behalf of the calling user (or with requester_id for an agent submission).
POST/tickets/{ticket}/repliesPost a public reply or internal note (use private: true).
PUT/tickets/{ticket}/assigneehelpdesk::agentSet or clear the assignee.
PUT/tickets/{ticket}/statushelpdesk::agentTransition status (validated against the state machine).
PUT/tickets/{ticket}/priorityhelpdesk::agentChange priority.
GET/tickets/{ticket}/watchersList watchers.
POST/tickets/{ticket}/watchersAdd a watcher (user_id).
DELETE/tickets/{ticket}/watchers/{user}Remove.
GET/tickets/{ticket}/linksLinked tickets.
POST/tickets/{ticket}/linksAdd a link (other_id, relation).
DELETE/tickets/{ticket}/links/{link}Remove a link.
GET/tickets/{ticket}/tasksSub-tasks (read-only).
GET/tickets/{ticket}/timehelpdesk::agentTime entries.
MethodPathPermissionNotes
GET/catalog/categoriesService-catalog tree.
GET/catalog/items/{item}One catalog item’s full form schema.
MethodPathPermissionNotes
GET/solutions/articlesList + search via ?q=.
GET/solutions/articles/{article}Show one article.
MethodPathPermissionNotes
GET/approvals/workflowshelpdesk::agentList configured workflows.
GET/approvals/workflows/{workflow}helpdesk::agentShow one workflow.
GET/approvals/requestshelpdesk::agentIndex.
GET/approvals/requests/{request}helpdesk::agentShow one approval request.
POST/approvals/requestshelpdesk::agentStart a new request against a workflow.
POST/approvals/requests/{request}/decisionApprove / decline. The approver permission is checked by the workflow definition.
GET/approvals/reportshelpdesk::agentReporting summary.
MethodPathPermissionNotes
GET/meta/statusesTicket-status enum.
GET/meta/typesTicket-type enum.
GET/meta/departmentsWorkspace departments.
GET/meta/sla-policieshelpdesk::agentSLA policy list.
GET/meta/calendarshelpdesk::agentBusiness calendars.
MethodPathPermissionNotes
GET/reports/overviewhelpdesk::agentHeadline KPI strip.
GET/reports/spendhelpdesk::agentSpend rollups.
POST/reports/runhelpdesk::agentRun a defined report by id.
MethodPathPermissionNotes
GET/assetsassets::agentList, filter by status / type / school / building.
GET/assets/categoriesassets::agentCategory tree.
GET/assets/lookup/barcode/{code}assets::agentFind an asset by barcode (kiosk / mobile scanner).
GET/assets/{asset}assets::agentShow.
POST/assets/{asset}/assignassets::registry::manageAssign to a person.
POST/assets/{asset}/returnassets::registry::manageClose the current assignment.
MethodPathPermissionNotes
GET/estates/sitesestates::agentSites + buildings hierarchy.
GET/estates/sites/{site}estates::agentOne site, expanded.
GET/estates/compliance/schedulesestates::agentStatutory schedules, filter by due window.
MethodPathPermissionNotes
GET/peoplepeople::agentStaff/pupils directory, search via ?q=.
GET/people/{person}people::agentShow, including engagements (NI number requires people::ni::view; confidential notes require people::confidential::view).
MethodPathPermissionNotes
GET/scr/checksscr::agentEngagement-check status across the workspace.
MethodPathPermissionNotes
GET/governance/risksgovernance::agentRisk register.
GET/governance/risks/{risk}governance::agentShow.
GET/governance/policiesgovernance::agentPolicy library.
GET/governance/policies/{policy}governance::agentShow.
GET/governance/actionsgovernance::agentAction register.
GET/governance/actions/{action}governance::agentShow.
POST/governance/actions/{action}/transitiongovernance::actions::manageMove along the action state machine.
POST/governance/actions/{action}/commentsgovernance::agentAdd a comment.
SurfaceLimit
auth/local, auth/mfa/challenge5 / minute / IP
auth/refresh30 / minute / IP
auth/oauth/exchange10 / minute / IP
All other authenticated routesDefault Sanctum throttle (per token)

Hitting a limit returns 429 with a Retry-After header.

Every write request produces a row in the trust audit log (the same one the web app populates) — DPO-defensible, HMAC-SHA256 signed by the Audit::creating hook. The audit row is tagged by source so an export later can be filtered to API traffic.

  • /api/v1 is stable. New endpoints are additive; non-breaking changes (new optional fields, new query params) ship in v1.
  • A breaking change ships a new major version (/api/v2). The previous version is supported for at least 12 months past the new version’s GA.
  • Field rename or removal is treated as breaking even if the request shape stays the same.

Use any HTTP client — curl, Insomnia, Postman, httpie. A minimal smoke test:

Terminal window
# 1. Sign in.
curl -s -X POST https://your-trust.example.org/api/v1/auth/local \
-H 'Content-Type: application/json' \
-d '{"email":"you@trust.example","password":"…"}'
# Response: { "access_token": "...", "refresh_token": "..." }
# 2. Use the token.
curl -s https://your-trust.example.org/api/v1/me \
-H 'Authorization: Bearer <access_token>' \
-H 'X-Workspace-Id: <workspace-uuid>'

Getting an API token without going through the login flow

Section titled “Getting an API token without going through the login flow”

Platform admins can mint a long-lived API key in the trust admin console (/admin/api-keys). API keys carry an explicit subset of permissions and never expire; revoke from the same screen. Use them for headless integrations (overnight sync jobs, monitoring) rather than embedding user credentials.