Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

Integrations & Marketing/API Guide

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

  1. Getting Started
  2. API Playground
  3. Authentication
  4. API Endpoints
  5. Request & Response Format
  6. Rate Limiting
  7. Error Handling
  8. Technical Architecture
  9. SDK Generation (Server-Side)
  10. SDK Examples
  11. API Endpoints Config Generator
  12. Related Features

Getting Started

Base URL

Production: https://your-domain.com/api/v1
Development: http://localhost:8000/api/v1

Quick Start

  1. Create an API token in your profile settings
  2. Include the token in your request headers
  3. 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

FeatureDescription
Domain GroupingEndpoints organized by domain (Authentication, Files, Invoices, etc.)
Stats DashboardShows total endpoints, domains, authenticated vs public counts
Collapsible SectionsExpand/collapse domain groups for easy navigation
SearchFilter endpoints by path, method, or description
Request BuilderConstruct requests with headers, body, and query params
API Key SelectionSelect from available API keys for authentication
Code ExamplesAuto-generated examples in cURL, JavaScript, PHP, and Python
Response ViewerView response body, headers, status, timing, and size

Using the Playground

  1. Select an Endpoint: Click on any endpoint in the left panel
  2. Configure Request: Add headers, query params, or body content
  3. Select API Key: Choose an API key from the dropdown
  4. Send Request: Click "Send" to execute the request
  5. 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's role, 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

AbilityDescription
clients:readRead client data
clients:writeCreate/update clients
projects:readRead project data
projects:writeCreate/update projects
invoices:readRead invoice data
invoices:writeCreate/update invoices
files:readDownload files
files:writeUpload files

Token Management

# List tokens
GET /api/v1/user/tokens

# Revoke token
DELETE /api/v1/user/tokens/{id}

API Endpoints

Clients

MethodEndpointDescription
GET/clientsList all clients
POST/clientsCreate client
GET/clients/{id}Get client
PUT/clients/{id}Update client
DELETE/clients/{id}Delete client
GET/clients/{id}/projectsList client projects
GET/clients/{id}/invoicesList client invoices

Projects

MethodEndpointDescription
GET/projectsList all projects
POST/projectsCreate project
GET/projects/{id}Get project
PUT/projects/{id}Update project
DELETE/projects/{id}Delete project
GET/projects/{id}/filesList project files
POST/projects/{id}/filesUpload file

Invoices

MethodEndpointDescription
GET/invoicesList all invoices
POST/invoicesCreate invoice
GET/invoices/{id}Get invoice
PUT/invoices/{id}Update invoice
DELETE/invoices/{id}Delete invoice
POST/invoices/{id}/sendMark as sent
POST/invoices/{id}/paidMark as paid
GET/invoices/{id}/pdfDownload PDF

Files

MethodEndpointDescription
GET/filesList all files
GET/files/{id}Get file info
GET/files/{id}/downloadDownload file
DELETE/files/{id}Delete file

Users

MethodEndpointDescription
GET/userCurrent user
PUT/userUpdate profile
GET/user/notificationsList 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

PlanRequests/MinuteRequests/Day
Standard6010,000
Premium12050,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

CodeDescription
200Success
201Created
204No Content
400Bad Request
401Unauthorized
403Forbidden
404Not Found
422Validation Error
429Rate Limited
500Server Error

Common Error Codes

CodeDescription
invalid_tokenToken invalid or expired
unauthorizedAuthentication required
forbiddenInsufficient permissions
not_foundResource not found
validation_errorInput validation failed
rate_limit_exceededToo 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).

MethodPathPurpose
GET/admin/completed-auditsList audits (filters: status, client_id, project_id, template_id, grade, my_audits, my_incomplete, stale, search, from_date, to_date)
POST/admin/completed-auditsCreate 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}/itemsFlat 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-updateAll-or-nothing batch update (transaction)
GET/admin/completed-audits/{id}/findings/{finding_id}/historyPer-finding change log
POST/admin/completed-audits/{id}/scope-answers/bulk-upsertIdempotent upsert of scope question answers
POST/admin/completed-audits/{id}/submit-for-reviewState transition → pending_review
POST/admin/completed-audits/{id}/completeState transition → completed. Optional body {executive_summary, next_audit_due}.
POST/admin/completed-audits/{id}/cancelState transition → cancelled. Optional body {reason}.

Error codes worth knowing:

  • AUDIT_LOCKED (403) — writing to an audit whose is_editable is false
  • INVALID_STATE_TRANSITION (409) — state machine rejected the transition
  • TOO_MANY_FINDINGS (422) — include=findings on an audit with > 1000 findings; use the items endpoint instead
  • EVIDENCE_REQUIRED (422) — the snapshot item requires ai_evidence when 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:

  1. Node + npm — already required for the deploy build pipeline
  2. A JRE — the wrapper downloads and invokes a generator JAR
  3. The CLI installed globallynode_modules is not shipped in the deploy archive (see FILES_TO_TRANSFER in scripts/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-headless if Java is missing
  • Run npm install -g @openapitools/openapi-generator-cli
  • Verify npx --no-install resolves 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

SymptomCauseFix
"OpenAPI Generator CLI is not installed" in admin UIServer missing the CLIRun the installer script
Deploy log warns about missing CLISame as aboveRun the installer script
Local SDK generation works but server failsPATH issue — global npm prefix not on Apache user's PATHAdd $(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:

DomainRoute 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:

  1. Scans all routes using Route::getRoutes()
  2. Filters for API routes (starting with api)
  3. Parses route parameters and middleware
  4. Groups endpoints by domain
  5. Generates clean PHP or JSON output

Dependencies

FeatureRelationship
AuthenticationToken auth
AuthorizationPermission checks

Complementary Features

FeatureDescription
WebhooksEvent notifications
Activity LoggingAPI request logging

Best Practices

  1. Store tokens securely - Never expose in client-side code
  2. Use appropriate scopes - Request only needed permissions
  3. Handle rate limits - Implement backoff strategies
  4. Validate responses - Check for errors
  5. Use pagination - Don't fetch all records at once

Troubleshooting

IssueSolution
401 UnauthorizedCheck token is valid and included
403 ForbiddenVerify token has required abilities
429 Rate LimitedReduce request frequency
Validation errorsCheck request payload format

See Also


Quick Reference

Admin URLs

PageURLDescription
API Playground/admin/api-playgroundInteractive API testing
SDK Downloads/admin/api-sdkDownload generated SDKs
Swagger UI/api/docsOpenAPI documentation

Artisan Commands

CommandDescription
php artisan api:generate-configGenerate API endpoints config
php artisan api:generate-sdkGenerate SDK packages