Admin API
Status: Final Version: 1.0 Last Updated: 2026-01-11
Table of Contents
- Purpose
- Authentication & Authorization
- Role-Based Access Control (RBAC)
- Submission Management
- Reporting Period Management
- Metric Catalog Management
- Report Generation
- Audit Log
- Task Queue & Notifications
- Pagination & Filtering
- Error Handling
- Implementation Guide
- Security Considerations
- Performance Guidelines
Purpose
Define REST endpoints for web dashboard users (Reviewers, Approvers, Admins) to manage configurations, review submissions, approve data, and generate reports.
The Admin API enables: - Reviewers: Validate and review field-submitted data - Approvers: Approve or reject reviewed submissions - Admins: Configure metrics, manage users, generate reports - All Roles: View audit logs and track changes
Authentication & Authorization
All Admin API endpoints require:
1. Valid JWT Token in Authorization: Bearer <token> header
2. Active User Account with appropriate role
3. Tenant Context enforced via tenant_id in JWT
See API Overview for authentication details.
Example Request:
GET /api/v1/admin/submissions HTTP/1.1
Host: api.esg-platform.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
Role-Based Access Control (RBAC)
The Admin API enforces role-based permissions on all endpoints.
Role Hierarchy
| Role | Permissions | Can Access |
|---|---|---|
| Admin | Full system access | All endpoints |
| Approver | Approve submissions, lock periods, generate reports | Submissions (R/W), Periods (R/W), Reports (R/W), Audit (R) |
| Reviewer | Review submissions, add comments | Submissions (R/W), Periods (R), Reports (R), Audit (R) |
| Collector | Submit field data | Collector API only (no Admin API access) |
RBAC Enforcement
Each endpoint documents required permissions in format: Permission: {role}.{resource}.{action}
Example:
- admin.submissions.approve - Requires Approver or Admin role
- admin.metrics.create - Requires Admin role only
- admin.submissions.view - Requires Reviewer, Approver, or Admin role
Error Response (403 Forbidden):
{
"error": {
"code": "AUTH_INSUFFICIENT_PERMISSIONS",
"message": "User does not have permission to perform this action",
"details": {
"required_permission": "admin.submissions.approve",
"user_role": "REVIEWER",
"allowed_roles": ["APPROVER", "ADMIN"]
},
"request_id": "req_xyz789",
"timestamp": "2026-01-11T14:30:00Z"
}
}
Submission Management
Administrative endpoints for reviewing, approving, and managing metric submissions.
GET /api/v1/admin/submissions
Fetch submissions for review with advanced filtering and pagination.
Permission: admin.submissions.view (Reviewer, Approver, Admin)
Query Parameters:
| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
filter[state] |
string | No | Filter by submission state | validated, processed, approved, rejected |
filter[reporting_period_id] |
uuid | No | Filter by reporting period | 550e8400-e29b-41d4-a716-446655440000 |
filter[site_id] |
uuid | No | Filter by site | 650e8400-e29b-41d4-a716-446655440001 |
filter[metric_id] |
string | No | Filter by metric | GRI_302_1_ELECTRICITY |
filter[assigned_to] |
uuid | No | Filter by assigned reviewer | 750e8400-e29b-41d4-a716-446655440002 |
filter[submitted_from] |
datetime | No | Filter submissions after date | 2026-01-01T00:00:00Z |
filter[submitted_to] |
datetime | No | Filter submissions before date | 2026-01-31T23:59:59Z |
filter[has_validation_warnings] |
boolean | No | Filter by validation warning presence | true, false |
search |
string | No | Full-text search in comments | electricity |
sort |
string | No | Sort field (prefix - for descending) |
-created_at, value, state |
page |
integer | No | Page number (1-indexed) | 1 |
pageSize |
integer | No | Items per page (max 100) | 50 |
Response (200 OK):
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440010",
"submissionUuid": "550e8400-e29b-41d4-a716-446655440011",
"metric": {
"metricId": "GRI_302_1_ELECTRICITY",
"name": "Electricity Consumption",
"unit": "MWh",
"category": "ENERGY"
},
"site": {
"id": "650e8400-e29b-41d4-a716-446655440001",
"name": "Factory A - Shanghai",
"country": "CN"
},
"reportingPeriod": {
"id": "750e8400-e29b-41d4-a716-446655440003",
"name": "Q1 2026",
"startDate": "2026-01-01",
"endDate": "2026-03-31"
},
"value": 1250.50,
"unit": "MWh",
"state": "VALIDATED",
"validationResults": {
"passed": true,
"warnings": [
{
"type": "ANOMALY_DETECTION",
"severity": "LOW",
"message": "Value 15% higher than Q4 2025 average"
}
],
"errors": []
},
"evidence": [
{
"id": "850e8400-e29b-41d4-a716-446655440004",
"filename": "utility_bill_jan2026.pdf",
"fileSize": 245632,
"contentType": "application/pdf",
"uploadedAt": "2026-01-05T10:15:00Z"
}
],
"assignedTo": {
"id": "950e8400-e29b-41d4-a716-446655440005",
"name": "Jane Reviewer",
"email": "jane.reviewer@example.com"
},
"submittedBy": {
"id": "a50e8400-e29b-41d4-a716-446655440006",
"name": "John Collector",
"email": "john.collector@example.com"
},
"submittedAt": "2026-01-05T10:30:00Z",
"reviewedAt": null,
"approvedAt": null,
"metadata": {
"dataSource": "MANUAL_ENTRY",
"collectorApp": "ESG Mobile v2.1.0"
},
"createdAt": "2026-01-05T10:30:00Z",
"updatedAt": "2026-01-05T14:22:00Z"
}
],
"meta": {
"page": 1,
"pageSize": 50,
"total": 328,
"totalPages": 7
},
"links": {
"first": "/api/v1/admin/submissions?page=1&pageSize=50",
"prev": null,
"next": "/api/v1/admin/submissions?page=2&pageSize=50",
"last": "/api/v1/admin/submissions?page=7&pageSize=50"
}
}
Error Responses:
- 401 Unauthorized - Invalid or expired JWT token
- 403 Forbidden - User lacks admin.submissions.view permission
- 422 Unprocessable Entity - Invalid filter parameters
- 429 Too Many Requests - Rate limit exceeded (500 req/hour)
Example Request:
curl -X GET "https://api.esg-platform.example.com/api/v1/admin/submissions?filter[state]=validated&filter[has_validation_warnings]=true&sort=-submitted_at&page=1&pageSize=20" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json"
GET /api/v1/admin/submissions/{id}
Fetch detailed submission information including full audit trail.
Permission: admin.submissions.view (Reviewer, Approver, Admin)
Path Parameters:
- id (uuid, required) - Submission ID
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440010",
"submissionUuid": "550e8400-e29b-41d4-a716-446655440011",
"metric": {
"metricId": "GRI_302_1_ELECTRICITY",
"name": "Electricity Consumption",
"unit": "MWh",
"category": "ENERGY",
"validationRules": [
{"type": "DOMAIN", "rule": "min", "value": 0},
{"type": "DOMAIN", "rule": "max", "value": 1000000}
]
},
"site": {
"id": "650e8400-e29b-41d4-a716-446655440001",
"name": "Factory A - Shanghai",
"country": "CN",
"organizationalBoundary": "OPERATIONAL_CONTROL"
},
"reportingPeriod": {
"id": "750e8400-e29b-41d4-a716-446655440003",
"name": "Q1 2026",
"startDate": "2026-01-01",
"endDate": "2026-03-31",
"state": "OPEN"
},
"value": 1250.50,
"unit": "MWh",
"state": "VALIDATED",
"validationResults": {
"passed": true,
"validatedAt": "2026-01-05T10:30:05Z",
"warnings": [
{
"type": "ANOMALY_DETECTION",
"severity": "LOW",
"message": "Value 15% higher than Q4 2025 average (1087.45 MWh)",
"context": {
"previousValue": 1087.45,
"percentageChange": 15.0
}
}
],
"errors": []
},
"evidence": [
{
"id": "850e8400-e29b-41d4-a716-446655440004",
"filename": "utility_bill_jan2026.pdf",
"fileSize": 245632,
"contentType": "application/pdf",
"uploadedAt": "2026-01-05T10:15:00Z",
"uploadedBy": {
"id": "a50e8400-e29b-41d4-a716-446655440006",
"name": "John Collector"
},
"downloadUrl": "/api/v1/collector/evidence/850e8400-e29b-41d4-a716-446655440004"
}
],
"assignedTo": {
"id": "950e8400-e29b-41d4-a716-446655440005",
"name": "Jane Reviewer",
"email": "jane.reviewer@example.com",
"role": "REVIEWER"
},
"reviewComments": [
{
"id": "b50e8400-e29b-41d4-a716-446655440007",
"author": {
"id": "950e8400-e29b-41d4-a716-446655440005",
"name": "Jane Reviewer"
},
"comment": "Checked against utility bill - value confirmed",
"createdAt": "2026-01-05T14:22:00Z"
}
],
"auditTrail": [
{
"action": "CREATED",
"actor": {
"id": "a50e8400-e29b-41d4-a716-446655440006",
"name": "John Collector"
},
"timestamp": "2026-01-05T10:30:00Z",
"metadata": {"source": "mobile_app"}
},
{
"action": "VALIDATED",
"actor": {"id": "system", "name": "Validation Engine"},
"timestamp": "2026-01-05T10:30:05Z",
"metadata": {"validationType": "AUTOMATED"}
},
{
"action": "ASSIGNED",
"actor": {"id": "system", "name": "Task Router"},
"assignedTo": "950e8400-e29b-41d4-a716-446655440005",
"timestamp": "2026-01-05T10:30:10Z"
},
{
"action": "COMMENT_ADDED",
"actor": {
"id": "950e8400-e29b-41d4-a716-446655440005",
"name": "Jane Reviewer"
},
"timestamp": "2026-01-05T14:22:00Z"
}
],
"submittedBy": {
"id": "a50e8400-e29b-41d4-a716-446655440006",
"name": "John Collector",
"email": "john.collector@example.com",
"role": "COLLECTOR"
},
"submittedAt": "2026-01-05T10:30:00Z",
"reviewedAt": null,
"approvedAt": null,
"metadata": {
"dataSource": "MANUAL_ENTRY",
"collectorApp": "ESG Mobile v2.1.0",
"location": {"lat": 31.2304, "lng": 121.4737}
},
"createdAt": "2026-01-05T10:30:00Z",
"updatedAt": "2026-01-05T14:22:00Z"
}
Error Responses:
- 404 Not Found - Submission not found or not accessible to this tenant
- 403 Forbidden - User lacks permission to view this submission
POST /api/v1/admin/submissions/{id}/approve
Approve a validated submission, marking it as approved for reporting.
Permission: admin.submissions.approve (Approver, Admin only)
Path Parameters:
- id (uuid, required) - Submission ID
Request Body:
{
"comment": "Data verified against utility bills. Jan 2026 electricity consumption aligns with invoice #INV-2026-0145.",
"metadata": {
"invoiceNumber": "INV-2026-0145",
"verificationMethod": "INVOICE_MATCH"
}
}
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440010",
"state": "APPROVED",
"approvedBy": {
"id": "c50e8400-e29b-41d4-a716-446655440008",
"name": "Jane Approver",
"email": "jane.approver@example.com"
},
"approvedAt": "2026-01-10T14:00:00Z",
"approvalComment": "Data verified against utility bills. Jan 2026 electricity consumption aligns with invoice #INV-2026-0145.",
"updatedAt": "2026-01-10T14:00:00Z"
}
Error Responses:
- 400 Bad Request - Submission not in approvable state (must be VALIDATED or PROCESSED)
{
"error": {
"code": "STATE_INVALID_TRANSITION",
"message": "Cannot approve submission in current state",
"details": {
"currentState": "DRAFT",
"requiredStates": ["VALIDATED", "PROCESSED"]
},
"request_id": "req_xyz789",
"timestamp": "2026-01-10T14:00:00Z"
}
}
403 Forbidden - User lacks admin.submissions.approve permission
- 404 Not Found - Submission not found
- 409 Conflict - Submission already approved by another approver
State Transition: VALIDATED → APPROVED or PROCESSED → APPROVED
Side Effects: - Creates audit log entry - Triggers notification to submitter - Updates reporting period completion percentage - Unlocks dependent calculations (if any)
POST /api/v1/admin/submissions/{id}/reject
Reject a submission with detailed feedback for the collector to address.
Permission: admin.submissions.reject (Reviewer, Approver, Admin)
Path Parameters:
- id (uuid, required) - Submission ID
Request Body:
{
"reason": "Value appears significantly higher than historical average. Please verify meter reading and unit conversion.",
"requiredCorrections": [
"Verify electricity meter reading from January 2026",
"Confirm unit is MWh (not kWh)",
"Attach clear photo of meter display",
"Cross-check against utility invoice"
],
"severity": "MAJOR",
"metadata": {
"historicalAverage": 1087.45,
"submittedValue": 1250.50,
"percentageDeviation": 15.0
}
}
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440010",
"state": "REJECTED",
"rejectedBy": {
"id": "950e8400-e29b-41d4-a716-446655440005",
"name": "Jane Reviewer",
"email": "jane.reviewer@example.com"
},
"rejectedAt": "2026-01-10T14:30:00Z",
"reviewerFeedback": {
"reason": "Value appears significantly higher than historical average. Please verify meter reading and unit conversion.",
"requiredCorrections": [
"Verify electricity meter reading from January 2026",
"Confirm unit is MWh (not kWh)",
"Attach clear photo of meter display",
"Cross-check against utility invoice"
],
"severity": "MAJOR"
},
"updatedAt": "2026-01-10T14:30:00Z"
}
Error Responses:
- 400 Bad Request - Submission not in rejectable state
- 403 Forbidden - User lacks permission
- 404 Not Found - Submission not found
- 422 Unprocessable Entity - Missing required rejection reason
State Transition: VALIDATED → REJECTED or PROCESSED → REJECTED
Side Effects: - Creates audit log entry - Sends notification to submitter with feedback - Unlocks submission for editing (submitter can update and resubmit) - Decrements reporting period completion percentage
POST /api/v1/admin/submissions/{id}/comments
Add a review comment to a submission without changing its state.
Permission: admin.submissions.comment (Reviewer, Approver, Admin)
Path Parameters:
- id (uuid, required) - Submission ID
Request Body:
{
"comment": "Waiting for clarification from site manager on meter replacement in mid-January. May explain higher reading.",
"visibility": "INTERNAL"
}
Response (201 Created):
{
"id": "d50e8400-e29b-41d4-a716-446655440009",
"submissionId": "550e8400-e29b-41d4-a716-446655440010",
"author": {
"id": "950e8400-e29b-41d4-a716-446655440005",
"name": "Jane Reviewer"
},
"comment": "Waiting for clarification from site manager on meter replacement in mid-January. May explain higher reading.",
"visibility": "INTERNAL",
"createdAt": "2026-01-10T15:00:00Z"
}
Visibility Options:
- INTERNAL - Visible only to reviewers and approvers
- PUBLIC - Visible to submitter as well
POST /api/v1/admin/submissions/bulk-assign
Bulk assign submissions to reviewers for efficient workload distribution.
Permission: admin.submissions.assign (Approver, Admin)
Request Body:
{
"submissionIds": [
"550e8400-e29b-41d4-a716-446655440010",
"550e8400-e29b-41d4-a716-446655440011",
"550e8400-e29b-41d4-a716-446655440012"
],
"assignToUserId": "950e8400-e29b-41d4-a716-446655440005",
"priority": "HIGH",
"dueDate": "2026-01-15T23:59:59Z"
}
Response (200 OK):
{
"assignedCount": 3,
"failedAssignments": [],
"assignedTo": {
"id": "950e8400-e29b-41d4-a716-446655440005",
"name": "Jane Reviewer",
"currentWorkload": 12
}
}
Reporting Period Management
Endpoints for managing reporting periods (quarters, fiscal years) and their lifecycles.
GET /api/v1/admin/reporting-periods
List all reporting periods with completion statistics.
Permission: admin.periods.view (Reviewer, Approver, Admin)
Query Parameters:
- filter[state] (string) - Filter by state: open, locked, archived
- filter[year] (integer) - Filter by year: 2026
- sort (string) - Sort by: start_date, -end_date, name
Response (200 OK):
{
"data": [
{
"id": "750e8400-e29b-41d4-a716-446655440003",
"name": "Q1 2026",
"description": "First Quarter 2026 Reporting Period",
"startDate": "2026-01-01",
"endDate": "2026-03-31",
"state": "OPEN",
"completionPercentage": 68.5,
"submissionsCount": {
"total": 500,
"draft": 25,
"validated": 98,
"processed": 50,
"approved": 327,
"rejected": 0
},
"mandatoryMetricsCount": {
"required": 120,
"submitted": 98,
"approved": 82
},
"slaDaysRemaining": 15,
"lockedAt": null,
"lockedBy": null,
"contentHash": null,
"createdAt": "2025-12-01T00:00:00Z",
"updatedAt": "2026-01-10T16:00:00Z"
},
{
"id": "850e8400-e29b-41d4-a716-446655440012",
"name": "Q4 2025",
"description": "Fourth Quarter 2025 Reporting Period",
"startDate": "2025-10-01",
"endDate": "2025-12-31",
"state": "LOCKED",
"completionPercentage": 100.0,
"submissionsCount": {
"total": 480,
"approved": 480,
"draft": 0,
"validated": 0,
"processed": 0,
"rejected": 0
},
"mandatoryMetricsCount": {
"required": 120,
"submitted": 120,
"approved": 120
},
"slaDaysRemaining": 0,
"lockedAt": "2025-12-15T16:00:00Z",
"lockedBy": {
"id": "c50e8400-e29b-41d4-a716-446655440008",
"name": "Jane Approver"
},
"contentHash": "sha256:a3c4f8b912e5d7f6c3b2a1e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9",
"createdAt": "2025-09-01T00:00:00Z",
"updatedAt": "2025-12-15T16:00:00Z"
}
],
"meta": {
"page": 1,
"pageSize": 50,
"total": 8,
"totalPages": 1
}
}
GET /api/v1/admin/reporting-periods/{id}
Get detailed information about a specific reporting period.
Permission: admin.periods.view (Reviewer, Approver, Admin)
Response (200 OK):
{
"id": "750e8400-e29b-41d4-a716-446655440003",
"name": "Q1 2026",
"description": "First Quarter 2026 Reporting Period",
"startDate": "2026-01-01",
"endDate": "2026-03-31",
"state": "OPEN",
"completionPercentage": 68.5,
"submissionsCount": {
"total": 500,
"draft": 25,
"validated": 98,
"processed": 50,
"approved": 327,
"rejected": 0
},
"mandatoryMetricsCount": {
"required": 120,
"submitted": 98,
"approved": 82,
"missingMetrics": [
"GRI_305_1_SCOPE1_CO2",
"GRI_306_3_WASTE_GENERATED"
]
},
"siteCoverage": {
"totalSites": 15,
"sitesReporting": 12,
"sitesComplete": 8,
"sitesMissing": [
{"id": "site_003", "name": "Factory C - Mumbai"},
{"id": "site_007", "name": "Warehouse G - Dubai"}
]
},
"slaDaysRemaining": 15,
"lockedAt": null,
"lockedBy": null,
"contentHash": null,
"createdAt": "2025-12-01T00:00:00Z",
"updatedAt": "2026-01-10T16:00:00Z"
}
POST /api/v1/admin/reporting-periods
Create a new reporting period.
Permission: admin.periods.create (Admin only)
Request Body:
{
"name": "Q2 2026",
"description": "Second Quarter 2026 Reporting Period",
"startDate": "2026-04-01",
"endDate": "2026-06-30",
"reportingDeadline": "2026-07-15",
"mandatoryMetrics": [
"GRI_302_1_ELECTRICITY",
"GRI_305_1_SCOPE1_CO2",
"GRI_306_3_WASTE_GENERATED"
]
}
Response (201 Created):
{
"id": "950e8400-e29b-41d4-a716-446655440013",
"name": "Q2 2026",
"description": "Second Quarter 2026 Reporting Period",
"startDate": "2026-04-01",
"endDate": "2026-06-30",
"state": "OPEN",
"completionPercentage": 0.0,
"createdAt": "2026-01-10T16:30:00Z"
}
POST /api/v1/admin/reporting-periods/{id}/lock
Lock a reporting period to prevent further modifications. This creates an immutable snapshot for compliance.
Permission: admin.periods.lock (Approver, Admin)
Path Parameters:
- id (uuid, required) - Reporting period ID
Request Body:
{
"justification": "All Q4 2025 data reviewed and approved for FY2025 annual sustainability report. Ready for external audit.",
"reviewedBy": [
"950e8400-e29b-41d4-a716-446655440005",
"c50e8400-e29b-41d4-a716-446655440008"
]
}
Response (200 OK):
{
"id": "850e8400-e29b-41d4-a716-446655440012",
"name": "Q4 2025",
"state": "LOCKED",
"lockedAt": "2026-01-10T16:45:00Z",
"lockedBy": {
"id": "c50e8400-e29b-41d4-a716-446655440008",
"name": "Jane Approver",
"email": "jane.approver@example.com"
},
"lockJustification": "All Q4 2025 data reviewed and approved for FY2025 annual sustainability report. Ready for external audit.",
"contentHash": "sha256:a3c4f8b912e5d7f6c3b2a1e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9",
"updatedAt": "2026-01-10T16:45:00Z"
}
Prerequisites:
- All mandatory metrics must be submitted
- No submissions in DRAFT or VALIDATED state (must be APPROVED or REJECTED)
- At least 95% of submissions must be APPROVED
Error Responses:
- 400 Bad Request - Prerequisites not met
{
"error": {
"code": "STATE_PREREQUISITES_NOT_MET",
"message": "Cannot lock period: prerequisites not met",
"details": {
"unreviewedSubmissions": 12,
"missingMandatoryMetrics": ["GRI_305_1_SCOPE1_CO2"],
"approvalRate": 92.5,
"minimumApprovalRate": 95.0
},
"request_id": "req_xyz789",
"timestamp": "2026-01-10T16:45:00Z"
}
}
State Transition: OPEN → LOCKED
Side Effects: - Generates SHA-256 content hash of all approved submissions - Prevents any further modifications to submissions in this period - Triggers compliance report generation - Sends notifications to stakeholders - Creates immutable audit log entry
POST /api/v1/admin/reporting-periods/{id}/unlock
Unlock a locked period for restatement (requires strong justification).
Permission: admin.periods.unlock (Admin only)
Request Body:
{
"justification": "Material error discovered in GRI 305-1 calculation methodology. Restatement required per GRI Standards.",
"approvedBy": "CFO Jane Smith",
"restatementReason": "CALCULATION_ERROR",
"affectedMetrics": ["GRI_305_1_SCOPE1_CO2"]
}
Response (200 OK):
{
"id": "850e8400-e29b-41d4-a716-446655440012",
"name": "Q4 2025",
"state": "OPEN",
"version": 2,
"previousContentHash": "sha256:a3c4f8b912e5d7f6c3b2a1e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9",
"restatement": {
"reason": "CALCULATION_ERROR",
"justification": "Material error discovered in GRI 305-1 calculation methodology. Restatement required per GRI Standards.",
"approvedBy": "CFO Jane Smith",
"unlockedAt": "2026-01-10T17:00:00Z",
"unlockedBy": {
"id": "admin_001",
"name": "System Admin"
}
}
}
State Transition: LOCKED → OPEN (version incremented)
Metric Catalog Management
Administrative endpoints for managing metric templates and custom KPIs (Admin role only).
GET /api/v1/admin/metrics
List all metric templates with validation rules and framework mappings.
Permission: admin.metrics.view (All roles can view; Admin can modify)
Query Parameters:
- filter[category] (string) - Filter by category: ENERGY, EMISSIONS, WATER, WASTE, SOCIAL, GOVERNANCE
- filter[is_mandatory] (boolean) - Filter mandatory metrics
- filter[framework] (string) - Filter by framework: GRI, SASB, TCFD, CUSTOM
- search (string) - Search metric name or ID
Response (200 OK):
{
"data": [
{
"metricId": "GRI_302_1_ELECTRICITY",
"name": "Electricity Consumption",
"description": "Total electricity consumed by the organization from all sources",
"unit": "MWh",
"dataType": "numeric",
"category": "ENERGY",
"isMandatory": true,
"isCustomKpi": false,
"dimensionality": "site",
"framework": {
"name": "GRI",
"version": "2021",
"disclosure": {
"code": "302-1",
"name": "Energy consumption within the organization"
}
},
"validationRules": [
{"type": "DOMAIN", "rule": "min", "value": 0, "message": "Value cannot be negative"},
{"type": "DOMAIN", "rule": "max", "value": 1000000, "message": "Value exceeds reasonable maximum"},
{"type": "REQUIRED_EVIDENCE", "minimumFiles": 1, "acceptedTypes": ["pdf", "jpg", "png"]}
],
"createdAt": "2025-01-01T00:00:00Z",
"updatedAt": "2025-06-15T10:00:00Z"
}
],
"meta": {
"page": 1,
"pageSize": 50,
"total": 145,
"totalPages": 3
}
}
POST /api/v1/admin/metrics
Create a custom KPI metric for organization-specific tracking.
Permission: admin.metrics.create (Admin only)
Request Body:
{
"metricId": "CUSTOM_WATER_RECYCLING",
"name": "Water Recycling Rate",
"description": "Percentage of water recycled and reused in manufacturing processes",
"unit": "%",
"dataType": "numeric",
"category": "WATER",
"isCustomKpi": true,
"dimensionality": "site",
"validationRules": [
{"type": "DOMAIN", "rule": "min", "value": 0},
{"type": "DOMAIN", "rule": "max", "value": 100},
{"type": "REQUIRED_EVIDENCE", "minimumFiles": 1}
],
"calculationMethod": "(recycled_water_volume / total_water_intake) * 100"
}
Response (201 Created):
{
"metricId": "CUSTOM_WATER_RECYCLING",
"name": "Water Recycling Rate",
"unit": "%",
"category": "WATER",
"isCustomKpi": true,
"createdAt": "2026-01-10T17:30:00Z"
}
Report Generation
Asynchronous report generation endpoints with job tracking.
POST /api/v1/admin/reports/generate
Queue a report generation job for a locked reporting period.
Permission: admin.reports.generate (Approver, Admin)
Request Body:
{
"reportingPeriodId": "850e8400-e29b-41d4-a716-446655440012",
"reportType": "gri_sustainability_report",
"format": "pdf",
"includeSections": ["environmental", "social", "governance"],
"options": {
"includeAuditTrail": true,
"includeEvidence": false,
"language": "en",
"template": "corporate_standard"
}
}
Report Types:
- gri_sustainability_report - Full GRI sustainability report
- carbon_footprint_report - Carbon emissions summary
- data_quality_report - Validation and completeness metrics
- custom_kpi_dashboard - Custom KPI summary
- human_capital_report - Human capital metrics (GRI 405-1, GRI 401, Employee Age)
- environment_report - Environmental metrics (G.1-G.10: Production, Materials, Energy, Water, Air Emissions, Waste, TSF, Rehabilitation, Incidents)
Response (202 Accepted):
{
"jobId": "a50e8400-e29b-41d4-a716-446655440020",
"status": "QUEUED",
"estimatedCompletion": "2026-01-10T18:00:00Z",
"statusUrl": "/api/v1/admin/reports/jobs/a50e8400-e29b-41d4-a716-446655440020",
"queuePosition": 3
}
Error Responses:
- 400 Bad Request - Period not locked or report prerequisites not met
- 403 Forbidden - User lacks permission
- 429 Too Many Requests - Rate limit: 10 reports/hour per tenant
GET /api/v1/admin/reports/jobs/{jobId}
Check status of a report generation job.
Permission: admin.reports.view (Approver, Admin)
Response (200 OK) - In Progress:
{
"jobId": "a50e8400-e29b-41d4-a716-446655440020",
"status": "PROCESSING",
"progress": 45,
"currentStep": "Generating PDF from template",
"estimatedCompletion": "2026-01-10T18:00:00Z",
"createdAt": "2026-01-10T17:45:00Z"
}
Response (200 OK) - Completed:
{
"jobId": "a50e8400-e29b-41d4-a716-446655440020",
"status": "COMPLETED",
"progress": 100,
"downloadUrl": "/api/v1/admin/reports/download/gri_report_q4_2025.pdf",
"fileSize": 2457600,
"expiresAt": "2026-01-17T17:45:00Z",
"completedAt": "2026-01-10T17:58:00Z"
}
Job Statuses:
- QUEUED - Waiting in queue
- PROCESSING - Currently generating
- COMPLETED - Ready for download
- FAILED - Generation failed (see error field)
GET /api/v1/admin/reports/download/{filename}
Download a generated report file.
Permission: admin.reports.download (Approver, Admin)
Response (200 OK):
- Content-Type: application/pdf or application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
- Content-Disposition: attachment; filename="gri_report_q4_2025.pdf"
- Binary file content
Error Responses:
- 404 Not Found - File not found or expired
- 410 Gone - File expired (7-day retention)
Human Capital Report Generation
Generate human capital reports covering GRI 405-1 (Employee Demographics), GRI 401 (Employment Type and Turnover), and Employee Age distribution.
POST /api/v1/admin/reports/human-capital/generate
Queue a human capital report generation job.
Permission: admin.reports.generate (Approver, Admin)
Request Body:
{
"reportingPeriodId": "850e8400-e29b-41d4-a716-446655440012",
"format": "xlsx",
"filters": {
"organisationId": "org_uuid",
"siteIds": ["site_uuid_1", "site_uuid_2"],
"employmentLevels": ["EXECUTIVE", "SALARIED_STAFF_NON_NEC", "WAGED_STAFF_NEC"],
"employmentTypes": ["PERMANENT", "FIXED_TERM"],
"genders": ["MALE", "FEMALE"],
"ageGroups": ["UNDER_30", "AGED_30_50", "OVER_50"]
},
"options": {
"includeDemographics": true,
"includeEmploymentType": true,
"includeAgeDistribution": true,
"aggregationLevel": "site",
"language": "en"
}
}
Request Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
reportingPeriodId |
uuid | Yes | Reporting period (fiscal year) for the report |
format |
string | Yes | Export format: csv or xlsx |
filters |
object | No | Optional filters to scope the report |
filters.organisationId |
uuid | Yes* | Organisation ID (required unless multi-org tenant) |
filters.siteIds |
array[uuid] | No | Specific sites to include (omit for all sites) |
filters.employmentLevels |
array[string] | No | Filter E.1 by employment level |
filters.employmentTypes |
array[string] | No | Filter E.2 by employment type |
filters.genders |
array[string] | No | Filter E.2 recruitment by gender |
filters.ageGroups |
array[string] | No | Filter E.3 by age group |
options |
object | No | Report generation options |
options.includeDemographics |
boolean | No | Include E.1 (GRI 405-1) section (default: true) |
options.includeEmploymentType |
boolean | No | Include E.2 (GRI 401) section (default: true) |
options.includeAgeDistribution |
boolean | No | Include E.3 (Employee Age) section (default: true) |
options.aggregationLevel |
string | No | site or organisation (default: organisation) |
options.language |
string | No | Report language: en (default), es, fr |
Response (202 Accepted):
{
"jobId": "b60e8400-e29b-41d4-a716-446655440030",
"status": "QUEUED",
"estimatedCompletion": "2026-01-17T22:10:00Z",
"statusUrl": "/api/v1/admin/reports/jobs/b60e8400-e29b-41d4-a716-446655440030",
"queuePosition": 1,
"reportType": "human_capital_report",
"format": "xlsx"
}
Error Responses:
- 400 Bad Request - Invalid filters or missing required parameters
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Invalid report generation request",
"details": {
"filters.employmentLevels": "Invalid employment level: MANAGER. Allowed values: EXECUTIVE, SALARIED_STAFF_NON_NEC, WAGED_STAFF_NEC"
},
"request_id": "req_xyz789",
"timestamp": "2026-01-17T22:00:00Z"
}
}
403 Forbidden - User lacks permission
- 404 Not Found - Reporting period not found
- 429 Too Many Requests - Rate limit: 10 reports/hour per tenant
Export Format Specifications
CSV Export
File Naming Convention:
Example: Eureka_Gold_Human_Capital_FY2025_2026-01-17.csv
Structure: Single CSV file with all sections concatenated, using section headers to separate E.1, E.2, and E.3.
E.1 Columns (Employee Demographics):
Employment_Level,Quarter,Male,Female,From_Local_Community,Percent_Male,Percent_Female,Percent_From_Local_Community,Total_Employees,Reporting_Period,Organisation_ID,Site_ID,Collection_Date
E.2 Columns (Employment Type and Turnover):
Metric_Type,Employment_Type_Or_Gender,Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec,Aggregate_Value,Aggregate_Type,Reporting_Period,Organisation_ID,Site_ID
E.3 Columns (Employee Age):
Age_Group,Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec,Annual_Total,Reporting_Period,Organisation_ID,Site_ID
XLSX Export
File Naming Convention:
Example: Eureka_Gold_Human_Capital_FY2025_2026-01-17.xlsx
Sheet Structure:
| Sheet # | Sheet Name | Content | Rows |
|---|---|---|---|
| 1 | E.1 Demographics | GRI 405-1 quarterly employee demographics by employment level | 1 header + N data rows (3 employment levels × 4 quarters) |
| 2 | E.2 Employment & Turnover | GRI 401 monthly employment type, recruitment, departures, casual workers | 4 sub-tables with headers |
| 3 | E.3 Age Distribution | Monthly employee age group distribution | 1 header + 3 age groups + 1 total row |
| 4 | Reference Data | Employment level definitions, age group ranges, data collection notes | Metadata |
Sheet 1 - E.1 Demographics:
| Employment Level | Quarter | Male | Female | From Local Community | % Male | % Female | % From Local Community | Total Employees | Site | Collection Date |
Sheet 2 - E.2 Employment & Turnover:
Four sub-tables separated by blank rows:
-
Permanent vs. Fixed Term Employment
-
New Recruitments (Permanent Staff by Gender)
-
Departures (Total Permanent Staff)
-
Casual Workers
Sheet 3 - E.3 Age Distribution:
| Age Group | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | Annual Total |
Sheet 4 - Reference Data:
Employment Levels:
- EXECUTIVE: Senior management and executives
- SALARIED_STAFF_NON_NEC: Salaried employees not covered by union agreements
- WAGED_STAFF_NEC: Hourly/waged employees covered by union agreements (NEC)
Age Groups:
- UNDER_30: Employees under 30 years old
- AGED_30_50: Employees aged 30-50 years
- OVER_50: Employees over 50 years old
Collection Frequency:
- E.1: Quarterly (end of quarter snapshot)
- E.2: Monthly (end of month values)
- E.3: Monthly (end of month snapshot)
Notes:
- Percentages in E.1 are calculated as (Count / Total) × 100
- Averages in E.2 are calculated as (Sum of monthly values) / (Number of months with data)
- Totals in E.2 (Departures, Casual Workers) represent annual sum
- Annual Total in E.3 represents unique employee counts (not simple sum of monthly totals)
Filter Behavior
Required Filters:
- reportingPeriodId: Fiscal year for the report
Optional Filters:
- organisationId: Scope to specific organisation (required for multi-org tenants)
- siteIds: Array of site UUIDs
- Omit: Include all sites
- Provide: Include only specified sites
- Applies to: E.1 (site-level demographics for Salaried/Waged), E.2, E.3
- employmentLevels: Filter E.1 demographics
- Values:
EXECUTIVE,SALARIED_STAFF_NON_NEC,WAGED_STAFF_NEC - Example:
["EXECUTIVE"]returns only executive demographics - employmentTypes: Filter E.2 employment type table
- Values:
PERMANENT,FIXED_TERM - Example:
["PERMANENT"]returns only permanent employee data - genders: Filter E.2 recruitment table
- Values:
MALE,FEMALE - Example:
["FEMALE"]returns only female recruitment data - ageGroups: Filter E.3 age distribution
- Values:
UNDER_30,AGED_30_50,OVER_50 - Example:
["UNDER_30", "AGED_30_50"]excludes over-50 employees
Filter Application Logic: - Filters are applied with AND logic within a category (e.g., siteIds) - Multiple filters across categories use AND logic (e.g., siteIds AND employmentLevels) - Empty array means "no filter" (include all values) - Null/undefined filter field means "no filter" (include all values)
Example Filtered Request:
{
"reportingPeriodId": "period_uuid",
"format": "xlsx",
"filters": {
"siteIds": ["site_001", "site_002"],
"employmentLevels": ["WAGED_STAFF_NEC"]
}
}
Aggregation Levels
Organisation-level Aggregation: - Default behavior - Aggregates all site-level data to organisation totals - E.1: Sum of site headcounts by employment level - E.2: Sum of site employment type, recruitment, departures, casual workers - E.3: Sum of site age group headcounts
Site-level Aggregation:
- Set options.aggregationLevel = "site"
- Returns separate rows/sheets for each site
- E.1: One row per employment level per quarter per site
- E.2: One table per site
- E.3: One table per site
- Useful for multi-site organisations tracking site-specific HR metrics
XLSX Sheet Structure (Site-level): - Sheet 1: E.1 Demographics (Site A) - Sheet 2: E.1 Demographics (Site B) - Sheet 3: E.2 Employment & Turnover (Site A) - Sheet 4: E.2 Employment & Turnover (Site B) - Sheet 5: E.3 Age Distribution (Site A) - Sheet 6: E.3 Age Distribution (Site B) - Sheet 7: Reference Data
Data Validation and Quality Checks
Before report generation, the API validates:
- Data Completeness:
- E.1: All 4 quarters have data (or missing quarters are future quarters)
- E.2: All 12 months have data (or missing months are future months)
- E.3: All 12 months have data (or missing months are future months)
-
Warning if completeness < 80% for current fiscal year
-
Data Consistency:
- E.1: Male + Female = Total for each row
- E.1: Percentages sum to 100% (Male% + Female% = 100%)
- E.1: From Local Community ≤ Total
- E.3: Under 30 + Aged 30-50 + Over 50 = Monthly Total
-
Error if validation fails
-
PII Protection:
- Minimum 5 employees per reported category
- Warning if any category has < 5 employees (suggest aggregation or suppression)
-
No individual employee identifiers in export
-
Calculation Accuracy:
- E.1: Percentages rounded to 2 decimal places
- E.2: Averages rounded to nearest integer
- E.2: Totals match sum of monthly values
Validation Response (if failed):
{
"error": {
"code": "DATA_QUALITY_VALIDATION_FAILED",
"message": "Report data failed quality checks",
"details": {
"completeness": {
"e1": 100.0,
"e2": 75.0,
"e3": 83.3
},
"validation_errors": [
{
"section": "E.1",
"row": "Executive, Q1 2025",
"issue": "Male + Female (7) does not equal Total (8)",
"severity": "ERROR"
},
{
"section": "E.2",
"issue": "Missing data for October, November, December",
"severity": "WARNING"
}
],
"pii_warnings": [
{
"section": "E.1",
"category": "Executive, From Local Community",
"count": 2,
"recommendation": "Suppress or aggregate to prevent identification"
}
]
},
"request_id": "req_xyz789",
"timestamp": "2026-01-17T22:05:00Z"
}
}
Performance Considerations
Estimated Generation Times: - Small organisation (1-3 sites, <1000 employees): 10-30 seconds - Medium organisation (4-10 sites, 1000-5000 employees): 30-90 seconds - Large organisation (10+ sites, 5000+ employees): 90-300 seconds
Resource Usage: - Memory: ~50MB per report generation job - Database queries: ~20-50 queries (depends on filters and aggregation level) - File size: CSV ~50KB-500KB, XLSX ~100KB-2MB
Optimization Tips: - Use site-level filters to reduce data volume - Generate reports during off-peak hours for large organisations - Cache intermediate aggregations for frequently requested reports
Environment Report Generation
Generate environment reports covering G.1-G.10 sections (Production, Materials Consumption, Energy Usage, Water Management, Air Emissions, Waste, TSF Management, Rehabilitation, Environmental Incidents).
POST /api/v1/admin/reports/environment/generate
Queue an environment report generation job.
Permission: admin.reports.generate (Approver, Admin)
Request Body:
{
"reportingPeriodId": "850e8400-e29b-41d4-a716-446655440012",
"format": "xlsx",
"filters": {
"organisationId": "org_uuid",
"siteIds": ["site_uuid_1", "site_uuid_2"]
},
"options": {
"includeProduction": true,
"includeMaterials": true,
"includeEnergy": true,
"includeWater": true,
"includeAirEmissions": true,
"includeWaste": true,
"includeTSF": true,
"includeRehabilitation": true,
"includeIncidents": true,
"aggregationLevel": "site",
"language": "en"
}
}
Request Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
reportingPeriodId |
uuid | Yes | Reporting period (fiscal year) for the report |
format |
string | Yes | Export format: csv or xlsx |
filters |
object | No | Optional filters to scope the report |
filters.organisationId |
uuid | Yes* | Organisation ID (required unless single-org tenant) |
filters.siteIds |
array[uuid] | No | Specific sites to include (omit for all sites) |
options |
object | No | Report generation options |
options.includeProduction |
boolean | No | Include G.1 (Production) section (default: true) |
options.includeMaterials |
boolean | No | Include G.2 (Materials Consumption) section (default: true) |
options.includeEnergy |
boolean | No | Include G.3 (Energy Usage) section (default: true) |
options.includeWater |
boolean | No | Include G.4 (Water Management) section (default: true) |
options.includeAirEmissions |
boolean | No | Include G.5 (Air Emissions) section (default: true) |
options.includeWaste |
boolean | No | Include G.6-G.7 (Waste) sections (default: true) |
options.includeTSF |
boolean | No | Include G.8 (TSF Management) section (default: true) |
options.includeRehabilitation |
boolean | No | Include G.9 (Rehabilitation) section (default: true) |
options.includeIncidents |
boolean | No | Include G.10 (Environmental Incidents) section (default: true) |
options.aggregationLevel |
string | No | site or organisation (default: organisation) |
options.language |
string | No | Report language: en (default), es, fr |
Response (202 Accepted):
{
"jobId": "c70e8400-e29b-41d4-a716-446655440040",
"status": "QUEUED",
"estimatedCompletion": "2026-01-18T03:10:00Z",
"statusUrl": "/api/v1/admin/reports/jobs/c70e8400-e29b-41d4-a716-446655440040",
"queuePosition": 1,
"reportType": "environment_report",
"format": "xlsx"
}
Error Responses:
- 400 Bad Request - Invalid filters or missing required parameters
- 403 Forbidden - User lacks permission
- 404 Not Found - Reporting period not found
- 429 Too Many Requests - Rate limit: 10 reports/hour per tenant
Export Format Specifications
CSV Export
File Naming Convention:
Example: Eureka_Gold_Environment_FY2025_2026-01-18.csv
Structure: Single CSV file with all sections concatenated, using section headers to separate G.1-G.10.
XLSX Export
File Naming Convention:
Example: Eureka_Gold_Environment_FY2025_2026-01-18.xlsx
Sheet Structure:
| Sheet # | Sheet Name | Content | Rows |
|---|---|---|---|
| 1 | G.1 Production | Monthly crushed ore, milled ore, gold produced with totals/averages | 1 header + 12 months + 2 summary rows |
| 2 | G.2 Materials | Monthly consumables (activated carbon, cyanide, etc.) with per-tonne intensity | 6 material types × 12 months + summary |
| 3 | G.3 Energy | Monthly electricity (grid/generator), fuel consumption, consumption rates | Multiple tables |
| 4 | G.4 Water | Monthly abstraction/recycled/consumed + quarterly quality monitoring | Multiple tables |
| 5 | G.5 Air Emissions | Quarterly pollutant measurements by monitoring area with quality bands | 4 pollutants × N areas × 4 quarters |
| 6 | G.6 Non-Mineralised Waste | Monthly waste by type with disposal methods and specific waste per tonne | N waste types × 12 months |
| 7 | G.7 Mineralised Waste | Monthly waste rock and tailings | 1 header + 12 months |
| 8 | G.8 TSF Management | Monthly slurry density, freeboard, surface area, rate of rise | 1 header + 12 months |
| 9 | G.9 Rehabilitation | Quarterly rehabilitation activities log | Variable (activity records) |
| 10 | G.10 Incidents | Quarterly environmental incidents log | Variable (incident records) |
| 11 | Reference Data | Metric definitions, units, quality band definitions | Metadata |
Filter Behavior
Required Filters:
- reportingPeriodId: Fiscal year for the report
Optional Filters:
- organisationId: Scope to specific organisation (required for multi-org tenants)
- siteIds: Array of site UUIDs
- Omit: Include all sites
- Provide: Include only specified sites
- Applies to: All G.1-G.10 sections
Filter Application Logic: - Filters are applied with AND logic within a category - Multiple filters across categories use AND logic - Empty array means "no filter" (include all values) - Null/undefined filter field means "no filter" (include all values)
Example Filtered Request:
{
"reportingPeriodId": "period_uuid",
"format": "xlsx",
"filters": {
"siteIds": ["site_001"]
},
"options": {
"includeIncidents": false
}
}
Aggregation Levels
Organisation-level Aggregation: - Default behavior - Aggregates all site-level data to organisation totals - G.1-G.8: Sum of site values by month/quarter - G.9-G.10: Concatenated activity and incident logs from all sites
Site-level Aggregation:
- Set options.aggregationLevel = "site"
- Returns separate sheets for each site
- Useful for multi-site organisations tracking site-specific environmental metrics
XLSX Sheet Structure (Site-level): - Sheets 1-10 repeated for each site with site name in sheet title - Final sheet: Reference Data
Data Validation and Quality Checks
Before report generation, the API validates:
- Data Completeness:
- G.1-G.3, G.6-G.8: All 12 months have data (or missing months are future months)
- G.4.4, G.5: All 4 quarters have data (or missing quarters are future quarters)
- G.9-G.10: Quarterly logs exist for completed quarters
-
Warning if completeness < 80% for current fiscal year
-
Data Consistency:
- G.1: Milled ore ≤ Crushed ore
- G.3: Generator fuel consumption aligns with generator electricity output
- G.4: Water balance consistency (abstracted + recycled ≈ consumed + losses)
- G.7-G.8: TSF tailings received aligns with tailings generated
-
Error if validation fails
-
Calculation Accuracy:
- G.2, G.3, G.4, G.6: Per-tonne intensity metrics calculated correctly
- G.3: Generator consumption rate (L/kWh) = fuel consumed / electricity generated
-
Precision: 3 decimal places for rates, 2 decimal places for percentages
-
Confidentiality:
- G.10 incidents classified as CONFIDENTIAL
- G.9 rehabilitation costs may be COMMERCIAL_IN_CONFIDENCE
- Ensure proper access control when exporting
Validation Response (if failed):
{
"error": {
"code": "DATA_QUALITY_VALIDATION_FAILED",
"message": "Report data failed quality checks",
"details": {
"completeness": {
"g1": 100.0,
"g2": 91.7,
"g3": 100.0,
"g4": 75.0,
"g5": 100.0,
"g6": 83.3,
"g7": 100.0,
"g8": 100.0,
"g9": 100.0,
"g10": 100.0
},
"validation_errors": [
{
"section": "G.4",
"issue": "Missing water quality data for Q2 and Q3",
"severity": "WARNING"
},
{
"section": "G.7-G.8",
"issue": "TSF tailings received (125,340t) does not match tailings generated (127,890t)",
"severity": "ERROR"
}
]
},
"request_id": "req_xyz789",
"timestamp": "2026-01-18T03:00:00Z"
}
}
Performance Considerations
Estimated Generation Times: - Small site (1 site, monthly data): 10-30 seconds - Medium organisation (3-5 sites): 30-90 seconds - Large organisation (10+ sites): 90-300 seconds
Resource Usage: - Memory: ~50MB per report generation job - Database queries: ~30-70 queries (depends on filters and aggregation level) - File size: CSV ~100KB-1MB, XLSX ~200KB-3MB
Optimization Tips: - Use site-level filters to reduce data volume - Generate reports during off-peak hours for large organisations - Cache intermediate aggregations for frequently requested reports
Audit Log
Immutable audit trail of all administrative actions.
GET /api/v1/admin/audit-logs
Query audit log entries with filtering and search.
Permission: admin.audit.view (All roles)
Query Parameters:
- filter[entity_type] (string) - Filter by entity: MetricSubmission, ReportingPeriod, Metric, User
- filter[action] (string) - Filter by action: created, approved, rejected, locked, unlocked
- filter[actor_user_id] (uuid) - Filter by user who performed action
- filter[entity_id] (uuid) - Filter by specific entity
- filter[date_from] (datetime) - Filter from date
- filter[date_to] (datetime) - Filter to date
- sort (string) - Sort by: -created_at (default), action, entity_type
Response (200 OK):
{
"data": [
{
"id": "b50e8400-e29b-41d4-a716-446655440021",
"actor": {
"id": "c50e8400-e29b-41d4-a716-446655440008",
"name": "Jane Approver",
"email": "jane.approver@example.com",
"role": "APPROVER"
},
"action": "submission.approved",
"entityType": "MetricSubmission",
"entityId": "550e8400-e29b-41d4-a716-446655440010",
"entitySummary": {
"metricId": "GRI_302_1_ELECTRICITY",
"value": 1250.50,
"site": "Factory A - Shanghai"
},
"beforeState": {
"state": "PROCESSED",
"approvedAt": null
},
"afterState": {
"state": "APPROVED",
"approvedAt": "2026-01-10T14:00:00Z"
},
"justification": "Data verified against utility bills. Jan 2026 electricity consumption aligns with invoice #INV-2026-0145.",
"metadata": {
"ipAddress": "203.0.113.42",
"userAgent": "Mozilla/5.0...",
"requestId": "req_xyz789"
},
"createdAt": "2026-01-10T14:00:00Z"
},
{
"id": "c50e8400-e29b-41d4-a716-446655440022",
"actor": {
"id": "c50e8400-e29b-41d4-a716-446655440008",
"name": "Jane Approver"
},
"action": "period.locked",
"entityType": "ReportingPeriod",
"entityId": "850e8400-e29b-41d4-a716-446655440012",
"entitySummary": {
"name": "Q4 2025",
"submissionCount": 480
},
"beforeState": {
"state": "OPEN",
"contentHash": null
},
"afterState": {
"state": "LOCKED",
"contentHash": "sha256:a3c4f8b912e5d7f6c3b2a1e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9"
},
"justification": "All Q4 2025 data reviewed and approved for FY2025 annual sustainability report.",
"createdAt": "2026-01-10T16:45:00Z"
}
],
"meta": {
"page": 1,
"pageSize": 50,
"total": 15432,
"totalPages": 309
}
}
Audit Log Properties: - Immutable: Cannot be modified or deleted once created - Tamper-proof: Each entry includes cryptographic hash chain - Retention: 7 years minimum for compliance - Performance: Indexed on entity_id, actor_user_id, created_at
Task Queue & Notifications
GET /api/v1/admin/tasks/my-queue
Get submissions assigned to current user for review.
Permission: admin.tasks.view (Reviewer, Approver, Admin)
Query Parameters:
- filter[priority] (string) - Filter by priority: HIGH, MEDIUM, LOW
- filter[due_soon] (boolean) - Filter tasks due within 48 hours
- sort (string) - Sort by: due_date, -priority, -created_at
Response (200 OK):
{
"data": [
{
"submissionId": "550e8400-e29b-41d4-a716-446655440010",
"metric": {"metricId": "GRI_302_1_ELECTRICITY", "name": "Electricity Consumption"},
"site": {"id": "site_001", "name": "Factory A - Shanghai"},
"value": 1250.50,
"state": "VALIDATED",
"priority": "HIGH",
"dueDate": "2026-01-12T23:59:59Z",
"assignedAt": "2026-01-10T10:00:00Z",
"ageInHours": 8,
"hasValidationWarnings": true
}
],
"summary": {
"totalTasks": 12,
"dueSoon": 3,
"overdue": 0,
"avgAgeInHours": 24
}
}
Pagination & Filtering
All list endpoints support consistent pagination and filtering.
Pagination
Parameters:
- page (integer, default: 1) - Page number (1-indexed)
- pageSize (integer, default: 50, max: 100) - Items per page
Response Meta:
{
"meta": {
"page": 2,
"pageSize": 50,
"total": 328,
"totalPages": 7
},
"links": {
"first": "/api/v1/admin/submissions?page=1&pageSize=50",
"prev": "/api/v1/admin/submissions?page=1&pageSize=50",
"next": "/api/v1/admin/submissions?page=3&pageSize=50",
"last": "/api/v1/admin/submissions?page=7&pageSize=50"
}
}
Filtering
Syntax:
- Simple: filter[field]=value
- Array: filter[field]=value1,value2
- Comparison: filter[field_gt]=100, filter[field_lt]=1000
- Date range: filter[submitted_from]=2026-01-01&filter[submitted_to]=2026-01-31
Example:
GET /api/v1/admin/submissions?filter[state]=validated,processed&filter[has_validation_warnings]=true&sort=-submitted_at
Sorting
Syntax:
- Ascending: sort=field_name
- Descending: sort=-field_name
- Multiple: sort=-priority,created_at
Error Handling
All Admin API endpoints follow the standard error handling conventions documented in Error Handling.
Common Error Codes:
- AUTH_INSUFFICIENT_PERMISSIONS - User role lacks required permission
- STATE_INVALID_TRANSITION - Invalid state transition attempted
- STATE_PREREQUISITES_NOT_MET - Action prerequisites not satisfied
- RESOURCE_NOT_FOUND - Submission or period not found
- VALIDATION_FAILED - Request validation failed
- RATE_LIMIT_EXCEEDED - Too many requests
Example Error Response:
{
"error": {
"code": "AUTH_INSUFFICIENT_PERMISSIONS",
"message": "User does not have permission to perform this action",
"details": {
"required_permission": "admin.submissions.approve",
"user_role": "REVIEWER",
"allowed_roles": ["APPROVER", "ADMIN"]
},
"request_id": "req_xyz789",
"timestamp": "2026-01-11T14:30:00Z"
}
}
Implementation Guide
Quarkus JAX-RS Resource Examples (Kotlin)
import jakarta.annotation.security.RolesAllowed
import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.core.Response
import io.quarkus.security.identity.SecurityIdentity
import java.util.UUID
@Path("/api/v1/admin/submissions")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class AdminSubmissionResource @Inject constructor(
private val submissionService: SubmissionService,
private val auditLogService: AuditLogService,
private val securityIdentity: SecurityIdentity
) {
@GET
@RolesAllowed("REVIEWER", "APPROVER", "ADMIN")
fun listSubmissions(
@QueryParam("filter") filter: Map<String, String>?,
@QueryParam("page") @DefaultValue("1") page: Int,
@QueryParam("pageSize") @DefaultValue("50") pageSize: Int,
@QueryParam("sort") sort: String?
): PagedResponse<SubmissionDto> {
val user = securityIdentity.principal as UserPrincipal
val tenantId = user.tenantId
val submissions = submissionService.findAll(
tenantId = tenantId,
filters = filter ?: emptyMap(),
page = page - 1,
pageSize = pageSize.coerceAtMost(100),
sort = sort
)
return submissions
}
@GET
@Path("/{id}")
@RolesAllowed("REVIEWER", "APPROVER", "ADMIN")
fun getSubmission(@PathParam("id") id: UUID): SubmissionDetailDto {
val user = securityIdentity.principal as UserPrincipal
val tenantId = user.tenantId
val submission = submissionService.findById(id, tenantId)
?: throw NotFoundException("Submission not found")
return submission
}
@POST
@Path("/{id}/approve")
@RolesAllowed("APPROVER", "ADMIN")
fun approveSubmission(
@PathParam("id") id: UUID,
request: ApproveSubmissionRequest
): SubmissionDto {
val user = securityIdentity.principal as UserPrincipal
val submission = submissionService.approve(
submissionId = id,
tenantId = user.tenantId,
approvedBy = user.userId,
comment = request.comment,
metadata = request.metadata
)
auditLogService.log(
actor = user,
action = "submission.approved",
entityType = "MetricSubmission",
entityId = id,
beforeState = mapOf("state" to "VALIDATED"),
afterState = mapOf("state" to "APPROVED"),
justification = request.comment
)
return submission
}
@POST
@Path("/{id}/reject")
@RolesAllowed("REVIEWER", "APPROVER", "ADMIN")
fun rejectSubmission(
@PathParam("id") id: UUID,
request: RejectSubmissionRequest
): SubmissionDto {
val user = securityIdentity.principal as UserPrincipal
if (request.reason.isBlank()) {
throw ValidationException("Rejection reason is required")
}
val submission = submissionService.reject(
submissionId = id,
tenantId = user.tenantId,
rejectedBy = user.userId,
reason = request.reason,
requiredCorrections = request.requiredCorrections,
severity = request.severity
)
return submission
}
@POST
@Path("/bulk-assign")
@RolesAllowed("APPROVER", "ADMIN")
fun bulkAssign(request: BulkAssignRequest): BulkAssignResponse {
val user = securityIdentity.principal as UserPrincipal
val tenantId = user.tenantId
val result = submissionService.bulkAssign(
submissionIds = request.submissionIds,
assignToUserId = request.assignToUserId,
tenantId = tenantId,
priority = request.priority,
dueDate = request.dueDate
)
return result
}
}
@Path("/api/v1/admin/reporting-periods")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class AdminReportingPeriodResource @Inject constructor(
private val periodService: ReportingPeriodService,
private val auditLogService: AuditLogService,
private val securityIdentity: SecurityIdentity
) {
@POST
@Path("/{id}/lock")
@RolesAllowed("APPROVER", "ADMIN")
fun lockPeriod(
@PathParam("id") id: UUID,
request: LockPeriodRequest
): ReportingPeriodDto {
val user = securityIdentity.principal as UserPrincipal
// Validate prerequisites
val period = periodService.findById(id, user.tenantId)
?: throw NotFoundException("Reporting period not found")
if (period.hasUnreviewedSubmissions()) {
throw StatePrerequisiteException(
"Cannot lock period: ${period.unreviewedSubmissionsCount} submissions still need review",
details = mapOf(
"unreviewedSubmissions" to period.unreviewedSubmissionsCount,
"minimumApprovalRate" to 95.0,
"currentApprovalRate" to period.approvalRate
)
)
}
// Generate content hash
val contentHash = periodService.generateContentHash(id, user.tenantId)
// Lock period
val lockedPeriod = periodService.lock(
periodId = id,
tenantId = user.tenantId,
lockedBy = user.userId,
justification = request.justification,
contentHash = contentHash
)
auditLogService.log(
actor = user,
action = "period.locked",
entityType = "ReportingPeriod",
entityId = id,
beforeState = mapOf("state" to "OPEN"),
afterState = mapOf("state" to "LOCKED", "contentHash" to contentHash),
justification = request.justification
)
return lockedPeriod
}
}
Service Layer Example
import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.event.Event
import jakarta.inject.Inject
import jakarta.transaction.Transactional
import java.time.Instant
import java.util.UUID
@ApplicationScoped
class SubmissionService @Inject constructor(
private val submissionRepository: SubmissionRepository,
private val notificationService: NotificationService,
private val submissionApprovedEvent: Event<SubmissionApprovedEvent>
) {
@Transactional
fun approve(
submissionId: UUID,
tenantId: UUID,
approvedBy: UUID,
comment: String?,
metadata: Map<String, Any>?
): SubmissionDto {
val submission = submissionRepository.findByIdAndTenantId(submissionId, tenantId)
?: throw NotFoundException("Submission not found")
// Validate state transition
if (submission.state !in listOf(State.VALIDATED, State.PROCESSED)) {
throw StateTransitionException(
"Cannot approve submission in state ${submission.state}",
currentState = submission.state,
requiredStates = listOf(State.VALIDATED, State.PROCESSED)
)
}
// Update submission
submission.state = State.APPROVED
submission.approvedBy = approvedBy
submission.approvedAt = Instant.now()
submission.approvalComment = comment
submission.metadata = (submission.metadata ?: emptyMap()) + (metadata ?: emptyMap())
val saved = submissionRepository.persist(submission)
// Send notification to submitter
notificationService.notifySubmissionApproved(saved)
// Fire CDI event for async processing
submissionApprovedEvent.fire(SubmissionApprovedEvent(saved))
return saved.toDto()
}
}
RBAC Enforcement with Quarkus Security
import jakarta.annotation.Priority
import jakarta.inject.Inject
import jakarta.ws.rs.Priorities
import jakarta.ws.rs.container.ContainerRequestContext
import jakarta.ws.rs.container.ContainerRequestFilter
import jakarta.ws.rs.ext.Provider
import io.quarkus.security.identity.SecurityIdentity
// Tenant isolation is enforced via ContainerRequestFilter
@Provider
@Priority(Priorities.AUTHORIZATION + 1)
class TenantIsolationFilter @Inject constructor(
private val securityIdentity: SecurityIdentity
) : ContainerRequestFilter {
override fun filter(requestContext: ContainerRequestContext) {
// Only process authenticated requests to admin endpoints
if (requestContext.uriInfo.path.startsWith("/api/v1/admin/")) {
if (securityIdentity.isAnonymous) {
return
}
if (securityIdentity.principal is UserPrincipal) {
val tenantId = (securityIdentity.principal as UserPrincipal).tenantId
TenantContext.setTenantId(tenantId)
}
}
}
}
// Clean up tenant context after request (using ContainerResponseFilter)
@Provider
@Priority(Priorities.AUTHORIZATION + 2)
class TenantContextCleanupFilter : ContainerResponseFilter {
override fun filter(
requestContext: ContainerRequestContext,
responseContext: ContainerResponseContext
) {
TenantContext.clear()
}
}
// RBAC is enforced using @RolesAllowed at the resource method level
// Example:
// @GET
// @Path("/{id}")
// @RolesAllowed("REVIEWER", "APPROVER", "ADMIN")
// fun getSubmission(@PathParam("id") id: UUID): SubmissionDetailDto { ... }
Security Considerations
1. Tenant Isolation
CRITICAL: All database queries MUST include tenant_id filter to prevent cross-tenant data access.
// Always include tenant_id in WHERE clause
@Query("SELECT s FROM Submission s WHERE s.tenantId = :tenantId AND s.id = :id")
fun findByIdAndTenantId(id: UUID, tenantId: UUID): Submission?
// Use JPA @Where annotation for automatic tenant filtering
@Entity
@Where(clause = "tenant_id = current_tenant_id()")
class MetricSubmission {
// ...
}
2. Role-Based Access Control
Enforcement Points:
- Controller: @RolesAllowed annotations on endpoints
- Service: Business logic validates user permissions
- Database: Row-level security policies
Permission Matrix:
| Action | Admin | Approver | Reviewer | Collector |
|---|---|---|---|---|
| View submissions | ✓ | ✓ | ✓ | Own only |
| Approve submissions | ✓ | ✓ | ✗ | ✗ |
| Reject submissions | ✓ | ✓ | ✓ | ✗ |
| Lock periods | ✓ | ✓ | ✗ | ✗ |
| Unlock periods | ✓ | ✗ | ✗ | ✗ |
| Create metrics | ✓ | ✗ | ✗ | ✗ |
| Generate reports | ✓ | ✓ | ✗ | ✗ |
3. Audit Logging
Requirements: - Log ALL state-changing operations - Include before/after state snapshots - Store actor IP address and user agent - Immutable (append-only, no updates/deletes) - 7-year retention for compliance
4. Content Hash for Period Locking
Generate SHA-256 hash of all approved submissions to ensure data integrity:
fun generateContentHash(periodId: UUID, tenantId: UUID): String {
val submissions = submissionRepository.findApprovedByPeriod(periodId, tenantId)
.sortedBy { it.id } // Deterministic ordering
val content = submissions.joinToString("\n") {
"${it.id}|${it.metricId}|${it.value}|${it.approvedAt}"
}
return MessageDigest.getInstance("SHA-256")
.digest(content.toByteArray())
.joinToString("") { "%02x".format(it) }
.let { "sha256:$it" }
}
5. Input Validation
- Validate all request bodies with JSR-380 annotations
- Sanitize user inputs to prevent injection attacks
- Validate UUIDs are properly formatted
- Check enum values are in allowed set
- Validate date ranges are logical
6. Rate Limiting
Implement at API gateway level: - 500 requests/hour per user+tenant for admin endpoints - 10 reports/hour per tenant for report generation - Use Redis-backed sliding window algorithm
Performance Guidelines
1. Database Indexing
Required Indexes:
-- Submissions table
CREATE INDEX idx_submissions_tenant_state ON metric_submissions(tenant_id, state);
CREATE INDEX idx_submissions_tenant_period ON metric_submissions(tenant_id, reporting_period_id);
CREATE INDEX idx_submissions_tenant_created ON metric_submissions(tenant_id, created_at DESC);
CREATE INDEX idx_submissions_assigned_to ON metric_submissions(assigned_to_user_id, state) WHERE assigned_to_user_id IS NOT NULL;
-- Audit log table
CREATE INDEX idx_audit_logs_tenant_created ON audit_logs(tenant_id, created_at DESC);
CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_logs_actor ON audit_logs(actor_user_id, created_at DESC);
2. Pagination
- Default page size: 50
- Maximum page size: 100
- Use offset pagination for simplicity
- Consider cursor-based pagination for large datasets (>10K records)
3. Caching Strategy
Redis Caching: - Metric templates (rarely change): 24-hour TTL - Reporting period metadata: 1-hour TTL - User permissions: 15-minute TTL - Audit log queries: No caching (always fresh)
import io.quarkus.cache.CacheResult
import io.quarkus.cache.CacheKey
@CacheResult(cacheName = "metrics")
fun getMetrics(@CacheKey tenantId: UUID): List<MetricDto> {
return metricRepository.findByTenantId(tenantId).map { it.toDto() }
}
4. Async Processing
Queue long-running operations:
- Report generation → Background job with Quarkus Scheduler or Quartz extension
- Notification sending → Async with @RunOnVirtualThread or Mutiny Uni
- Audit log writes → CDI async event observer with @ObservesAsync
5. Query Optimization
- Use JPA
fetch joinsto avoid N+1 queries - Select only required columns for list views
- Use database views for complex aggregations
- Monitor slow queries with Hibernate statistics
6. Performance SLAs
Target Response Times: - GET list endpoints: p95 < 500ms - GET single resource: p95 < 200ms - POST approval/reject: p95 < 1000ms - Report generation: Complete within 5 minutes
Testing Checklist
Unit Tests
- RBAC enforcement for each endpoint
- State transition validation
- Tenant isolation in queries
- Input validation and sanitization
- Error handling for invalid states
Integration Tests
- End-to-end submission approval workflow
- Period locking with prerequisites check
- Bulk assignment distributes load fairly
- Report generation job completes successfully
- Audit logs created for all state changes
Security Tests
- User cannot access other tenant's data
- Reviewer cannot approve (only Approver/Admin can)
- Cannot approve submission in DRAFT state
- Cannot lock period with unreviewed submissions
- Rate limiting enforced correctly
Performance Tests
- List 10,000 submissions in <500ms
- Approve submission in <1000ms
- Lock period in <2000ms
- Generate report for 1000 submissions in <5min
- Concurrent approvals don't cause race conditions
Cross-References
- API Overview - Authentication and versioning
- Error Handling - Standard error codes
- Collector API - Field data submission
- Review & Approval Workflow - Workflow details
- Locking & Restatements - Period locking process
Change Log
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-01-03 | Senior Product Architect | Initial Admin API specification |
| 1.1 | 2026-01-11 | Ralph Agent | Comprehensive expansion with RBAC, pagination, Kotlin examples |
| 1.2 | 2026-01-17 | Ralph Agent | Added Human Capital Report Generation with CSV/XLSX export specs, filters, and aggregation levels |
| 1.3 | 2026-01-18 | Ralph Agent | Added Environment Report Generation (G.1-G.10) with CSV/XLSX export specs, validation rules, and aggregation levels |