Collector API
Status: Final Version: 1.0 Last Updated: 2026-01-11
Purpose
Define REST endpoints for mobile/field data collectors to fetch templates, submit ESG data, upload evidence, and sync submission status. This API is designed for offline-first mobile applications with support for draft storage, queued submissions, and incremental sync.
Table of Contents
- Authentication
- Metric Templates
- Submissions Management
- Evidence Management
- Master Data Reference Lists
- Offline-First Sync
- Error Handling
- Implementation Guide
Authentication
All collector endpoints require JWT authentication. See API Overview - Authentication for details.
Quick Example:
Metric Templates
GET /api/v1/collector/templates
Fetch metric templates for data collection. Templates define what metrics collectors should submit for specific reporting periods and sites.
Authentication: Required (COLLECTOR role)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
reportingPeriodId |
UUID | Yes | Reporting period to fetch templates for |
siteId |
UUID | No | Filter templates for specific site |
metricCategory |
string | No | Filter by category (ENERGY, WATER, WASTE, EMISSIONS, PRODUCTION, MATERIALS, TSF, REHABILITATION, INCIDENTS, etc.) |
Request Example:
GET /api/v1/collector/templates?reportingPeriodId=period-uuid&siteId=site-uuid
Authorization: Bearer token...
Response (200 OK):
{
"data": [
{
"id": "template-uuid-1",
"reportingPeriod": {
"id": "period-uuid",
"name": "FY2025",
"startDate": "2025-01-01",
"endDate": "2025-12-31",
"state": "OPEN"
},
"site": {
"id": "site-uuid",
"name": "Factory A - Renewable Energy Plant",
"code": "FAC-A",
"location": {
"country": "USA",
"region": "California"
}
},
"metrics": [
{
"id": "metric-uuid-1",
"metricCode": "GRI_302_1_ELECTRICITY",
"name": "Electricity Consumption",
"description": "Total electricity consumed from grid and renewable sources",
"category": "ENERGY",
"unit": "MWh",
"dataType": "NUMERIC",
"isMandatory": true,
"collectionFrequency": "MONTHLY",
"validationRules": [
{
"type": "RANGE",
"min": 0,
"max": 10000,
"message": "Value must be between 0 and 10,000 MWh"
},
{
"type": "ANOMALY_DETECTION",
"threshold": 50,
"message": "Value deviates significantly from historical average"
}
],
"allowedEvidenceTypes": [
"UTILITY_BILL",
"METER_READING",
"INVOICE"
],
"requiredEvidenceCount": 1,
"helpText": "Include all electricity from grid, solar panels, and backup generators",
"dimensions": []
},
{
"id": "metric-uuid-2",
"metricCode": "ENV_PRODUCTION_CRUSHED_ORE",
"name": "Crushed Ore Production",
"description": "Monthly crushed ore production in tonnes",
"category": "PRODUCTION",
"unit": "tonnes",
"dataType": "NUMERIC",
"isMandatory": true,
"collectionFrequency": "MONTHLY",
"validationRules": [
{
"type": "RANGE",
"min": 0,
"message": "Value cannot be negative"
}
],
"allowedEvidenceTypes": [
"PRODUCTION_LOG",
"SHIFT_REPORT"
],
"requiredEvidenceCount": 1,
"helpText": "Total crushed ore production for the month",
"dimensions": []
},
{
"id": "metric-uuid-3",
"metricCode": "ENV_WATER_QUALITY_MONITORING",
"name": "Water Quality Monitoring",
"description": "Quarterly water quality monitoring by sampling point",
"category": "WATER",
"unit": "N/A",
"dataType": "CATEGORICAL",
"isMandatory": true,
"collectionFrequency": "QUARTERLY",
"validationRules": [
{
"type": "ALLOWED_VALUES",
"values": ["GREEN", "AMBER", "RED"],
"message": "Quality band must be GREEN, AMBER, or RED"
}
],
"allowedEvidenceTypes": [
"LAB_REPORT",
"WATER_QUALITY_CERTIFICATE"
],
"requiredEvidenceCount": 1,
"helpText": "Record water quality band for each sampling point based on lab analysis",
"dimensions": [
{
"name": "samplingPoint",
"label": "Sampling Point",
"type": "STRING",
"required": true,
"masterDataSource": "/api/v1/master-data/water-sampling-points"
},
{
"name": "qualityBand",
"label": "Quality Band",
"type": "CATEGORICAL",
"required": true,
"allowedValues": ["GREEN", "AMBER", "RED"]
},
{
"name": "nonComplianceParameter",
"label": "Non-Compliance Parameter",
"type": "STRING",
"required": false,
"helpText": "Specify parameter if band is AMBER or RED"
}
]
}
]
}
],
"pagination": {
"totalItems": 1
}
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 403 | AUTH_INSUFFICIENT_PERMISSIONS | User lacks COLLECTOR role |
| 404 | RESOURCE_NOT_FOUND | Reporting period not found or not accessible |
| 422 | VALIDATION_ERROR | Invalid query parameters |
Error Example (404):
{
"error": "RESOURCE_NOT_FOUND",
"message": "Reporting period not found or you don't have access",
"timestamp": "2026-01-11T10:30:00Z",
"request_id": "req_abc123"
}
Submissions Management
POST /api/v1/collector/submissions
Create a new metric submission. Supports idempotency to prevent duplicate submissions from network retries.
Authentication: Required (COLLECTOR role)
Headers:
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer JWT token |
Idempotency-Key |
Yes | UUID v4 for idempotent submission |
Content-Type |
Yes | application/json |
Request Body:
{
"submissionUuid": "550e8400-e29b-41d4-a716-446655440000",
"reportingPeriodId": "period-uuid",
"siteId": "site-uuid",
"metricTemplateId": "metric-uuid",
"activityDate": "2025-03-31",
"value": 1250.50,
"unit": "MWh",
"metadata": {
"collectionMethod": "MANUAL_ENTRY",
"collectorNotes": "Q1 total from utility bills",
"dataSource": "Pacific Gas & Electric",
"confidenceLevel": "HIGH"
}
}
Field Descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
submissionUuid |
UUID | Yes | Client-generated UUID for deduplication |
reportingPeriodId |
UUID | Yes | Target reporting period |
siteId |
UUID | Yes | Site where data was collected |
metricTemplateId |
UUID | Yes | Metric being submitted |
activityDate |
Date | Yes | Date when the activity occurred (ISO 8601) |
value |
Number | Yes | Metric value (type depends on metric definition) |
unit |
String | Yes | Unit of measurement (must match template) |
metadata |
Object | No | Additional context and notes |
Response (201 Created):
{
"id": "submission-uuid",
"submissionUuid": "550e8400-e29b-41d4-a716-446655440000",
"state": "RECEIVED",
"submittedAt": "2025-04-05T10:30:00Z",
"submittedBy": {
"id": "user-uuid",
"name": "Jane Collector",
"email": "jane@example.com"
},
"validationStatus": "PENDING",
"nextSteps": [
"Upload required evidence files",
"Wait for automatic validation",
"Review validation results"
],
"requiredEvidence": [
{
"type": "UTILITY_BILL",
"description": "Upload utility bill to support this submission",
"required": true
}
]
}
Response (200 OK - Idempotent Retry):
If the same Idempotency-Key is used within 24 hours, the original 201 response is returned:
{
"id": "submission-uuid",
"submissionUuid": "550e8400-e29b-41d4-a716-446655440000",
"state": "VALIDATED",
"submittedAt": "2025-04-05T10:30:00Z",
"validationStatus": "PASSED",
"message": "This submission was already created (idempotent retry)"
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request format or missing required fields |
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 403 | AUTH_SCOPE_VIOLATION | User cannot submit to this site |
| 409 | RESOURCE_ALREADY_EXISTS | Duplicate submissionUuid (idempotency bypass) |
| 409 | RESOURCE_LOCKED | Reporting period is locked |
| 422 | VALIDATION_RULE_FAILED | Value fails validation rules |
Error Example (422 - Validation Failed):
{
"error": "VALIDATION_RULE_FAILED",
"message": "Submission failed validation",
"timestamp": "2026-01-11T10:30:00Z",
"request_id": "req_abc123",
"details": [
{
"field": "value",
"code": "VALUE_OUT_OF_RANGE",
"message": "Value 12500.50 exceeds maximum allowed value of 10000 MWh"
},
{
"field": "unit",
"code": "UNIT_MISMATCH",
"message": "Unit 'MWh' does not match expected unit 'kWh'"
}
]
}
GET /api/v1/collector/submissions
Retrieve submissions created by the authenticated collector. Supports filtering and pagination.
Authentication: Required (COLLECTOR role)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
state |
String | No | Filter by state (DRAFT, QUEUED, RECEIVED, VALIDATED, REJECTED, APPROVED) |
periodId |
UUID | No | Filter by reporting period |
siteId |
UUID | No | Filter by site |
metricTemplateId |
UUID | No | Filter by metric template |
createdAfter |
ISO 8601 | No | Filter submissions created after this date |
createdBefore |
ISO 8601 | No | Filter submissions created before this date |
search |
String | No | Full-text search in notes and metadata |
page |
Integer | No | Page number (default: 1) |
pageSize |
Integer | No | Items per page (default: 50, max: 100) |
sort |
String | No | Sort field and direction (e.g., createdAt:desc) |
Request Example:
GET /api/v1/collector/submissions?state=VALIDATED&siteId=site-uuid&page=1&pageSize=20
Authorization: Bearer token...
Response (200 OK):
{
"data": [
{
"id": "submission-uuid-1",
"submissionUuid": "550e8400-e29b-41d4-a716-446655440000",
"metric": {
"id": "metric-uuid",
"name": "Electricity Consumption",
"code": "GRI_302_1_ELECTRICITY"
},
"site": {
"id": "site-uuid",
"name": "Factory A",
"code": "FAC-A"
},
"activityDate": "2025-03-31",
"value": 1250.50,
"unit": "MWh",
"state": "VALIDATED",
"validationStatus": "PASSED",
"submittedAt": "2025-04-05T10:30:00Z",
"updatedAt": "2025-04-05T11:00:00Z",
"evidenceCount": 1
}
],
"pagination": {
"page": 1,
"pageSize": 20,
"totalPages": 5,
"totalItems": 87,
"hasNext": true,
"hasPrevious": false
},
"links": {
"self": "/api/v1/collector/submissions?state=VALIDATED&page=1&pageSize=20",
"next": "/api/v1/collector/submissions?state=VALIDATED&page=2&pageSize=20",
"last": "/api/v1/collector/submissions?state=VALIDATED&page=5&pageSize=20"
}
}
GET /api/v1/collector/submissions/{id}
Retrieve detailed information about a specific submission.
Authentication: Required (COLLECTOR role)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Submission UUID |
Request Example:
GET /api/v1/collector/submissions/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer token...
Response (200 OK):
{
"id": "submission-uuid",
"submissionUuid": "550e8400-e29b-41d4-a716-446655440000",
"reportingPeriod": {
"id": "period-uuid",
"name": "FY2025",
"startDate": "2025-01-01",
"endDate": "2025-12-31"
},
"metric": {
"id": "metric-uuid",
"name": "Electricity Consumption",
"code": "GRI_302_1_ELECTRICITY",
"unit": "MWh"
},
"site": {
"id": "site-uuid",
"name": "Factory A",
"code": "FAC-A"
},
"activityDate": "2025-03-31",
"value": 1250.50,
"unit": "MWh",
"state": "VALIDATED",
"validationStatus": "PASSED",
"submittedAt": "2025-04-05T10:30:00Z",
"submittedBy": {
"id": "user-uuid",
"name": "Jane Collector",
"email": "jane@example.com"
},
"updatedAt": "2025-04-05T11:00:00Z",
"metadata": {
"collectionMethod": "MANUAL_ENTRY",
"collectorNotes": "Q1 total from utility bills",
"dataSource": "Pacific Gas & Electric"
},
"evidence": [
{
"id": "evidence-uuid",
"filename": "utility_bill_mar2025.pdf",
"evidenceType": "UTILITY_BILL",
"fileSize": 245760,
"uploadedAt": "2025-04-05T10:35:00Z"
}
],
"validationResults": [
{
"type": "RANGE_CHECK",
"status": "PASSED",
"message": "Value within acceptable range"
},
{
"type": "ANOMALY_DETECTION",
"status": "WARNING",
"message": "Value is 15% higher than previous month",
"severity": "LOW"
}
],
"reviewerFeedback": null,
"auditTrail": [
{
"timestamp": "2025-04-05T10:30:00Z",
"action": "CREATED",
"user": "Jane Collector"
},
{
"timestamp": "2025-04-05T10:35:00Z",
"action": "EVIDENCE_UPLOADED",
"user": "Jane Collector"
},
{
"timestamp": "2025-04-05T11:00:00Z",
"action": "VALIDATED",
"user": "System"
}
]
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 403 | AUTH_SCOPE_VIOLATION | User cannot access this submission |
| 404 | RESOURCE_NOT_FOUND | Submission not found |
PATCH /api/v1/collector/submissions/{id}
Update a submission in DRAFT or REJECTED state. Once a submission is QUEUED or beyond, it cannot be modified (create a restatement instead).
Authentication: Required (COLLECTOR role)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Submission UUID |
Request Body:
{
"value": 1275.00,
"activityDate": "2025-03-31",
"metadata": {
"collectorNotes": "Updated after verifying utility bill"
}
}
Response (200 OK):
{
"id": "submission-uuid",
"submissionUuid": "550e8400-e29b-41d4-a716-446655440000",
"state": "DRAFT",
"value": 1275.00,
"updatedAt": "2025-04-05T12:00:00Z",
"message": "Submission updated successfully"
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 403 | AUTH_SCOPE_VIOLATION | User cannot modify this submission |
| 404 | RESOURCE_NOT_FOUND | Submission not found |
| 409 | STATE_TRANSITION_INVALID | Cannot modify submission in current state |
| 409 | RESOURCE_LOCKED | Reporting period is locked |
| 422 | VALIDATION_RULE_FAILED | Updated value fails validation |
Error Example (409 - Invalid State):
{
"error": "STATE_TRANSITION_INVALID",
"message": "Cannot modify submission in VALIDATED state. Create a restatement instead.",
"timestamp": "2026-01-11T10:30:00Z",
"request_id": "req_abc123",
"details": {
"currentState": "VALIDATED",
"allowedStates": ["DRAFT", "REJECTED"],
"suggestedAction": "Use /api/v1/admin/submissions/{id}/restatement to create a restatement"
}
}
POST /api/v1/collector/submissions/{id}/queue
Queue a DRAFT submission for validation and review. Once queued, the submission enters the ingestion pipeline.
Authentication: Required (COLLECTOR role)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Submission UUID |
Request Example:
POST /api/v1/collector/submissions/550e8400-e29b-41d4-a716-446655440000/queue
Authorization: Bearer token...
Response (202 Accepted):
{
"id": "submission-uuid",
"submissionUuid": "550e8400-e29b-41d4-a716-446655440000",
"state": "QUEUED",
"queuedAt": "2025-04-05T12:00:00Z",
"message": "Submission queued for validation",
"estimatedProcessingTime": "5-10 minutes"
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 403 | AUTH_SCOPE_VIOLATION | User cannot queue this submission |
| 404 | RESOURCE_NOT_FOUND | Submission not found |
| 409 | STATE_TRANSITION_INVALID | Submission not in DRAFT state |
| 409 | STATE_PREREQUISITE_MISSING | Required evidence not attached |
| 503 | SYSTEM_QUEUE_FULL | Validation queue at capacity, retry later |
Error Example (409 - Missing Evidence):
{
"error": "STATE_PREREQUISITE_MISSING",
"message": "Cannot queue submission without required evidence",
"timestamp": "2026-01-11T10:30:00Z",
"request_id": "req_abc123",
"details": {
"missingEvidence": [
{
"type": "UTILITY_BILL",
"description": "At least one utility bill is required",
"required": true
}
]
}
}
Evidence Management
POST /api/v1/collector/evidence
Upload evidence file to S3 storage. Returns evidence metadata that can be linked to submissions.
Authentication: Required (COLLECTOR role)
Content-Type: multipart/form-data
Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
file |
File | Yes | Evidence file (PDF, PNG, JPG, XLSX, CSV) |
evidenceType |
String | Yes | Type (UTILITY_BILL, INVOICE, METER_READING, PHOTO, SPREADSHEET, OTHER) |
description |
String | No | Human-readable description |
File Requirements: - Max file size: 50 MB - Allowed types: PDF, PNG, JPG, JPEG, XLSX, CSV - Virus scanning performed automatically - Files stored in S3 with encryption at rest
Request Example:
POST /api/v1/collector/evidence
Authorization: Bearer token...
Content-Type: multipart/form-data
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="utility_bill_mar2025.pdf"
Content-Type: application/pdf
[binary file data]
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="evidenceType"
UTILITY_BILL
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="description"
March 2025 electricity bill from Pacific Gas & Electric
------WebKitFormBoundary7MA4YWxkTrZu0gW--
Response (201 Created):
{
"id": "evidence-uuid",
"filename": "utility_bill_mar2025.pdf",
"originalFilename": "PGE_Bill_March_2025.pdf",
"evidenceType": "UTILITY_BILL",
"description": "March 2025 electricity bill from Pacific Gas & Electric",
"fileSize": 245760,
"mimeType": "application/pdf",
"contentHash": "sha256:8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4",
"uploadedAt": "2025-04-05T10:35:00Z",
"uploadedBy": {
"id": "user-uuid",
"name": "Jane Collector"
},
"s3Key": "evidence/tenant-uuid/2025/04/evidence-uuid.pdf",
"virusScanStatus": "CLEAN"
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | File type not allowed or file too large |
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 413 | FILE_TOO_LARGE | File exceeds 50 MB limit |
| 422 | VALIDATION_EVIDENCE_VIRUS_DETECTED | Virus detected in file |
| 503 | SYSTEM_ERROR | S3 upload failed, retry later |
Error Example (400 - Invalid File Type):
{
"error": "VALIDATION_ERROR",
"message": "File type not allowed",
"timestamp": "2026-01-11T10:30:00Z",
"request_id": "req_abc123",
"details": {
"file": [
"File must be one of: PDF, PNG, JPG, JPEG, XLSX, CSV"
],
"receivedType": "application/msword",
"allowedTypes": ["application/pdf", "image/png", "image/jpeg", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv"]
}
}
GET /api/v1/collector/evidence/{id}
Download evidence file from S3 storage.
Authentication: Required (COLLECTOR role)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Evidence UUID |
Request Example:
Response (200 OK):
Returns the file binary data with appropriate headers:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="utility_bill_mar2025.pdf"
Content-Length: 245760
X-Content-Hash: sha256:8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4
[binary file data]
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 403 | AUTH_SCOPE_VIOLATION | User cannot access this evidence |
| 404 | RESOURCE_NOT_FOUND | Evidence not found |
| 503 | SYSTEM_ERROR | S3 download failed, retry later |
POST /api/v1/collector/submissions/{id}/evidence
Link uploaded evidence to a submission. Evidence must be uploaded first via POST /api/v1/collector/evidence.
Authentication: Required (COLLECTOR role)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Submission UUID |
Request Body:
Response (200 OK):
{
"submissionId": "submission-uuid",
"evidenceId": "evidence-uuid",
"linkedAt": "2025-04-05T10:40:00Z",
"message": "Evidence linked successfully"
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 403 | AUTH_SCOPE_VIOLATION | User cannot modify this submission |
| 404 | RESOURCE_NOT_FOUND | Submission or evidence not found |
| 409 | STATE_TRANSITION_INVALID | Cannot attach evidence to submission in current state |
Master Data Reference Lists
The Collector API provides master data endpoints for fetching reference lists used in dimensional data collection (e.g., water sampling points, air emission monitoring areas, waste types).
GET /api/v1/master-data/water-sampling-points
Fetch water sampling points configured for the site.
Authentication: Required (COLLECTOR role)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
siteId |
UUID | Yes | Site ID to fetch sampling points for |
Request Example:
Response (200 OK):
{
"data": [
{
"id": "sp-001",
"code": "TSF_OUTLET",
"name": "TSF Outlet Point",
"description": "Water quality monitoring at tailings storage facility outlet",
"active": true
},
{
"id": "sp-002",
"code": "RIVER_UPSTREAM",
"name": "River Upstream Monitoring Point",
"description": "Upstream monitoring point on adjacent river",
"active": true
},
{
"id": "sp-003",
"code": "PROCESS_PLANT_DISCHARGE",
"name": "Process Plant Discharge Point",
"description": "Discharge point from processing plant",
"active": true
}
]
}
GET /api/v1/master-data/air-emission-areas
Fetch air emission monitoring areas configured for the site.
Authentication: Required (COLLECTOR role)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
siteId |
UUID | Yes | Site ID to fetch monitoring areas for |
Request Example:
Response (200 OK):
{
"data": [
{
"id": "area-001",
"code": "CRUSHING_PLANT",
"name": "Crushing Plant",
"description": "Air quality monitoring at crushing plant operations",
"active": true,
"pollutants": ["PM10", "SO2", "NO2", "CO"]
},
{
"id": "area-002",
"code": "MILLING_CIRCUIT",
"name": "Milling Circuit",
"description": "Air quality monitoring at milling operations",
"active": true,
"pollutants": ["PM10", "CO"]
},
{
"id": "area-003",
"code": "GENERATOR_STATION",
"name": "Generator Station",
"description": "Diesel generator station emissions monitoring",
"active": true,
"pollutants": ["SO2", "NO2", "CO"]
}
]
}
GET /api/v1/master-data/waste-types
Fetch waste types for non-mineralised waste reporting.
Authentication: Required (COLLECTOR role)
Response (200 OK):
{
"data": [
{
"id": "waste-001",
"code": "GENERAL_WASTE",
"name": "General Waste",
"category": "NON_HAZARDOUS",
"active": true
},
{
"id": "waste-002",
"code": "RECYCLABLE_METAL",
"name": "Recyclable Metal (Steel, Aluminum)",
"category": "RECYCLABLE",
"active": true
},
{
"id": "waste-003",
"code": "HAZARDOUS_CHEMICAL",
"name": "Hazardous Chemical Waste",
"category": "HAZARDOUS",
"active": true
},
{
"id": "waste-004",
"code": "E_WASTE",
"name": "Electronic Waste",
"category": "HAZARDOUS",
"active": true
},
{
"id": "waste-005",
"code": "OIL_LUBRICANTS",
"name": "Used Oil and Lubricants",
"category": "HAZARDOUS",
"active": true
}
]
}
GET /api/v1/master-data/waste-disposal-methods
Fetch waste disposal methods for non-mineralised waste.
Authentication: Required (COLLECTOR role)
Response (200 OK):
{
"data": [
{
"id": "method-001",
"code": "LANDFILL",
"name": "Landfill (On-site or Licensed)",
"active": true
},
{
"id": "method-002",
"code": "RECYCLING",
"name": "Recycling / Reuse",
"active": true
},
{
"id": "method-003",
"code": "INCINERATION",
"name": "Incineration / Thermal Treatment",
"active": true
},
{
"id": "method-004",
"code": "LICENSED_DISPOSAL",
"name": "Licensed Third-Party Disposal",
"active": true
},
{
"id": "method-005",
"code": "ON_SITE_TREATMENT",
"name": "On-site Treatment",
"active": true
}
]
}
Offline-First Sync
The Collector API is designed for offline-first mobile applications. This section explains the sync strategy, draft storage, and queue behavior.
Offline-First Architecture
Key Principles:
- Local-First: All data is stored locally on the device first
- UUID-Based Deduplication: Client-generated UUIDs prevent duplicate submissions
- Idempotency: All POST endpoints support idempotency keys for safe retries
- Incremental Sync: Only changed data is synced, using timestamps
- Conflict-Free: Server state always wins; clients cannot overwrite server data
Draft Storage Mechanism
Mobile applications should store submissions locally in DRAFT state before syncing:
Draft Storage Flow:
- User enters data → Store locally in DRAFT state
- User uploads evidence → Store files locally, upload when online
- User queues submission → Change local state to QUEUED
- Network available → Sync queued submissions to server
- Server responds → Update local state from server response
Local Storage Schema (SQLite):
CREATE TABLE submissions (
submission_uuid TEXT PRIMARY KEY,
reporting_period_id TEXT NOT NULL,
site_id TEXT NOT NULL,
metric_template_id TEXT NOT NULL,
activity_date TEXT NOT NULL,
value REAL NOT NULL,
unit TEXT NOT NULL,
metadata TEXT, -- JSON blob
state TEXT NOT NULL, -- DRAFT, QUEUED, SYNCED
synced_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE evidence_local (
evidence_id TEXT PRIMARY KEY,
submission_uuid TEXT NOT NULL,
file_path TEXT NOT NULL, -- Local file path
evidence_type TEXT NOT NULL,
description TEXT,
uploaded BOOLEAN DEFAULT 0,
created_at TEXT NOT NULL,
FOREIGN KEY (submission_uuid) REFERENCES submissions(submission_uuid)
);
Sync Flow
Complete Sync Workflow:
┌─────────────────────────────────────────────────────────┐
│ Mobile App │
│ │
│ 1. User creates submission → DRAFT state (local) │
│ 2. User uploads evidence → Store locally │
│ 3. User queues submission → QUEUED state (local) │
│ 4. Sync service detects QUEUED submissions │
│ 5. Upload evidence files → S3 │
│ 6. POST /collector/submissions → Create submission │
│ 7. POST /collector/submissions/{id}/evidence → Link │
│ 8. POST /collector/submissions/{id}/queue → Queue │
│ 9. Update local state → SYNCED │
│ 10. GET /collector/sync → Fetch server updates │
│ 11. Update local submissions with server state │
└─────────────────────────────────────────────────────────┘
GET /api/v1/collector/sync
Fetch submission updates from the server. Used for incremental sync to get validation results, reviewer feedback, and state changes.
Authentication: Required (COLLECTOR role)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
since |
ISO 8601 | No | Return submissions updated since this timestamp (default: last 24 hours) |
siteId |
UUID | No | Filter by site |
includeStates |
String | No | Comma-separated states to include (default: all) |
Request Example:
GET /api/v1/collector/sync?since=2025-04-05T00:00:00Z&siteId=site-uuid
Authorization: Bearer token...
Response (200 OK):
{
"data": [
{
"submissionUuid": "550e8400-e29b-41d4-a716-446655440000",
"state": "VALIDATED",
"validationStatus": "PASSED",
"validationErrors": [],
"validationWarnings": [
{
"type": "ANOMALY_DETECTION",
"message": "Value is 15% higher than previous month",
"severity": "LOW"
}
],
"reviewerFeedback": null,
"updatedAt": "2025-04-05T11:00:00Z"
},
{
"submissionUuid": "660f9511-f3ac-52e5-b827-557766551111",
"state": "REJECTED",
"validationStatus": "FAILED",
"validationErrors": [
{
"field": "value",
"code": "VALUE_OUT_OF_RANGE",
"message": "Value exceeds expected range (1000% increase year-over-year)"
}
],
"reviewerFeedback": {
"reviewer": "John Reviewer",
"comment": "Please verify this value and resubmit with corrected data or additional documentation.",
"reviewedAt": "2025-04-05T10:45:00Z"
},
"updatedAt": "2025-04-05T10:45:00Z"
}
],
"syncTimestamp": "2025-04-05T12:00:00Z",
"hasMore": false
}
Mobile App Sync Logic:
// Kotlin example for mobile sync
suspend fun syncSubmissions() {
val lastSyncTime = prefsManager.getLastSyncTime()
// 1. Upload queued submissions
val queuedSubmissions = database.getQueuedSubmissions()
for (submission in queuedSubmissions) {
try {
// Upload evidence first
for (evidence in submission.evidence) {
if (!evidence.uploaded) {
val evidenceResponse = apiClient.uploadEvidence(evidence.file)
database.updateEvidenceId(evidence.localId, evidenceResponse.id)
}
}
// Create submission with idempotency key
val response = apiClient.createSubmission(
submission = submission,
idempotencyKey = submission.uuid
)
// Link evidence
for (evidence in submission.evidence) {
apiClient.linkEvidence(response.id, evidence.id)
}
// Queue for validation
apiClient.queueSubmission(response.id)
// Mark as synced locally
database.updateSubmissionState(submission.uuid, "SYNCED")
} catch (e: Exception) {
// Log error, will retry next sync
logger.error("Failed to sync submission ${submission.uuid}", e)
}
}
// 2. Fetch server updates
val updates = apiClient.getSync(since = lastSyncTime)
// 3. Update local database
for (update in updates.data) {
database.updateSubmissionFromServer(update)
}
// 4. Update last sync time
prefsManager.setLastSyncTime(updates.syncTimestamp)
}
Queue and Retry Logic
Exponential Backoff for Failed Syncs:
class SyncRetryPolicy {
private var retryCount = 0
private val maxRetries = 5
private val baseDelay = 2000L // 2 seconds
suspend fun retryWithBackoff(action: suspend () -> Unit) {
while (retryCount < maxRetries) {
try {
action()
retryCount = 0 // Reset on success
return
} catch (e: NetworkException) {
retryCount++
val delay = baseDelay * (2.0.pow(retryCount - 1)).toLong()
delay(delay)
} catch (e: ServerException) {
// Don't retry on 4xx errors
throw e
}
}
throw MaxRetriesExceededException()
}
}
Sync Frequency Recommendations:
| Scenario | Frequency | Trigger |
|---|---|---|
| Active user | Every 5 minutes | Foreground activity |
| Background | Every 30 minutes | Background task |
| Manual | On demand | User action (pull to refresh) |
| After submission | Immediate | After queuing submission |
| Network change | Immediate | WiFi connected |
Conflict Resolution
Server State Always Wins:
If local state conflicts with server state, the server state is authoritative:
fun resolveConflict(local: Submission, server: Submission) {
when {
local.state == "DRAFT" && server.state != "DRAFT" -> {
// Server has processed draft, update local
database.updateSubmission(server)
}
local.state == "QUEUED" && server.state == "RECEIVED" -> {
// Server has received submission, update local
database.updateSubmission(server)
}
local.updatedAt < server.updatedAt -> {
// Server has newer data, update local
database.updateSubmission(server)
}
}
}
Error Handling
All error responses follow the standard error format defined in Error Handling.
Common Error Scenarios
1. Network Timeout
Mobile apps should implement exponential backoff and retry:
try {
apiClient.createSubmission(submission)
} catch (e: SocketTimeoutException) {
// Retry with exponential backoff
retryPolicy.retryWithBackoff {
apiClient.createSubmission(submission)
}
}
2. Invalid Authentication
Token expired, refresh and retry:
try {
apiClient.createSubmission(submission)
} catch (e: UnauthorizedException) {
// Refresh token
val newToken = authManager.refreshToken()
// Retry with new token
apiClient.createSubmission(submission)
}
3. Validation Errors
Display field-level errors to user:
try {
apiClient.createSubmission(submission)
} catch (e: ValidationException) {
e.errors.forEach { (field, messages) ->
formView.showFieldError(field, messages.joinToString("\n"))
}
}
4. Rate Limiting
Respect Retry-After header:
try {
apiClient.createSubmission(submission)
} catch (e: RateLimitException) {
val retryAfter = e.retryAfter // seconds
delay(retryAfter * 1000L)
// Retry after delay
}
Implementation Guide
Mobile App Checklist
Core Requirements:
- Implement local SQLite database for offline storage
- Generate UUID v4 for all submissions locally
- Store submissions in DRAFT state initially
- Implement evidence upload with retry logic
- Support idempotency keys for all POST requests
- Implement incremental sync with
sinceparameter - Handle all error codes with appropriate UI feedback
- Respect rate limiting headers
- Implement exponential backoff for retries
- Store last sync timestamp locally
- Sync on network reconnection
- Display validation errors and warnings to users
- Support offline evidence file storage
- Implement JWT token refresh logic
- Handle submission state transitions correctly
Security Requirements:
- Store JWT tokens securely (Keychain/Keystore)
- Use HTTPS for all API calls
- Validate SSL certificates
- Encrypt local database if storing sensitive data
- Clear tokens on logout
- Implement biometric authentication (optional)
UX Requirements:
- Show sync status indicator
- Display pending upload count
- Show validation warnings prominently
- Allow users to retry failed uploads
- Show offline mode indicator
- Prevent data entry when period is locked
- Show submission state visually (DRAFT, VALIDATED, etc.)
Backend Implementation Notes
Quarkus JAX-RS Resource Example:
import jakarta.inject.Inject
import jakarta.validation.Valid
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.core.Response
import io.quarkus.security.identity.SecurityIdentity
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.UUID
@Path("/api/v1/collector")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class CollectorResource @Inject constructor(
private val submissionService: SubmissionService,
private val evidenceService: EvidenceService,
private val validationService: ValidationService,
private val securityIdentity: SecurityIdentity
) {
@POST
@Path("/submissions")
fun createSubmission(
@HeaderParam("Idempotency-Key") idempotencyKey: String,
@Valid request: CreateSubmissionRequest
): Response {
val user = securityIdentity.principal as AuthUser
// Check idempotency
val existing = submissionService.findByIdempotencyKey(idempotencyKey)
if (existing != null) {
return Response.ok(existing.toResponse()).build()
}
// Validate user has access to site
if (!user.hasAccessToSite(request.siteId)) {
throw AuthScopeViolationException("Cannot submit to this site")
}
// Create submission
val submission = submissionService.create(
request = request,
userId = user.id,
tenantId = user.tenantId,
idempotencyKey = idempotencyKey
)
// Queue async validation
validationService.queueValidation(submission.id)
return Response.status(201).entity(submission.toResponse()).build()
}
@GET
@Path("/sync")
fun sync(
@QueryParam("since") since: Instant?,
@QueryParam("siteId") siteId: UUID?
): SyncResponse {
val user = securityIdentity.principal as AuthUser
val sinceTime = since ?: Instant.now().minus(24, ChronoUnit.HOURS)
val updates = submissionService.findUpdatedSubmissions(
userId = user.id,
tenantId = user.tenantId,
since = sinceTime,
siteId = siteId
)
return SyncResponse(
data = updates.map { it.toSyncItem() },
syncTimestamp = Instant.now(),
hasMore = false
)
}
}
Acceptance Criteria
Done When:
- All collector endpoints documented with request/response examples
- Offline-first sync behavior fully documented
- Error handling examples provided for all scenarios
- Draft storage mechanism explained
- Queue and retry logic documented
- Mobile app implementation checklist provided
- Idempotency behavior documented and tested
- Rate limiting respected
- JWT authentication enforced on all endpoints
- Tenant isolation enforced at database level
- Evidence uploads support S3 with virus scanning
- Sync endpoint returns only accessible submissions
- State transition rules enforced
- Validation errors returned in structured format
Cross-References
- API Overview - Authentication, versioning, rate limiting
- Error Handling - Standard error codes and responses
- Collector Workflow - Workflow diagrams and state machine
- Evidence Management - File storage and chain-of-custody
- Validation Engine - 6 validation types
Change Log
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-01-11 | Senior Product Architect | Comprehensive Collector API documentation with offline-first sync |
| 1.1 | 2026-01-18 | Ralph Agent | Added environmental metric templates, master data endpoints (water sampling points, air emission areas, waste types/disposal methods), dimensional data collection examples |