API Guide
Last Updated: 2026-05-19 Status: Implemented Plan Reference: 032-api-enhancements-documentation.md
Overview
The REST API provides programmatic access to the client portal, enabling integrations with external systems, mobile applications, and automation tools. It uses Laravel Sanctum for authentication and follows RESTful conventions.
Table of Contents
- Getting Started
- API Playground
- Authentication
- API Endpoints
- Request & Response Format
- Rate Limiting
- Error Handling
- Technical Architecture
- SDK Generation (Server-Side)
- SDK Examples
- API Endpoints Config Generator
- Related Features
Getting Started
Base URL
Production: https://your-domain.com/api/v1
Development: http://localhost:8000/api/v1
Quick Start
- Create an API token in your profile settings
- Include the token in your request headers
- Make API calls to the endpoints
Example Request
curl -X GET "https://your-domain.com/api/v1/clients" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
API Playground
The API Playground provides an interactive web interface for testing API endpoints directly in the browser.
Access: Admin → API Playground (/admin/api-playground)
Features
| Feature | Description |
|---|---|
| Domain Grouping | Endpoints organized by domain (Authentication, Files, Invoices, etc.) |
| Stats Dashboard | Shows total endpoints, domains, authenticated vs public counts |
| Collapsible Sections | Expand/collapse domain groups for easy navigation |
| Search | Filter endpoints by path, method, or description |
| Request Builder | Construct requests with headers, body, and query params |
| API Key Selection | Select from available API keys for authentication |
| Code Examples | Auto-generated examples in cURL, JavaScript, PHP, and Python |
| Response Viewer | View response body, headers, status, timing, and size |
Using the Playground
- Select an Endpoint: Click on any endpoint in the left panel
- Configure Request: Add headers, query params, or body content
- Select API Key: Choose an API key from the dropdown
- Send Request: Click "Send" to execute the request
- View Response: See the response body and headers in the right panel
Code Examples Tab
The playground generates ready-to-use code snippets:
- cURL: Command-line example
- JavaScript: Fetch API example
- PHP: Guzzle HTTP client example
- Python: Requests library example
Endpoint Information
When you select an endpoint, you'll see:
- Summary and description
- Auth Required badge (if authentication needed)
- Rate Limited badge (if rate limiting applies)
- Parameters table with name, type, location, and requirements
Authentication
The API uses Laravel Sanctum personal access tokens, sent as
Authorization: Bearer <token>. The token is the plaintext returned at
mint time (format <id>|<random>).
For everything related to minting, listing, and revoking tokens, plus
the difference between Sanctum tokens and the ApiKey model (which is
not wired to any route in this deployment), see the dedicated
API Tokens guide.
Quick mint (admin token, via tinker on the server):
sudo -u www-data XDG_CONFIG_HOME=/tmp php artisan tinker --execute='
$u = \App\Models\Core\User::where("email", "philip@scopeforged.com")->first();
$t = $u->createToken("Token label", ["read","write"]);
echo $t->plainTextToken;
'
Using the Token:
Authorization: Bearer YOUR_API_TOKEN
⚠️ Token abilities (
read,write, etc.) are stored on the token but are not currently enforced at the route layer in this codebase. Admin access is gated by the user'srole, not the token's abilities. Treat each token as roughly equivalent to its user's full permissions until per-ability route gating is added.
Token Abilities
| Ability | Description |
|---|---|
clients:read | Read client data |
clients:write | Create/update clients |
projects:read | Read project data |
projects:write | Create/update projects |
invoices:read | Read invoice data |
invoices:write | Create/update invoices |
files:read | Download files |
files:write | Upload files |
Token Management
# List tokens
GET /api/v1/user/tokens
# Revoke token
DELETE /api/v1/user/tokens/{id}
API Endpoints
Clients
| Method | Endpoint | Description |
|---|---|---|
| GET | /clients | List all clients |
| POST | /clients | Create client |
| GET | /clients/{id} | Get client |
| PUT | /clients/{id} | Update client |
| DELETE | /clients/{id} | Delete client |
| GET | /clients/{id}/projects | List client projects |
| GET | /clients/{id}/invoices | List client invoices |
Projects
| Method | Endpoint | Description |
|---|---|---|
| GET | /projects | List all projects |
| POST | /projects | Create project |
| GET | /projects/{id} | Get project |
| PUT | /projects/{id} | Update project |
| DELETE | /projects/{id} | Delete project |
| GET | /projects/{id}/files | List project files |
| POST | /projects/{id}/files | Upload file |
Invoices
| Method | Endpoint | Description |
|---|---|---|
| GET | /invoices | List all invoices |
| POST | /invoices | Create invoice |
| GET | /invoices/{id} | Get invoice |
| PUT | /invoices/{id} | Update invoice |
| DELETE | /invoices/{id} | Delete invoice |
| POST | /invoices/{id}/send | Mark as sent |
| POST | /invoices/{id}/paid | Mark as paid |
| GET | /invoices/{id}/pdf | Download PDF |
Files
| Method | Endpoint | Description |
|---|---|---|
| GET | /files | List all files |
| GET | /files/{id} | Get file info |
| GET | /files/{id}/download | Download file |
| DELETE | /files/{id} | Delete file |
Users
| Method | Endpoint | Description |
|---|---|---|
| GET | /user | Current user |
| PUT | /user | Update profile |
| GET | /user/notifications | List notifications |
Request & Response Format
Request Headers
Accept: application/json
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
Pagination
Request:
GET /api/v1/clients?page=1&per_page=20
Response:
{
"data": [...],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 150,
"last_page": 8
},
"links": {
"first": "https://example.com/api/v1/clients?page=1",
"last": "https://example.com/api/v1/clients?page=8",
"prev": null,
"next": "https://example.com/api/v1/clients?page=2"
}
}
Filtering
GET /api/v1/projects?status=active&client_id=5
GET /api/v1/invoices?status=sent&created_after=2026-01-01
Sorting
GET /api/v1/clients?sort=-created_at
GET /api/v1/invoices?sort=due_date,-total
Including Relations
GET /api/v1/projects/{id}?include=client,files
GET /api/v1/invoices/{id}?include=items,client
Response Format
Success (Single Resource):
{
"data": {
"id": 1,
"type": "client",
"attributes": {
"company_name": "Acme Corp",
"email": "contact@acme.com"
},
"relationships": {
"projects": {
"data": [{"id": 1, "type": "project"}]
}
}
}
}
Success (Collection):
{
"data": [
{"id": 1, "type": "client", "attributes": {...}},
{"id": 2, "type": "client", "attributes": {...}}
],
"meta": {...},
"links": {...}
}
Rate Limiting
Limits
| Plan | Requests/Minute | Requests/Day |
|---|---|---|
| Standard | 60 | 10,000 |
| Premium | 120 | 50,000 |
Headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1704820800
Rate Limit Exceeded
{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests. Please retry after 60 seconds.",
"retry_after": 60
}
}
Error Handling
Error Response Format
{
"error": {
"code": "validation_error",
"message": "The given data was invalid.",
"details": {
"email": ["The email field is required."],
"company_name": ["The company name must be at least 2 characters."]
}
}
}
HTTP Status Codes
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 204 | No Content |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 422 | Validation Error |
| 429 | Rate Limited |
| 500 | Server Error |
Common Error Codes
| Code | Description |
|---|---|
invalid_token | Token invalid or expired |
unauthorized | Authentication required |
forbidden | Insufficient permissions |
not_found | Resource not found |
validation_error | Input validation failed |
rate_limit_exceeded | Too many requests |
Technical Architecture
Routes
Location: routes/api.php
Route::prefix('v1')->group(function () {
// Public routes
Route::post('login', [AuthController::class, 'login']);
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('clients', ClientController::class);
Route::apiResource('projects', ProjectController::class);
Route::apiResource('invoices', InvoiceController::class);
// Nested resources
Route::get('clients/{client}/projects', [ClientController::class, 'projects']);
Route::get('projects/{project}/files', [ProjectController::class, 'files']);
Route::post('projects/{project}/files', [ProjectController::class, 'uploadFile']);
});
});
API Controllers
Location: app/Http/Controllers/Api/V1/
class ClientController extends Controller
{
public function index(Request $request)
{
$clients = QueryBuilder::for(Client::class)
->allowedFilters(['status', 'created_at'])
->allowedSorts(['company_name', 'created_at'])
->allowedIncludes(['projects', 'invoices'])
->paginate($request->per_page ?? 20);
return ClientResource::collection($clients);
}
public function store(ClientRequest $request)
{
$client = Client::create($request->validated());
return new ClientResource($client);
}
}
API Resources
Location: app/Http/Resources/
class ClientResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'type' => 'client',
'attributes' => [
'company_name' => $this->company_name,
'email' => $this->email,
'phone' => $this->phone,
'is_active' => $this->is_active,
'created_at' => $this->created_at->toISOString(),
],
'relationships' => [
'projects' => ProjectResource::collection($this->whenLoaded('projects')),
],
];
}
}
Rate Limiting Configuration
// app/Providers/RouteServiceProvider.php
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
API Versioning
The API uses URL-based versioning:
/api/v1/... (current version)
/api/v2/... (future version)
Completed Audits API (AI Execution)
The v1 admin API exposes the audit-execution workflow for programmatic
consumers. Same auth as the rest of api/v1/admin/* (Sanctum bearer
token + admin role).
| Method | Path | Purpose |
|---|---|---|
| GET | /admin/completed-audits | List audits (filters: status, client_id, project_id, template_id, grade, my_audits, my_incomplete, stale, search, from_date, to_date) |
| POST | /admin/completed-audits | Create a new audit from a template (auto-snapshots, materializes findings) |
| GET | /admin/completed-audits/{id} | Show one audit. include= csv: client, project, template, conductor, findings, snapshot, scope_answers |
| DELETE | /admin/completed-audits/{id} | Soft-delete |
| GET | /admin/completed-audits/{id}/items | Flat list of (section + item + finding) ready to iterate. Filters: section_uuid, status, risk_level. Pagination caps at 200/page. |
| PATCH | /admin/completed-audits/{id}/findings/{finding_id} | Update one finding (status, notes, response_value, score, risk_level, ai_evidence) |
| POST | /admin/completed-audits/{id}/findings/bulk-update | All-or-nothing batch update (transaction) |
| GET | /admin/completed-audits/{id}/findings/{finding_id}/history | Per-finding change log |
| POST | /admin/completed-audits/{id}/scope-answers/bulk-upsert | Idempotent upsert of scope question answers |
| POST | /admin/completed-audits/{id}/submit-for-review | State transition → pending_review |
| POST | /admin/completed-audits/{id}/complete | State transition → completed. Optional body {executive_summary, next_audit_due}. |
| POST | /admin/completed-audits/{id}/cancel | State transition → cancelled. Optional body {reason}. |
Error codes worth knowing:
AUDIT_LOCKED(403) — writing to an audit whoseis_editableis falseINVALID_STATE_TRANSITION(409) — state machine rejected the transitionTOO_MANY_FINDINGS(422) —include=findingson an audit with > 1000 findings; use the items endpoint insteadEVIDENCE_REQUIRED(422) — the snapshot item requiresai_evidencewhen answering pass/fail/partial
For the AI Audit Sandbox client (used when an audit doesn't map to a
real ScopeForged customer), call
AiAuditSandboxClientSeeder::resolve() server-side or run
php artisan db:seed --class=AiAuditSandboxClientSeeder.
See Plan 268 for the full implementation contract and design decisions.
SDK Generation (Server-Side)
The admin SDK page (/admin/api-sdk) and the api:generate-sdk artisan
command produce client libraries from the live OpenAPI spec. The command
shells out to npx @openapitools/openapi-generator-cli, which is a Node
wrapper around a Java JAR — so the production server needs three
things present:
- Node + npm — already required for the deploy build pipeline
- A JRE — the wrapper downloads and invokes a generator JAR
- The CLI installed globally —
node_modulesis not shipped in the deploy archive (seeFILES_TO_TRANSFERinscripts/deploy/deploy-linux.cjs), so the CLI must be installed directly on the server
Installing on the Server (One-Time)
Run on the production host:
sudo bash scripts/setup/install-openapi-generator.sh
The installer is idempotent. Re-run with --force to reinstall. It will:
- Install
default-jre-headlessif Java is missing - Run
npm install -g @openapitools/openapi-generator-cli - Verify
npx --no-installresolves the binary
Deploy-Time Verification
After each release, the deploy script runs a non-blocking check and logs either:
✅ OpenAPI Generator CLI available: <version>
or a warning pointing at the installer script. Deploys never fail on this check — it's a regression guard for server rebuilds.
Failure Modes
| Symptom | Cause | Fix |
|---|---|---|
| "OpenAPI Generator CLI is not installed" in admin UI | Server missing the CLI | Run the installer script |
| Deploy log warns about missing CLI | Same as above | Run the installer script |
| Local SDK generation works but server fails | PATH issue — global npm prefix not on Apache user's PATH | Add $(npm config get prefix)/bin to www-data's PATH (via vhost SetEnv PATH or /etc/environment) |
Fallback: Spec-Only
The "Generate Spec" button on /admin/api-sdk and --spec-only on the
artisan command don't require the CLI — they just emit the OpenAPI JSON.
Users can then run the generator locally from any machine.
SDK Examples
PHP
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://your-domain.com/api/v1/',
'headers' => [
'Authorization' => 'Bearer ' . $apiToken,
'Accept' => 'application/json',
],
]);
// Get clients
$response = $client->get('clients');
$clients = json_decode($response->getBody(), true);
// Create client
$response = $client->post('clients', [
'json' => [
'company_name' => 'New Client',
'email' => 'client@example.com',
],
]);
JavaScript
const API_BASE = 'https://your-domain.com/api/v1';
const API_TOKEN = 'your_token';
async function getClients() {
const response = await fetch(`${API_BASE}/clients`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Accept': 'application/json',
},
});
return response.json();
}
async function createClient(data) {
const response = await fetch(`${API_BASE}/clients`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
return response.json();
}
API Endpoints Config Generator
An artisan command to generate a static configuration file of all API endpoints, enabling faster page loads and consistent endpoint documentation.
Running the Command
# Generate PHP config file (default)
php artisan api:generate-config
# Generate JSON format
php artisan api:generate-config --format=json
# Custom output location
php artisan api:generate-config --output=config/custom-endpoints.php
Output
The command generates config/api-endpoints.php containing:
return [
'stats' => [
'total' => 36,
'domains' => 6,
'methods' => ['GET' => 18, 'POST' => 12, 'DELETE' => 4, ...],
'authenticated' => 36,
'public' => 0,
'generated_at' => '2026-01-13T...',
],
'domains' => [
'Authentication' => [...],
'Files' => [...],
'Invoices' => [...],
// etc.
],
];
Domain Groupings
Endpoints are automatically grouped by domain based on their path:
| Domain | Route Patterns |
|---|---|
| Authentication | /me, /tokens, /auth |
| Clients | /clients |
| Projects | /projects |
| Invoices | /invoices |
| Files | /files, /uploads |
| Notifications | /notifications |
| Sync & Presence | /sync, /presence, /typing |
| Webhooks | /webhooks |
Endpoint Data Structure
Each endpoint includes:
[
'path' => '/api/v1/clients/{client}',
'method' => 'GET',
'name' => 'api.clients.show',
'summary' => 'Get Client details',
'description' => 'Retrieve detailed information about a specific Client.',
'resource' => 'clients',
'action' => 'show',
'controller' => 'ClientController',
'parameters' => [
['name' => 'client', 'in' => 'path', 'required' => true, 'type' => 'integer'],
],
'requires_auth' => true,
'rate_limited' => true,
]
When to Regenerate
Run the command after:
- Adding new API routes
- Modifying existing route definitions
- Changing route middleware
- Updating controller names
Technical Details
Location: app/Console/Commands/GenerateApiEndpointsConfig.php
The command:
- Scans all routes using
Route::getRoutes() - Filters for API routes (starting with
api) - Parses route parameters and middleware
- Groups endpoints by domain
- Generates clean PHP or JSON output
Related Features
Dependencies
| Feature | Relationship |
|---|---|
| Authentication | Token auth |
| Authorization | Permission checks |
Complementary Features
| Feature | Description |
|---|---|
| Webhooks | Event notifications |
| Activity Logging | API request logging |
Best Practices
- Store tokens securely - Never expose in client-side code
- Use appropriate scopes - Request only needed permissions
- Handle rate limits - Implement backoff strategies
- Validate responses - Check for errors
- Use pagination - Don't fetch all records at once
Troubleshooting
| Issue | Solution |
|---|---|
| 401 Unauthorized | Check token is valid and included |
| 403 Forbidden | Verify token has required abilities |
| 429 Rate Limited | Reduce request frequency |
| Validation errors | Check request payload format |
See Also
- Authentication - Auth system
- Webhooks - Event webhooks
- Activity Logging - Audit trail
Quick Reference
Admin URLs
| Page | URL | Description |
|---|---|---|
| API Playground | /admin/api-playground | Interactive API testing |
| SDK Downloads | /admin/api-sdk | Download generated SDKs |
| Swagger UI | /api/docs | OpenAPI documentation |
Artisan Commands
| Command | Description |
|---|---|
php artisan api:generate-config | Generate API endpoints config |
php artisan api:generate-sdk | Generate SDK packages |