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"
}
}
| Field | Type | Description |
|---|---|---|
latitude | f64 | Latitude in degrees |
longitude | f64 | Longitude in degrees |
altitude | f64 | Altitude in meters |
timestamp | i64 | Unix timestamp (seconds) |
speedKmh | f64 | Speed in km/h |
speedMps | f64 | Speed in meters per second |
speedMph | f64 | Speed in miles per hour |
speedKnots | f64 | Speed 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
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
start | i64 | No | 24h ago | Start of range as unix timestamp (seconds) |
end | i64 | No | now | End of range as unix timestamp (seconds) |
Validation:
startmust be a valid unix timestampendmust be a valid unix timestampstartmust be beforeend
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)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
startLatitude | f64 | Yes | -- | Start latitude (-90 to 90) |
startLongitude | f64 | Yes | -- | Start longitude (-180 to 180) |
cargo | string | Yes | -- | Cargo description (must not be empty) |
categoryId | UUID | Yes | -- | Cargo category ID (must exist) |
weight | f64 | Yes | -- | Cargo weight |
senderId | UUID | Yes | -- | Sender client ID (must exist) |
receiverId | UUID | Yes | -- | Receiver client ID (must exist) |
logGps | bool | No | false | Whether 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
cargomust not be empty (after trim)categoryIdmust reference an existing categorysenderIdmust reference an existing clientreceiverIdmust 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
| Parameter | Type | Description |
|---|---|---|
id | UUID | Trip 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
| Parameter | Type | Description |
|---|---|---|
id | UUID | Trip ID |
Request Body (JSON, camelCase -- all fields optional)
| Field | Type | Description |
|---|---|---|
cargo | string | Cargo description |
categoryId | UUID | Cargo category ID |
weight | f64 | Cargo weight |
senderId | UUID | Sender client ID |
receiverId | UUID | Receiver client ID |
status | string | Trip status: ACTIVE, COMPLETED, or CANCELLED |
logGps | bool | Whether to log GPS locations |
Status Transition Rules:
ACTIVE->COMPLETED: AllowedACTIVE->CANCELLED: AllowedCOMPLETED->ACTIVE: Not allowed (returns 400)CANCELLED->ACTIVE: Not allowed (returns 400)COMPLETED↔CANCELLED: 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
| Parameter | Type | Description |
|---|---|---|
id | UUID | Trip 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
| Parameter | Type | Description |
|---|---|---|
id | UUID | Trip ID |
Request Body (JSON, camelCase)
| Field | Type | Required | Description |
|---|---|---|---|
endLatitude | f64 | Yes | End latitude (-90 to 90) |
endLongitude | f64 | Yes | End longitude (-180 to 180) |
Example
{
"endLatitude": 52.5200,
"endLongitude": 13.4050
}
Validation:
- Trip must be in
ACTIVEstatus (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)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | -- | Client name (must not be empty) |
email | string | Yes | -- | Email address |
phone | string | Yes | -- | Phone number |
street | string | Yes | -- | Street name |
houseNumber | string | Yes | -- | House number |
postalCode | string | Yes | -- | Postal code |
city | string | Yes | -- | City |
country | string | Yes | -- | Country |
additionalInfo | string | No | null | Additional address info |
contacts | array | No | [] | Array of contact person objects |
Contact Person Object
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | -- | Contact name |
email | string | Yes | -- | Contact email |
phone | string | Yes | -- | Contact phone |
position | string | Yes | -- | Job position/title |
isPrimary | bool | No | false | Whether 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
| Parameter | Type | Description |
|---|---|---|
id | UUID | Client 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
| Parameter | Type | Description |
|---|---|---|
id | UUID | Client ID |
Request Body (JSON, camelCase -- all fields optional)
| Field | Type | Description |
|---|---|---|
name | string | Client name |
email | string | Email address |
phone | string | Phone number |
street | string | Street name |
houseNumber | string | House number |
postalCode | string | Postal code |
city | string | City |
country | string | Country |
additionalInfo | string | Additional 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
| Parameter | Type | Description |
|---|---|---|
id | UUID | Client ID |
Request Body (JSON, camelCase)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | -- | Contact name |
email | string | Yes | -- | Contact email |
phone | string | Yes | -- | Contact phone |
position | string | Yes | -- | Job position/title |
isPrimary | bool | No | false | Whether 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
| Parameter | Type | Description |
|---|---|---|
client_id | UUID | Client ID |
contact_id | UUID | Contact ID |
Request Body (JSON, camelCase -- all fields optional)
| Field | Type | Description |
|---|---|---|
name | string | Contact name |
email | string | Contact email |
phone | string | Contact phone |
position | string | Job position/title |
isPrimary | bool | Whether 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
| Parameter | Type | Description |
|---|---|---|
client_id | UUID | Client ID |
contact_id | UUID | Contact 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)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Category name (must not be empty, must be unique) |
description | string | No | Category 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:
namemust not be empty (after trim)namemust 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
| Parameter | Type | Description |
|---|---|---|
id | UUID | Category 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
| Parameter | Type | Description |
|---|---|---|
id | UUID | Category ID |
Request Body (JSON -- all fields optional)
| Field | Type | Description |
|---|---|---|
name | string | Category name (must be unique) |
description | string | Category description |
Validation:
- If
nameis 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
| Parameter | Type | Description |
|---|---|---|
id | UUID | Category 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"
}
| Field | Description |
|---|---|
uptime | Server uptime in seconds |
services.websocket.status | "running" or "closing" (during graceful shutdown) |
services.websocket.connectedClients | Number 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.