Skip to content

Collector API

Status: Final Version: 1.0 Last Updated: 2026-01-03


Purpose

Define REST endpoints for mobile/field data collectors to fetch templates, submit ESG data, upload evidence, and sync submission status.


Endpoints

1. Authentication

POST /api/v1/auth/login

Request:

{
  "email": "collector@example.com",
  "password": "***",
  "device_id": "ABC123"
}

Response (200):

{
  "access_token": "eyJ0eXAi...",
  "refresh_token": "eyJ0eXAi...",
  "expires_in": 3600,
  "user": {
    "id": 42,
    "email": "collector@example.com",
    "roles": ["collector"],
    "tenant_id": 5,
    "sites": [123, 124]
  }
}


2. Fetch Collection Templates

GET /api/v1/collector/templates

Query Parameters: - reporting_period_id (required): Reporting period ID - site_id (optional): Filter by site

Response (200):

{
  "data": [
    {
      "template_id": "tpl_550e8400",
      "reporting_period": {
        "id": 10,
        "name": "FY2025",
        "start_date": "2025-01-01",
        "end_date": "2025-12-31",
        "state": "OPEN"
      },
      "site": {
        "id": 123,
        "name": "Factory A",
        "code": "FAC-A"
      },
      "metrics": [
        {
          "metric_id": "GRI_302_1_ELECTRICITY",
          "name": "Electricity Consumption",
          "unit": "MWh",
          "data_type": "numeric",
          "is_mandatory": true,
          "collection_frequency": "monthly",
          "validation_rules": [...],
          "allowed_evidence_types": ["UTILITY_BILL", "METER_READING"]
        }
      ]
    }
  ]
}


3. Submit Data

POST /api/v1/collector/submissions

Headers: - X-Idempotency-Key: UUID (required)

Request:

{
  "submission_uuid": "550e8400-e29b-41d4-a716-446655440000",
  "reporting_period_id": 10,
  "site_id": 123,
  "metric_id": "GRI_302_1_ELECTRICITY",
  "activity_date": "2025-03-31",
  "value": 1250.50,
  "unit": "MWh",
  "metadata": {
    "collection_method": "manual_entry",
    "collector_notes": "Q1 total from utility bills"
  }
}

Response (201):

{
  "id": 5001,
  "submission_uuid": "550e8400-e29b-41d4-a716-446655440000",
  "state": "RECEIVED",
  "submitted_at": "2025-04-05T10:30:00Z",
  "validation_status": "PENDING",
  "next_steps": ["Upload evidence files", "Wait for validation"]
}

Errors: - 409 Conflict: Duplicate submission_uuid - 422 Validation Error: Invalid data


4. Upload Evidence

POST /api/v1/collector/submissions/{uuid}/evidence

Content-Type: multipart/form-data

Request:

file: <binary>
evidence_type: UTILITY_BILL
description: March 2025 electricity bill

Response (201):

{
  "evidence_id": 7001,
  "filename": "utility_bill_mar2025.pdf",
  "file_size": 245760,
  "content_hash": "sha256:abc123...",
  "uploaded_at": "2025-04-05T10:35:00Z"
}


5. Sync Status

GET /api/v1/collector/submissions/sync

Query Parameters: - since: ISO timestamp (returns submissions updated since this time) - site_id: Filter by site

Response (200):

{
  "data": [
    {
      "submission_uuid": "550e8400-e29b-41d4-a716-446655440000",
      "state": "VALIDATED",
      "validation_errors": [],
      "reviewer_feedback": null
    },
    {
      "submission_uuid": "660f9511-f3ac-52e5-b827-557766551111",
      "state": "REJECTED",
      "validation_errors": [
        {
          "field": "value",
          "message": "Value exceeds expected range (1000% increase year-over-year)"
        }
      ],
      "reviewer_feedback": "Please verify this value and resubmit with corrected data."
    }
  ],
  "sync_timestamp": "2025-04-05T11:00:00Z"
}


Laravel Controllers

class CollectorController extends Controller
{
    public function templates(Request $request)
    {
        $this->authorize('collector.templates.view');

        $templates = MetricTemplateService::generateForUser(
            auth()->user(),
            $request->reporting_period_id,
            $request->site_id
        );

        return response()->json(['data' => $templates]);
    }

    public function submit(SubmissionRequest $request)
    {
        $this->authorize('collector.submissions.create');

        // Idempotency check
        if ($existing = MetricSubmission::where('submission_uuid', $request->submission_uuid)->first()) {
            return response()->json($existing, 200);
        }

        $submission = SubmissionService::create($request->validated());

        dispatch(new ValidateSubmissionJob($submission->id));

        return response()->json($submission, 201);
    }

    public function uploadEvidence(Request $request, $uuid)
    {
        $submission = MetricSubmission::where('submission_uuid', $uuid)->firstOrFail();
        $this->authorize('update', $submission);

        $evidence = EvidenceService::upload($submission, $request->file('file'), $request->evidence_type);

        return response()->json($evidence, 201);
    }

    public function sync(Request $request)
    {
        $submissions = MetricSubmission::where('tenant_id', auth()->user()->tenant_id)
            ->whereIn('site_id', auth()->user()->site_ids)
            ->where('updated_at', '>=', $request->since ?? now()->subHours(24))
            ->with(['validationResults', 'reviewFeedback'])
            ->get();

        return response()->json([
            'data' => $submissions,
            'sync_timestamp' => now()->toIso8601String()
        ]);
    }
}

Acceptance Criteria

  • All endpoints require JWT authentication
  • Idempotency keys prevent duplicate submissions
  • Evidence uploads support multipart/form-data
  • Sync endpoint returns only user's accessible sites
  • Validation errors returned in structured format

Cross-References


Change Log

Version Date Author Changes
1.0 2026-01-03 Senior Product Architect Initial Collector API specification