API Overview
Status: Final Version: 1.0 Last Updated: 2026-01-03
Purpose
Define the API architecture, authentication, versioning, conventions, and common patterns for the ESG platform's REST APIs. This document establishes the foundation for both Collector APIs (mobile/field) and Admin APIs (web dashboard).
API Structure
API Separation
| API Type | Base Path | Primary Users | Purpose |
|---|---|---|---|
| Collector API | /api/v1/collector |
Mobile app, field collectors | Data submission, template retrieval, sync |
| Admin API | /api/v1/admin |
Web dashboard, reviewers, approvers | Configuration, review, approval, reporting |
| Public API | /api/v1/public |
External integrations (vNext) | Read-only exports, webhooks |
Authentication & Authorization
Authentication Methods
1. JWT (JSON Web Tokens) - Primary Method
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "collector@example.com",
"password": "***",
"device_id": "ABC123" // Optional for mobile
}
Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"user": {
"id": 42,
"email": "collector@example.com",
"roles": ["collector"],
"tenant_id": 5,
"scopes": ["site:123", "site:124"]
}
}
Usage:
Token Refresh:
2. Session-Based (Admin UI Fallback)
Laravel session cookies for web dashboard.
Authorization Model
Middleware Stack:
Request
→ TenantContext (set tenant_id from token)
→ Authenticate (verify JWT)
→ RoleCheck (verify user has required role)
→ ScopeCheck (verify site/project access)
→ RateLimiter
→ Route Handler
Laravel Implementation:
Route::group(['prefix' => 'api/v1/collector', 'middleware' => ['auth:api', 'tenant.context', 'role:collector']], function () {
Route::get('/templates', [CollectorController::class, 'templates']);
Route::post('/submissions', [CollectorController::class, 'submit']);
});
API Versioning
Strategy: URL Path Versioning
- Current:
/api/v1/... - Future:
/api/v2/...(when breaking changes required)
Backward Compatibility:
- v1 maintained for minimum 12 months after v2 release
- Deprecation headers on v1 endpoints: Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Common Conventions
Request Headers
| Header | Required | Description | Example |
|---|---|---|---|
Authorization |
✅ | JWT bearer token | Bearer eyJ0eXAi... |
X-Tenant-Id |
✅ | Tenant context | 5 |
Content-Type |
✅ (POST/PUT) | Request body format | application/json |
X-Idempotency-Key |
⚠️ (Submissions) | UUID for idempotent operations | 550e8400-e29b-41d4-a716-446655440000 |
Accept |
❌ | Response format (default: JSON) | application/json |
X-Device-Id |
❌ (Mobile only) | Device identifier | ABC123 |
Response Headers
| Header | Description |
|---|---|
X-RateLimit-Limit |
Total requests allowed per window |
X-RateLimit-Remaining |
Remaining requests in current window |
X-RateLimit-Reset |
Unix timestamp when limit resets |
X-Request-Id |
Unique request identifier for debugging |
Pagination
Standard Pagination (Cursor-Based):
Response:
{
"data": [...],
"meta": {
"current_page": 1,
"per_page": 50,
"total": 500
},
"links": {
"next": "/api/v1/admin/submissions?cursor=eyJpZCI6MTczfQ&limit=50",
"prev": null
}
}
Laravel Implementation:
return MetricSubmission::where('tenant_id', $tenantId)
->orderBy('created_at', 'desc')
->cursorPaginate(50);
Filtering & Sorting
Query Parameters:
Conventions:
- filter[field]=value: Equality filter
- filter[field][gte]=100: Range filter (gte, lte, gt, lt)
- sort=field: Ascending sort
- sort=-field: Descending sort (note minus sign)
Rate Limiting
Limits (per user per minute):
| Role | Requests/min | Burst |
|---|---|---|
| Collector | 60 | 10 |
| Reviewer | 120 | 20 |
| Approver | 120 | 20 |
| Admin | 300 | 50 |
| Auditor | 60 | 10 |
Response when rate limit exceeded:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1672531200
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry after 45 seconds.",
"retry_after": 45
}
}
Idempotency
Critical for Submissions and Mutations
Header:
Laravel Middleware:
class EnsureIdempotency
{
public function handle($request, $next)
{
$key = $request->header('X-Idempotency-Key');
if ($key && $cached = Cache::get("idempotency:{$key}")) {
return response()->json($cached['response'], $cached['status']);
}
$response = $next($request);
if ($key && $request->isMethod('post')) {
Cache::put("idempotency:{$key}", [
'response' => $response->getData(),
'status' => $response->status()
], 86400); // 24 hours
}
return $response;
}
}
Error Handling
See Error Handling & Status Codes for complete specification.
Standard Error Response:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The given data was invalid.",
"details": {
"value": ["The value must be a number."],
"evidence": ["At least one evidence file is required."]
},
"request_id": "req_abc123"
}
}
Common HTTP Status Codes
| Code | Meaning | Use Case |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST (resource created) |
| 202 | Accepted | Async operation queued |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid request format |
| 401 | Unauthorized | Missing or invalid token |
| 403 | Forbidden | Valid token but insufficient permissions |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate submission (idempotency violation) |
| 422 | Unprocessable Entity | Validation errors |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server error |
| 503 | Service Unavailable | Maintenance mode |
Cross-References
- Related: Collector API - Mobile/field endpoints
- Related: Admin API - Dashboard endpoints
- Related: Error Handling - Error response model
Change Log
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-01-03 | Senior Product Architect | Initial API overview and conventions |