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¶
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
httpOnlycookie 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
ECOCASHpayments the status is initiallyPENDING. It transitions toSUCCESSorFAILEDwhen the EcoCash webhook is received. ForCASHpayments the status is immediately set toSUCCESS.
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