API
The starter kit exposes a versioned JSON API under /api/v1. For a browsable, always-current view of that contract, use the api-dock panel at /api-dock (gated by the seeded api-docs.read permission) rather than reading route files by hand.
Response Standard
All API responses use the shared envelope:
{
"success": true,
"status": 200,
"message": "Operation successful.",
"data": {},
"meta": {},
"trace_id": "uuid"
}
Use:
to_api()ApiResponseApiException
Do not use raw response()->json() for normal package-style endpoints.
Route File Structure
All API routes live under /api/v1 with a global throttle:api middleware applied. Route files in routes/api/ are loaded automatically and fall into three tiers:
Public (routes/api/public-api.php) — no authentication required. Register, login, and API two-factor challenge each carry an additional throttle:5,1 (5 requests per minute).
Auth-only (auth-route.php, service-route.php) — wrapped with auth:api. No permission check.
Permission-protected (all other route files, including user-route.php) — wrapped with ['auth:api', 'check.permission']. The check.permission middleware resolves the expected permission from the route name and verifies it against the authenticated user.
Auth Endpoints
Public (no token required):
POST /api/v1/auth/register— throttled 5/minPOST /api/v1/auth/login— throttled 5/minPOST /api/v1/auth/two-factor-challenge— throttled 5/min
Protected (auth:api):
POST /api/v1/auth/logoutGET /api/v1/auth/me
Service Endpoints
Protected (auth:api):
GET /api/v1/definitions
Resource Endpoints
Protected (auth:api + check.permission):
Route::apiResource('users', UserController::class)— defined inroutes/api/user-route.php. Theindexaction delegates toUserDatatableQuery, the same query class the admin panel uses, so the role-hierarchy filter (a non-system_admincaller cannot see higher-rank users) is enforced identically on both surfaces. See roles-permissions.md.
Authentication Model
API protection uses Passport and the auth:api guard.
Successful auth responses are no longer guaranteed to include a token:
registermay return{ user, requires_verification: true }loginmay return{ requires_verification: true }or{ requires_two_factor: true, challenge }two-factor-challengeexchanges the issuedchallengepluscodeorrecovery_codefor{ user, token }
Request Tracing
Every response carries a trace_id in the envelope and an X-Request-ID header.
trace_idis always server-generated viaStr::uuid(). It uniquely identifies the request in application logs — pass it to support when reporting an issue.- Client-supplied
X-Request-IDheaders are accepted as correlation metadata only when they match[A-Za-z0-9._-]{1,128}. The sanitised value is logged asclient_request_idalongside the servertrace_id; values outside the charset or longer than 128 chars are dropped silently. The response header always carries the server-generated id, never the client-supplied one.
CORS
The starter kit ships fruitcake/laravel-cors / Laravel's bundled CORS middleware with config/cors.php. The default max_age is 7200 (2 hours), so browsers cache the preflight OPTIONS response and SPA / mobile clients don't pay a handshake on every mutating call. Tune allowed_origins, supports_credentials, and max_age to match your deployment before going to production.
Error Handling
Validation, authentication, authorization, not found, and other expected failures are normalized through the API exception layer. Unhandled 5xx errors return a generic A server error occurred. message regardless of APP_DEBUG — exception details live only in the logs and, when APP_DEBUG=true, in an additional debug block next to the envelope.