Skip to main content

REST API Reference

All REST endpoints are mounted under /v1. Responses use JSON and include HATEOAS _links where applicable.

Base URL: http://localhost:3000/v1


GPS Routes

GET /v1/peplink/gps

Returns the current GPS position from the Peplink router.

Response 200 OK

{
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 100.0,
"timestamp": 1717934400,
"speedKmh": 45.2,
"speedMps": 12.56,
"speedMph": 28.09,
"speedKnots": 24.41,
"_links": {
"self": "/v1/peplink/gps"
}
}
FieldTypeDescription
latitudef64Latitude in degrees
longitudef64Longitude in degrees
altitudef64Altitude in meters
timestampi64Unix timestamp (seconds)
speedKmhf64Speed in km/h
speedMpsf64Speed in meters per second
speedMphf64Speed in miles per hour
speedKnotsf64Speed in knots

Status Codes: 200, 500


GET /v1/peplink/gps/speed

Returns only speed data from the current GPS position.

Response 200 OK

{
"speedKmh": 45.2,
"speedMps": 12.56,
"speedMph": 28.09,
"speedKnots": 24.41,
"timestamp": 1717934400,
"_links": {
"self": "/v1/peplink/gps/speed"
}
}

Status Codes: 200, 500


GET /v1/peplink/gps/history

Returns GPS location history within a time range. Defaults to the last 24 hours.

Query Parameters

ParameterTypeRequiredDefaultDescription
starti64No24h agoStart of range as unix timestamp (seconds)
endi64NonowEnd of range as unix timestamp (seconds)

Validation:

  • start must be a valid unix timestamp
  • end must be a valid unix timestamp
  • start must be before end

Response 200 OK

{
"locations": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-06-09T12:00:00Z",
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 100.0,
"speed_kmh": 45.2,
"speed_mps": 12.56,
"speed_mph": 28.09,
"speed_knots": 24.41,
"trip_id": null
}
],
"_links": {
"self": "/v1/peplink/gps/history"
}
}

Status Codes: 200, 400, 500


Trip Routes

POST /v1/trips

Create a new trip. Validates coordinates and checks that the referenced category, sender, and receiver exist.

Request Body (JSON, camelCase)

FieldTypeRequiredDefaultDescription
startLatitudef64Yes--Start latitude (-90 to 90)
startLongitudef64Yes--Start longitude (-180 to 180)
cargostringYes--Cargo description (must not be empty)
categoryIdUUIDYes--Cargo category ID (must exist)
weightf64Yes--Cargo weight
senderIdUUIDYes--Sender client ID (must exist)
receiverIdUUIDYes--Receiver client ID (must exist)
logGpsboolNofalseWhether to log GPS locations for this trip

Example

{
"startLatitude": 51.5074,
"startLongitude": -0.1278,
"cargo": "Electronics",
"categoryId": "550e8400-e29b-41d4-a716-446655440001",
"weight": 250.5,
"senderId": "550e8400-e29b-41d4-a716-446655440002",
"receiverId": "550e8400-e29b-41d4-a716-446655440003",
"logGps": true
}

Response 201 Created

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"start_time": "2026-06-09T12:00:00Z",
"end_time": null,
"start_latitude": 51.5074,
"start_longitude": -0.1278,
"end_latitude": null,
"end_longitude": null,
"cargo": "Electronics",
"category_id": "550e8400-e29b-41d4-a716-446655440001",
"weight": 250.5,
"sender_id": "550e8400-e29b-41d4-a716-446655440002",
"receiver_id": "550e8400-e29b-41d4-a716-446655440003",
"status": "Active",
"log_gps": true,
"_links": {
"self": "/v1/trips/550e8400-e29b-41d4-a716-446655440000"
}
}

Validation Rules:

  • Latitude must be between -90 and 90
  • Longitude must be between -180 and 180
  • cargo must not be empty (after trim)
  • categoryId must reference an existing category
  • senderId must reference an existing client
  • receiverId must reference an existing client

Status Codes: 201, 400, 500


GET /v1/trips

List all trips ordered by start time descending.

Response 200 OK

{
"trips": [
{
"id": "...",
"start_time": "2026-06-09T12:00:00Z",
"end_time": null,
"start_latitude": 51.5074,
"start_longitude": -0.1278,
"end_latitude": null,
"end_longitude": null,
"cargo": "Electronics",
"category_id": "...",
"weight": 250.5,
"sender_id": "...",
"receiver_id": "...",
"status": "Active",
"log_gps": true
}
],
"_links": {
"self": "/v1/trips"
}
}

Status Codes: 200, 500


GET /v1/trips/:id

Get a single trip by ID. Includes GPS locations recorded for this trip, ordered by timestamp ascending.

Path Parameters

ParameterTypeDescription
idUUIDTrip ID

Response 200 OK

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"start_time": "2026-06-09T12:00:00Z",
"end_time": null,
"start_latitude": 51.5074,
"start_longitude": -0.1278,
"end_latitude": null,
"end_longitude": null,
"cargo": "Electronics",
"category_id": "...",
"weight": 250.5,
"sender_id": "...",
"receiver_id": "...",
"status": "Active",
"log_gps": true,
"locations": [
{
"id": "...",
"timestamp": "2026-06-09T12:01:00Z",
"latitude": 51.5080,
"longitude": -0.1270,
"altitude": 98.5,
"speed_kmh": 35.0,
"speed_mps": 9.72,
"speed_mph": 21.75,
"speed_knots": 18.90,
"trip_id": "550e8400-e29b-41d4-a716-446655440000"
}
],
"_links": {
"self": "/v1/trips/550e8400-e29b-41d4-a716-446655440000"
}
}

Status Codes: 200, 404, 500


PUT /v1/trips/:id

Update a trip. Only provided fields are changed (partial update). Validates status transitions and foreign key existence for any changed FK fields.

Path Parameters

ParameterTypeDescription
idUUIDTrip ID

Request Body (JSON, camelCase -- all fields optional)

FieldTypeDescription
cargostringCargo description
categoryIdUUIDCargo category ID
weightf64Cargo weight
senderIdUUIDSender client ID
receiverIdUUIDReceiver client ID
statusstringTrip status: ACTIVE, COMPLETED, or CANCELLED
logGpsboolWhether to log GPS locations

Status Transition Rules:

  • ACTIVE -> COMPLETED: Allowed
  • ACTIVE -> CANCELLED: Allowed
  • COMPLETED -> ACTIVE: Not allowed (returns 400)
  • CANCELLED -> ACTIVE: Not allowed (returns 400)
  • COMPLETEDCANCELLED: Allowed

Response 200 OK -- Returns the full updated trip object with _links.

Status Codes: 200, 400, 404, 500


DELETE /v1/trips/:id

Delete a trip and its associated GPS locations (transactional).

Path Parameters

ParameterTypeDescription
idUUIDTrip ID

Response 204 No Content (empty body)

Status Codes: 204, 404, 500


POST /v1/trips/:id/complete

Mark an active trip as completed with end coordinates.

Path Parameters

ParameterTypeDescription
idUUIDTrip ID

Request Body (JSON, camelCase)

FieldTypeRequiredDescription
endLatitudef64YesEnd latitude (-90 to 90)
endLongitudef64YesEnd longitude (-180 to 180)

Example

{
"endLatitude": 52.5200,
"endLongitude": 13.4050
}

Validation:

  • Trip must be in ACTIVE status (returns 400 if COMPLETED or CANCELLED)
  • Coordinates must be valid ranges

Response 200 OK -- Returns the completed trip with status: "Completed", end_time set, and _links.

Status Codes: 200, 400, 404, 500


Client Routes

POST /v1/clients

Create a new client with optional contact persons. If contacts are provided and none has isPrimary set, the first contact is automatically promoted to primary.

Request Body (JSON, camelCase)

FieldTypeRequiredDefaultDescription
namestringYes--Client name (must not be empty)
emailstringYes--Email address
phonestringYes--Phone number
streetstringYes--Street name
houseNumberstringYes--House number
postalCodestringYes--Postal code
citystringYes--City
countrystringYes--Country
additionalInfostringNonullAdditional address info
contactsarrayNo[]Array of contact person objects

Contact Person Object

FieldTypeRequiredDefaultDescription
namestringYes--Contact name
emailstringYes--Contact email
phonestringYes--Contact phone
positionstringYes--Job position/title
isPrimaryboolNofalseWhether this is the primary contact

Example

{
"name": "ACME Logistics",
"email": "info@acme.example.com",
"phone": "+49 30 12345678",
"street": "Hauptstrasse",
"houseNumber": "42",
"postalCode": "10115",
"city": "Berlin",
"country": "DE",
"additionalInfo": "Building B, 3rd floor",
"contacts": [
{
"name": "John Doe",
"email": "john@acme.example.com",
"phone": "+49 30 12345679",
"position": "Logistics Manager",
"isPrimary": true
}
]
}

Response 201 Created

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "ACME Logistics",
"email": "info@acme.example.com",
"phone": "+49 30 12345678",
"street": "Hauptstrasse",
"house_number": "42",
"postal_code": "10115",
"city": "Berlin",
"country": "DE",
"additional_info": "Building B, 3rd floor",
"contacts": [
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"email": "john@acme.example.com",
"phone": "+49 30 12345679",
"position": "Logistics Manager",
"is_primary": true,
"client_id": "550e8400-e29b-41d4-a716-446655440000"
}
],
"_links": {
"self": "/v1/clients/550e8400-e29b-41d4-a716-446655440000"
}
}

Status Codes: 201, 400, 500


GET /v1/clients

List all clients with their contacts, ordered by name ascending.

Response 200 OK

{
"clients": [
{
"id": "...",
"name": "ACME Logistics",
"email": "info@acme.example.com",
"phone": "+49 30 12345678",
"street": "Hauptstrasse",
"house_number": "42",
"postal_code": "10115",
"city": "Berlin",
"country": "DE",
"additional_info": null,
"contacts": [ ... ]
}
],
"_links": {
"self": "/v1/clients"
}
}

Status Codes: 200, 500


GET /v1/clients/:id

Get a single client by ID with contacts and associated trips (where the client is sender or receiver).

Path Parameters

ParameterTypeDescription
idUUIDClient ID

Response 200 OK

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "ACME Logistics",
"email": "info@acme.example.com",
"phone": "+49 30 12345678",
"street": "Hauptstrasse",
"house_number": "42",
"postal_code": "10115",
"city": "Berlin",
"country": "DE",
"additional_info": null,
"contacts": [ ... ],
"trips": [ ... ],
"_links": {
"self": "/v1/clients/550e8400-e29b-41d4-a716-446655440000"
}
}

The trips array contains all trips where this client is either the sender or receiver, ordered by start time descending.

Status Codes: 200, 404, 500


PUT /v1/clients/:id

Update client fields. Only provided fields are changed (partial update).

Path Parameters

ParameterTypeDescription
idUUIDClient ID

Request Body (JSON, camelCase -- all fields optional)

FieldTypeDescription
namestringClient name
emailstringEmail address
phonestringPhone number
streetstringStreet name
houseNumberstringHouse number
postalCodestringPostal code
citystringCity
countrystringCountry
additionalInfostringAdditional address info

Response 200 OK -- Returns the updated client (without contacts) with _links.

Status Codes: 200, 404, 500


POST /v1/clients/:id/contacts

Add a contact person to an existing client. If isPrimary is true, existing contacts are demoted (handled by the DB layer).

Path Parameters

ParameterTypeDescription
idUUIDClient ID

Request Body (JSON, camelCase)

FieldTypeRequiredDefaultDescription
namestringYes--Contact name
emailstringYes--Contact email
phonestringYes--Contact phone
positionstringYes--Job position/title
isPrimaryboolNofalseWhether this is the primary contact

Response 201 Created

{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Jane Smith",
"email": "jane@acme.example.com",
"phone": "+49 30 12345680",
"position": "Fleet Manager",
"is_primary": false,
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"_links": {
"self": "/v1/clients/550e8400-.../contacts/660e8400-..."
}
}

Status Codes: 201, 404, 500


PUT /v1/clients/:client_id/contacts/:contact_id

Update a contact person. Only provided fields are changed.

Path Parameters

ParameterTypeDescription
client_idUUIDClient ID
contact_idUUIDContact ID

Request Body (JSON, camelCase -- all fields optional)

FieldTypeDescription
namestringContact name
emailstringContact email
phonestringContact phone
positionstringJob position/title
isPrimaryboolWhether this is the primary contact

Response 200 OK -- Returns the updated contact with _links.

Status Codes: 200, 404, 500


DELETE /v1/clients/:client_id/contacts/:contact_id

Delete a contact person. If the deleted contact was primary, the DB layer auto-promotes the next contact alphabetically.

Path Parameters

ParameterTypeDescription
client_idUUIDClient ID
contact_idUUIDContact ID

Response 204 No Content (empty body)

Status Codes: 204, 404, 500


Category Routes

POST /v1/categories

Create a new cargo category. The name is trimmed before insertion.

Request Body (JSON)

FieldTypeRequiredDescription
namestringYesCategory name (must not be empty, must be unique)
descriptionstringNoCategory description

Example

{
"name": "Fragile Goods",
"description": "Items requiring careful handling"
}

Response 201 Created

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Fragile Goods",
"description": "Items requiring careful handling",
"created_at": "2026-06-09T12:00:00Z",
"updated_at": "2026-06-09T12:00:00Z",
"_links": {
"self": "/v1/categories/550e8400-e29b-41d4-a716-446655440000"
}
}

Validation:

  • name must not be empty (after trim)
  • name must be unique (returns 400 if duplicate: "A category with this name already exists")

Status Codes: 201, 400, 500


GET /v1/categories

List all categories ordered by name ascending.

Response 200 OK

{
"categories": [
{
"id": "...",
"name": "Fragile Goods",
"description": "Items requiring careful handling",
"created_at": "2026-06-09T12:00:00Z",
"updated_at": "2026-06-09T12:00:00Z"
}
],
"_links": {
"self": "/v1/categories"
}
}

Status Codes: 200, 500


GET /v1/categories/:id

Get a single category by ID with associated trips.

Path Parameters

ParameterTypeDescription
idUUIDCategory ID

Response 200 OK

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Fragile Goods",
"description": "Items requiring careful handling",
"created_at": "2026-06-09T12:00:00Z",
"updated_at": "2026-06-09T12:00:00Z",
"trips": [
{
"id": "...",
"cargo": "Electronics",
"status": "Active",
"..."
}
],
"_links": {
"self": "/v1/categories/550e8400-e29b-41d4-a716-446655440000"
}
}

Status Codes: 200, 404, 500


PUT /v1/categories/:id

Update a category. Only provided fields are changed. The name is trimmed.

Path Parameters

ParameterTypeDescription
idUUIDCategory ID

Request Body (JSON -- all fields optional)

FieldTypeDescription
namestringCategory name (must be unique)
descriptionstringCategory description

Validation:

  • If name is provided and conflicts with an existing category, returns 400

Response 200 OK -- Returns the updated category with _links.

Status Codes: 200, 400, 404, 500


DELETE /v1/categories/:id

Delete a category. Returns 400 if trips still reference this category.

Path Parameters

ParameterTypeDescription
idUUIDCategory ID

Validation:

  • Cannot delete a category that is still referenced by trips (returns 400: "Cannot delete category: trips still reference it")

Response 204 No Content (empty body)

Status Codes: 204, 400, 404, 500


System Routes

GET /health

System health check. Returns database, Peplink, forwarding, and WebSocket status plus server uptime.

Response 200 OK (healthy) or 503 Service Unavailable (unhealthy)

{
"status": "healthy",
"uptime": 3600,
"services": {
"database": {
"status": "connected"
},
"peplink": {
"status": "connected"
},
"forwarding": {
"enabled": true,
"healthy": true,
"queueLength": 0,
"consecutiveFailures": 0
},
"websocket": {
"status": "running",
"connectedClients": 2
}
},
"timestamp": "2026-06-09T12:00:00+00:00"
}
FieldDescription
uptimeServer uptime in seconds
services.websocket.status"running" or "closing" (during graceful shutdown)
services.websocket.connectedClientsNumber of active WebSocket connections

The overall status is "healthy" only when the database is reachable. Returns 503 when the database check fails.

Status Codes: 200, 503


GET /v1

Returns API version, server info, and client info.

Response 200 OK

{
"version": "2026.6.1",
"server": {
"platform": "linux",
"arch": "x86_64"
},
"client": {
"ip": "127.0.0.1",
"userAgent": "curl/8.1.0"
},
"timestamp": "2026-06-09T12:00:00+00:00"
}

Status Codes: 200


GET /

Redirects to /v1 with 301 Moved Permanently.


GET /v1/openapi.json

Returns the OpenAPI 3.0 specification as JSON. Returns 404 if disabled in configuration (docs.openapi_json = false).

Status Codes: 200, 404


GET /v1/swagger-ui/

Serves the Swagger UI for interactive API exploration. Only available when docs.swagger_ui = true in configuration.