REST APIJSONIdempotentv1

Integrate vehicle and payment records in minutes.

One endpoint accepts vehicle, customer and payment information, normalizes it, and stores it in MySQL. References are unique and retries are safe, so a dropped connection never means a duplicate payment.

Endpoint

POST /api/v1/transactions

Authentication

x-api-key header

Idempotency

Unique transactionRef

POSTv1

/api/v1/transactions

Idempotent by transactionRef

Matched by chassis, then plate

Mismatches stored for internal review

Raw payload kept for audit

201 Created · record stored

200 OK · duplicate: true

409 Conflict · reference reused

01

Overview

How the ingestion flow works end to end.

1

Receive

Your system posts one flat JSON payload containing vehicle, customer and payment information.

2

Match & normalize

Vehicles are deduplicated by chassis then plate. Customers are matched by phone then email. Data is trimmed and standardized.

3

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.

02

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.

Request headers
Content-Type: application/json
x-api-key: YOUR_API_KEY

Missing or invalid keys return 401 UNAUTHORIZED. The public health endpoint does not require a key.

03

Quick start

Send your first record with curl or JavaScript.

Base URLhttp://localhost:8086
curl
curl -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"
  }'
Node.js / TypeScript
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);
04

Submit a record

The primary ingestion endpoint.

POST/api/v1/transactionsapplication/json · 1 MB max

Success status codes

  • 201 Created — the payload was received and the record was stored.
  • 200 OK — idempotent replay; the same reference is acknowledged with duplicate: true.

Content rules

  • Flat JSON object, not nested groups.
  • Unknown fields are ignored.
  • Empty optional strings are treated as omitted.
Example payload
{
  "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"
}
05

Payload fields

All three record groups are sent in one flat object.

Vehicle information

8 fields
FieldTypeRequiredNotes
plateNumber

e.g. RIV-482-AA

string (max 20)RequiredVehicle plate number. Normalized to uppercase. Used as the secondary vehicle matching key.
vehicleMake

e.g. Toyota

string (max 80)RequiredManufacturer of the vehicle.
vehicleModel

e.g. Corolla

string (max 80)RequiredVehicle model.
chassisNumber

e.g. JTDBR32E720123456

string (max 60)RequiredChassis/VIN. Normalized to uppercase. Primary vehicle matching key and unique per vehicle.
engineNumber

e.g. 1ZZ-FE-789012

string (max 60)RequiredEngine number. Normalized to uppercase with whitespace removed.
vehicleColour

e.g. Silver

string (max 40)OptionalVehicle colour.
vehicleType

e.g. Saloon

string (max 60)OptionalVehicle type, for example Saloon, SUV or Bus.
vehicleCategory

e.g. Private

string (max 60)OptionalVehicle category, for example Private or Commercial.

Customer information

4 fields
FieldTypeRequiredNotes
fullName

e.g. Chinedu Okafor

string (max 150)RequiredFull name of the vehicle owner or customer.
phoneNumber

e.g. +2348031234567

string (max 30)RequiredOwner phone number. Spaces, dashes, dots and parentheses are removed; a 00 prefix becomes +. Primary owner matching key.
address

e.g. 12 Aba Road, Port Harcourt

string (max 1000)OptionalPostal or residential address.
emailAddress

e.g. chinedu.okafor@example.com

email (max 190)OptionalValid email address. Used as the secondary owner matching key when the phone number is not found.

Payment information

5 fields
FieldTypeRequiredNotes
amountPaid

e.g. 15000.50

number | numeric stringRequiredAmount paid. Must be greater than 0 and at most 9,999,999,999.99. Stored as DECIMAL(12,2) and returned as a string.
transactionRef

e.g. RVS-2026-000184

string (max 100)RequiredUnique transaction reference. Idempotency key: retries with the same reference and data are safe. References are treated case-insensitively by the database.
transactionDate

e.g. 2026-11-04T10:15:00.000Z

ISO 8601 date-timeRequiredWhen the transaction occurred. Unix timestamps in milliseconds are also accepted. Stored in UTC.
serviceName

e.g. Vehicle License

enumRequiredService paid for. Must match serviceCode: Vehicle License → VL001, Hackney Permit → HP002.
serviceCode

e.g. VL001

enumRequiredService code. Normalized to uppercase. Must match serviceName: Vehicle License → VL001, Hackney Permit → HP002.

Supported services

Only these service pairs are accepted. serviceName and serviceCode must match. Unsupported or mismatched values return 422 VALIDATION_FAILED.

Vehicle LicenseVL001
Hackney PermitHP002

Field-name note: the vehicle engine field is engineNumber. Send it exactly as shown in the payload example.

06

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.

201 Created
{
  "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"
  }
}
200 OK — idempotent replay
{
  "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.

422 Validation failed
{
  "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.

07

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.

08

Errors

A consistent error envelope is used for every failure.

StatusCodeWhen
400BAD_REQUESTMalformed JSON, invalid query parameters or an empty body.
401UNAUTHORIZEDMissing, invalid or revoked API key.
404NOT_FOUNDThe requested transactionRef does not exist.
409TRANSACTION_REF_CONFLICTThe reference exists but the payload is different. Use a new reference or resend the original data.
413BAD_REQUESTRequest body exceeds 1 MB.
415BAD_REQUESTContent-Type is not application/json.
422VALIDATION_FAILEDOne or more fields failed validation. The error details array identifies each field.
500INTERNAL_ERRORUnexpected server error. Retry with the same reference and quote the request ID if it persists.
09

Other endpoints

Verify records and monitor the service.

GET/api/v1/transactions

Paginated transaction list with free-text search, date filters and flagged-record review.

List transactions
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 fields
FieldTypeRequiredNotes
pageintegerOptionalPage number, starting at 1. Default 1.
pageSizeintegerOptionalRecords per page, 1–100. Default 20.
searchstringOptionalMatches reference, service, plate, chassis, make, model, owner name, phone and email.
serviceCodestringOptionalFilter by exact service code.
fromISO date-timeOptionalInclusive lower bound for transactionDate.
toISO date-timeOptionalInclusive upper bound for transactionDate.
flaggedbooleanOptionalFilter by internal review state. true returns records marked for review; false returns records without review flags.

Reviewing flags: append flagged=true to list only records marked for internal review. The same view is available in the dashboard at /transactions?flagged=1.

GET/api/v1/transactions/{transactionRef}

Retrieve a single transaction using its unique reference.

Get one transaction
curl "http://localhost:8086/api/v1/transactions/RVS-2026-000184" \
  -H "x-api-key: YOUR_API_KEY"
GET/api/v1/health

Public health check. Verifies that the database is reachable. No API key required.

Health check
curl "http://localhost:8086/api/v1/health"
10

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.

Creates a stored record
x-api-key

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.

11

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.