Skip to content

TrafficGrid — API Design Specification

Version 1.0 | 2026

This document defines the complete REST API for the TrafficGrid platform. It serves as the contract between the backend and all consumers — mobile apps, web frontend, and third-party integrations.


Table of Contents

  1. Conventions
  2. Authentication
  3. Response Envelope
  4. Pagination
  5. Error Handling
  6. Modules

1. Conventions

Base URL

https://api.trafficgrid.co.zw/api/v1

URL Structure

  • All resource URLs use plural nouns/fines, /vehicles, /users
  • HTTP verbs express the action — never put verbs in URLs
  • Nested URLs express relationships — /vehicles/{id}/fines
  • Nesting is limited to two levels — deeper relationships are accessed directly
  • Query parameters are used for filtering, sorting, and pagination — not for identifying resources

HTTP Verbs

Verb Usage
GET Fetch a resource or collection. Never modifies state.
POST Create a new resource.
PUT Replace a resource entirely.
PATCH Partially update a resource.
DELETE Remove or deactivate a resource.

Date & Time Format

All timestamps use ISO 8601 in UTC: 2026-03-06T10:30:00Z All dates use: 2026-03-06

Naming Convention

All request and response fields use camelCase.

Role Abbreviations Used in This Document

Abbreviation Role
PUBLIC No authentication required
CITIZEN Authenticated citizen
ZRP ZRP officer
COUNCIL Council officer
PARKING Parking officer
ANY_OFFICER Any of ZRP, COUNCIL, or PARKING
AUTH_ADMIN Authority admin
SUPER Super admin
AUTHENTICATED Any authenticated user regardless of role

2. Authentication

All protected endpoints require a Bearer token in the Authorization header:

Authorization: Bearer <access_token>

Access tokens expire after 15 minutes. Use the refresh endpoint to obtain a new one.

Refresh tokens are delivered as an httpOnly cookie for web clients and in the response body for mobile clients. They expire after 7 days.


3. Response Envelope

Every response from the API — success or error — is wrapped in the same envelope. Clients must always check success before reading data.

Success Envelope

{
  "success": true,
  "data": { },
  "message": null,
  "errors": null
}

Error Envelope

{
  "success": false,
  "data": null,
  "message": "Human-readable summary of what went wrong",
  "errors": [
    "Specific error detail 1",
    "Specific error detail 2"
  ]
}

All response examples in this document show the complete envelope including data. This is exactly what the client receives.


4. Pagination

All collection endpoints support pagination via query parameters: page (integer, 0-based, default 0), size (integer, default 20, max 100), sortBy (string), direction (ASC|DESC, default DESC).

Paginated responses nest metadata inside data: content (array), page, size, totalElements, totalPages, first, last.

Query Parameters

Parameter Type Default Description
page integer 0 Zero-based page number
size integer 20 Number of items per page (max 100)
sortBy string varies Field to sort by
direction ASC | DESC DESC Sort direction

Paginated Response Structure

Paginated responses nest the page metadata inside data:

{
  "success": true,
  "data": {
    "content": [ ],
    "page": 0,
    "size": 20,
    "totalElements": 143,
    "totalPages": 8,
    "first": true,
    "last": false
  },
  "message": null
}

5. Error Handling

HTTP Status Codes

Code Meaning When Used
200 OK Success Successful GET, PATCH, PUT
201 Created Resource created Successful POST that creates a resource
204 No Content Success, no body Successful DELETE or logout
400 Bad Request Invalid input Missing fields, wrong types, constraint violations
401 Unauthorized Not authenticated Missing, expired, or invalid token
403 Forbidden Not authorised Authenticated but wrong role or accessing another user's resource
404 Not Found Resource missing ID that doesn't exist
409 Conflict State conflict Paying an already-paid fine, duplicate email on register
422 Unprocessable Entity Business rule violated Semantically valid request that violates domain logic
500 Internal Server Error Server fault Unhandled exception — never returned intentionally

Validation Error Example

{
  "success": false,
  "data": null,
  "message": "Validation failed",
  "errors": [
    "fullName must not be blank",
    "email must be a valid email address",
    "phoneNumber must not be blank"
  ]
}

6. Modules


6.1 Auth

POST /auth/register

Registers a new citizen account. Officers are created by admins — see 6.10 Admin — Users.

Auth: PUBLIC Service: auth-service

Request Body:

{
  "fullName": "Tendai Moyo",
  "email": "tendai.moyo@gmail.com",
  "phoneNumber": "+263771234567",
  "nationalId": "63-123456-A-21",
  "password": "SecurePass123!"
}
Field Type Required Rules
fullName string Min 2 characters
email string Valid email format, must be unique
phoneNumber string Must be unique
nationalId string Must be unique
password string Min 8 characters

Response: 201 Created

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "fullName": "Tendai Moyo",
      "email": "tendai.moyo@gmail.com",
      "phoneNumber": "+263771234567",
      "role": "CITIZEN"
    }
  },
  "message": null
}

Refresh token is set as an httpOnly cookie and also included in the response body for mobile clients.

Error Cases:

Status Condition
400 Any required field missing or invalid
409 Email, phone number, or national ID already registered

POST /auth/login

Authenticates a user and issues tokens.

Auth: PUBLIC Service: auth-service

Request Body:

{
  "email": "tendai.moyo@gmail.com",
  "password": "SecurePass123!"
}

Response: 200 OK

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
  },
  "message": null
}

Error Cases:

Status Condition
400 Missing email or password
401 Invalid credentials
403 Account is deactivated

POST /auth/refresh

Issues a new access token using a valid refresh token. Rotates the refresh token on every call.

Auth: PUBLIC Service: auth-service

Request: Refresh token is read from the refreshToken httpOnly cookie automatically (web). Mobile clients send it in the request body.

{
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}

Response: 200 OK

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9..."
  },
  "message": null
}

Error Cases:

Status Condition
401 Refresh token missing, expired, or already revoked

POST /auth/logout

Revokes the current access token and refresh token. Clears the refresh token cookie.

Auth: AUTHENTICATED Service: auth-service

Request Body: None

Response: 204 No Content


6.2 Vehicles

POST /vehicles

Creates a new vehicle record in the system. Used by officers when logging a fine against a vehicle that doesn't yet exist in the database.

Auth: ANY_OFFICER Service: vehicle-service

Request Body:

{
  "numberPlate": "ABC 1234",
  "vin": "1HGBH41JXMN109186",
  "engineNumber": "ENG123456",
  "make": "Toyota",
  "model": "Hilux",
  "year": 2019,
  "colour": "White",
  "vehicleType": "TRUCK"
}
Field Type Required Rules
numberPlate string Must be unique
vin string Must be unique if provided
engineNumber string
make string
model string
year integer 1900 – current year
colour string
vehicleType VehicleType See enum reference in data model

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "numberPlate": "ABC 1234",
    "vin": "1HGBH41JXMN109186",
    "engineNumber": "ENG123456",
    "make": "Toyota",
    "model": "Hilux",
    "year": 2019,
    "colour": "White",
    "vehicleType": "TRUCK",
    "isVerified": false,
    "createdAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

Error Cases:

Status Condition
400 Missing required fields or invalid field values
409 Number plate or VIN already exists

GET /vehicles/{id}

Fetches full details for a vehicle by ID.

Auth: ANY_OFFICER, SUPER, AUTH_ADMIN

Citizens access their vehicles via /citizens/me/vehicles — they cannot look up arbitrary vehicles by ID.

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "numberPlate": "ABC 1234",
    "vin": "1HGBH41JXMN109186",
    "engineNumber": "ENG123456",
    "make": "Toyota",
    "model": "Hilux",
    "year": 2019,
    "colour": "White",
    "vehicleType": "TRUCK",
    "isVerified": false,
    "createdAt": "2026-03-06T10:30:00Z",
    "updatedAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

Error Cases:

Status Condition
404 Vehicle not found

GET /vehicles

Searches vehicles. Primary use case is officers searching by number plate in the field.

Auth: ANY_OFFICER, SUPER, AUTH_ADMIN

Query Parameters:

Parameter Type Description
numberPlate string Exact or partial plate match
make string Filter by make
vehicleType VehicleType Filter by type
isVerified boolean Filter by verification status
page integer See pagination
size integer See pagination

Response: 200 OK

{
  "success": true,
  "data": {
    "content": [
      {
        "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "numberPlate": "ABC 1234",
        "make": "Toyota",
        "model": "Hilux",
        "year": 2019,
        "colour": "White",
        "vehicleType": "TRUCK",
        "isVerified": false
      }
    ],
    "page": 0,
    "size": 20,
    "totalElements": 1,
    "totalPages": 1,
    "first": true,
    "last": true
  },
  "message": null
}

GET /vehicles/{id}/fines

Returns all fines issued against a specific vehicle.

Auth: ANY_OFFICER, SUPER, AUTH_ADMIN

Query Parameters:

Parameter Type Description
status FineStatus Filter by fine status
from date Filter by issue date range start
to date Filter by issue date range end
page integer See pagination
size integer See pagination
sortBy string Default: issuedAt

Response: 200 OK — paginated, same envelope shape as GET /vehicles. See 6.6 Fines for the fine object shape inside content.


6.3 Vehicle Documents

GET /vehicles/{vehicleId}/documents

Returns all documents registered for a vehicle.

Auth: ANY_OFFICER, SUPER, AUTH_ADMIN, CITIZEN (own linked vehicles only)

Response: 200 OK

{
  "success": true,
  "data": [
    {
      "id": "a1b2c3d4-...",
      "vehicleId": "7c9e6679-...",
      "documentType": "VEHICLE_LICENCE",
      "expiryDate": "2026-12-31",
      "isSelfDeclared": true,
      "createdAt": "2026-03-06T10:30:00Z",
      "updatedAt": "2026-03-06T10:30:00Z"
    }
  ],
  "message": null
}

POST /vehicles/{vehicleId}/documents

Adds a document record for a vehicle. Citizens can self-declare documents for their own linked vehicles.

Auth: ANY_OFFICER, SUPER, CITIZEN (own linked vehicles only)

Request Body:

{
  "documentType": "VEHICLE_LICENCE",
  "expiryDate": "2026-12-31"
}
Field Type Required Rules
documentType DocumentType See enum reference in data model
expiryDate date Must be a future date

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "a1b2c3d4-...",
    "vehicleId": "7c9e6679-...",
    "documentType": "VEHICLE_LICENCE",
    "expiryDate": "2026-12-31",
    "isSelfDeclared": true,
    "createdAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

Error Cases:

Status Condition
400 Missing fields or invalid date
403 Citizen trying to add a document to a vehicle they haven't linked
404 Vehicle not found

PATCH /vehicles/{vehicleId}/documents/{documentId}

Updates the expiry date of an existing document.

Auth: ANY_OFFICER, SUPER, CITIZEN (own linked vehicles only)

Request Body:

{
  "expiryDate": "2027-12-31"
}

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "a1b2c3d4-...",
    "vehicleId": "7c9e6679-...",
    "documentType": "VEHICLE_LICENCE",
    "expiryDate": "2027-12-31",
    "isSelfDeclared": true,
    "createdAt": "2026-03-06T10:30:00Z",
    "updatedAt": "2026-03-06T11:00:00Z"
  },
  "message": null
}

6.4 Citizen Vehicles

These endpoints allow citizens to link and manage vehicles on their own account.

GET /citizens/me/vehicles

Returns all vehicles linked to the authenticated citizen's account.

Auth: CITIZEN

Response: 200 OK

{
  "success": true,
  "data": [
    {
      "id": "link-uuid-...",
      "vehicle": {
        "id": "7c9e6679-...",
        "numberPlate": "ABC 1234",
        "make": "Toyota",
        "model": "Hilux",
        "year": 2019,
        "colour": "White",
        "vehicleType": "TRUCK",
        "isVerified": false
      },
      "isPrimary": true,
      "linkedAt": "2026-03-06T10:30:00Z",
      "pendingFines": 2,
      "documents": [
        {
          "documentType": "VEHICLE_LICENCE",
          "expiryDate": "2026-12-31",
          "isSelfDeclared": true
        }
      ]
    }
  ],
  "message": null
}

POST /citizens/me/vehicles

Links a vehicle to the authenticated citizen's account by number plate.

Auth: CITIZEN

Request Body:

{
  "numberPlate": "ABC 1234",
  "isPrimary": false
}
Field Type Required Rules
numberPlate string Vehicle must exist in the system
isPrimary boolean Default false. Setting true unsets primary on all other linked vehicles.

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "link-uuid-...",
    "vehicle": {
      "id": "7c9e6679-...",
      "numberPlate": "ABC 1234",
      "make": "Toyota",
      "model": "Hilux",
      "year": 2019,
      "colour": "White",
      "vehicleType": "TRUCK"
    },
    "isPrimary": false,
    "linkedAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

Error Cases:

Status Condition
404 No vehicle found with that number plate
409 Citizen has already linked this vehicle

DELETE /citizens/me/vehicles/{linkId}

Removes a vehicle from the citizen's account. Does not delete the vehicle itself.

Auth: CITIZEN

Response: 204 No Content

Error Cases:

Status Condition
403 Link does not belong to the authenticated citizen
404 Link not found

GET /citizens/me/fines

Returns all fines across all vehicles linked to the authenticated citizen.

Auth: CITIZEN

Query Parameters:

Parameter Type Description
status FineStatus Filter by status
vehicleId UUID Filter to a specific linked vehicle
page integer See pagination
size integer See pagination

Response: 200 OK — paginated, same envelope shape as other paginated endpoints. See 6.6 Fines for the fine object shape inside content.


6.5 Fine Categories

GET /fine-categories

Returns all active fine categories. Used to populate the fine issuing form on officer apps.

Auth: ANY_OFFICER, SUPER, AUTH_ADMIN

Query Parameters:

Parameter Type Description
issuingOrganisation IssuingOrganisation Filter to categories the calling officer can issue

Response: 200 OK

{
  "success": true,
  "data": [
    {
      "id": "cat-uuid-...",
      "code": "SPEEDING_01",
      "name": "Exceeding Speed Limit",
      "description": "Vehicle exceeding the posted speed limit.",
      "amount": 200.00,
      "issuingOrganisation": "ZRP",
      "isActive": true
    }
  ],
  "message": null
}

GET /fine-categories/{id}

Returns a single fine category.

Auth: ANY_OFFICER, SUPER, AUTH_ADMIN

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "cat-uuid-...",
    "code": "SPEEDING_01",
    "name": "Exceeding Speed Limit",
    "description": "Vehicle exceeding the posted speed limit.",
    "amount": 200.00,
    "issuingOrganisation": "ZRP",
    "isActive": true
  },
  "message": null
}

Error Cases:

Status Condition
404 Category not found

Fine category creation and management is under 6.10 Admin.


6.6 Fines

POST /fines

Issues a new fine against a vehicle. The amount is automatically copied from the fine category — it cannot be manually set by the officer.

Auth: ZRP, COUNCIL

Request Body:

{
  "vehicleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "fineCategoryId": "cat-uuid-...",
  "location": "Corner Samora Machel Ave & Julius Nyerere Way, Harare",
  "notes": "Vehicle observed doing 95km/h in a 60km/h zone.",
  "dueDate": "2026-04-06"
}
Field Type Required Rules
vehicleId UUID Vehicle must exist
fineCategoryId UUID Category must be active and issuable by the officer's role
location string Where the offence occurred
notes string Officer notes
dueDate date Defaults to 30 days from today if not provided

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "fine-uuid-...",
    "referenceNumber": "TG-2026-00123",
    "vehicle": {
      "id": "7c9e6679-...",
      "numberPlate": "ABC 1234",
      "make": "Toyota",
      "model": "Hilux"
    },
    "fineCategory": {
      "id": "cat-uuid-...",
      "code": "SPEEDING_01",
      "name": "Exceeding Speed Limit"
    },
    "issuedBy": {
      "id": "officer-uuid-...",
      "fullName": "Const. J. Dube",
      "organisation": "ZRP"
    },
    "amount": 200.00,
    "status": "PENDING",
    "location": "Corner Samora Machel Ave & Julius Nyerere Way, Harare",
    "notes": "Vehicle observed doing 95km/h in a 60km/h zone.",
    "dueDate": "2026-04-06",
    "issuedAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

Error Cases:

Status Condition
400 Missing required fields
403 Officer's role is not allowed to issue this fine category
404 Vehicle or fine category not found
422 Fine category is inactive

GET /fines/{id}

Returns full details for a single fine.

Auth: ANY_OFFICER, SUPER, AUTH_ADMIN, CITIZEN (own linked vehicles only)

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "fine-uuid-...",
    "referenceNumber": "TG-2026-00123",
    "vehicle": {
      "id": "7c9e6679-...",
      "numberPlate": "ABC 1234",
      "make": "Toyota",
      "model": "Hilux"
    },
    "fineCategory": {
      "id": "cat-uuid-...",
      "code": "SPEEDING_01",
      "name": "Exceeding Speed Limit"
    },
    "issuedBy": {
      "id": "officer-uuid-...",
      "fullName": "Const. J. Dube",
      "organisation": "ZRP"
    },
    "amount": 200.00,
    "status": "PENDING",
    "location": "Corner Samora Machel Ave & Julius Nyerere Way, Harare",
    "notes": "Vehicle observed doing 95km/h in a 60km/h zone.",
    "dueDate": "2026-04-06",
    "issuedAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

Error Cases:

Status Condition
403 Citizen trying to access a fine on a vehicle they haven't linked
404 Fine not found

GET /fines

Searches and filters fines. Officers and admins use this for reporting and case management.

Auth: ANY_OFFICER, SUPER, AUTH_ADMIN

Query Parameters:

Parameter Type Description
vehicleId UUID Filter by vehicle
issuedBy UUID Filter by officer
organisationId UUID Filter by issuing organisation
fineCategoryId UUID Filter by category
status FineStatus Filter by status
from date Issue date range start
to date Issue date range end
referenceNumber string Search by reference number
page integer See pagination
size integer See pagination
sortBy string Default: issuedAt
direction ASC | DESC Default: DESC

Response: 200 OK

{
  "success": true,
  "data": {
    "content": [
      {
        "id": "fine-uuid-...",
        "referenceNumber": "TG-2026-00123",
        "vehicle": {
          "id": "7c9e6679-...",
          "numberPlate": "ABC 1234"
        },
        "fineCategory": {
          "code": "SPEEDING_01",
          "name": "Exceeding Speed Limit"
        },
        "amount": 200.00,
        "status": "PENDING",
        "dueDate": "2026-04-06",
        "issuedAt": "2026-03-06T10:30:00Z"
      }
    ],
    "page": 0,
    "size": 20,
    "totalElements": 87,
    "totalPages": 5,
    "first": true,
    "last": false
  },
  "message": null
}

PATCH /fines/{id}/status

Updates the status of a fine. Used by admins to clear, cancel, or mark disputed fines.

Auth: SUPER, AUTH_ADMIN

Request Body:

{
  "status": "CLEARED",
  "reason": "Duplicate entry — original fine reference TG-2026-00120"
}
Field Type Required Rules
status FineStatus Cannot transition to PAID via this endpoint — payments handle that
reason string Required for all manual status changes for audit purposes

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "fine-uuid-...",
    "referenceNumber": "TG-2026-00123",
    "amount": 200.00,
    "status": "CLEARED",
    "dueDate": "2026-04-06",
    "issuedAt": "2026-03-06T10:30:00Z"
  },
  "message": "Fine status updated successfully"
}

Error Cases:

Status Condition
400 Invalid status transition
404 Fine not found
422 Attempting to set status to PAID directly — use the payments endpoint

6.7 Parking Tickets

⚠️ Status: Planned — pending City Parking API integration.

POST /parking-tickets

Issues a new parking ticket.

Auth: PARKING

Request Body:

{
  "vehicleId": "7c9e6679-...",
  "location": "First Street Car Park, Bay 14, Harare CBD",
  "amount": 5.00,
  "dueDate": "2026-03-13",
  "externalReference": "CP-2026-98765"
}

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "ticket-uuid-...",
    "referenceNumber": "PKT-2026-00456",
    "vehicle": {
      "id": "7c9e6679-...",
      "numberPlate": "ABC 1234"
    },
    "issuedBy": {
      "id": "officer-uuid-...",
      "fullName": "P. Ncube"
    },
    "location": "First Street Car Park, Bay 14, Harare CBD",
    "amount": 5.00,
    "status": "PENDING",
    "externalReference": "CP-2026-98765",
    "dueDate": "2026-03-13",
    "issuedAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

GET /parking-tickets/{id}

Returns details for a single parking ticket.

Auth: PARKING, SUPER, AUTH_ADMIN, CITIZEN (own linked vehicles only)

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "ticket-uuid-...",
    "referenceNumber": "PKT-2026-00456",
    "vehicle": {
      "id": "7c9e6679-...",
      "numberPlate": "ABC 1234"
    },
    "issuedBy": {
      "id": "officer-uuid-...",
      "fullName": "P. Ncube"
    },
    "location": "First Street Car Park, Bay 14, Harare CBD",
    "amount": 5.00,
    "status": "PENDING",
    "externalReference": "CP-2026-98765",
    "dueDate": "2026-03-13",
    "issuedAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

GET /parking-tickets

Search and filter parking tickets.

Auth: PARKING, SUPER, AUTH_ADMIN

Query Parameters: vehicleId, status, from, to, page, size, sortBy, direction

Response: 200 OK

{
  "success": true,
  "data": {
    "content": [
      {
        "id": "ticket-uuid-...",
        "referenceNumber": "PKT-2026-00456",
        "vehicle": {
          "id": "7c9e6679-...",
          "numberPlate": "ABC 1234"
        },
        "amount": 5.00,
        "status": "PENDING",
        "dueDate": "2026-03-13",
        "issuedAt": "2026-03-06T10:30:00Z"
      }
    ],
    "page": 0,
    "size": 20,
    "totalElements": 12,
    "totalPages": 1,
    "first": true,
    "last": true
  },
  "message": null
}

6.8 Payments

POST /payments

Initiates a payment transaction. A single payment can cover multiple fines and/or parking tickets via items.

Auth: CITIZEN, ANY_OFFICER (for cash payments)

Request Body:

{
  "paymentMethod": "ECOCASH",
  "idempotencyKey": "client-generated-unique-key-abc123",
  "items": [
    {
      "payableType": "FINE",
      "payableId": "fine-uuid-...",
      "amount": 200.00
    },
    {
      "payableType": "FINE",
      "payableId": "fine-uuid-2-...",
      "amount": 50.00
    }
  ]
}
Field Type Required Rules
paymentMethod PaymentMethod See enum reference in data model
idempotencyKey string Must be unique per payment attempt. Generated by client.
items array At least one item required
items[].payableType PayableType FINE or PARKING_TICKET
items[].payableId UUID Must reference a PENDING fine or ticket
items[].amount decimal Must match the outstanding amount of the referenced item

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "payment-uuid-...",
    "referenceNumber": "PAY-2026-00789",
    "paymentMethod": "ECOCASH",
    "amount": 250.00,
    "status": "PENDING",
    "items": [
      {
        "payableType": "FINE",
        "payableId": "fine-uuid-...",
        "amount": 200.00
      },
      {
        "payableType": "FINE",
        "payableId": "fine-uuid-2-...",
        "amount": 50.00
      }
    ],
    "createdAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

For ECOCASH payments the status is initially PENDING. It transitions to SUCCESS or FAILED when the EcoCash webhook is received. For CASH payments the status is immediately set to SUCCESS.

Error Cases:

Status Condition
400 Missing fields, empty items array
404 Referenced fine or parking ticket not found
409 Idempotency key already used — returns the existing payment record
422 Fine or ticket is not in PENDING status — already paid, cleared, or cancelled

GET /payments/{id}

Returns details for a single payment.

Auth: CITIZEN (own payments only), ANY_OFFICER, SUPER, AUTH_ADMIN

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "payment-uuid-...",
    "referenceNumber": "PAY-2026-00789",
    "paidBy": {
      "id": "citizen-uuid-...",
      "fullName": "Tendai Moyo"
    },
    "paymentMethod": "ECOCASH",
    "amount": 250.00,
    "status": "SUCCESS",
    "providerReference": "ECO-TXN-987654",
    "items": [
      {
        "payableType": "FINE",
        "payableId": "fine-uuid-...",
        "amount": 200.00
      },
      {
        "payableType": "FINE",
        "payableId": "fine-uuid-2-...",
        "amount": 50.00
      }
    ],
    "paidAt": "2026-03-06T10:30:45Z",
    "createdAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

GET /payments

Returns payment history.

Auth: CITIZEN (own payments only), SUPER, AUTH_ADMIN

Query Parameters:

Parameter Type Description
paidBy UUID Filter by citizen (admin use)
status PaymentStatus Filter by status
paymentMethod PaymentMethod Filter by method
from date Date range start
to date Date range end
page integer See pagination
size integer See pagination

Response: 200 OK

{
  "success": true,
  "data": {
    "content": [
      {
        "id": "payment-uuid-...",
        "referenceNumber": "PAY-2026-00789",
        "paymentMethod": "ECOCASH",
        "amount": 250.00,
        "status": "SUCCESS",
        "paidAt": "2026-03-06T10:30:45Z",
        "createdAt": "2026-03-06T10:30:00Z"
      }
    ],
    "page": 0,
    "size": 20,
    "totalElements": 5,
    "totalPages": 1,
    "first": true,
    "last": true
  },
  "message": null
}

POST /payments/webhook

Receives payment status callbacks from EcoCash or other payment providers. Updates payment status and marks associated fines as PAID if successful.

Auth: PUBLIC — secured via webhook signature verification, not Bearer tokens

Request Body: Provider-specific payload. Verified against a shared secret before processing.

Response: 200 OK

{
  "success": true,
  "data": null,
  "message": "Webhook received"
}

This endpoint must respond quickly — processing happens asynchronously via a queue after acknowledging receipt.


6.9 Notifications

GET /notifications

Returns the authenticated user's notification history.

Auth: AUTHENTICATED

Query Parameters:

Parameter Type Description
status NotificationStatus Filter by delivery status
type NotificationType Filter by notification type
page integer See pagination
size integer See pagination

Response: 200 OK

{
  "success": true,
  "data": {
    "content": [
      {
        "id": "notif-uuid-...",
        "type": "FINE_ISSUED",
        "channel": "SMS",
        "title": "New Fine Issued",
        "message": "A fine of $200.00 has been issued against your vehicle ABC 1234. Reference: TG-2026-00123. Due: 2026-04-06.",
        "status": "SENT",
        "referenceId": "fine-uuid-...",
        "referenceType": "FINE",
        "sentAt": "2026-03-06T10:30:05Z",
        "createdAt": "2026-03-06T10:30:01Z"
      }
    ],
    "page": 0,
    "size": 20,
    "totalElements": 14,
    "totalPages": 1,
    "first": true,
    "last": true
  },
  "message": null
}

GET /notifications/preferences

Returns the authenticated user's notification preferences.

Auth: AUTHENTICATED

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "pref-uuid-...",
    "channels": ["SMS", "EMAIL"],
    "updatedAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

PUT /notifications/preferences

Replaces the authenticated user's notification preferences entirely.

Auth: AUTHENTICATED

Request Body:

{
  "channels": ["SMS", "PUSH"]
}

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "pref-uuid-...",
    "channels": ["SMS", "PUSH"],
    "updatedAt": "2026-03-06T11:00:00Z"
  },
  "message": null
}

Error Cases:

Status Condition
400 Empty channels array — at least one channel must be selected

6.10 Admin — Users

POST /admin/users

Creates an officer or admin account. Citizens self-register — they cannot be created via this endpoint.

Auth: SUPER

Request Body:

{
  "fullName": "Const. John Dube",
  "email": "j.dube@zrp.gov.zw",
  "phoneNumber": "+263772345678",
  "nationalId": "63-234567-B-22",
  "password": "TempPass123!",
  "role": "ZRP_OFFICER",
  "organisationId": "org-uuid-..."
}
Field Type Required Rules
fullName string
email string Must be unique
phoneNumber string Must be unique
nationalId string Must be unique
password string Temporary — officer should change on first login
role UserRole Cannot be CITIZEN
organisationId UUID Must reference an active organisation

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "user-uuid-...",
    "fullName": "Const. John Dube",
    "email": "j.dube@zrp.gov.zw",
    "phoneNumber": "+263772345678",
    "role": "ZRP_OFFICER",
    "organisation": {
      "id": "org-uuid-...",
      "name": "Zimbabwe Republic Police",
      "shortName": "ZRP"
    },
    "isActive": true,
    "createdAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

Error Cases:

Status Condition
400 Missing or invalid fields
403 Attempting to create a CITIZEN account
404 Organisation not found
409 Email, phone, or national ID already registered

GET /admin/users

Returns a paginated list of all users.

Auth: SUPER, AUTH_ADMIN

Query Parameters:

Parameter Type Description
role UserRole Filter by role
organisationId UUID Filter by organisation
isActive boolean Filter by active status
search string Search by name or email
page integer See pagination
size integer See pagination

Response: 200 OK

{
  "success": true,
  "data": {
    "content": [
      {
        "id": "user-uuid-...",
        "fullName": "Const. John Dube",
        "email": "j.dube@zrp.gov.zw",
        "role": "ZRP_OFFICER",
        "organisation": {
          "id": "org-uuid-...",
          "shortName": "ZRP"
        },
        "isActive": true,
        "createdAt": "2026-03-06T10:30:00Z"
      }
    ],
    "page": 0,
    "size": 20,
    "totalElements": 34,
    "totalPages": 2,
    "first": true,
    "last": false
  },
  "message": null
}

GET /admin/users/{id}

Returns full details for a single user.

Auth: SUPER, AUTH_ADMIN

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "user-uuid-...",
    "fullName": "Const. John Dube",
    "email": "j.dube@zrp.gov.zw",
    "phoneNumber": "+263772345678",
    "nationalId": "63-234567-B-22",
    "role": "ZRP_OFFICER",
    "organisation": {
      "id": "org-uuid-...",
      "name": "Zimbabwe Republic Police",
      "shortName": "ZRP"
    },
    "isActive": true,
    "createdAt": "2026-03-06T10:30:00Z",
    "updatedAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

Error Cases:

Status Condition
404 User not found

PATCH /admin/users/{id}/status

Activates or deactivates a user account.

Auth: SUPER

Request Body:

{
  "isActive": false,
  "reason": "Officer suspended pending investigation"
}

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "user-uuid-...",
    "fullName": "Const. John Dube",
    "isActive": false,
    "updatedAt": "2026-03-06T11:00:00Z"
  },
  "message": "User account deactivated"
}

Error Cases:

Status Condition
400 Missing isActive field
404 User not found

POST /admin/fine-categories

Creates a new fine category.

Auth: SUPER

Request Body:

{
  "code": "NO_LICENCE_01",
  "name": "Driving Without a Valid Licence",
  "description": "Driver found operating a vehicle without a valid driver's licence.",
  "amount": 300.00,
  "issuingOrganisation": "ZRP"
}

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "cat-uuid-...",
    "code": "NO_LICENCE_01",
    "name": "Driving Without a Valid Licence",
    "description": "Driver found operating a vehicle without a valid driver's licence.",
    "amount": 300.00,
    "issuingOrganisation": "ZRP",
    "isActive": true
  },
  "message": null
}

Error Cases:

Status Condition
409 Category code already exists

PUT /admin/fine-categories/{id}

Replaces a fine category. Note: changing the amount here does not affect any previously issued fines.

Auth: SUPER

Request Body: Same shape as POST.

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "cat-uuid-...",
    "code": "NO_LICENCE_01",
    "name": "Driving Without a Valid Licence",
    "description": "Driver found operating a vehicle without a valid driver's licence.",
    "amount": 350.00,
    "issuingOrganisation": "ZRP",
    "isActive": true
  },
  "message": null
}

PATCH /admin/fine-categories/{id}/status

Activates or deactivates a fine category.

Auth: SUPER

Request Body:

{
  "isActive": false
}

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "cat-uuid-...",
    "code": "NO_LICENCE_01",
    "isActive": false
  },
  "message": "Fine category deactivated"
}

6.11 Admin — Organisations

POST /admin/organisations

Creates a new organisation.

Auth: SUPER

Request Body:

{
  "name": "Zimbabwe Republic Police",
  "shortName": "ZRP",
  "type": "LAW_ENFORCEMENT"
}

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "org-uuid-...",
    "name": "Zimbabwe Republic Police",
    "shortName": "ZRP",
    "type": "LAW_ENFORCEMENT",
    "isActive": true,
    "createdAt": "2026-03-06T10:30:00Z"
  },
  "message": null
}

GET /admin/organisations

Returns all organisations.

Auth: SUPER, AUTH_ADMIN

Response: 200 OK

{
  "success": true,
  "data": [
    {
      "id": "org-uuid-...",
      "name": "Zimbabwe Republic Police",
      "shortName": "ZRP",
      "type": "LAW_ENFORCEMENT",
      "isActive": true,
      "createdAt": "2026-03-06T10:30:00Z"
    }
  ],
  "message": null
}

PATCH /admin/organisations/{id}/status

Activates or deactivates an organisation.

Auth: SUPER

Request Body:

{
  "isActive": false
}

Response: 200 OK

{
  "success": true,
  "data": {
    "id": "org-uuid-...",
    "name": "Zimbabwe Republic Police",
    "shortName": "ZRP",
    "isActive": false
  },
  "message": "Organisation deactivated"
}

6.12 Admin — Audit Logs

GET /admin/audit-logs

Returns a paginated, filterable view of the audit log. Read-only — audit logs are never modified.

Auth: SUPER

Query Parameters:

Parameter Type Description
actorId UUID Filter by the user who performed the action
organisationId UUID Filter by organisation
action string Filter by action type e.g. FINE_CREATED
entityType string Filter by affected table e.g. fines
entityId string Filter by affected record ID
from datetime Date range start
to datetime Date range end
page integer See pagination
size integer See pagination
sortBy string Default: createdAt
direction ASC | DESC Default: DESC

Response: 200 OK

{
  "success": true,
  "data": {
    "content": [
      {
        "id": "log-uuid-...",
        "actor": {
          "id": "officer-uuid-...",
          "fullName": "Const. J. Dube",
          "role": "ZRP_OFFICER",
          "organisation": "ZRP"
        },
        "action": "FINE_CREATED",
        "entityType": "fines",
        "entityId": "fine-uuid-...",
        "beforeState": null,
        "afterState": {
          "id": "fine-uuid-...",
          "referenceNumber": "TG-2026-00123",
          "amount": 200.00,
          "status": "PENDING"
        },
        "ipAddress": "196.42.1.100",
        "createdAt": "2026-03-06T10:30:00Z"
      }
    ],
    "page": 0,
    "size": 20,
    "totalElements": 4821,
    "totalPages": 242,
    "first": true,
    "last": false
  },
  "message": null
}

TrafficGrid API Specification | 2026 | Internal Document