Overview
How the ingestion flow works end to end.
Receive
Your system posts one flat JSON payload containing vehicle, customer and payment information.
Match & normalize
Vehicles are deduplicated by chassis then plate. Customers are matched by phone then email. Data is trimmed and standardized.
Store & confirm
The transaction is written to MySQL and a minimal acknowledgement is returned immediately. Retries are idempotent.
One vehicle can have many transactions.
Vehicle, owner and transaction data are stored in separate related tables. The same vehicle can be submitted repeatedly for different services, and each payment becomes a new transaction row linked to that vehicle and owner.
Authentication
Every ingestion and read request must be authenticated.
Send the key from your server, never from a browser.
Use the x-api-key request header. An Authorization: Bearer <key> header is also accepted. Keys are configured by the API operator and can be rotated without downtime.
Content-Type: application/json
x-api-key: YOUR_API_KEYMissing or invalid keys return 401 UNAUTHORIZED. The public health endpoint does not require a key.
Quick start
Send your first record with curl or JavaScript.
http://localhost:8086curl -X POST "http://localhost:8086/api/v1/transactions" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d ' {
"plateNumber": "RIV-482-AA",
"vehicleMake": "Toyota",
"vehicleModel": "Corolla",
"chassisNumber": "JTDBR32E720123456",
"engineNumber": "1ZZ-FE-789012",
"vehicleColour": "Silver",
"vehicleType": "Saloon",
"vehicleCategory": "Private",
"fullName": "Chinedu Okafor",
"phoneNumber": "+2348031234567",
"address": "12 Aba Road, Port Harcourt, Rivers State",
"emailAddress": "chinedu.okafor@example.com",
"amountPaid": 15000.5,
"transactionRef": "RVS-2026-000190",
"transactionDate": "2026-11-04T10:15:00.000Z",
"serviceName": "Vehicle License",
"serviceCode": "VL001"
}'const response = await fetch("http://localhost:8086/api/v1/transactions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.RIVERS_API_KEY,
},
body: JSON.stringify({
"plateNumber": "RIV-482-AA",
"vehicleMake": "Toyota",
"vehicleModel": "Corolla",
"chassisNumber": "JTDBR32E720123456",
"engineNumber": "1ZZ-FE-789012",
"vehicleColour": "Silver",
"vehicleType": "Saloon",
"vehicleCategory": "Private",
"fullName": "Chinedu Okafor",
"phoneNumber": "+2348031234567",
"address": "12 Aba Road, Port Harcourt, Rivers State",
"emailAddress": "chinedu.okafor@example.com",
"amountPaid": 15000.5,
"transactionRef": "RVS-2026-000190",
"transactionDate": "2026-11-04T10:15:00.000Z",
"serviceName": "Vehicle License",
"serviceCode": "VL001"
}),
});
const result = await response.json();
if (!response.ok) {
console.error(result.error);
throw new Error(result.error.message);
}
console.log(result.data.transactionRef, result.data.duplicate);Submit a record
The primary ingestion endpoint.
/api/v1/transactionsapplication/json · 1 MB maxSuccess status codes
201 Created— the payload was received and the record was stored.200 OK— idempotent replay; the same reference is acknowledged withduplicate: true.
Content rules
- Flat JSON object, not nested groups.
- Unknown fields are ignored.
- Empty optional strings are treated as omitted.
{
"plateNumber": "RIV-482-AA",
"vehicleMake": "Toyota",
"vehicleModel": "Corolla",
"chassisNumber": "JTDBR32E720123456",
"engineNumber": "1ZZ-FE-789012",
"vehicleColour": "Silver",
"vehicleType": "Saloon",
"vehicleCategory": "Private",
"fullName": "Chinedu Okafor",
"phoneNumber": "+2348031234567",
"address": "12 Aba Road, Port Harcourt, Rivers State",
"emailAddress": "chinedu.okafor@example.com",
"amountPaid": 15000.5,
"transactionRef": "RVS-2026-000190",
"transactionDate": "2026-11-04T10:15:00.000Z",
"serviceName": "Vehicle License",
"serviceCode": "VL001"
}Payload fields
All three record groups are sent in one flat object.
Vehicle information
8 fieldsCustomer information
4 fieldsPayment information
5 fieldsSupported services
Only these service pairs are accepted. serviceName and serviceCode must match. Unsupported or mismatched values return 422 VALIDATION_FAILED.
VL001HP002Field-name note: the vehicle engine field is engineNumber. Send it exactly as shown in the payload example.
Responses
The POST endpoint returns a small success receipt or a failure error. Full records are available from the detail endpoint.
To keep ingestion fast and avoid client timeouts, POST /api/v1/transactions returns only the transaction reference and the duplicate indicator. Internal review flags are not exposed in the ingestion response.
{
"success": true,
"message": "Record received and stored successfully.",
"data": {
"transactionRef": "RVS-2026-000190",
"duplicate": false
},
"meta": {
"requestId": "8f14e45f-ea0f-4c0c-9b5e-9b0a1f4f0f1a",
"timestamp": "2026-11-04T10:15:03.120Z"
}
}{
"success": true,
"message": "Record already received (idempotent replay).",
"data": {
"transactionRef": "RVS-2026-000190",
"duplicate": true
},
"meta": {
"requestId": "8f14e45f-ea0f-4c0c-9b5e-9b0a1f4f0f1a",
"timestamp": "2026-11-04T10:15:03.120Z"
}
}If the chassis/plate or phone/email identifiers point at different existing records, the transaction is still stored. The mismatch is flagged internally for dashboard review, while the API response remains the same success receipt. Use GET /api/v1/transactions/{transactionRef} to retrieve the full normalized record.
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The payload failed validation.",
"details": [
{
"field": "plateNumber",
"message": "plateNumber is required",
"code": "too_small"
},
{
"field": "amountPaid",
"message": "amountPaid must be greater than 0",
"code": "custom"
},
{
"field": "serviceCode",
"message": "serviceCode must be one of: VL001, HP002. Supported services: Vehicle License (VL001), Hackney Permit (HP002).",
"code": "custom"
}
]
},
"meta": {
"requestId": "8f14e45f-ea0f-4c0c-9b5e-9b0a1f4f0f1a",
"timestamp": "2026-11-04T10:15:03.120Z"
}
}Every response includes meta.requestId. Quote this value when contacting support; it is also returned in the x-request-id response header.
Matching & idempotency
What happens when a record is submitted more than once.
Vehicle matching
- Matched by chassisNumber first.
- Falls back to plateNumber.
- If chassis and plate belong to different vehicles, the record is stored against the chassis match and flagged internally for review.
- Existing vehicle details and current owner are updated.
Owner matching
- Matched by normalized phoneNumber first.
- Falls back to emailAddress when provided.
- If phone and email belong to different owners, the record is stored against the phone match and flagged internally for review.
- Vehicles and transactions are linked to the matched owner.
Identifier mismatches are stored for internal review.
If the chassis and plate point at different vehicles, or the phone and email point at different owners, the transaction is still stored against the primary match (chassis or phone). The mismatch is flagged internally and shown in the dashboard, but the ingestion response remains the same success receipt. Only an existing transaction reference blocks storage.
transactionRef is the idempotency key.
Retrying the exact same normalized payload with the same reference returns 200 and the original acknowledgement, with duplicate: true. Reusing a reference with different data returns 409 TRANSACTION_REF_CONFLICT.
Errors
A consistent error envelope is used for every failure.
Other endpoints
Verify records and monitor the service.
/api/v1/transactionsPaginated transaction list with free-text search, date filters and flagged-record review.
curl "http://localhost:8086/api/v1/transactions?page=1&pageSize=20&search=RIV-482-AA" \
-H "x-api-key: YOUR_API_KEY"Query parameters
7 fieldsReviewing flags: append flagged=true to list only records marked for internal review. The same view is available in the dashboard at /transactions?flagged=1.
/api/v1/transactions/{transactionRef}Retrieve a single transaction using its unique reference.
curl "http://localhost:8086/api/v1/transactions/RVS-2026-000184" \
-H "x-api-key: YOUR_API_KEY"/api/v1/healthPublic health check. Verifies that the database is reachable. No API key required.
curl "http://localhost:8086/api/v1/health"Try it live
Send a real request from this page. The record is stored in this environment.
Live request playground
Sends a real request to this environment.
The key is sent only to this server and is never stored in the page.
Response
Your response will appear here.
A successful request returns 201. Replaying the same payload returns 200 with duplicate: true.
Integration checklist
A short list before going live.
- Store the API key in a server-side secret manager, never in client-side code.
- Generate transactionRef from your system and persist it before sending. Reuse it for retries.
- Send transactionDate as an ISO 8601 date-time with a timezone, ideally UTC.
- Treat 200 with duplicate: true as success, not an error.
- Log meta.requestId for every request to simplify support investigations.
- Use the health endpoint for readiness checks and alerting.
- Validate phone numbers and plate numbers before sending to reduce conflict errors.
