Skip to main content

REST API Reference

Complete reference for all REST API endpoints.

REST handlers and routes are implemented in the heimdall-rest crate (crates/heimdall-rest/). Handlers live in heimdall-rest::handlers, routes in heimdall-rest::routes.

Interactive Documentation

For an interactive experience, visit our Swagger UI where you can test endpoints directly.

Base URL

https://api.elcto.com

Health & Info

Get Health Status

Check the health status of the API and all services.

GET /health
GET /v1/health

Authentication: None required

Both endpoints return the same response. /health is available at the root level for load balancers and monitoring tools, while /v1/health is available under the versioned API for consistency.

Response:

{
"status": "healthy",
"version": "1.0.0",
"timestamp": "2025-11-24T10:30:00Z",
"uptime": {
"days": 5,
"hours": 12,
"minutes": 30,
"seconds": 45,
"total": 475845.0
},
"services": {
"database": {
"status": "up",
"responseTime": 15
},
"redis": {
"status": "up",
"responseTime": 3
},
"websocket": {
"status": "up",
"connections": 42,
"subscriptions": 128
}
},
"_links": {
"self": "https://api.elcto.com/health"
}
}

Get API Info

Get information about the API version and client details.

GET /v1

Authentication: None required

Response:

{
"version": "1.0.0",
"server": {
"uptime": 475845000
},
"client": {
"ip": "192.168.1.100",
"userAgent": "Mozilla/5.0...",
"browser": "Chrome",
"os": "Windows"
},
"timestamp": "2025-11-24T10:30:00Z",
"_links": {
"self": "https://api.elcto.com/v1"
}
}

GPS Data

List GPS Data

Retrieve a paginated list of GPS data points.

GET /v1/gps?page=1&limit=10

Authentication: Required (Viewer or Admin)

Query Parameters:

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger10Items per page (max: 100)

Response:

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 11.0,
"timestamp": 1700000000,
"speed": 5.5,
"createdAt": "2025-11-24T10:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 100,
"total_pages": 10
},
"_links": {
"self": "https://api.elcto.com/v1/gps?page=1&limit=10",
"first": "https://api.elcto.com/v1/gps?page=1&limit=10",
"next": "https://api.elcto.com/v1/gps?page=2&limit=10",
"last": "https://api.elcto.com/v1/gps?page=10&limit=10"
}
}

Get Current GPS

Get the most recent GPS data point, optionally filtered by device.

GET /v1/gps/current
GET /v1/gps/current?device_id=my-tracker-1

Authentication: Required (gps:read)

Query Parameters:

ParameterTypeDefaultDescription
device_idstring-Filter by device ID. Omit for globally latest point.

Results are cached in Redis (TTL 5 min). Cache key: gps:latest:{device_id} or gps:latest:global.

Response:

{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"device_id": "my-tracker-1",
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 11.0,
"heading": 270.0,
"timestamp": "2025-11-24T10:00:00Z",
"speed_kmh": 72.4,
"speed_mps": 20.1,
"speed_mph": 45.0,
"speed_knots": 39.1,
"created_at": "2025-11-24T10:00:00Z"
},
"_links": {
"self": "https://api.elcto.com/v1/gps/550e8400-e29b-41d4-a716-446655440000"
}
}

Get Current GPS Per Device

Get the latest GPS point for each device. Useful for multi-device live maps.

GET /v1/gps/current/devices

Authentication: Required (gps:read)

Response:

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"device_id": "tracker-1",
"latitude": 51.5074,
"longitude": -0.1278,
"heading": 270.0,
"speed_kmh": 72.4,
"timestamp": "2025-11-24T10:00:00Z",
"created_at": "2025-11-24T10:00:00Z"
},
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"device_id": "tracker-2",
"latitude": 49.4875,
"longitude": 8.4660,
"heading": 90.0,
"speed_kmh": 45.0,
"timestamp": "2025-11-24T09:58:00Z",
"created_at": "2025-11-24T09:58:00Z"
}
]

Get GPS by ID

Retrieve a specific GPS data point by ID.

GET /v1/gps/{id}

Authentication: Required (Viewer or Admin)

Path Parameters:

ParameterTypeDescription
idUUIDGPS data ID

Response:

{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 11.0,
"timestamp": 1700000000,
"speed": 5.5,
"createdAt": "2025-11-24T10:00:00Z"
},
"_links": {
"self": "https://api.elcto.com/v1/gps/550e8400-e29b-41d4-a716-446655440000"
}
}

Create GPS Data

Create a new GPS data point.

POST /v1/gps
Content-Type: application/json

Authentication: Required (Admin only)

Rate Limit: 30 requests per 60 seconds

Request Body:

{
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 11.0,
"timestamp": 1700000000,
"speed": 5.5
}

Response (201 Created):

{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 11.0,
"timestamp": 1700000000,
"speed": 5.5,
"createdAt": "2025-11-24T10:00:00Z"
},
"_links": {
"self": "https://api.elcto.com/v1/gps/550e8400-e29b-41d4-a716-446655440000"
}
}

Devices

Manage GPS tracking devices.

List Devices

GET /v1/devices?page=1&limit=10

Authentication: Required Permission: devices:read

Query Parameters:

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger10Items per page (max: 100)

Response:

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "GPS Tracker 1",
"description": "Primary vehicle tracker",
"device_type": "gps_tracker",
"is_active": true,
"created_at": "2025-11-24T10:00:00Z",
"updated_at": "2025-11-24T10:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 5,
"total_pages": 1
},
"_links": {
"self": "https://api.elcto.com/v1/devices?page=1&limit=10",
"first": "https://api.elcto.com/v1/devices?page=1&limit=10",
"last": "https://api.elcto.com/v1/devices?page=1&limit=10"
}
}

Get Device

GET /v1/devices/{id}

Authentication: Required Permission: devices:read

Create Device

POST /v1/devices
Content-Type: application/json

Authentication: Required Permission: devices:write

Request Body:

{
"name": "GPS Tracker 1",
"description": "Primary vehicle tracker",
"device_type": "gps_tracker",
"api_key_id": "21eebc99-9c0b-4ef8-bb6d-6bb9bd380a19"
}
FieldTypeRequiredDescription
namestringYesDevice name
descriptionstringNoDevice description
device_typestringNoDevice type identifier
api_key_idUUIDNoAPI key to link to this device

If api_key_id is provided, the API key is linked to the device. Only the key owner or system keys can be linked. Any key previously linked to this device is unlinked.

Response (201 Created):

{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "GPS Tracker 1",
"description": "Primary vehicle tracker",
"device_type": "gps_tracker",
"is_active": true,
"created_at": "2025-11-24T10:00:00Z",
"updated_at": "2025-11-24T10:00:00Z"
},
"_links": {
"self": "https://api.elcto.com/v1/devices/550e8400-e29b-41d4-a716-446655440000"
}
}

Update Device

PATCH /v1/devices/{id}
Content-Type: application/json

Authentication: Required Permission: devices:write

Request Body: Same fields as create, all optional. At least one field required. Additionally accepts is_active (boolean).

Delete Device

DELETE /v1/devices/{id}

Authentication: Required Permission: devices:delete

Response (204 No Content): Empty response on success

Trips

Manage trips with status lifecycle and GPS data tracking.

List Trips

GET /v1/trips?page=1&limit=20&status=active

Authentication: Required Permission: trips:read

Query Parameters:

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger20Items per page (max: 100)
statusstring-Filter by status (draft, active, paused, completed)

Response:

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Hamburg to Berlin",
"description": "Weekly delivery run",
"status": "active",
"gps_logging_enabled": true,
"device_id": "660e8400-e29b-41d4-a716-446655440001",
"category_id": "770e8400-e29b-41d4-a716-446655440002",
"origin_id": "880e8400-e29b-41d4-a716-446655440003",
"destination_id": "990e8400-e29b-41d4-a716-446655440004",
"weight_kg": 1500.0,
"notes": "Fragile cargo",
"tags": ["urgent", "fragile"],
"start_latitude": 53.5511,
"start_longitude": 9.9937,
"end_latitude": null,
"end_longitude": null,
"started_at": "2025-11-24T08:00:00Z",
"ended_at": null,
"created_at": "2025-11-24T07:30:00Z",
"updated_at": "2025-11-24T08:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 50,
"total_pages": 3
},
"_links": { "..." }
}

Get Trip

GET /v1/trips/{id}

Authentication: Required Permission: trips:read

Create Trip

POST /v1/trips
Content-Type: application/json

Authentication: Required Permission: trips:write

Request Body:

{
"name": "Hamburg to Berlin",
"description": "Weekly delivery run",
"gps_logging_enabled": true,
"device_id": "660e8400-e29b-41d4-a716-446655440001",
"category_id": "770e8400-e29b-41d4-a716-446655440002",
"origin_id": "880e8400-e29b-41d4-a716-446655440003",
"destination_id": "990e8400-e29b-41d4-a716-446655440004",
"weight_kg": 1500.0,
"notes": "Fragile cargo",
"tags": ["urgent", "fragile"]
}
FieldTypeRequiredDescription
namestringYesTrip name
descriptionstringNoTrip description
gps_logging_enabledbooleanNoEnable GPS logging (default: false)
device_idUUIDNoDevice to use for GPS tracking
category_idUUIDNoTrip category
origin_idUUIDNoOrigin location ID
destination_idUUIDNoDestination location ID
weight_kgnumberNoCargo weight in kg
notesstringNoAdditional notes
tagsstring[]NoTags for filtering

New trips always start with status draft.

Response (201 Created): ResourceResponse<Trip>

Update Trip

PATCH /v1/trips/{id}
Content-Type: application/json

Authentication: Required Permission: trips:write

All fields from create are optional, plus:

FieldTypeDescription
statusstringTransition trip status (see below)
start_latitudenumberStart coordinates
start_longitudenumberStart coordinates
end_latitudenumberEnd coordinates
end_longitudenumberEnd coordinates

Status Transitions:

FromToSide Effects
draftactiveSets started_at automatically
activepaused-
pausedactive-
activecompletedSets ended_at automatically
pausedcompletedSets ended_at automatically

Invalid transitions return 400 Bad Request.

GPS Logging Conflict

Activating a trip with gps_logging_enabled: true returns 409 Conflict if another active trip with GPS logging is already running on the same device.

Delete Trip

DELETE /v1/trips/{id}

Authentication: Required Permission: trips:delete

Only draft or completed trips can be deleted. Active or paused trips return 409 Conflict.

When deleting a completed trip, linked GPS data points have their trip_id set to NULL (data is preserved).

Response (204 No Content): Empty response on success

Get Trip GPS Data

Retrieve paginated GPS data points for a specific trip.

GET /v1/trips/{id}/gps?page=1&limit=50

Authentication: Required Permission: trips:read

Query Parameters:

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger10Items per page (max: 100)

GPS points are ordered by timestamp ASC.

Response:

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"device_id": "tracker-1",
"trip_id": "660e8400-e29b-41d4-a716-446655440001",
"latitude": 53.5511,
"longitude": 9.9937,
"altitude": 15.0,
"heading": 90.0,
"timestamp": "2025-11-24T08:05:00Z",
"speed_kmh": 80.5,
"speed_mps": 22.4,
"speed_mph": 50.0,
"speed_knots": 43.5,
"pdop": 1.2,
"hdop": 0.8,
"vdop": 0.9,
"created_at": "2025-11-24T08:05:00Z"
}
],
"pagination": { "..." },
"_links": { "..." }
}

Locations

Manage locations (origins, destinations) with contact information.

List Locations

GET /v1/locations?page=1&limit=10

Authentication: Required Permission: locations:read

Locations are ordered by name (ASC).

Response:

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Hamburg Port",
"street": "Hafenstrasse 1",
"zip": "20457",
"city": "Hamburg",
"country": "DE",
"latitude": 53.5411,
"longitude": 9.9837,
"notes": "Gate 3, loading dock B",
"is_active": true,
"created_at": "2025-11-24T10:00:00Z",
"updated_at": "2025-11-24T10:00:00Z"
}
],
"pagination": { "..." },
"_links": { "..." }
}

Get Location

Returns location details with contacts.

GET /v1/locations/{id}

Authentication: Required Permission: locations:read

Response:

{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Hamburg Port",
"street": "Hafenstrasse 1",
"zip": "20457",
"city": "Hamburg",
"country": "DE",
"latitude": 53.5411,
"longitude": 9.9837,
"notes": "Gate 3, loading dock B",
"is_active": true,
"created_at": "2025-11-24T10:00:00Z",
"updated_at": "2025-11-24T10:00:00Z",
"contacts": [
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"location_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Max Mustermann",
"phone": "+49 40 123456",
"email": "max@example.com",
"role": "Dock Manager",
"created_at": "2025-11-24T10:00:00Z"
}
]
},
"_links": { "..." }
}

Create Location

POST /v1/locations
Content-Type: application/json

Authentication: Required Permission: locations:write

Request Body:

{
"name": "Hamburg Port",
"street": "Hafenstrasse 1",
"zip": "20457",
"city": "Hamburg",
"country": "DE",
"latitude": 53.5411,
"longitude": 9.9837,
"notes": "Gate 3, loading dock B",
"contacts": [
{
"name": "Max Mustermann",
"phone": "+49 40 123456",
"email": "max@example.com",
"role": "Dock Manager"
}
]
}
FieldTypeRequiredDescription
namestringYesLocation name
streetstringNoStreet address
zipstringNoPostal code
citystringNoCity
countrystringNoCountry code
latitudenumberNoGPS latitude
longitudenumberNoGPS longitude
notesstringNoAdditional notes
contactsarrayNoInline contacts to create with the location

Response (201 Created): Location with contacts (same shape as Get Location)

Update Location

PATCH /v1/locations/{id}
Content-Type: application/json

Authentication: Required Permission: locations:write

Same fields as create (all optional, at least one required). Additionally accepts is_active (boolean). Does not update contacts -- use the contact sub-resource endpoints.

Delete Location

DELETE /v1/locations/{id}

Authentication: Required Permission: locations:delete

Response (204 No Content): Empty response on success

Add Location Contact

POST /v1/locations/{id}/contacts
Content-Type: application/json

Authentication: Required Permission: locations:write

Request Body:

{
"name": "Max Mustermann",
"phone": "+49 40 123456",
"email": "max@example.com",
"role": "Dock Manager"
}
FieldTypeRequiredDescription
namestringYesContact name
phonestringNoPhone number
emailstringNoEmail address
rolestringNoContact role/title

Response (201 Created): ResourceResponse<LocationContact>

Update Location Contact

PATCH /v1/locations/{id}/contacts/{cid}
Content-Type: application/json

Authentication: Required Permission: locations:write

All fields optional (name, phone, email, role). At least one required.

Delete Location Contact

DELETE /v1/locations/{id}/contacts/{cid}

Authentication: Required Permission: locations:write

Response (204 No Content): Empty response on success

Geofences

Manage circular geofence zones for GPS monitoring.

List Geofences

GET /v1/geofences?page=1&limit=10

Authentication: Required Permission: geofences:read

Response:

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Hamburg Port Zone",
"description": "Loading dock area",
"latitude": 53.5411,
"longitude": 9.9837,
"radius_km": 0.5,
"is_active": true,
"metadata": { "alert_on_exit": true },
"created_at": "2025-11-24T10:00:00Z",
"updated_at": "2025-11-24T10:00:00Z"
}
],
"pagination": { "..." },
"_links": { "..." }
}

Get Geofence

GET /v1/geofences/{id}

Authentication: Required Permission: geofences:read

Create Geofence

POST /v1/geofences
Content-Type: application/json

Authentication: Required Permission: geofences:write

Request Body:

{
"name": "Hamburg Port Zone",
"description": "Loading dock area",
"latitude": 53.5411,
"longitude": 9.9837,
"radius_km": 0.5,
"metadata": { "alert_on_exit": true }
}
FieldTypeRequiredDescription
namestringYesGeofence name
descriptionstringNoDescription
latitudenumberYesCenter latitude
longitudenumberYesCenter longitude
radius_kmnumberYesRadius in kilometers
metadataobjectNoArbitrary JSON metadata

Response (201 Created): ResourceResponse<Geofence>

Update Geofence

PATCH /v1/geofences/{id}
Content-Type: application/json

Authentication: Required Permission: geofences:write

Same fields as create (all optional, at least one required). Additionally accepts is_active (boolean).

Delete Geofence

DELETE /v1/geofences/{id}

Authentication: Required Permission: geofences:delete

Response (204 No Content): Empty response on success

Trip Categories

Manage hierarchical trip categories for organizing trips.

List Trip Categories

Returns a flat list of all categories (ordered by sort_order, then name). Not paginated.

GET /v1/trip-categories

Authentication: Required Permission: trip_categories:read

Response:

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"parent_id": null,
"name": "Delivery",
"description": "Standard deliveries",
"color": "#3B82F6",
"is_active": true,
"sort_order": 0,
"created_at": "2025-11-24T10:00:00Z",
"updated_at": "2025-11-24T10:00:00Z"
},
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"parent_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Express Delivery",
"description": "Time-sensitive deliveries",
"color": "#EF4444",
"is_active": true,
"sort_order": 1,
"created_at": "2025-11-24T10:00:00Z",
"updated_at": "2025-11-24T10:00:00Z"
}
]

Get Trip Category

GET /v1/trip-categories/{id}

Authentication: Required Permission: trip_categories:read

Create Trip Category

POST /v1/trip-categories
Content-Type: application/json

Authentication: Required Permission: trip_categories:write

Request Body:

{
"parent_id": null,
"name": "Delivery",
"description": "Standard deliveries",
"color": "#3B82F6",
"sort_order": 0
}
FieldTypeRequiredDescription
parent_idUUIDNoParent category ID for hierarchy
namestringYesCategory name
descriptionstringNoDescription
colorstringNoHex color code for UI
sort_orderintegerNoSort order (default: 0)

Response (201 Created): ResourceResponse<TripCategory>

Update Trip Category

PATCH /v1/trip-categories/{id}
Content-Type: application/json

Authentication: Required Permission: trip_categories:write

Optional fields: name, description, color, is_active, sort_order. At least one required.

Delete Trip Category

DELETE /v1/trip-categories/{id}

Authentication: Required Permission: trip_categories:delete

Response (204 No Content): Empty response on success

Trip Templates

Reusable trip templates for quickly creating new trips with pre-filled values.

List Trip Templates

Returns all templates (ordered by name). Not paginated.

GET /v1/trip-templates

Authentication: Required Permission: trips:read

Response:

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Hamburg-Berlin Express",
"description": "Regular express run",
"category_id": "770e8400-e29b-41d4-a716-446655440002",
"origin_id": "880e8400-e29b-41d4-a716-446655440003",
"destination_id": "990e8400-e29b-41d4-a716-446655440004",
"device_id": "660e8400-e29b-41d4-a716-446655440001",
"gps_logging_enabled": true,
"default_weight_kg": 1200.0,
"notes": "Use loading dock B",
"tags": ["express", "regular"],
"created_at": "2025-11-24T10:00:00Z",
"updated_at": "2025-11-24T10:00:00Z"
}
]

Get Trip Template

GET /v1/trip-templates/{id}

Authentication: Required Permission: trips:read

Create Trip Template

POST /v1/trip-templates
Content-Type: application/json

Authentication: Required Permission: trips:write

Request Body:

{
"name": "Hamburg-Berlin Express",
"description": "Regular express run",
"category_id": "770e8400-e29b-41d4-a716-446655440002",
"origin_id": "880e8400-e29b-41d4-a716-446655440003",
"destination_id": "990e8400-e29b-41d4-a716-446655440004",
"device_id": "660e8400-e29b-41d4-a716-446655440001",
"gps_logging_enabled": true,
"default_weight_kg": 1200.0,
"notes": "Use loading dock B",
"tags": ["express", "regular"]
}
FieldTypeRequiredDescription
namestringYesTemplate name
descriptionstringNoDescription
category_idUUIDNoDefault trip category
origin_idUUIDNoDefault origin location
destination_idUUIDNoDefault destination location
device_idUUIDNoDefault GPS device
gps_logging_enabledbooleanNoDefault GPS logging (default: false)
default_weight_kgnumberNoDefault cargo weight
notesstringNoDefault notes
tagsstring[]NoDefault tags

Response (201 Created): ResourceResponse<TripTemplate>

Update Trip Template

PATCH /v1/trip-templates/{id}
Content-Type: application/json

Authentication: Required Permission: trips:write

Same fields as create, all optional.

Delete Trip Template

DELETE /v1/trip-templates/{id}

Authentication: Required Permission: trips:delete

Response (204 No Content): Empty response on success

Create Trip from Template

Create a new trip using a template's pre-configured values.

POST /v1/trip-templates/{id}/create-trip
Content-Type: application/json

Authentication: Required Permission: trips:write

Request Body:

{
"weight_kg": 1500.0
}
FieldTypeRequiredDescription
weight_kgnumberYesCargo weight for this trip

The new trip copies all values from the template (name, description, category, origin, destination, device, GPS logging, notes, tags). The trip name is set to "{template_name} (from template)" and status starts as draft.

Response (201 Created): ResourceResponse<Trip>

System Settings

Get All Settings

Retrieve all system settings.

GET /v1/settings

Authentication: Required (Admin only)

Get Public Settings

Retrieve public-facing settings.

GET /v1/settings/public

Authentication: None required

Get Setting by Key

Retrieve a specific setting by key.

GET /v1/settings/{key}

Authentication: Required (Admin only)

Create Setting

Create a new system setting.

POST /v1/settings
Content-Type: application/json

Authentication: Required (Admin only)

Update Setting

Update an existing setting.

PUT /v1/settings/{key}
Content-Type: application/json

Authentication: Required (Admin only)

Delete Setting

Delete a system setting.

DELETE /v1/settings/{key}

Authentication: Required (Admin only)

System Config

Pre-seeded configuration values (e.g. GPS settings). Unlike System Settings, these are update-only — no creation or deletion.

List System Configs

Retrieve all system config entries, optionally filtered by key prefix.

GET /v1/system-config?prefix=gps.

Authentication: Required Permission: settings:read

Response:

[
{
"key": "gps.update_interval_seconds",
"value": 30,
"description": "Interval between GPS updates",
"updated_at": "2026-06-25T12:00:00Z"
}
]

Get System Config by Key

GET /v1/system-config/{key}

Authentication: Required Permission: settings:read

Update System Config

PATCH /v1/system-config/{key}
Content-Type: application/json

Authentication: Required Permission: settings:write

Request Body:

{
"value": 60
}

The value field accepts any valid JSON value (number, string, boolean, object, array).

Pegelonline (Water Levels)

Query water level data from the German Pegelonline API. Only available when the Pegelonline service is configured.

Get Nearest Station

Find the nearest water level station, optionally filtered by water body.

GET /v1/pegel/nearest?water=RHEIN&radius=50

Authentication: Required Permission: pegel:read

Query Parameters:

ParameterTypeRequiredDescription
waterstringNoFilter by water body shortname (e.g. RHEIN)
radiusnumberNoSearch radius in km

Response:

{
"station_id": "a6ee8177-107b-47dd-bcfd-30960ccc3e9c",
"shortname": "KÖLN",
"longname": "KÖLN",
"water_shortname": "RHEIN",
"water_longname": "RHEIN",
"latitude": 50.9377,
"longitude": 6.9633,
"km": 688.0,
"agency": "WSA RHEIN",
"level_cm": 312.0,
"trend": 1,
"distance_km": 4.2
}

Get Stations

List water level stations, optionally filtered by water body.

GET /v1/pegel/stations?water=RHEIN

Authentication: Required Permission: pegel:read

Response: Array of station objects (same shape as above).

Statistics

Get Public Statistics

Retrieve public platform statistics.

GET /v1/stats/public

Authentication: None required

Users

List Users

Retrieve a paginated list of all users.

GET /v1/users?page=1&limit=20

Authentication: Required Permission: users:read

Query Parameters:

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger20Items per page (max: 100)

Response:

{
"data": [
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"username": "johndoe",
"primary_platform_account_id": "8f14e45f-ceea-467f-8a53-c9d5889d76b2",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-20T15:30:00Z",
"last_login_at": "2025-01-25T09:00:00Z",
"deleted_at": null,
"privacy_mode": false
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"total_pages": 8
},
"_links": {
"self": "https://api.elcto.com/v1/users?page=1&limit=20",
"first": "https://api.elcto.com/v1/users?page=1&limit=20",
"next": "https://api.elcto.com/v1/users?page=2&limit=20",
"last": "https://api.elcto.com/v1/users?page=8&limit=20"
}
}

Get User by ID

Retrieve a specific user by their ID.

GET /v1/users/{id}

Authentication: Required Permission: users:read (or self)

Path Parameters:

ParameterTypeDescription
idstringUser ID

Response:

{
"data": {
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"username": "johndoe",
"primary_platform_account_id": "8f14e45f-ceea-467f-8a53-c9d5889d76b2",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-20T15:30:00Z",
"last_login_at": "2025-01-25T09:00:00Z",
"deleted_at": null,
"privacy_mode": false
},
"_links": {
"self": "https://api.elcto.com/v1/users/6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
}

Get User Profile

Retrieve a user's profile with platform account details.

GET /v1/users/{id}/profile

Authentication: Required Permission: users:read (or self)

Response:

{
"name": "John Doe",
"display_name": "johndoe",
"picture": "https://cdn.example.com/avatar.png",
"provider": "twitch",
"primary_provider": "twitch",
"last_login_at": "2025-01-25T09:00:00Z",
"created_at": "2025-01-15T10:00:00Z",
"privacy_mode": false,
"avatars_enabled": true
}

Get Current User

Retrieve the currently authenticated user.

GET /v1/users/me

Authentication: Required

Response: Same as Get User by ID

Update User

Update a user's information.

PATCH /v1/users/{id}
Content-Type: application/json

Authentication: Required Permission: users:write (or self)

Request Body:

{
"username": "newusername",
"email": "newemail@example.com",
"avatar_url": "https://cdn.example.com/new-avatar.png",
"privacy_mode": true,
"avatars_enabled": false
}

All fields are optional. Only provided fields will be updated.

Note: Setting avatars_enabled to false will remove all existing avatar URLs from the user's platform accounts and prevent new OAuth logins from populating avatars.

Response:

{
"data": {
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"username": "newusername",
"primary_platform_account_id": "8f14e45f-ceea-467f-8a53-c9d5889d76b2",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-26T12:00:00Z",
"last_login_at": "2025-01-25T09:00:00Z",
"deleted_at": null,
"privacy_mode": true,
"preferred_locale": "en",
"avatars_enabled": true
},
"_links": {
"self": "https://api.elcto.com/v1/users/6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
}

Update User Locale

Update a user's preferred locale for email communications.

PATCH /v1/users/{id}/locale
Content-Type: application/json

Authentication: Required Permission: users:write (or self)

Request Body:

{
"locale": "de"
}
FieldTypeRequiredDescription
localestring | nullNoPreferred locale (e.g., "en", "de"). Set to null to clear.

Supported Locales: The list of supported locales is configurable via the email.supported_locales configuration. Default: ["en", "de"].

Response:

{
"data": {
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"username": "johndoe",
"primary_platform_account_id": "8f14e45f-ceea-467f-8a53-c9d5889d76b2",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-26T12:00:00Z",
"last_login_at": "2025-01-25T09:00:00Z",
"deleted_at": null,
"privacy_mode": false,
"preferred_locale": "de"
},
"_links": {
"self": "https://api.elcto.com/v1/users/6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
}

Delete User

Soft delete a user (sets deleted_at timestamp and revokes all sessions/API keys).

DELETE /v1/users/{id}

Authentication: Required Permission: users:delete

Response (204 No Content): Empty response on success

Get User Permissions

Retrieve a user's effective permissions (including role-based permissions).

GET /v1/users/{id}/permissions

Authentication: Required Permission: users:read (or self)

Response:

{
"permissions": [
"users:read",
"users:write",
"gps:read",
"*:*"
]
}

Get User Roles

Retrieve a user's assigned roles.

GET /v1/users/{id}/roles

Authentication: Required Permission: users:read (or self)

Response:

{
"roles": [
{
"id": "role_admin",
"name": "Admin",
"description": "Full administrative access to manage users, roles, and all resources"
}
]
}

Assign Role to User

Assign a role to a user.

POST /v1/users/{id}/roles
Content-Type: application/json

Authentication: Required Permission: users:write

Request Body:

{
"role_id": "41eebc99-9c0b-4ef8-bb6d-6bb9bd380a21"
}

Remove Role from User

Remove a role from a user.

DELETE /v1/users/{id}/roles/{role_id}

Authentication: Required Permission: users:write

Platform Accounts

List User Accounts

Retrieve all platform accounts linked to a user.

GET /v1/users/{id}/accounts

Authentication: Required Permission: users:read (or self)

Response:

{
"data": [
{
"id": "8f14e45f-ceea-467f-8a53-c9d5889d76b2",
"platform_id": "platform-twitch",
"platform_name": "Twitch",
"platform_slug": "twitch",
"platform_user_id": "12345678",
"is_oauth": true,
"is_primary": true,
"username": "johndoe",
"email": "john@example.com",
"avatar_url": "https://cdn.example.com/avatar.png",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-20T15:30:00Z"
}
],
"_links": {
"self": "https://api.elcto.com/v1/users/6ba7b810-9dad-11d1-80b4-00c04fd430c8/accounts"
}
}

Set Primary Platform Account

Change which platform account is used as the primary account for profile information.

PUT /v1/users/{id}/accounts/{account_id}/primary

Authentication: Required Permission: users:write (or self)

Path Parameters:

ParameterTypeDescription
idstringUser ID
account_idstringPlatform account ID to set as primary

Response:

{
"data": {
"id": "8f14e45f-ceea-467f-8a53-c9d5889d76b2",
"platform_id": "platform-discord",
"platform_name": "Discord",
"platform_slug": "discord",
"platform_user_id": "98765432",
"is_oauth": true,
"is_primary": true,
"username": "johndoe#1234",
"email": "john@example.com",
"avatar_url": "https://cdn.example.com/avatar.png",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-25T14:30:00Z"
},
"_links": {
"self": "https://api.elcto.com/v1/users/6ba7b810-9dad-11d1-80b4-00c04fd430c8/accounts/8f14e45f-ceea-467f-8a53-c9d5889d76b2"
}
}

Remove a platform account link from a user.

DELETE /v1/users/{id}/accounts/{account_id}

Authentication: Required Permission: users:write (or self)

Response (204 No Content): Empty response on success

Restrictions
  • Cannot unlink the email platform account (use account deletion instead)
  • Cannot unlink the last platform account (user must have at least one login method)
  • Cannot unlink the primary account (change primary first)

Start the process of linking an email account to a user. Sends a verification email.

POST /v1/users/{id}/email-link/request
Authorization: Bearer {token}
Content-Type: application/json

Authentication: Required Permission: users:write (or self)

Request Body:

{
"email": "newemail@example.com",
"password": "securepassword123"
}

Response (200 OK):

{
"message": "Verification email sent. Please check your inbox.",
"expires_at": "2025-01-25T15:30:00Z"
}
Password Requirements

The password must be at least 8 characters long.

Complete the email account linking by verifying the token from the email.

POST /v1/users/email-link/verify
Authorization: Bearer {token}
Content-Type: application/json

Authentication: Required

Request Body:

{
"token": "abc123def456..."
}

Response (200 OK):

{
"message": "Email account linked successfully",
"platform_account_id": "c0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13"
}

Check if there's a pending email link verification for a user.

GET /v1/users/{id}/email-link/pending
Authorization: Bearer {token}

Authentication: Required Permission: users:read (or self)

Response (200 OK) - With pending link:

{
"pending": true,
"email": "newemail@example.com",
"expires_at": "2025-01-25T15:30:00Z"
}

Response (200 OK) - No pending link:

{
"pending": false,
"email": null,
"expires_at": null
}

Cancel a pending email link request.

DELETE /v1/users/{id}/email-link/pending
Authorization: Bearer {token}

Authentication: Required Permission: users:write (or self)

Response (200 OK):

{
"message": "Pending email link cancelled"
}
Email Account Authentication

Once an email account is linked, users can authenticate using email/password on platforms that support it. This provides an alternative to OAuth authentication.

Active Sessions Management

Endpoints for users to view and manage their active login sessions across devices.

List Active Sessions

Get all active sessions for the authenticated user.

GET /v1/users/me/sessions
Authorization: Bearer {token}

Authentication: Required (Session token)

Response:

{
"sessions": [
{
"id": "d0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
"device_type": "desktop",
"browser_name": "Chrome",
"os_name": "Windows",
"provider": "twitch",
"provider_name": "Twitch",
"last_activity_at": "2025-01-25T10:30:00Z",
"created_at": "2025-01-20T08:00:00Z",
"expires_at": "2025-02-20T08:00:00Z",
"is_current": true
},
{
"id": "e0eebc99-9c0b-4ef8-bb6d-6bb9bd380a15",
"ip_address": "10.0.0.50",
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)...",
"device_type": "mobile",
"browser_name": "Safari",
"os_name": "iOS",
"provider": "email",
"provider_name": "Email",
"last_activity_at": "2025-01-24T15:00:00Z",
"created_at": "2025-01-15T12:00:00Z",
"expires_at": "2025-02-15T12:00:00Z",
"is_current": false
}
],
"current_session_id": "d0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
"_links": {
"self": "https://api.elcto.com/v1/users/me/sessions"
}
}

Session Fields:

FieldTypeDescription
idstringSession ID
ip_addressstringIP address used to create the session
user_agentstringFull user agent string
device_typestringParsed device type (desktop, mobile, tablet)
browser_namestringParsed browser name
os_namestringParsed operating system name
providerstringAuth provider slug used (twitch, discord, email, etc.)
provider_namestringAuth provider display name (Twitch, Discord, Email, etc.)
last_activity_atdatetimeLast activity timestamp
created_atdatetimeSession creation time
expires_atdatetimeSession expiration time
is_currentbooleanWhether this is the current session

Revoke Session

Revoke a specific session by ID.

DELETE /v1/users/me/sessions/{session_id}
Authorization: Bearer {token}

Authentication: Required (Session token)

Path Parameters:

ParameterTypeDescription
session_idstringSession ID to revoke

Response (200 OK):

{
"success": true,
"message": "Session revoked successfully"
}

Error Response (400 Bad Request):

{
"success": false,
"message": "Cannot revoke current session"
}
Current Session

You cannot revoke your current session. Use the logout functionality instead.

Revoke All Other Sessions

Revoke all sessions except the current one.

DELETE /v1/users/me/sessions
Authorization: Bearer {token}

Authentication: Required (Session token)

Response (200 OK):

{
"success": true,
"revoked_count": 3,
"message": "Revoked 3 session(s)"
}
Use Case

This is useful when you suspect your account has been compromised or when you want to sign out from all other devices.

Two-Factor Authentication

Endpoints for managing two-factor authentication (2FA) using TOTP (Time-based One-Time Password).

Get 2FA Status

Check if 2FA is enabled for the authenticated user.

GET /v1/users/me/2fa/status
Authorization: Bearer {token}

Query Parameters:

ParameterTypeRequiredDescription
user_idstringNoUser ID (required for system API keys)

Response:

{
"enabled": true,
"enabled_at": "2025-01-15T10:00:00Z",
"backup_codes_remaining": 8,
"platform_slug": "email"
}
FieldTypeDescription
enabledbooleanWhether 2FA is enabled
enabled_atstringWhen 2FA was enabled (ISO 8601)
backup_codes_remainingnumberNumber of unused backup codes
platform_slugstringPlatform used for 2FA

Get 2FA Requirement

Check if 2FA is required for the authenticated user based on their roles.

GET /v1/users/me/2fa/requirement
Authorization: Bearer {token}

Query Parameters:

ParameterTypeRequiredDescription
user_idstringNoUser ID (required for system API keys)

Response:

{
"required": true,
"enabled": false,
"reason": "Your role requires two-factor authentication"
}
FieldTypeDescription
requiredbooleanWhether 2FA is required
enabledbooleanWhether 2FA is enabled
reasonstringExplanation of why 2FA is required

Setup 2FA

Initiate 2FA setup. Returns a TOTP secret and QR code for authenticator apps.

POST /v1/users/me/2fa/setup
Authorization: Bearer {token}
Content-Type: application/json

Request Body:

{
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"platform_id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
}
FieldTypeRequiredDescription
user_idstringNoUser ID (required for system API keys)
platform_idstringNoPlatform to use for 2FA (defaults to email)

Response:

{
"secret": "JBSWY3DPEHPK3PXP",
"qr_code_url": "otpauth://totp/Heimdall:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Heimdall",
"qr_code_image": "data:image/png;base64,iVBORw0KGgo...",
"expires_at": "2025-01-25T10:10:00Z",
"platform_slug": "email"
}
FieldTypeDescription
secretstringTOTP secret for manual entry
qr_code_urlstringOTPAuth URL for QR code
qr_code_imagestringBase64-encoded QR code PNG image
expires_atstringWhen the setup expires (10 minutes)
platform_slugstringPlatform used for 2FA
Setup Expiration

The setup expires after 10 minutes. You must verify the code before then, or initiate setup again.

Verify 2FA

Verify a TOTP code to complete 2FA setup.

POST /v1/users/me/2fa/verify
Authorization: Bearer {token}
Content-Type: application/json

Request Body:

{
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"code": "123456"
}
FieldTypeRequiredDescription
user_idstringNoUser ID (required for system API keys)
codestringYes6-digit TOTP code from authenticator app

Response:

{
"success": true,
"backup_codes": [
"ABCD-EFGH-IJKL",
"MNOP-QRST-UVWX",
"1234-5678-9012",
"3456-7890-1234",
"5678-9012-3456",
"7890-1234-5678",
"9012-3456-7890",
"1234-5678-9012"
],
"message": "Two-factor authentication enabled successfully"
}
FieldTypeDescription
successbooleanWhether verification succeeded
backup_codesstring[]One-time backup codes (shown once!)
messagestringSuccess message
Store Backup Codes Securely

Backup codes are only shown once. Store them in a secure location - they cannot be retrieved later.

Disable 2FA

Disable two-factor authentication.

DELETE /v1/users/me/2fa
Authorization: Bearer {token}
Content-Type: application/json

Request Body:

{
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"code": "123456"
}
FieldTypeRequiredDescription
user_idstringNoUser ID (required for system API keys)
codestringYes6-digit TOTP code or backup code

Response:

{
"success": true,
"message": "Two-factor authentication disabled"
}

Regenerate Backup Codes

Generate new backup codes (invalidates all previous codes).

POST /v1/users/me/2fa/backup-codes/regenerate
Authorization: Bearer {token}
Content-Type: application/json

Request Body:

{
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"code": "123456"
}
FieldTypeRequiredDescription
user_idstringNoUser ID (required for system API keys)
codestringYes6-digit TOTP code to confirm regeneration

Response:

{
"success": true,
"backup_codes": [
"ABCD-EFGH-IJKL",
"MNOP-QRST-UVWX",
"1234-5678-9012",
"3456-7890-1234",
"5678-9012-3456",
"7890-1234-5678",
"9012-3456-7890",
"1234-5678-9012"
],
"message": "Backup codes regenerated successfully"
}
Previous Codes Invalidated

Regenerating backup codes invalidates all previous backup codes immediately. Make sure to store the new codes securely.

Permissions

ActionSelfOther Users
Get status✅ No special permissiontwo_factor:manage
Setup✅ No special permissiontwo_factor:manage
Verify✅ No special permissiontwo_factor:manage
Disable✅ No special permissiontwo_factor:manage
Regenerate codes✅ No special permissiontwo_factor:manage

Users can always manage their own 2FA without special permissions. Managing other users' 2FA requires the two_factor:manage permission or super admin role.

Sessions (Internal)

Session management endpoints for NextAuth integration. These endpoints require a system API key for authentication.

Internal Use Only

These endpoints are used internally by NextAuth for session management. They are not intended for direct client use.

Create Session

Create a new database session for a user.

POST /v1/sessions
Authorization: Bearer {SYSTEM_API_KEY}
Content-Type: application/json

Request Body:

{
"userId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"expires": "2025-02-25T10:00:00Z"
}

Response (201 Created):

{
"id": "sess-uuid",
"sessionToken": "sess_abc123...",
"userId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"expires": "2025-02-25T10:00:00Z",
"created_at": "2025-01-25T10:00:00Z"
}

Get Session

Retrieve a session by token.

GET /v1/sessions/{token}
Authorization: Bearer {SYSTEM_API_KEY}

Response:

{
"id": "sess-uuid",
"sessionToken": "sess_abc123...",
"userId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"expires": "2025-02-25T10:00:00Z",
"created_at": "2025-01-25T10:00:00Z"
}

Delete Session

Delete a session (sign out).

DELETE /v1/sessions/{token}?reason=user_initiated
Authorization: Bearer {SYSTEM_API_KEY}

Query Parameters:

ParameterTypeRequiredDescription
reasonstringNoReason for logout. Defaults to user_initiated.

Logout Reasons:

ReasonWhen Used
user_initiatedUser clicked sign out (default)
token_deletedAdmin deleted the session
session_expiredSession timed out
revokedSession was forcefully revoked
no_accessUser lost access (banned, deleted)

Response (204 No Content): Empty response on success

Audit Event: Logs a logout event with metadata:

{
"reason": "user_initiated",
"session_id": "abc123...",
"app": "id"
}

The app field is populated from the X-Source-Service header if provided.

Session Flow with NextAuth

  1. Login: User authenticates via credentials or OAuth
  2. Create: NextAuth calls POST /v1/sessions to create database session
  3. Use: Session token stored in session.accessToken for WebSocket auth
  4. Logout: NextAuth calls DELETE /v1/sessions/{token}?reason=user_initiated on sign out
  5. Cleanup: Sessions are also deleted when user account is deleted

Platforms

List All Platforms

Get all authentication platforms with their status.

GET /v1/platforms

Authentication: None required

Response:

{
"data": [
{
"id": "platform-twitch",
"name": "Twitch",
"slug": "twitch",
"is_oauth": true,
"enabled": true,
"two_factor_supported": true,
"two_factor_required": false,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-20T15:30:00Z"
}
]
}

List Enabled Platforms

Get only enabled platforms (for login/registration UI).

GET /v1/platforms/enabled

Authentication: None required

Get Platform by ID or Slug

Get a specific platform.

GET /v1/platforms/{id_or_slug}

Authentication: None required

Update Platform Enabled Status

Enable or disable a platform.

PATCH /v1/platforms/{id_or_slug}/enabled
Content-Type: application/json

Authentication: Required Permission: settings:write

Request Body:

{
"enabled": true
}

Response:

{
"data": {
"id": "platform-discord",
"name": "Discord",
"slug": "discord",
"enabled": true
}
}
warning

Disabling a platform prevents new logins/registrations via that provider. Existing users can still access their accounts via other linked platforms.

User Bans

Get User Ban Status

Check if a user is currently banned.

GET /v1/users/{id}/ban

Authentication: Required Permission: users:read (or self)

Response:

{
"is_banned": true,
"active_ban": {
"id": "01eebc99-9c0b-4ef8-bb6d-6bb9bd380a17",
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"reason": "Violation of terms of service",
"banned_by": "51eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
"banned_at": "2025-01-25T10:00:00Z",
"expires_at": "2025-02-01T10:00:00Z",
"is_permanent": false
}
}

Ban User

Ban a user from the platform.

POST /v1/users/{id}/ban
Content-Type: application/json

Authentication: Required Permission: users:ban

Request Body:

{
"reason": "Violation of terms of service",
"duration_seconds": 604800
}
FieldTypeRequiredDescription
reasonstringNoReason for the ban
duration_secondsintegerNoBan duration in seconds. null for permanent ban.

Response:

{
"data": {
"id": "01eebc99-9c0b-4ef8-bb6d-6bb9bd380a17",
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"reason": "Violation of terms of service",
"banned_by": "51eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
"banned_at": "2025-01-25T10:00:00Z",
"expires_at": "2025-02-01T10:00:00Z",
"is_permanent": false
}
}

Unban User

Remove a ban from a user.

DELETE /v1/users/{id}/ban

Authentication: Required Permission: users:ban

Response (200 OK):

{
"success": true,
"message": "User has been unbanned"
}

Get User Ban History

Get the complete ban history for a user.

GET /v1/users/{id}/bans

Authentication: Required Permission: users:read

Response:

{
"data": [
{
"id": "01eebc99-9c0b-4ef8-bb6d-6bb9bd380a17",
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"reason": "Spam",
"banned_by": "51eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
"banned_at": "2025-01-20T10:00:00Z",
"expires_at": "2025-01-21T10:00:00Z",
"is_permanent": false,
"unbanned_at": "2025-01-20T15:00:00Z",
"unbanned_by": "61eebc99-9c0b-4ef8-bb6d-6bb9bd380a23"
}
]
}

Admin User Deletion

Get User Deletion Status

Get deletion status for a user (admin view with additional fields).

GET /v1/admin/users/{id}/deletion-status

Authentication: Required Permission: users:read

Response:

{
"is_scheduled_for_deletion": true,
"scheduled_deletion_at": "2025-02-01T10:00:00Z",
"is_deleted": false,
"deleted_at": null
}

Force Delete User

Immediately delete a user (skip 7-day grace period).

POST /v1/admin/users/{id}/force-delete
Content-Type: application/json

Authentication: Required Permission: users:delete

Request Body:

{
"reason": "GDPR urgent request"
}

Response:

{
"success": true,
"message": "User has been permanently deleted"
}
Use with Caution

Force deletion is immediate and irreversible. Use only for:

  • GDPR urgent requests
  • Compromised accounts
  • Legal requirements

Cancel User Deletion

Cancel scheduled deletion for a user.

POST /v1/admin/users/{id}/cancel-deletion

Authentication: Required Permission: users:write

Response:

{
"success": true,
"message": "Account deletion has been cancelled"
}

OAuth Clients

List My OAuth Clients

Get OAuth clients owned by the current user.

GET /v1/oauth/clients

Authentication: Required

Response:

{
"data": [
{
"id": "11eebc99-9c0b-4ef8-bb6d-6bb9bd380a18",
"name": "My App",
"description": "My application",
"logo_url": "https://example.com/logo.png",
"homepage_url": "https://example.com",
"redirect_uris": ["https://example.com/callback"],
"allowed_scopes": ["openid", "profile", "email"],
"client_type": "confidential",
"is_active": true,
"is_first_party": false,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-20T15:30:00Z"
}
]
}

List All OAuth Clients (Admin)

List all OAuth clients in the system.

GET /v1/oauth/clients/all

Authentication: Required Permission: Admin

Get OAuth Client

Get a specific OAuth client by ID.

GET /v1/oauth/clients/{id}

Authentication: Required (must be owner or admin)

Create OAuth Client

Create a new OAuth client application.

POST /v1/oauth/clients
Content-Type: application/json

Authentication: Required

Request Body:

{
"name": "My App",
"description": "My awesome application",
"logo_url": "https://example.com/logo.png",
"homepage_url": "https://example.com",
"privacy_policy_url": "https://example.com/privacy",
"terms_of_service_url": "https://example.com/terms",
"redirect_uris": ["https://example.com/callback"],
"allowed_scopes": ["openid", "profile", "email"],
"client_type": "confidential",
"is_first_party": false
}

Response (201 Created):

{
"client": {
"id": "11eebc99-9c0b-4ef8-bb6d-6bb9bd380a18",
"name": "My App",
"client_type": "confidential",
"is_active": true
},
"client_secret": "hm_secret_abc123..."
}
warning

The client_secret is only returned once at creation time. Store it securely!

Update OAuth Client

Update an existing OAuth client.

PATCH /v1/oauth/clients/{id}
Content-Type: application/json

Authentication: Required (must be owner or admin)

Request Body: Same fields as create, all optional.

Regenerate Client Secret

Regenerate the client secret for a confidential client.

POST /v1/oauth/clients/{id}/regenerate-secret

Authentication: Required (must be owner or admin)

Response:

{
"client_id": "11eebc99-9c0b-4ef8-bb6d-6bb9bd380a18",
"client_secret": "hm_secret_new123...",
"message": "Secret regenerated successfully"
}
Important

The new client secret is only shown once in this response. Store it securely before closing this page.

Delete OAuth Client

Delete an OAuth client.

DELETE /v1/oauth/clients/{id}

Authentication: Required (must be owner or admin)

Response (204 No Content): Empty response on success

API Keys

List API Keys

Get all API keys (without the actual key value).

GET /v1/api-keys

Authentication: Required Permission: api_keys:read

Response:

{
"data": [
{
"id": "21eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
"key_prefix": "hm_abc12345...",
"name": "Production API Key",
"description": "Used for production services",
"scopes": ["gps:read", "gps:write"],
"is_active": true,
"is_system": false,
"expires_at": "2026-01-01T00:00:00Z",
"last_used_at": "2025-01-25T10:00:00Z",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-20T15:30:00Z"
}
]
}

Get API Key

Get a specific API key by ID.

GET /v1/api-keys/{id}

Authentication: Required Permission: api_keys:read

Create API Key

Create a new API key.

POST /v1/api-keys
Content-Type: application/json

Authentication: Required Permission: api_keys:write

Scope Validation

Assigned scopes must be a subset of your own permissions. Attempting to assign a scope you don't have returns 403 Forbidden. Super admins can assign any scope.

Request Body:

{
"name": "My API Key",
"description": "For my application",
"scopes": ["gps:read"],
"expires_at": "2026-01-01T00:00:00Z"
}

Response (201 Created):

{
"id": "21eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
"key": "hm_abc123def456...",
"name": "My API Key",
"scopes": ["gps:read"],
"expires_at": "2026-01-01T00:00:00Z"
}
warning

The full API key is only returned once at creation time. Store it securely!

Update API Key

Update an existing API key. Scope validation applies — you cannot escalate beyond your own permissions.

PATCH /v1/api-keys/{id}
Content-Type: application/json

Authentication: Required Permission: api_keys:write

Regenerate API Key

Regenerate the secret key for an existing API key. The old key is invalidated immediately.

POST /v1/api-keys/{id}/regenerate

Authentication: Required Permission: api_keys:write

Response:

{
"id": "21eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
"key": "hm_newkey123def456...",
"key_prefix": "hm_newk...",
"name": "Production API Key",
"scopes": ["gps:read", "gps:write"],
"is_active": true,
"expires_at": "2026-01-01T00:00:00Z"
}
warning

The new API key is only shown once in this response. Store it securely! System keys can only be regenerated by super admins.

Delete API Key

Delete an API key.

DELETE /v1/api-keys/{id}

Authentication: Required Permission: api_keys:delete

Response (204 No Content): Empty response on success

warning

System API keys cannot be deleted via the API.

Get API Key Roles

Get roles assigned to an API key.

GET /v1/api-keys/{id}/roles

Authentication: Required Permission: api_keys:read

Assign Role to API Key

Assign a role to an API key. You can only assign roles that you yourself hold. Super admins can assign any role.

POST /v1/api-keys/{id}/roles
Content-Type: application/json

Authentication: Required Permission: api_keys:write

Request Body:

{
"role_id": "41eebc99-9c0b-4ef8-bb6d-6bb9bd380a21"
}

Remove Role from API Key

Remove a role from an API key.

DELETE /v1/api-keys/{id}/roles/{role_id}

Authentication: Required Permission: api_keys:write

Roles

List Roles

Get all roles.

GET /v1/roles

Authentication: Required Permission: roles:read

Response:

{
"data": [
{
"id": "role_admin",
"name": "Admin",
"description": "Full administrative access",
"is_system": true,
"requires_two_factor": true,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}

Get Role

Get a specific role by ID.

GET /v1/roles/{id}

Authentication: Required Permission: roles:read

Create Role

Create a new role.

POST /v1/roles
Content-Type: application/json

Authentication: Required Permission: roles:write

Request Body:

{
"name": "Custom Role",
"description": "A custom role",
"requires_two_factor": false
}

Update Role

Update an existing role.

PATCH /v1/roles/{id}
Content-Type: application/json

Authentication: Required Permission: roles:write

Delete Role

Delete a role.

DELETE /v1/roles/{id}

Authentication: Required Permission: roles:delete

warning

System roles cannot be deleted.

Get Role Permissions

Get permissions assigned to a role.

GET /v1/roles/{id}/permissions

Authentication: Required Permission: roles:read

Assign Permission to Role

Assign a permission to a role.

POST /v1/roles/{id}/permissions
Content-Type: application/json

Authentication: Required Permission: roles:write

Request Body:

{
"permission_id": "31eebc99-9c0b-4ef8-bb6d-6bb9bd380a20"
}

Remove Permission from Role

Remove a permission from a role.

DELETE /v1/roles/{id}/permissions/{permission_id}

Authentication: Required Permission: roles:write

User Data Export (GDPR/DSGVO)

Export User Data

Download a ZIP archive containing all personal data for the authenticated user. This endpoint complies with GDPR/DSGVO Article 15 (Right of Access) and Article 20 (Right to Data Portability).

GET /v1/user/export

Authentication: Required (Session or API Key)

Response: ZIP file download (application/zip)

Headers:

  • Content-Type: application/zip
  • Content-Disposition: attachment; filename="heimdall-data-export-{date}.zip"

ZIP Archive Contents:

FileDescription
data.jsonMachine-readable export of all user data
report.pdfHuman-readable PDF summary report
README.txtExplanation of the export files

Data Included in Export:

  • User Profile: ID, username, privacy mode, avatars enabled, account creation date, last login, preferred locale
  • Platform Accounts: Connected platforms (Twitch, Discord, GitHub, YouTube, Email), usernames, emails, avatars
  • Connected Apps: OAuth applications the user has authorized, with scopes and consent dates
  • Owned Apps: OAuth applications created by the user (if developer)
  • API Keys: Key names, prefixes (not full keys), scopes, creation/usage dates
  • Roles: Assigned role names
  • Two-Factor Auth: 2FA enabled status, setup date, remaining backup codes count
  • Deletion Status: Scheduled deletion date (if applicable)
  • Export History: Previous data export timestamps
  • Active Sessions: All active sessions with device info, IP addresses, authentication provider used, and activity timestamps

Example Response Headers:

HTTP/1.1 200 OK
Content-Type: application/zip
Content-Disposition: attachment; filename="heimdall-data-export-2025-01-03.zip"
Content-Length: 45678

data.json Structure:

{
"exportInfo": {
"exportedAt": "2025-01-03T12:00:00Z",
"format": "GDPR/DSGVO Data Export",
"version": "1.0"
},
"user": {
"id": "user_xxx",
"username": "johndoe",
"privacyMode": false,
"avatarsEnabled": true,
"createdAt": "2024-01-01T00:00:00Z",
"lastLoginAt": "2025-01-03T10:00:00Z"
},
"platformAccounts": [
{
"platform": "Twitch",
"username": "johndoe",
"email": "john@example.com",
"isPrimary": true,
"connectedAt": "2024-01-01T00:00:00Z"
}
],
"connectedApps": [
{
"name": "Third Party App",
"scopes": ["openid", "profile"],
"isFirstParty": false,
"consentedAt": "2024-06-15T10:00:00Z"
}
],
"ownedApps": [],
"apiKeys": [],
"roles": ["user"],
"twoFactorAuth": {
"enabled": true,
"enabledAt": "2024-03-01T12:00:00Z",
"backupCodesRemaining": 8
},
"deletionStatus": null,
"exportLogs": [
{
"exportedAt": "2024-12-01T10:00:00Z",
"format": "zip"
}
],
"sessions": [
{
"id": "sess_xxx",
"ipAddress": "192.168.1.xxx",
"deviceType": "desktop",
"browserName": "Chrome",
"osName": "Windows",
"provider": "twitch",
"providerName": "Twitch",
"createdAt": "2025-01-01T10:00:00Z",
"expiresAt": "2025-02-01T10:00:00Z",
"lastActivityAt": "2025-01-03T09:00:00Z"
}
]
}

Error Responses:

CodeDescription
401Unauthorized - Authentication required
500Internal Server Error - Export generation failed
GDPR Compliance

This endpoint is designed to comply with:

  • Article 15: Right of Access - Users can obtain a copy of their personal data
  • Article 20: Right to Data Portability - Data is provided in a structured, machine-readable format (JSON)

The export includes all personal data stored in our system. Data is provided in both human-readable (PDF) and machine-readable (JSON) formats.

Permissions

List Permissions

Get all available permissions.

GET /v1/permissions

Authentication: Required Permission: permissions:read

Response:

{
"data": [
{
"id": "31eebc99-9c0b-4ef8-bb6d-6bb9bd380a20",
"resource": "users",
"action": "read",
"description": "Read user information",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}

Get Permission

Get a specific permission by ID.

GET /v1/permissions/{id}

Authentication: Required Permission: permissions:read

Create Permission

Create a new permission.

POST /v1/permissions
Content-Type: application/json

Authentication: Required Permission: permissions:write

Request Body:

{
"resource": "custom",
"action": "read",
"description": "Read custom resources"
}

Discord Bot Management

Endpoints for managing Discord guilds and channels synced from the Discord bot.

List Discord Guilds

Get all Discord guilds synced from the bot.

GET /v1/discord/guilds

Authentication: Required Permission: discord:read

Response:

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"guild_id": "123456789012345678",
"name": "My Server",
"icon": "a_1234567890abcdef",
"icon_url": "https://cdn.discordapp.com/icons/123456789012345678/a_1234567890abcdef.png",
"owner_id": "987654321098765432",
"member_count": 1500,
"boost_count": 14,
"premium_tier": 2,
"bot_joined_at": "2025-01-15T10:00:00Z",
"last_synced_at": "2025-01-25T14:30:00Z"
}
]
}

Guild Fields:

FieldTypeDescription
idstringInternal UUID primary key
guild_idstringDiscord guild ID (snowflake)
namestringGuild name
iconstringGuild icon hash
icon_urlstringFull URL to guild icon
owner_idstringDiscord user ID of guild owner
member_countintegerApproximate member count
boost_countintegerNumber of server boosts
premium_tierintegerPremium tier (0=none, 1=tier1, 2=tier2, 3=tier3)
bot_joined_atdatetimeWhen the bot joined this guild
last_synced_atdatetimeLast sync timestamp

Get Discord Guild

Get a specific Discord guild by its Discord guild ID.

GET /v1/discord/guilds/{guild_id}

Authentication: Required Permission: discord:read

Path Parameters:

ParameterTypeDescription
guild_idstringDiscord guild ID (snowflake)

Response:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"guild_id": "123456789012345678",
"name": "My Server",
"icon": "a_1234567890abcdef",
"icon_url": "https://cdn.discordapp.com/icons/123456789012345678/a_1234567890abcdef.png",
"owner_id": "987654321098765432",
"member_count": 1500,
"boost_count": 14,
"premium_tier": 2,
"bot_joined_at": "2025-01-15T10:00:00Z",
"last_synced_at": "2025-01-25T14:30:00Z"
}

List Discord Channels

Get all channels for a specific guild.

GET /v1/discord/guilds/{guild_id}/channels

Authentication: Required Permission: discord:read

Path Parameters:

ParameterTypeDescription
guild_idstringDiscord guild ID (snowflake)

Response:

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"channel_id": "111222333444555666",
"guild_id": "123456789012345678",
"name": "general",
"channel_type": "Text",
"channel_type_raw": 0,
"parent_id": "999888777666555444",
"position": 0,
"topic": "General chat",
"nsfw": false,
"is_text_based": true,
"is_voice": false
}
]
}

Channel Fields:

FieldTypeDescription
idstringInternal UUID primary key
channel_idstringDiscord channel ID (snowflake)
guild_idstringDiscord guild ID (snowflake)
namestringChannel name
channel_typestringChannel type name (Text, Voice, etc.)
channel_type_rawintegerRaw channel type value
parent_idstringParent category channel ID (Discord snowflake)
positionintegerPosition in channel list
topicstringChannel topic/description
nsfwbooleanWhether channel is NSFW
is_text_basedbooleanWhether channel supports text messages
is_voicebooleanWhether channel is a voice channel

Channel Types:

TypeNameDescription
0TextText channel
2VoiceVoice channel
4CategoryChannel category
5AnnouncementAnnouncement channel
10Announcement ThreadThread in announcement channel
11Public ThreadPublic thread
12Private ThreadPrivate thread
13StageStage channel
15ForumForum channel
16MediaMedia channel

Get Discord Guild with Channels

Get a guild with all its channels in a single request.

GET /v1/discord/guilds/{guild_id}/full

Authentication: Required Permission: discord:read

Response:

{
"guild": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"guild_id": "123456789012345678",
"name": "My Server",
"icon": "a_1234567890abcdef",
"icon_url": "https://cdn.discordapp.com/icons/123456789012345678/a_1234567890abcdef.png",
"owner_id": "987654321098765432",
"member_count": 1500,
"boost_count": 14,
"premium_tier": 2,
"bot_joined_at": "2025-01-15T10:00:00Z",
"last_synced_at": "2025-01-25T14:30:00Z"
},
"channels": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"channel_id": "111222333444555666",
"guild_id": "123456789012345678",
"name": "general",
"channel_type": "Text",
"channel_type_raw": 0,
"parent_id": "999888777666555444",
"position": 0,
"topic": "General chat",
"nsfw": false,
"is_text_based": true,
"is_voice": false
}
]
}

Sync Discord Guilds

Trigger a sync of all guilds and channels from the Discord bot. This sends a request to the connected Discord bot via WebSocket and waits for the response.

POST /v1/discord/sync

Authentication: Required Permission: discord:sync

Request Body (optional):

{
"guild_id": "123456789012345678"
}
FieldTypeRequiredDescription
guild_idstringNoSpecific guild ID to sync. If omitted, syncs all guilds.

Response:

{
"success": true,
"error": null,
"guilds_synced": 5,
"channels_synced": 47,
"roles_synced": 38
}

Response Fields:

FieldTypeDescription
successbooleanWhether the sync was successful
errorstringError message if failed
guilds_syncedintegerNumber of guilds synced
channels_syncedintegerNumber of channels synced
roles_syncedintegerNumber of roles synced
Timeout

The sync operation has a 30-second timeout. If the Discord bot doesn't respond within this time, the request will fail. Ensure the Discord bot is connected and running.

Delete Discord Guild

Remove a Discord guild from the database. This does not remove the bot from the Discord server - the guild data will be re-synced on the next sync operation if the bot is still in the server.

DELETE /v1/discord/guilds/{guild_id}

Authentication: Required Permission: discord:delete

Path Parameters:

ParameterTypeDescription
guild_idstringInternal guild UUID (database ID)

Response (204 No Content): Empty response on success

Get Discord Bot Settings

Get global Discord bot settings.

GET /v1/discord/settings

Authentication: Required Permission: discord:read

Response:

{
"id": "00000000-0000-0000-0000-000000000001",
"auto_sync_enabled": true,
"sync_interval_seconds": 86400,
"initial_delay_seconds": 300,
"default_language": "en",
"notify_on_guild_join": true,
"notify_on_guild_leave": true,
"command_cooldown_seconds": 3,
"updated_at": "2025-01-15T10:30:00Z"
}

Update Discord Bot Settings

Update global Discord bot settings.

PUT /v1/discord/settings

Authentication: Required Permission: discord:edit

Request Body:

{
"auto_sync_enabled": true,
"sync_interval_seconds": 86400,
"initial_delay_seconds": 300,
"default_language": "en",
"notify_on_guild_join": true,
"notify_on_guild_leave": true,
"command_cooldown_seconds": 3
}

All fields are optional. Only include fields you want to update.

Response:

{
"success": true
}

Get Guild Settings

Get per-guild settings for a specific Discord guild.

GET /v1/discord/guilds/{guild_id}/settings

Authentication: Required Permission: discord:read

Path Parameters:

ParameterTypeDescription
guild_idstringDiscord guild snowflake ID

Response:

{
"guild_id": "123456789012345678",
"default_language": "de"
}

The default_language field is null if the guild uses the global default.

Update Guild Settings

Update per-guild settings for a specific Discord guild.

PUT /v1/discord/guilds/{guild_id}/settings

Authentication: Required Permission: discord:edit

Path Parameters:

ParameterTypeDescription
guild_idstringDiscord guild snowflake ID

Request Body:

{
"default_language": "de"
}

Set default_language to null to use the global default.

Response:

{
"success": true
}

List Discord Members

Get all synced members for a specific Discord guild.

GET /v1/discord/guilds/{guild_id}/members

Authentication: Required Permission: discord:read

Path Parameters:

ParameterTypeDescription
guild_idstringInternal guild UUID (database ID)

Query Parameters:

ParameterTypeDefaultDescription
searchstring-Search by username, global_name, or nickname
bot_onlybooleanfalseFilter to only show bots
humans_onlybooleanfalseFilter to only show humans (non-bots)
has_rolestring-Filter by role ID
limitinteger50Items per page (max: 100)
offsetinteger0Offset for pagination

Response:

{
"members": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"guild_id": "550e8400-e29b-41d4-a716-446655440001",
"user_id": "123456789012345678",
"username": "johndoe",
"discriminator": "0",
"global_name": "John Doe",
"nickname": "Johnny",
"avatar": "a_1234567890abcdef",
"avatar_url": "https://cdn.discordapp.com/avatars/123456789012345678/a_1234567890abcdef.png",
"bot": false,
"system": false,
"joined_at": "2024-01-15T10:00:00Z",
"premium_since": "2024-06-01T12:00:00Z",
"deaf": false,
"mute": false,
"pending": false,
"communication_disabled_until": null,
"roles": ["111222333444555666", "222333444555666777"],
"display_name": "Johnny",
"formatted_username": "@johndoe",
"is_timed_out": false
}
],
"total_count": 1500,
"has_more": true
}

Member Fields:

FieldTypeDescription
idstringInternal UUID primary key
guild_idstringInternal guild UUID
user_idstringDiscord user ID (snowflake)
usernamestringDiscord username
discriminatorstringLegacy discriminator (may be "0" for new usernames)
global_namestringGlobal display name
nicknamestringServer-specific nickname
avatarstringAvatar hash
avatar_urlstringFull URL to avatar image
botbooleanWhether this is a bot account
systembooleanWhether this is a system account
joined_atdatetimeWhen the member joined the guild
premium_sincedatetimeWhen the member started boosting
deafbooleanWhether deafened in voice
mutebooleanWhether muted in voice
pendingbooleanWhether passed membership screening
communication_disabled_untildatetimeTimeout expiry timestamp
rolesarrayRole IDs the member has
display_namestringComputed display name (nickname > global_name > username)
formatted_usernamestringFormatted username (username#discriminator or @username)
is_timed_outbooleanWhether member is currently timed out

Get Discord Member

Get a specific member from a Discord guild by user ID.

GET /v1/discord/guilds/{guild_id}/members/{user_id}

Authentication: Required Permission: discord:read

Path Parameters:

ParameterTypeDescription
guild_idstringInternal guild UUID (database ID)
user_idstringDiscord user ID (snowflake)

Response:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"guild_id": "550e8400-e29b-41d4-a716-446655440001",
"user_id": "123456789012345678",
"username": "johndoe",
"discriminator": "0",
"global_name": "John Doe",
"nickname": "Johnny",
"avatar": "a_1234567890abcdef",
"avatar_url": "https://cdn.discordapp.com/avatars/123456789012345678/a_1234567890abcdef.png",
"bot": false,
"system": false,
"joined_at": "2024-01-15T10:00:00Z",
"premium_since": "2024-06-01T12:00:00Z",
"deaf": false,
"mute": false,
"pending": false,
"communication_disabled_until": null,
"roles": ["111222333444555666", "222333444555666777"],
"display_name": "Johnny",
"formatted_username": "@johndoe",
"is_timed_out": false
}

Sync Discord Members

Trigger a sync of all members for a specific guild from the Discord bot. This sends a request to the connected Discord bot via WebSocket and waits for the response.

POST /v1/discord/guilds/{guild_id}/members/sync

Authentication: Required Permission: discord:guild.sync

Path Parameters:

ParameterTypeDescription
guild_idstringDiscord guild ID (snowflake)

Response:

{
"success": true,
"error": null,
"members_synced": 1500
}
Timeout

The member sync operation has a 60-second timeout. For large guilds with many members, this may take longer. Ensure the Discord bot is connected and running.

Activity Log (Audit Events)

Query and manage audit logs for account activity tracking and security.

Get My Activity Log

Get the activity log for the authenticated user.

Endpoint: GET /v1/users/me/audit

Authentication: Required (Bearer token)

Query Parameters:

ParameterTypeRequiredDescription
pageIntegerNoPage number (default: 1)
limitIntegerNoItems per page (default: 20, max: 100)
event_typeStringNoFilter by event type
resource_typeStringNoFilter by resource type
statusStringNoFilter by status (success, failure)

Response:

{
"events": [
{
"id": "81eebc99-9c0b-4ef8-bb6d-6bb9bd380a25",
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"event_type": "login",
"resource_type": "session",
"resource_id": "e0eebc99-9c0b-4ef8-bb6d-6bb9bd380a15",
"actor_id": null,
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"description": "Signed in via Twitch",
"metadata": { "provider": "twitch" },
"status": "success",
"error_message": null,
"country_code": "DE",
"country_name": "Germany",
"city": "Berlin",
"region": "Berlin",
"source_service": "id",
"created_at": "2025-01-05T10:30:00Z",
"is_reported": false
}
],
"total_count": 150,
"page": 1,
"limit": 20,
"total_pages": 8,
"_links": {
"self": "/v1/users/me/audit?page=1&limit=20"
}
}

Event Fields:

FieldTypeDescription
idStringUnique audit event ID
user_idStringUser ID associated with the event
event_typeStringType of event (see event types table below)
resource_typeStringType of resource affected
resource_idStringID of the affected resource
actor_idStringID of actor if different from user
ip_addressStringIP address of the request
user_agentStringUser agent string
descriptionStringHuman-readable description
metadataObjectAdditional event-specific data (JSON)
statusStringEvent status (success or failure)
error_messageStringError message if status is failure
country_codeStringISO 3166-1 alpha-2 country code
country_nameStringFull country name
cityStringCity name (from GeoIP)
regionStringRegion/state name (from GeoIP)
source_serviceStringService that created the event
created_atStringISO 8601 timestamp
is_reportedBooleanWhether user reported this event

Source Services:

ValueDescription
apiDirect API calls
idHeimdall ID webapp
backendBackend dashboard
policiesPolicies webapp
discord_botDiscord bot
twitch_botTwitch bot

Event Types:

Event TypeDescription
loginSuccessful sign-in
login_failedFailed sign-in attempt
logoutUser signed out
session_createdNew session created
session_revokedSession revoked
2fa_enabledTwo-factor authentication enabled
2fa_disabledTwo-factor authentication disabled
2fa_verifiedTwo-factor code verified
2fa_backup_codes_regeneratedBackup codes regenerated
password_changedPassword was changed
password_reset_requestedPassword reset requested
user_createdAccount created
user_updatedAccount details updated
user_deletedAccount deleted
account_linkedPlatform account linked
account_unlinkedPlatform account unlinked
account_reconnectedPlatform account reconnected
data_exportedUser data exported
client_createdOAuth client application created
client_updatedOAuth client application updated
client_secret_regeneratedOAuth client secret regenerated
client_deletedOAuth client application deleted
consent_grantedOAuth consent granted
consent_revokedOAuth consent revoked
api_key_createdAPI key created
api_key_revokedAPI key revoked
bot_command_executedBot command executed (Discord/Twitch)
bot_guild_configuredBot guild/server settings configured

Report Suspicious Activity

Report an audit event as suspicious activity.

Endpoint: POST /v1/users/me/audit/{event_id}/report

Authentication: Required (Bearer token)

Request Body:

{
"reason": "not_me",
"description": "I didn't make this login"
}

Request Parameters:

ParameterTypeRequiredDescription
reasonStringYesReason for reporting (not_me, suspicious, unknown_device, unknown_location, other)
descriptionStringNoAdditional details

Response (201 Created):

{
"success": true,
"report": {
"id": "91eebc99-9c0b-4ef8-bb6d-6bb9bd380a26",
"audit_event_id": "81eebc99-9c0b-4ef8-bb6d-6bb9bd380a25",
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"reason": "not_me",
"description": "I didn't make this login",
"status": "pending",
"reviewed_by": null,
"reviewed_at": null,
"resolution_notes": null,
"created_at": "2025-01-05T12:00:00Z",
"updated_at": "2025-01-05T12:00:00Z"
},
"error": null
}

Get My Audit Reports

Get all audit reports submitted by the current user.

Endpoint: GET /v1/users/me/audit/reports

Authentication: Required (Bearer token)

Response:

[
{
"id": "91eebc99-9c0b-4ef8-bb6d-6bb9bd380a26",
"audit_event_id": "81eebc99-9c0b-4ef8-bb6d-6bb9bd380a25",
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"reason": "not_me",
"description": "I didn't make this login",
"status": "resolved",
"reviewed_by": "admin_123",
"reviewed_at": "2025-01-05T14:00:00Z",
"resolution_notes": "Confirmed unauthorized access. Password reset initiated.",
"created_at": "2025-01-05T12:00:00Z",
"updated_at": "2025-01-05T14:00:00Z"
}
]

Admin: Get All Audit Reports

Get all audit reports across the system.

Endpoint: GET /v1/admin/audit/reports

Authentication: Required (Bearer token) Permission: audit:read

Query Parameters:

ParameterTypeRequiredDescription
statusStringNoFilter by status (pending, reviewed, resolved, dismissed)

Response:

[
{
"id": "91eebc99-9c0b-4ef8-bb6d-6bb9bd380a26",
"audit_event_id": "81eebc99-9c0b-4ef8-bb6d-6bb9bd380a25",
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"reason": "not_me",
"description": "I didn't make this login",
"status": "pending",
"reviewed_by": null,
"reviewed_at": null,
"resolution_notes": null,
"created_at": "2025-01-05T12:00:00Z",
"updated_at": "2025-01-05T12:00:00Z"
}
]

Admin: Update Audit Report

Update the status of an audit report and send a notification email to the user.

Endpoint: PATCH /v1/admin/audit/reports/{report_id}

Authentication: Required (Bearer token) Permission: audit:write

Request Body:

{
"status": "resolved",
"resolution_notes": "Confirmed unauthorized access. Password reset initiated and all sessions revoked."
}

Request Parameters:

ParameterTypeRequiredDescription
statusStringNoNew status (pending, reviewed, resolved, dismissed)
resolution_notesStringNoNotes about the resolution

Response:

{
"id": "91eebc99-9c0b-4ef8-bb6d-6bb9bd380a26",
"audit_event_id": "81eebc99-9c0b-4ef8-bb6d-6bb9bd380a25",
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"reason": "not_me",
"description": "I didn't make this login",
"status": "resolved",
"reviewed_by": "admin_123",
"reviewed_at": "2025-01-05T14:00:00Z",
"resolution_notes": "Confirmed unauthorized access. Password reset initiated and all sessions revoked.",
"created_at": "2025-01-05T12:00:00Z",
"updated_at": "2025-01-05T14:00:00Z"
}

Platform Integrations

Endpoints for managing platform integrations (e.g., Twitch channel integration for bot features).

About Integrations

Platform integrations connect your Heimdall account to external platforms like Twitch, enabling features such as channel point rewards, bot commands, and stream statistics tracking.

List All Integrations

Get all integrations for the authenticated user.

GET /integrations
Authorization: Bearer {token}

Response:

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"platform_slug": "twitch",
"platform_name": "Twitch",
"platform_user_id": "12345678",
"username": "streamer_name",
"display_name": "Streamer Name",
"profile_image_url": "https://static-cdn.jtvnw.net/jtv_user_pictures/...",
"scopes": ["channel:read:subscriptions", "channel:manage:redemptions"],
"is_active": true,
"bot_status": "active",
"connected_at": "2025-01-15T10:00:00Z",
"last_refreshed_at": "2025-01-25T14:30:00Z"
}
]

List Platform Integrations

Get integrations for a specific platform.

GET /integrations/platform/{slug}
Authorization: Bearer {token}

Path Parameters:

ParameterTypeDescription
slugstringPlatform slug (e.g., twitch)

Response: Same format as List All Integrations.

Get Integration

Get a specific integration by ID.

GET /integrations/{id}
Authorization: Bearer {token}

Path Parameters:

ParameterTypeDescription
idstringIntegration UUID

Response: Single integration object.

Connect Integration

Initiate OAuth connection to a platform.

POST /integrations/platform/{slug}/connect
Authorization: Bearer {token}
Content-Type: application/json

Path Parameters:

ParameterTypeDescription
slugstringPlatform slug (e.g., twitch)

Request Body:

{
"redirect_uri": "https://your-app.com/integrations/callback",
"scopes": ["channel:read:subscriptions", "channel:manage:redemptions"]
}
FieldTypeRequiredDescription
redirect_uristringYesURL to redirect after OAuth
scopesstring[]NoRequested OAuth scopes

Response:

{
"authorization_url": "https://id.twitch.tv/oauth2/authorize?...",
"state": "abc123"
}

Redirect the user to authorization_url to complete the OAuth flow.

Disconnect Integration

Disconnect a platform integration.

DELETE /integrations/{id}
Authorization: Bearer {token}

Path Parameters:

ParameterTypeDescription
idstringIntegration UUID

Response:

{
"success": true,
"message": "Integration disconnected successfully"
}

Refresh Integration Token

Refresh the OAuth token for an integration.

POST /integrations/{id}/refresh
Authorization: Bearer {token}

Path Parameters:

ParameterTypeDescription
idstringIntegration UUID

Response:

{
"success": true,
"message": "Token refreshed successfully",
"expires_at": "2025-02-25T10:00:00Z"
}

Refresh Channel Stats

Refresh statistics for a streaming channel integration (e.g., Twitch).

POST /integrations/{id}/stats
Authorization: Bearer {token}

Path Parameters:

ParameterTypeDescription
idstringIntegration UUID

Response:

{
"success": true,
"stats": {
"followers": 15000,
"subscribers": 250,
"is_live": true,
"current_viewers": 1500,
"stream_title": "Playing some games!",
"game_name": "Minecraft",
"updated_at": "2025-01-25T15:00:00Z"
}
}

Update Twitch Bot Status

Update the bot status for a Twitch integration.

PATCH /integrations/{id}/twitch-status
Authorization: Bearer {token}
Content-Type: application/json

Path Parameters:

ParameterTypeDescription
idstringIntegration UUID

Request Body:

{
"bot_status": "active"
}
FieldTypeRequiredDescription
bot_statusstringYesStatus: active, paused, disabled

Response:

{
"success": true,
"bot_status": "active"
}

Get Platform Scopes

Get available OAuth scopes for a platform.

GET /integrations/platform/{slug}/scopes
Authorization: Bearer {token}

Path Parameters:

ParameterTypeDescription
slugstringPlatform slug (e.g., twitch)

Response:

{
"platform": "twitch",
"scopes": [
{
"name": "channel:read:subscriptions",
"description": "Read subscriber list",
"required": false
},
{
"name": "channel:manage:redemptions",
"description": "Manage channel point rewards",
"required": true
}
]
}

List Integration Platforms

Get all available integration platforms.

GET /integrations/platforms
Authorization: Bearer {token}

Response:

{
"platforms": [
{
"slug": "twitch",
"name": "Twitch",
"description": "Connect your Twitch channel",
"icon_url": "https://static.twitch.tv/assets/...",
"enabled": true,
"features": ["channel_stats", "bot_commands", "channel_points"]
}
]
}

OAuth Callback

OAuth callback endpoint (called by the platform after authorization).

GET /integrations/callback/{platform}

Path Parameters:

ParameterTypeDescription
platformstringPlatform slug (e.g., twitch)

Query Parameters:

ParameterTypeDescription
codestringAuthorization code from OAuth provider
statestringState parameter for CSRF protection

This endpoint is called automatically by the OAuth provider after user authorization. It exchanges the code for tokens and creates the integration.

All responses include hypermedia links for API navigation:

  • self: Link to the current resource
  • first: First page (pagination)
  • prev: Previous page (pagination)
  • next: Next page (pagination)
  • last: Last page (pagination)

Error Responses

All errors follow a consistent format:

{
"error": "NotFound",
"message": "GPS data not found"
}

Common HTTP status codes:

CodeDescription
200OK - Request succeeded
201Created - Resource created
400Bad Request - Invalid input
401Unauthorized - Authentication required
403Forbidden - Insufficient permissions
404Not Found - Resource doesn't exist
429Too Many Requests - Rate limit exceeded
500Internal Server Error - Server error
503Service Unavailable - Service is down

Next Steps