Skip to content

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

  1. Authentication
  2. Metric Templates
  3. Submissions Management
  4. Evidence Management
  5. Master Data Reference Lists
  6. Offline-First Sync
  7. Error Handling
  8. Implementation Guide

Authentication

All collector endpoints require JWT authentication. See API Overview - Authentication for details.

Quick Example:

GET /api/v1/collector/templates
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...


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:

GET /api/v1/collector/evidence/evidence-uuid
Authorization: Bearer token...

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:

{
  "evidenceId": "evidence-uuid"
}

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:

GET /api/v1/master-data/water-sampling-points?siteId=site-uuid
Authorization: Bearer token...

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:

GET /api/v1/master-data/air-emission-areas?siteId=site-uuid
Authorization: Bearer token...

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:

  1. Local-First: All data is stored locally on the device first
  2. UUID-Based Deduplication: Client-generated UUIDs prevent duplicate submissions
  3. Idempotency: All POST endpoints support idempotency keys for safe retries
  4. Incremental Sync: Only changed data is synced, using timestamps
  5. 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:

  1. User enters data → Store locally in DRAFT state
  2. User uploads evidence → Store files locally, upload when online
  3. User queues submission → Change local state to QUEUED
  4. Network available → Sync queued submissions to server
  5. 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 since parameter
  • 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


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