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
Pegel Routes
GET /v1/pegel/nearest
Returns the nearest water gauge station from Pegelonline WSV with current water level measurement. The server uses its own GPS position (from Peplink) to determine the nearest station.
Two modes:
- Whitelist mode (recommended):
stationsconfigured — searches within 200 km, filters by whitelist, returns the nearest match regardless ofradius_km. - Radius mode:
stationsempty — searches by GPS proximity withinradius_km.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
water | string | No | Filter by water body name (e.g. RHEIN, MOSEL) |
radius | number | No | Search radius in km (radius mode only, capped at server config) |
Response (200):
{
"stationName": "OTTENHEIM",
"waterName": "RHEIN",
"km": 270.6,
"latitude": 48.3544,
"longitude": 7.7282,
"value": 324,
"unit": "cm",
"timestamp": "2026-06-15T14:00:00+02:00",
"state": "normal"
}
state values: low (≤ MNW), normal (MNW–MHW), high (≥ MHW), unknown, commented, out-dated
Response (404): Pegelonline disabled in config, no station found, or no data yet (first poll pending).
Status Codes: 200, 404, 500
Configuration:
[pegelonline]
enabled = true
base_url = "https://pegelonline.wsv.de/webservices/rest-api/v2"
radius_km = 30 # Only used when stations is empty
stations = ["OTTENHEIM", "IFFEZHEIM", "MAXAU"] # Exact PegelOnline API shortnames
Data is polled every 5 minutes and cached server-side. The endpoint reads from cache — no external API call per request.
Watchlist Routes
GET /v1/watchlist/entries
List all watchlist entries, ordered by name ascending.
Response 200 OK
{
"entries": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"mmsi": "211234567",
"name": "MS Harmonie",
"category_id": "...",
"alert_sound": "sonar",
"danger_sound": "alarm",
"star_color": "#EF4444",
"range_thresholds": [10.0, 7.0, 5.0, 2.0, 1.0],
"created_at": "2026-06-09T12:00:00Z",
"updated_at": "2026-06-09T12:00:00Z"
}
],
"_links": {
"self": "/v1/watchlist/entries"
}
}
Status Codes: 200, 500
POST /v1/watchlist/entries
Create a new watchlist entry.
Request Body (JSON, camelCase)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
mmsi | string | Yes | -- | MMSI number of the vessel |
name | string | Yes | -- | Display name |
categoryId | UUID | No | null | Watchlist category ID |
alertSound | string | No | null | Sound key for alert threshold |
dangerSound | string | No | null | Sound key for danger threshold |
starColor | string | No | null | Hex color for stars (inherits from category/settings) |
rangeThresholds | number[] | No | null | 5 distance thresholds in km (inherits from category/settings) |
Response 201 Created -- Returns the created entry with _links.
Status Codes: 201, 400, 500
GET /v1/watchlist/entries/:id
Get a single watchlist entry by ID.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Entry ID |
Response 200 OK
Status Codes: 200, 404, 500
PUT /v1/watchlist/entries/:id
Update a watchlist entry. Only provided fields are changed.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Entry ID |
Request Body (JSON, camelCase -- all fields optional)
| Field | Type | Description |
|---|---|---|
mmsi | string | MMSI number |
name | string | Display name |
categoryId | UUID | Category ID |
alertSound | string | Alert sound key |
dangerSound | string | Danger sound key |
starColor | string | Hex star color |
rangeThresholds | number[] | Distance thresholds |
Response 200 OK -- Returns the updated entry with _links.
Status Codes: 200, 404, 500
DELETE /v1/watchlist/entries/:id
Delete a watchlist entry.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Entry ID |
Response 204 No Content (empty body)
Status Codes: 204, 404, 500
GET /v1/watchlist/categories
List all watchlist categories, ordered by name ascending.
Response 200 OK
{
"categories": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Freighter",
"color": "#EF4444",
"display_type": "stars",
"sound_enabled": true,
"overlay_enabled": true,
"overlay_icon": null,
"alert_sound": "sonar",
"danger_sound": "alarm",
"range_thresholds": [10.0, 7.0, 5.0, 2.0, 1.0],
"created_at": "2026-06-09T12:00:00Z",
"updated_at": "2026-06-09T12:00:00Z"
}
],
"_links": {
"self": "/v1/watchlist/categories"
}
}
Status Codes: 200, 500
POST /v1/watchlist/categories
Create a new watchlist category.
Request Body (JSON, camelCase)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | -- | Category name |
color | string | Yes | -- | Hex color |
displayType | string | No | "stars" | Display mode: stars or custom |
soundEnabled | bool | No | true | Whether sound alerts are enabled |
overlayEnabled | bool | No | true | Whether overlay display is enabled |
overlayIcon | string | No | null | Lucide icon name for custom display |
alertSound | string | No | null | Sound key for alert threshold |
dangerSound | string | No | null | Sound key for danger threshold |
rangeThresholds | number[] | No | null | 5 distance thresholds in km |
Response 201 Created -- Returns the created category with _links.
Status Codes: 201, 400, 500
GET /v1/watchlist/categories/:id
Get a single watchlist category by ID.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Category ID |
Status Codes: 200, 404, 500
PUT /v1/watchlist/categories/:id
Update a watchlist category. Only provided fields are changed.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Category ID |
Request Body (JSON, camelCase -- all fields optional)
| Field | Type | Description |
|---|---|---|
name | string | Category name |
color | string | Hex color |
displayType | string | Display mode |
soundEnabled | bool | Sound alerts enabled |
overlayEnabled | bool | Overlay display enabled |
overlayIcon | string | Lucide icon name |
alertSound | string | Alert sound key |
dangerSound | string | Danger sound key |
rangeThresholds | number[] | Distance thresholds |
Response 200 OK -- Returns the updated category with _links.
Status Codes: 200, 400, 404, 500
DELETE /v1/watchlist/categories/:id
Delete a watchlist category.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Category ID |
Response 204 No Content (empty body)
Status Codes: 204, 404, 500
GET /v1/watchlist/settings
Get global watchlist settings (singleton).
Response 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"range_thresholds": [10.0, 7.0, 5.0, 2.0, 1.0],
"default_alert_sound": "sonar",
"default_danger_sound": "alarm",
"default_star_color": "#EF4444",
"updated_at": "2026-06-09T12:00:00Z",
"_links": {
"self": "/v1/watchlist/settings"
}
}
Status Codes: 200, 404, 500
PUT /v1/watchlist/settings
Update global watchlist settings (upserts). Only provided fields are changed.
Request Body (JSON, camelCase -- all fields optional)
| Field | Type | Description |
|---|---|---|
rangeThresholds | number[] | 5 distance thresholds in km |
defaultAlertSound | string | Default alert sound key |
defaultDangerSound | string | Default danger sound key |
defaultStarColor | string | Default hex star color |
Response 200 OK -- Returns the updated settings with _links.
Status Codes: 200, 500
GET /v1/watchlist/sounds
List all available watchlist sounds (built-in + custom).
Response 200 OK
{
"sounds": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"key": "sonar",
"name": "Sonar",
"volume": 80,
"file_id": "...",
"builtin": true,
"created_at": "2026-06-09T12:00:00Z"
}
],
"_links": {
"self": "/v1/watchlist/sounds"
}
}
Status Codes: 200, 500
POST /v1/watchlist/sounds/upload
Upload a custom sound file. Accepts multipart/form-data.
Form Fields
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | Audio file (MP3, WAV, OGG) |
name | string | Yes | Display name for the sound |
Response 201 Created -- Returns the created sound object.
Status Codes: 201, 400, 500
DELETE /v1/watchlist/sounds/:id
Delete a custom sound. Built-in sounds cannot be deleted. Returns 400 if the sound is still referenced by entries, categories, or settings.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Sound ID |
Response 204 No Content (empty body)
Status Codes: 204, 400, 404, 500
PUT /v1/watchlist/sounds/:id/volume
Update the volume of a watchlist sound (0-100).
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Sound ID |
Request Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
volume | integer | Yes | Volume level (0-100) |
Response 200 OK -- Returns the updated sound object.
Status Codes: 200, 404, 500
POST /v1/watchlist/test-alert
Send a test alert through the WebSocket watchlist channel. Useful for testing sound and overlay configuration.
Response 200 OK
Status Codes: 200, 500
Vessel Routes
GET /v1/vessels/active
Returns AIS vessels seen within a look-back window, ordered by lastSeen descending (newest first). Records come from the persistent vessels registry, so this includes vessels that are no longer on the live map. Requires the api_key header (see Authentication).
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
withinSecs | i64 | No | 3600 | Look-back window in seconds — only vessels whose lastSeen falls within it are returned |
Response 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"mmsi": 211234567,
"name": "MS Harmonie",
"shipType": 70,
"length": 110,
"beam": 11,
"callsign": "DA1234",
"firstSeen": "2026-07-20T08:00:00Z",
"lastSeen": "2026-07-27T12:34:56Z",
"updatedAt": "2026-07-27T12:34:56Z",
"source": "local"
}
]
| Field | Type | Description |
|---|---|---|
id | UUID | Vessel registry record ID |
mmsi | i64 | Maritime Mobile Service Identity |
name | string|null | Vessel name (null if never received) |
shipType | integer|null | Raw AIS ship type code (null if unknown) |
length | integer|null | Length in meters (null if unknown) |
beam | integer|null | Beam in meters (null if unknown) |
callsign | string|null | Radio call sign (null if unknown) |
firstSeen | string | RFC 3339 timestamp of first AIS contact |
lastSeen | string | RFC 3339 timestamp of most recent AIS contact |
updatedAt | string | RFC 3339 timestamp of the last registry write |
source | string|null | Last effective tracking source: local | aisstream (null until first activity flush) |
Status Codes: 200, 500
GET /v1/vessels
Returns all vessels in the persistent vessels registry, ordered by lastSeen descending (newest first). Requires the api_key header (see Authentication).
Response 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"mmsi": 211234567,
"name": "MS Harmonie",
"shipType": 70,
"length": 110,
"beam": 11,
"callsign": "DA1234",
"firstSeen": "2026-07-20T08:00:00Z",
"lastSeen": "2026-07-27T12:34:56Z",
"updatedAt": "2026-07-27T12:34:56Z"
}
]
Field shapes match GET /v1/vessels/active.
Status Codes: 200, 500
GET /v1/vessels/{mmsi}
Returns a single registry vessel by MMSI. Requires the api_key header.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
mmsi | i64 | Maritime Mobile Service Identity |
Response 200 OK -- Single vessel record (same shape as GET /v1/vessels/active entries).
Status Codes: 200, 404 (no vessel with that MMSI), 500
PUT /v1/vessels/{mmsi}
Manually overwrite a vessel's static fields. Full-representation update — the body always carries all 5 editable fields; an omitted field is written as null (no partial merge). Requires the api_key header.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
mmsi | i64 | Maritime Mobile Service Identity |
Request Body (JSON, camelCase -- all fields nullable, full representation)
| Field | Type | Description |
|---|---|---|
name | string|null | Vessel name |
shipType | integer|null | Raw AIS ship type code |
length | integer|null | Length in meters |
beam | integer|null | Beam in meters |
callsign | string|null | Radio call sign |
Example
{
"name": "MS Harmonie",
"shipType": 70,
"length": 110,
"beam": 11,
"callsign": "DA1234"
}
Response 200 OK -- Returns the updated vessel (same shape as GET /v1/vessels/active entries).
Status Codes: 200, 404 (no vessel with that MMSI), 500
DELETE /v1/vessels/{mmsi}
Hard-delete a vessel from the registry. Requires the api_key header.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
mmsi | i64 | Maritime Mobile Service Identity |
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
ignore | bool | No | false | Also add the MMSI to the persistent ignore list (union'd with [ais] ignore_mmsi config) so it stays blacklisted after deletion |
Response 204 No Content (empty body)
Status Codes: 204, 404 (no vessel with that MMSI), 500
GET /v1/vessels/history
Returns the static-field change audit log from vessels_history, ordered by changedAt descending (newest first). Each entry records one static field change. Requires the api_key header.
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
mmsi | i64 | No | -- | Filter to a single vessel; omit for all vessels |
limit | i64 | No | 200 | Max entries to return (max 1000) |
Response 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"mmsi": 211234567,
"field": "name",
"oldValue": null,
"newValue": "MS Harmonie",
"changedAt": "2026-07-27T12:34:56Z"
}
]
| Field | Type | Description |
|---|---|---|
id | UUID | History entry ID |
mmsi | i64 | Maritime Mobile Service Identity |
field | string | Changed static field (name, shipType, length, beam, callsign) |
oldValue | string|null | Previous value |
newValue | string|null | New value |
changedAt | string | RFC 3339 timestamp of the change |
Status Codes: 200, 500
Storage Routes
GET /v1/storage/{prefix}/files/{key}
Serve a file from storage. Used to serve uploaded sound files and other assets.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
prefix | string | Storage prefix (e.g. sounds) |
key | string | File key |
Response 200 OK -- Returns the file with appropriate Content-Type header.
Status Codes: 200, 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.