Skip to main content

GraphQL API

Heimdall provides a flexible GraphQL API for querying GPS data and system information.

GraphQL types, resolvers, and schema are implemented in the heimdall-graphql crate (crates/heimdall-graphql/). The schema is built with async-graphql.

Endpoints

The GraphQL API is available at multiple endpoints:

EndpointMethodAuth RequiredAvailabilityDescription
/v1/gqlPOST/GETYesAll environmentsGraphQL queries and mutations
/v1/graphiqlGETNoDevelopment onlyInteractive GraphiQL IDE
/v1/schemaGETNoAll environmentsGraphQL schema in SDL format

GraphiQL Interactive IDE

Interactive GraphiQL IDE is available in development environments only:

Development: http://localhost:3000/v1/graphiql
Development Only

GraphiQL is only accessible when APP_ENV is not set to production. In production environments, accessing this endpoint returns a 404 error. This is a security measure to prevent exposing the interactive IDE in production.

The GraphiQL IDE provides:

  • Schema documentation browser
  • Auto-completion for queries and fields
  • Query validation and syntax highlighting
  • Real-time query execution
  • Query history
  • Schema introspection

Note: To execute queries in GraphiQL, you'll need to add your Bearer token in the HTTP HEADERS section since all GraphQL queries require authentication.

Authentication

The /v1/gql endpoint requires Bearer token authentication for all queries and mutations. Include the Authorization header with your requests:

curl -X POST https://api.elcto.com/v1/gql \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ me { tokenId description role } }"}'

Authentication is required - Unlike some GraphQL APIs that allow unauthenticated access to certain queries, the Heimdall GraphQL endpoint requires authentication for all operations.

Queries

Get Current User Info

Retrieve information about the authenticated API token.

Authentication: Required

query {
me {
tokenId
description
role
isAdmin
}
}

Response:

{
"data": {
"me": {
"tokenId": "550e8400-e29b-41d4-a716-446655440000",
"description": "My API Token",
"role": "ADMIN",
"isAdmin": true
}
}
}

Get Current GPS Location

Retrieve the most recent GPS data point. Pass deviceId to get the latest point for a specific device.

query {
currentGps(deviceId: "my-tracker-1") {
id
deviceId
latitude
longitude
altitude
heading
timestamp
speedKmh
speedMps
speedMph
speedKnots
createdAt
}
}

The deviceId argument is optional. Omit it to get the globally latest point across all devices. Results are cached in Redis (TTL 5 min).

Response:

{
"data": {
"currentGps": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"deviceId": "my-tracker-1",
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 11.0,
"heading": 270.0,
"timestamp": "2025-11-24T10:00:00Z",
"speedKmh": 72.4,
"speedMps": 20.1,
"speedMph": 45.0,
"speedKnots": 39.1,
"createdAt": "2025-11-24T10:00:00Z"
}
}
}

Get Current GPS Per Device

Get the latest GPS point for each device. Returns one entry per device with a recorded position.

query {
currentGpsPerDevice {
deviceId
latitude
longitude
heading
speedKmh
timestamp
}
}

Response:

{
"data": {
"currentGpsPerDevice": [
{
"deviceId": "tracker-1",
"latitude": 51.5074,
"longitude": -0.1278,
"heading": 270.0,
"speedKmh": 72.4,
"timestamp": "2025-11-24T10:00:00Z"
},
{
"deviceId": "tracker-2",
"latitude": 49.4875,
"longitude": 8.4660,
"heading": 90.0,
"speedKmh": 45.0,
"timestamp": "2025-11-24T09:58:00Z"
}
]
}
}

List GPS Data

Retrieve a paginated list of GPS data points.

query {
gpsList(page: 1, limit: 10) {
data {
id
latitude
longitude
altitude
timestamp
speed
createdAt
}
pagination {
page
limit
total
totalPages
}
}
}

Response:

{
"data": {
"gpsList": {
"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,
"totalPages": 10
}
}
}
}

Get GPS by ID

Retrieve a specific GPS data point by ID.

query {
gpsById(id: "550e8400-e29b-41d4-a716-446655440000") {
id
latitude
longitude
altitude
timestamp
speed
createdAt
}
}

List Users

Retrieve a paginated list of all users.

Authentication: Required Permission: users:read

Parameters:

ParameterTypeDefaultDescription
pageInt1Page number
limitInt20Items per page
searchStringnullFilter by username or email
bannedOnlyBooleanfalseOnly return currently banned users
query {
users(page: 1, limit: 20, search: "john") {
users {
id
username
email
avatarUrl
platform
isBanned
createdAt
updatedAt
lastLoginAt
privacyMode
preferredLocale
avatarsEnabled
}
totalCount
page
limit
totalPages
hasNextPage
hasPreviousPage
}
}

Response:

{
"data": {
"users": {
"users": [
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"username": "johndoe",
"email": "john@example.com",
"avatarUrl": "https://cdn.example.com/avatar.png",
"platform": "twitch",
"isBanned": false,
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-01-20T15:30:00Z",
"lastLoginAt": "2025-01-25T09:00:00Z",
"privacyMode": false,
"preferredLocale": "en",
"avatarsEnabled": true
}
],
"totalCount": 150,
"page": 1,
"limit": 20,
"totalPages": 8,
"hasNextPage": true,
"hasPreviousPage": false
}
}
}

List Banned Users Only

query {
users(bannedOnly: true) {
users {
id
username
isBanned
}
totalCount
}
}

Get User by ID

Retrieve a specific user by their ID.

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

query {
user(id: "user-123") {
id
username
email
avatarUrl
platform
createdAt
updatedAt
lastLoginAt
privacyMode
avatarsEnabled
}
}

Get User Profile

Retrieve a user's profile with login provider information.

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

query {
userProfile(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
name
displayName
picture
provider
primaryProvider
lastLoginAt
createdAt
privacyMode
}
}

Get User Permissions

Retrieve a user's effective permissions.

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

query {
userPermissions(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8")
}

Response:

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

Get User Accounts (Platform Accounts)

Retrieve all platform accounts linked to a user.

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

query {
userAccounts(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
id
platformId
platformName
platformSlug
platformUserId
isOauth
isPrimary
username
email
avatarUrl
createdAt
updatedAt
}
}

Response:

{
"data": {
"userAccounts": [
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"platformId": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"platformName": "Twitch",
"platformSlug": "twitch",
"platformUserId": "12345678",
"isOauth": true,
"isPrimary": true,
"username": "johndoe",
"email": "john@example.com",
"avatarUrl": "https://cdn.example.com/avatar.png",
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-01-20T15:30:00Z"
}
]
}
}

Get User Ban Status

Check if a user is currently banned.

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

query {
userBanStatus(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
isBanned
activeBan {
id
reason
bannedAt
expiresAt
isPermanent
}
}
}

Get User Roles

Retrieve all roles assigned to a user.

Authentication: Required Permission: users:read

query {
userRoles(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
id
name
description
isSystem
requiresTwoFactor
createdAt
updatedAt
}
}

Mutations

Create GPS Data

Create a new GPS data point.

Authentication: Required (Admin only)

mutation {
createGps(input: {
latitude: 51.5074
longitude: -0.1278
altitude: 11.0
timestamp: 1700000000
speed: 5.5
}) {
id
latitude
longitude
altitude
timestamp
speed
createdAt
}
}

Response:

{
"data": {
"createGps": {
"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"
}
}
}

Update User

Update a user's information.

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

mutation {
updateUser(
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
username: "newusername"
email: "newemail@example.com"
avatarUrl: "https://cdn.example.com/new-avatar.png"
) {
id
username
email
avatarUrl
preferredLocale
updatedAt
}
}

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

Note: User objects include preferredLocale (e.g., "en", "de") for email communication preferences.

Update Display Name

Update only the user's display name (username).

Authentication: Required (self or system API key)

mutation {
updateDisplayName(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", newUsername: "mynewname") {
id
username
}
}

Update Privacy Mode

Toggle the user's privacy mode setting.

Authentication: Required (self or system API key)

mutation {
updatePrivacyMode(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", privacyMode: true) {
success
privacyMode
}
}

Update Avatars Enabled

Toggle whether avatars are displayed for a user. When disabled, all existing avatar URLs on platform accounts are removed and new OAuth logins will not populate avatars.

Authentication: Required Permission: users:write

mutation {
updateAvatarsEnabled(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", avatarsEnabled: false) {
success
avatarsEnabled
}
}

Update User Locale

Update the user's preferred locale for email communications.

Authentication: Required (self or system API key)

mutation {
updateUserLocale(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", locale: "de") {
success
locale
}
}

Response:

{
"data": {
"updateUserLocale": {
"success": true,
"locale": "de"
}
}
}

Supported locales: en, de

Delete User

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

Authentication: Required Permission: users:delete

mutation {
deleteUser(id: "user-123")
}

Response:

{
"data": {
"deleteUser": true
}
}

Request Account Deletion

Schedule an account for deletion after a 7-day grace period.

Authentication: Required (self or system API key)

mutation {
requestAccountDeletion(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
isScheduledForDeletion
scheduledDeletionAt
}
}

Cancel Account Deletion

Cancel a scheduled account deletion.

Authentication: Required (self or system API key)

mutation {
cancelAccountDeletion(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8")
}

Request Email Change

Request an email change (sends verification to new email).

Authentication: Required (self or system API key)

mutation {
requestEmailChange(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", newEmail: "newemail@example.com") {
pending
newEmail
expiresAt
}
}

Verify Email Change

Complete the email change with verification token.

mutation {
verifyEmailChange(token: "verification-token-from-email")
}

Cancel Email Change

Cancel a pending email change request.

Authentication: Required (self or system API key)

mutation {
cancelEmailChange(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8")
}

Link an OAuth provider account to a user.

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

mutation {
linkOauthAccount(
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
input: {
oauthProvider: "twitch"
oauthProviderId: "12345678"
username: "johndoe"
email: "john@example.com"
avatarUrl: "https://cdn.example.com/avatar.png"
}
) {
id
platformId
platformName
platformSlug
platformUserId
isOauth
isPrimary
username
email
avatarUrl
createdAt
updatedAt
}
}

Unlink a platform account from a user.

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

mutation {
unlinkPlatformAccount(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", accountId: "pa-456")
}

Response:

{
"data": {
"unlinkPlatformAccount": true
}
}
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)
  • If unlinking the primary account, another account will be automatically set as primary

Set Primary Platform Account

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

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

mutation {
setPrimaryPlatformAccount(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", accountId: "pa-456") {
id
platformId
platformName
platformSlug
platformUserId
isOauth
isPrimary
username
email
avatarUrl
createdAt
updatedAt
}
}

Response:

{
"data": {
"setPrimaryPlatformAccount": {
"id": "8f14e45f-ceea-467f-8a53-c9d5889d76b2",
"platformId": "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
"platformName": "Discord",
"platformSlug": "discord",
"platformUserId": "98765432",
"isOauth": true,
"isPrimary": true,
"username": "johndoe#1234",
"email": "john@example.com",
"avatarUrl": "https://cdn.example.com/avatar.png",
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-01-25T14:30:00Z"
}
}
}
Primary Account

The primary account determines the user's default profile information (avatar, display name). When changed, the previous primary account's isPrimary flag is set to false.

Start the process of linking an email account to your user profile. This sends a verification email.

mutation RequestLinkEmailAccount($input: RequestEmailLinkInput!) {
requestLinkEmailAccount(input: $input) {
message
expiresAt
}
}

Variables:

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

Response:

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

The password must be at least 8 characters long. This password will be used to authenticate with the email account after verification.

Complete the email account linking process by verifying the token sent to the email.

mutation VerifyLinkEmailAccount($input: VerifyEmailLinkInput!) {
verifyLinkEmailAccount(input: $input) {
message
platformAccountId
}
}

Variables:

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

Response:

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

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

mutation PendingEmailLinkStatus {
pendingEmailLinkStatus {
pending
email
expiresAt
}
}

Response (with pending link):

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

Response (no pending link):

{
"data": {
"pendingEmailLinkStatus": {
"pending": false,
"email": null,
"expiresAt": null
}
}
}

Cancel a pending email link request.

mutation CancelLinkEmailAccount {
cancelLinkEmailAccount
}

Response:

{
"data": {
"cancelLinkEmailAccount": true
}
}
Email Account vs OAuth Account

Email accounts are a type of platform account that allows users to authenticate with email/password instead of OAuth. This is useful for users who don't want to link their social accounts or for platforms that don't support OAuth.

Schema Introspection

You can introspect the GraphQL schema to discover available types, queries, and mutations:

query {
__schema {
types {
name
kind
description
}
}
}

Get Type Details

query {
__type(name: "GpsData") {
name
kind
fields {
name
type {
name
kind
}
}
}
}

Variables

Use variables for dynamic queries:

query GetGpsById($id: String!) {
gpsById(id: $id) {
id
latitude
longitude
altitude
}
}

Variables:

{
"id": "550e8400-e29b-41d4-a716-446655440000"
}

Error Handling

GraphQL errors follow the standard format:

{
"errors": [
{
"message": "GPS data not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["gpsById"]
}
],
"data": null
}

Best Practices

GraphQL Best Practices
  1. Request only needed fields - GraphQL allows you to select exactly what you need
  2. Use variables - For dynamic values, use variables instead of string interpolation
  3. Batch queries - Combine multiple queries in a single request
  4. Handle partial errors - GraphQL can return partial data with errors
  5. Use fragments - Reuse common field selections with fragments

Code Examples

JavaScript (Fetch)

const query = `
query {
currentGps {
latitude
longitude
altitude
}
}
`;

const response = await fetch('https://api.elcto.com/v1/gql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({ query })
});

const { data } = await response.json();
console.log(data.currentGps);

Python (requests)

import requests

query = """
query {
currentGps {
latitude
longitude
altitude
}
}
"""

response = requests.post(
'https://api.elcto.com/v1/gql',
json={'query': query},
headers={'Authorization': 'Bearer YOUR_TOKEN'}
)

data = response.json()['data']
print(data['currentGps'])

cURL

curl -X POST https://api.elcto.com/v1/gql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"query":"{ currentGps { latitude longitude altitude } }"}'

Active Sessions Management

Query and manage active login sessions across devices.

Queries

Get My Active Sessions

Get all active sessions for the authenticated user.

Authentication: Required

query {
myActiveSessions {
id
ipAddress
userAgent
deviceType
browserName
osName
provider
providerName
lastActivityAt
createdAt
expiresAt
isCurrent
}
}

Response:

{
"data": {
"myActiveSessions": [
{
"id": "d0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
"ipAddress": "192.168.1.100",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
"deviceType": "desktop",
"browserName": "Chrome",
"osName": "Windows",
"provider": "twitch",
"providerName": "Twitch",
"lastActivityAt": "2025-01-25T10:30:00Z",
"createdAt": "2025-01-20T08:00:00Z",
"expiresAt": "2025-02-20T08:00:00Z",
"isCurrent": true
},
{
"id": "e0eebc99-9c0b-4ef8-bb6d-6bb9bd380a15",
"ipAddress": "10.0.0.50",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)...",
"deviceType": "mobile",
"browserName": "Safari",
"osName": "iOS",
"provider": "email",
"providerName": "Email",
"lastActivityAt": "2025-01-24T15:00:00Z",
"createdAt": "2025-01-15T12:00:00Z",
"expiresAt": "2025-02-15T12:00:00Z",
"isCurrent": false
}
]
}
}

Session Fields:

FieldTypeDescription
idString!Session ID
ipAddressStringIP address used to create the session
userAgentStringFull user agent string
deviceTypeStringParsed device type (desktop, mobile, tablet)
browserNameStringParsed browser name
osNameStringParsed operating system name
providerStringAuth provider slug (twitch, discord, email, etc.)
providerNameStringAuth provider display name (Twitch, Discord, Email)
lastActivityAtDateTimeLast activity timestamp
createdAtDateTime!Session creation time
expiresAtDateTime!Session expiration time
isCurrentBoolean!Whether this is the current session

Mutations

Revoke Session

Revoke a specific session by ID.

Authentication: Required

mutation {
revokeSession(sessionId: "sess-456")
}

Response:

{
"data": {
"revokeSession": true
}
}
Current Session

You cannot revoke your current session. The mutation will return an error if you try.

Revoke All Other Sessions

Revoke all sessions except the current one.

Authentication: Required

mutation {
revokeAllOtherSessions
}

Response:

{
"data": {
"revokeAllOtherSessions": 3
}
}

The returned number indicates how many sessions were revoked.

Security Feature

Use this when you suspect unauthorized access to your account. It will sign out all other devices while keeping you logged in.

Two-Factor Authentication

Manage two-factor authentication (2FA) using TOTP (Time-based One-Time Password).

Queries

Get 2FA Status

Check if 2FA is enabled for a user.

Authentication: Required

query {
twoFactorStatus(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
enabled
enabledAt
backupCodesRemaining
platformSlug
}
}

Parameters:

ParameterTypeRequiredDescription
userIdStringNoUser ID (defaults to current user)

Response:

{
"data": {
"twoFactorStatus": {
"enabled": true,
"enabledAt": "2025-01-15T10:00:00Z",
"backupCodesRemaining": 8,
"platformSlug": "email"
}
}
}

Check 2FA Requirement

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

Authentication: Required

query {
userRequiresTwoFactor(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
required
enabled
reason
}
}

Response:

{
"data": {
"userRequiresTwoFactor": {
"required": true,
"enabled": false,
"reason": "Your role requires two-factor authentication"
}
}
}

Mutations

Setup 2FA

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

Authentication: Required

mutation {
setupTwoFactor(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
platformId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
}) {
secret
qrCodeUrl
qrCodeImage
expiresAt
platformSlug
}
}

Input:

FieldTypeRequiredDescription
userIdStringNoUser ID (defaults to current user)
platformIdStringNoPlatform for 2FA (defaults to email)

Response:

{
"data": {
"setupTwoFactor": {
"secret": "JBSWY3DPEHPK3PXP",
"qrCodeUrl": "otpauth://totp/Heimdall:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Heimdall",
"qrCodeImage": "data:image/png;base64,iVBORw0KGgo...",
"expiresAt": "2025-01-25T10:10:00Z",
"platformSlug": "email"
}
}
}
Setup Expiration

The setup expires after 10 minutes. Verify the code before then, or initiate setup again.

Verify 2FA

Verify a TOTP code to complete 2FA setup.

Authentication: Required

mutation {
verifyTwoFactor(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
code: "123456"
}) {
success
backupCodes
message
}
}

Input:

FieldTypeRequiredDescription
userIdStringNoUser ID (defaults to current user)
codeStringYes6-digit TOTP code

Response:

{
"data": {
"verifyTwoFactor": {
"success": true,
"backupCodes": [
"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"
}
}
}
Store Backup Codes Securely

Backup codes are only shown once. Store them in a secure location.

Disable 2FA

Disable two-factor authentication.

Authentication: Required

mutation {
disableTwoFactor(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
code: "123456"
}) {
success
message
}
}

Input:

FieldTypeRequiredDescription
userIdStringNoUser ID (defaults to current user)
codeStringYesTOTP code or backup code

Response:

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

Regenerate Backup Codes

Generate new backup codes (invalidates all previous codes).

Authentication: Required

mutation {
regenerateBackupCodes(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
code: "123456"
}) {
success
backupCodes
message
}
}

Input:

FieldTypeRequiredDescription
userIdStringNoUser ID (defaults to current user)
codeStringYesTOTP code to confirm

Response:

{
"data": {
"regenerateBackupCodes": {
"success": true,
"backupCodes": [
"ABCD-EFGH-IJKL",
"MNOP-QRST-UVWX",
"..."
],
"message": "Backup codes regenerated successfully"
}
}
}
Previous Codes Invalidated

Regenerating backup codes invalidates all previous backup codes immediately.

Verify 2FA During Login

Verify 2FA code during login flow (for auth systems).

Authentication: Required (System API key)

mutation {
verifyTwoFactorLogin(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
code: "123456"
}) {
success
message
}
}

This mutation verifies a TOTP code during login without modifying 2FA settings.

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

OAuth Client Management

Manage OAuth client applications for third-party integrations.

Queries

List Available OAuth Scopes

Get all available OAuth scopes that can be requested by clients.

query {
oauthScopes {
name
description
}
}

Get My OAuth Clients

Get OAuth clients owned by the current user.

Authentication: Required Permission: OAuth client owner

query {
myOauthClients {
id
name
description
logoUrl
homepageUrl
privacyPolicyUrl
termsOfServiceUrl
redirectUris
allowedScopes
clientType
isActive
isFirstParty
createdAt
updatedAt
}
}

Get OAuth Client by ID

Get a specific OAuth client (must be owner or admin).

query {
oauthClient(id: "client-123") {
id
name
description
redirectUris
allowedScopes
clientType
isActive
isFirstParty
ownerUserId
createdAt
}
}

List All OAuth Clients (Admin)

List all OAuth clients in the system.

Authentication: Required Permission: Admin

query {
allOauthClients {
id
name
ownerUserId
isActive
isFirstParty
createdAt
}
}

Get My OAuth Consents

Get consents the current user has granted to OAuth clients.

query {
myOauthConsents {
id
userId
clientId
clientName
clientLogo
scopes
isFirstParty
createdAt
updatedAt
}
}

Get My OAuth Tokens

Get active OAuth access tokens for the current user.

query {
myOauthTokens {
id
clientId
clientName
scopes
expiresAt
createdAt
}
}

Mutations

Create OAuth Client

Create a new OAuth client application.

Authentication: Required

mutation {
createOauthClient(input: {
name: "My App"
description: "My awesome application"
logoUrl: "https://example.com/logo.png"
homepageUrl: "https://example.com"
privacyPolicyUrl: "https://example.com/privacy"
termsOfServiceUrl: "https://example.com/terms"
redirectUris: ["https://example.com/callback"]
allowedScopes: ["openid", "profile", "email"]
clientType: "confidential"
isFirstParty: false
}) {
client {
id
name
clientType
}
clientSecret
}
}
Client Secret

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

Input Fields:

FieldTypeRequiredDescription
nameStringYesClient application name
descriptionStringNoApplication description
logoUrlStringNoURL to client logo
homepageUrlStringNoClient homepage URL
privacyPolicyUrlStringNoPrivacy policy URL
termsOfServiceUrlStringNoTerms of service URL
redirectUris[String!]!YesAllowed redirect URIs
allowedScopes[String!]!YesScopes client can request
clientTypeStringYes"confidential" or "public"
isFirstPartyBooleanNoFirst-party apps skip consent (default: false)

Update OAuth Client

Update an existing OAuth client.

mutation {
updateOauthClient(
id: "client-123"
input: {
name: "Updated Name"
description: "Updated description"
redirectUris: ["https://example.com/new-callback"]
}
) {
id
name
description
updatedAt
}
}

Regenerate Client Secret

Regenerate the client secret for a confidential client.

mutation {
regenerateOauthClientSecret(id: "client-123") {
success
clientSecret
error
}
}

Response:

FieldTypeDescription
successBoolean!Whether the operation succeeded
clientSecretStringThe new client secret (only shown once!)
errorStringError message if operation failed

Delete OAuth Client

Delete an OAuth client.

mutation {
deleteOauthClient(id: "client-123") {
success
error
}
}

Response:

FieldTypeDescription
successBoolean!Whether the deletion succeeded
errorStringError message if deletion failed

Revoke user consent for an OAuth client (disconnect the app).

mutation {
revokeOauthConsent(clientId: "client-123")
}

Revoke OAuth Token

Revoke a specific OAuth access token.

mutation {
revokeOauthToken(tokenId: "token-123")
}

API Key Management

Manage API keys for programmatic access to the Heimdall API.

Queries

List API Keys

Get all API keys for the authenticated user.

Authentication: Required Permission: api_keys:read

query {
apiKeys {
id
keyPrefix
name
description
scopes
isActive
isSystem
expiresAt
lastUsedAt
createdAt
updatedAt
}
}

Response:

{
"data": {
"apiKeys": [
{
"id": "ak_550e8400-e29b-41d4-a716-446655440000",
"keyPrefix": "hm_abc12345...",
"name": "My API Key",
"description": "Key for CI/CD pipeline",
"scopes": ["gps:read", "gps:write"],
"isActive": true,
"isSystem": false,
"expiresAt": "2025-12-31T23:59:59Z",
"lastUsedAt": "2025-01-25T10:30:00Z",
"createdAt": "2025-01-01T00:00:00Z",
"updatedAt": "2025-01-15T12:00:00Z"
}
]
}
}

API Key Fields:

FieldTypeDescription
idString!Unique API key identifier
keyPrefixString!Visible prefix of the key (e.g., hm_abc12345...)
nameString!Name/label for the key
descriptionStringOptional description
scopes[String!]!Permission scopes granted to this key
isActiveBoolean!Whether the key is currently active
isSystemBoolean!Whether this is a system-managed key
expiresAtDateTimeOptional expiration date
lastUsedAtDateTimeLast time the key was used
createdAtDateTime!Creation timestamp
updatedAtDateTime!Last update timestamp

Get API Key by ID

Get a specific API key by its ID.

Authentication: Required Permission: api_keys:read

query {
apiKey(id: "ak_550e8400-e29b-41d4-a716-446655440000") {
id
keyPrefix
name
description
scopes
isActive
isSystem
expiresAt
lastUsedAt
createdAt
updatedAt
}
}

Mutations

Create API Key

Create a new API key. The full key is only returned once at creation time.

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 an error. Super admins can assign any scope.

mutation {
createApiKey(input: {
name: "CI/CD Pipeline Key"
description: "Key for automated deployments"
scopes: ["gps:read", "gps:write"]
expiresAt: "2025-12-31T23:59:59Z"
}) {
id
key
keyPrefix
name
description
scopes
isActive
isSystem
expiresAt
createdAt
updatedAt
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringYesName/label for the key
descriptionStringNoOptional description
scopes[String!]NoPermission scopes (defaults to empty)
expiresAtDateTimeNoOptional expiration date

Response:

{
"data": {
"createApiKey": {
"id": "ak_550e8400-e29b-41d4-a716-446655440000",
"key": "hm_abcdefghijklmnopqrstuvwxyz123456",
"keyPrefix": "hm_abc12345...",
"name": "CI/CD Pipeline Key",
"description": "Key for automated deployments",
"scopes": ["gps:read", "gps:write"],
"isActive": true,
"isSystem": false,
"expiresAt": "2025-12-31T23:59:59Z",
"createdAt": "2025-01-25T14:00:00Z",
"updatedAt": "2025-01-25T14:00:00Z"
}
}
}
Store Your Key Securely

The key field contains the full API key and is only returned once at creation time. Store it securely immediately - you won't be able to retrieve it again.

Update API Key

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

Authentication: Required Permission: api_keys:write

mutation {
updateApiKey(
id: "ak_550e8400-e29b-41d4-a716-446655440000"
input: {
name: "Updated Key Name"
description: "Updated description"
scopes: ["gps:read"]
isActive: false
expiresAt: "2026-06-30T23:59:59Z"
}
) {
id
keyPrefix
name
description
scopes
isActive
expiresAt
updatedAt
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringNoNew name for the key
descriptionStringNoNew description
scopes[String!]NoNew permission scopes
isActiveBooleanNoEnable or disable the key
expiresAtDateTimeNoNew expiration date

All fields are optional - only include fields you want to update.

System Keys

System API keys cannot be modified except by super admins. Attempting to update a system key will return an error.

Delete API Key

Delete an API key permanently.

Authentication: Required Permission: api_keys:delete

mutation {
deleteApiKey(id: "ak_550e8400-e29b-41d4-a716-446655440000") {
success
error
}
}

Response:

{
"data": {
"deleteApiKey": {
"success": true,
"error": null
}
}
}
System Keys

System API keys cannot be deleted. Attempting to delete a system key will return an error.

Ownership

Users can only manage their own API keys. Super admins can manage all API keys.

Platform Management

Manage authentication platform providers.

Queries

List All Platforms

Get all authentication platforms with their status.

query {
platforms {
id
name
slug
isOauth
enabled
twoFactorSupported
twoFactorRequired
createdAt
updatedAt
}
}

List Enabled Platforms

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

query {
enabledPlatforms {
id
name
slug
isOauth
}
}

Get Platform by ID or Slug

Get a specific platform.

query {
platform(idOrSlug: "twitch") {
id
name
slug
enabled
twoFactorSupported
twoFactorRequired
}
}

Mutations

Enable Platform

Enable a platform for authentication.

Authentication: Required Permission: settings:write

mutation {
enablePlatform(idOrSlug: "discord") {
success
message
}
}

Disable Platform

Disable a platform for authentication.

Authentication: Required Permission: settings:write

mutation {
disablePlatform(idOrSlug: "discord") {
success
message
}
}
warning

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

Set Platform Enabled Status

Set platform enabled status directly.

mutation {
setPlatformEnabled(idOrSlug: "discord", enabled: true) {
id
name
enabled
}
}

Roles

Query and manage roles in the system.

Queries

List All Roles

Get all roles in the system.

Authentication: Required Permission: roles:read

query {
roles {
id
name
description
isSystem
requiresTwoFactor
permissions {
id
name
description
}
createdAt
updatedAt
}
}

Response:

{
"data": {
"roles": [
{
"id": "role_admin",
"name": "Admin",
"description": "Full administrative access",
"isSystem": true,
"requiresTwoFactor": true,
"permissions": [
{
"id": "perm_users_write",
"name": "users:write",
"description": "Write user data"
}
],
"createdAt": "2025-01-01T00:00:00Z",
"updatedAt": "2025-01-01T00:00:00Z"
}
]
}
}

Get Role by ID

Get a specific role by its ID.

Authentication: Required Permission: roles:read

query {
role(id: "role_admin") {
id
name
description
isSystem
requiresTwoFactor
permissions {
id
name
}
createdAt
updatedAt
}
}

Role Fields:

FieldTypeDescription
idString!Unique role identifier
nameString!Role display name
descriptionStringRole description
isSystemBoolean!Whether this is a system-managed role
requiresTwoFactorBoolean!Whether users with this role must have 2FA enabled
permissions[Permission!]!Permissions granted by this role
createdAtDateTime!Creation timestamp
updatedAtDateTime!Last update timestamp

Permissions

Query available permissions in the system.

Queries

List All Permissions

Get all available permissions.

Authentication: Required Permission: permissions:read

query {
permissions {
id
name
description
resource
action
createdAt
}
}

Response:

{
"data": {
"permissions": [
{
"id": "perm_users_read",
"name": "users:read",
"description": "Read user data",
"resource": "users",
"action": "read",
"createdAt": "2025-01-01T00:00:00Z"
},
{
"id": "perm_users_write",
"name": "users:write",
"description": "Write user data",
"resource": "users",
"action": "write",
"createdAt": "2025-01-01T00:00:00Z"
}
]
}
}

Get Permission by ID

Get a specific permission by its ID.

Authentication: Required Permission: permissions:read

query {
permission(id: "perm_users_read") {
id
name
description
resource
action
createdAt
}
}

Permission Fields:

FieldTypeDescription
idString!Unique permission identifier
nameString!Permission name in resource:action format
descriptionStringPermission description
resourceString!Resource this permission applies to
actionString!Action this permission allows
createdAtDateTime!Creation timestamp

Common Permissions:

PermissionDescription
users:readRead user profiles
users:writeUpdate user data
users:deleteDelete users
users:banBan/unban users
roles:readView roles
roles:writeCreate/update roles
permissions:readView permissions
audit:readView audit logs
audit:writeManage audit reports
discord:readView Discord data
discord:syncSync Discord data
api_keys:readView API keys
api_keys:writeCreate/update API keys
api_keys:deleteDelete API keys
two_factor:manageManage other users' 2FA
settings:readView system settings
settings:writeUpdate system settings

Platform Integrations

Query and manage platform integrations (e.g., Twitch channel integration).

Queries

List Platform Integrations

Get integrations for a specific platform.

Authentication: Required

query {
platformIntegrations(platformSlug: "twitch") {
id
userId
platformSlug
platformName
platformUserId
username
displayName
profileImageUrl
scopes
isActive
botStatus
connectedAt
lastRefreshedAt
}
}

Response:

{
"data": {
"platformIntegrations": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"userId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"platformSlug": "twitch",
"platformName": "Twitch",
"platformUserId": "12345678",
"username": "streamer_name",
"displayName": "Streamer Name",
"profileImageUrl": "https://static-cdn.jtvnw.net/...",
"scopes": ["channel:read:subscriptions"],
"isActive": true,
"botStatus": "active",
"connectedAt": "2025-01-15T10:00:00Z",
"lastRefreshedAt": "2025-01-25T14:30:00Z"
}
]
}
}

Get Integration

Get a specific integration by ID.

Authentication: Required

query {
integration(id: "550e8400-e29b-41d4-a716-446655440000") {
id
platformSlug
username
isActive
botStatus
}
}

List All Integrations

Get all integrations for the current user.

Authentication: Required

query {
allIntegrations {
id
platformSlug
platformName
username
isActive
}
}

Get Integration Scopes

Get available OAuth scopes for a platform integration.

Authentication: Required

query {
integrationScopes(platformSlug: "twitch") {
name
description
required
}
}

List Integration Platforms

Get all available integration platforms.

Authentication: Required

query {
integrationPlatforms {
slug
name
description
iconUrl
enabled
features
}
}

Mutations

Connect Integration

Initiate OAuth connection to a platform.

Authentication: Required

mutation {
connectIntegration(input: {
platformSlug: "twitch"
redirectUri: "https://your-app.com/integrations/callback"
scopes: ["channel:read:subscriptions"]
}) {
authorizationUrl
state
}
}

Redirect the user to authorizationUrl to complete the OAuth flow.

Disconnect Integration

Disconnect a platform integration.

Authentication: Required

mutation {
disconnectIntegration(id: "550e8400-e29b-41d4-a716-446655440000") {
success
message
}
}

Refresh Integration Token

Refresh the OAuth token for an integration.

Authentication: Required

mutation {
refreshIntegrationToken(id: "550e8400-e29b-41d4-a716-446655440000") {
success
expiresAt
}
}

Update Twitch Bot Status

Update the bot status for a Twitch integration.

Authentication: Required

mutation {
updateTwitchBotStatus(id: "550e8400-e29b-41d4-a716-446655440000", status: "active") {
success
botStatus
}
}

Status Values: active, paused, disabled

Refresh Channel Stats

Refresh statistics for a streaming channel integration.

Authentication: Required

mutation {
refreshChannelStats(id: "550e8400-e29b-41d4-a716-446655440000") {
success
stats {
followers
subscribers
isLive
currentViewers
streamTitle
gameName
updatedAt
}
}
}

Discord Bot Management

Query and manage Discord guilds and channels synced from the Discord bot.

Queries

List Discord Guilds

Get all Discord guilds synced from the bot.

Authentication: Required Permission: discord:read

query {
discordGuilds {
id
guildId
name
icon
iconUrl
ownerId
memberCount
boostCount
premiumTier
botJoinedAt
lastSyncedAt
}
}

Response:

{
"data": {
"discordGuilds": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"guildId": "123456789012345678",
"name": "My Server",
"icon": "a_1234567890abcdef",
"iconUrl": "https://cdn.discordapp.com/icons/123456789012345678/a_1234567890abcdef.png",
"ownerId": "987654321098765432",
"memberCount": 1500,
"boostCount": 14,
"premiumTier": 2,
"botJoinedAt": "2025-01-15T10:00:00Z",
"lastSyncedAt": "2025-01-25T14:30:00Z"
}
]
}
}

Guild Fields:

FieldTypeDescription
idString!Internal UUID primary key
guildIdString!Discord guild ID (snowflake)
nameString!Guild name
iconStringGuild icon hash
iconUrlStringFull URL to guild icon
ownerIdString!Discord user ID of guild owner
memberCountIntApproximate member count
boostCountIntNumber of server boosts
premiumTierIntPremium tier (0=none, 1=tier1, 2=tier2, 3=tier3)
botJoinedAtDateTimeWhen the bot joined this guild
lastSyncedAtDateTime!Last sync timestamp

Get Discord Guild

Get a specific Discord guild by its Discord guild ID.

Authentication: Required Permission: discord:read

query {
discordGuild(id: "123456789012345678") {
id
guildId
name
icon
iconUrl
ownerId
memberCount
boostCount
premiumTier
botJoinedAt
lastSyncedAt
}
}

Note: The id parameter requires the internal UUID (database ID), not the Discord snowflake.

List Discord Channels

Get all channels for a specific guild.

Authentication: Required Permission: discord:read

query {
discordChannels(guildId: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe") {
id
channelId
guildId
name
channelType
channelTypeRaw
parentId
position
topic
nsfw
isTextBased
isVoice
}
}

Response:

{
"data": {
"discordChannels": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"channelId": "111222333444555666",
"guildId": "123456789012345678",
"name": "general",
"channelType": "Text",
"channelTypeRaw": 0,
"parentId": "999888777666555444",
"position": 0,
"topic": "General chat",
"nsfw": false,
"isTextBased": true,
"isVoice": false
}
]
}
}

Channel Fields:

FieldTypeDescription
idString!Internal UUID primary key
channelIdString!Discord channel ID (snowflake)
guildIdString!Discord guild ID (snowflake)
nameString!Channel name
channelTypeEnumChannel type (Text, Voice, Category, etc.)
channelTypeRawInt!Raw channel type value
parentIdStringParent category channel ID
positionInt!Position in channel list
topicStringChannel topic/description
nsfwBoolean!Whether channel is NSFW
isTextBasedBoolean!Whether channel supports text messages
isVoiceBoolean!Whether 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 Channel

Get a specific Discord channel by its Discord channel ID.

Authentication: Required Permission: discord:read

query {
discordChannel(channelId: "111222333444555666") {
id
channelId
guildId
name
channelType
channelTypeRaw
parentId
position
topic
nsfw
isTextBased
isVoice
}
}

Note: The channelId parameter requires the internal UUID (database ID), not the Discord snowflake.

Get Discord Guild with Channels

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

Authentication: Required Permission: discord:read

query {
discordGuildWithChannels(guildId: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe") {
guild {
id
guildId
name
icon
iconUrl
ownerId
memberCount
boostCount
premiumTier
botJoinedAt
lastSyncedAt
}
channels {
id
channelId
name
channelType
channelTypeRaw
parentId
position
topic
nsfw
isTextBased
isVoice
}
}
}

List Discord Members

Get paginated members for a specific guild with optional filtering.

Authentication: Required Permission: discord:read

query {
discordMembers(
guildId: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe"
page: 1
limit: 50
filter: { search: "john", humansOnly: true }
) {
members {
id
userId
username
globalName
nickname
displayName
formattedUsername
avatarUrl
bot
joinedAt
premiumSince
roles
isTimedOut
communicationDisabledUntil
}
totalCount
hasMore
}
}

Parameters:

ParameterTypeRequiredDescription
guildIdStringYesInternal guild UUID (database ID)
pageIntNoPage number (default: 1)
limitIntNoItems per page (default: 50, max: 100)
filterObjectNoFilter options

Filter Options:

FieldTypeDescription
searchStringSearch by username, global name, or nickname
botOnlyBooleanOnly show bots
humansOnlyBooleanOnly show humans (non-bots)
hasRoleStringFilter by role ID

Response:

{
"data": {
"discordMembers": {
"members": [
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"userId": "111222333444555666",
"username": "johndoe",
"globalName": "John Doe",
"nickname": "Johnny",
"displayName": "Johnny",
"formattedUsername": "@johndoe",
"avatarUrl": "https://cdn.discordapp.com/avatars/111222333444555666/abc123.png",
"bot": false,
"joinedAt": "2024-06-15T10:00:00Z",
"premiumSince": "2024-12-01T00:00:00Z",
"roles": ["role-id-1", "role-id-2"],
"isTimedOut": false,
"communicationDisabledUntil": null
}
],
"totalCount": 1500,
"hasMore": true
}
}
}

Member Fields:

FieldTypeDescription
idString!Internal UUID primary key
userIdString!Discord user ID (snowflake)
usernameString!Discord username
discriminatorStringLegacy discriminator (may be "0")
globalNameStringGlobal display name
nicknameStringServer-specific nickname
displayNameString!Computed: nickname > globalName > username
formattedUsernameString!Formatted: @username or username#discriminator
avatarStringAvatar hash
avatarUrlStringFull URL to avatar
botBoolean!Whether this is a bot account
systemBoolean!Whether this is a system account
joinedAtDateTimeWhen the member joined
premiumSinceDateTimeWhen the member started boosting
roles[String!]!List of role IDs
isTimedOutBoolean!Whether member is currently timed out
communicationDisabledUntilDateTimeTimeout expiry

Get Discord Member

Get a specific member by their Discord user ID.

Authentication: Required Permission: discord:read

query {
discordMember(guildId: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe", userId: "111222333444555666") {
id
userId
username
globalName
nickname
displayName
avatarUrl
bot
joinedAt
roles
isTimedOut
}
}

Mutations

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.

Authentication: Required Permission: discord:sync

mutation {
syncDiscordGuilds {
success
error
guildsSynced
channelsSynced
rolesSynced
}
}

With specific guild ID:

mutation {
syncDiscordGuilds(guildId: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe") {
success
error
guildsSynced
channelsSynced
rolesSynced
}
}
Guild ID Format

The guildId parameter requires the internal UUID (database ID), not the Discord snowflake. Example: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe"

Response:

{
"data": {
"syncDiscordGuilds": {
"success": true,
"error": null,
"guildsSynced": 5,
"channelsSynced": 47,
"rolesSynced": 38
}
}
}

Response Fields:

FieldTypeDescription
successBooleanWhether the sync was successful
errorStringError message if failed
guildsSyncedIntNumber of guilds synced
channelsSyncedIntNumber of channels synced
rolesSyncedIntNumber 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.

Sync Discord Guilds Only

Sync only guild metadata from Discord (without channels or roles). Use this for a lightweight sync when you only need to update guild information.

Authentication: Required Permission: discord:sync

mutation {
syncDiscordGuildsOnly {
success
error
guildsSynced
}
}

Response:

{
"data": {
"syncDiscordGuildsOnly": {
"success": true,
"error": null,
"guildsSynced": 5
}
}
}

Sync Discord Channels Only

Sync only channels for a specific guild from Discord. Use this when you only need to update channel information.

Authentication: Required Permission: discord:guild.sync

mutation {
syncDiscordChannelsOnly(guildId: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe") {
success
error
channelsSynced
}
}

The guildId must be the internal UUID (database ID).

Response:

{
"data": {
"syncDiscordChannelsOnly": {
"success": true,
"error": null,
"channelsSynced": 30
}
}
}

Sync Discord Roles Only

Sync only roles for a specific guild from Discord. Use this when you only need to update role information.

Authentication: Required Permission: discord:guild.sync

mutation {
syncDiscordRolesOnly(guildId: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe") {
success
error
rolesSynced
}
}

The guildId must be the internal UUID (database ID).

Response:

{
"data": {
"syncDiscordRolesOnly": {
"success": true,
"error": null,
"rolesSynced": 16
}
}
}

Sync Discord Members

Sync all members for a specific guild from Discord. This can take longer for large guilds (60 second timeout).

Authentication: Required Permission: discord:guild.sync

mutation {
syncDiscordMembers(guildId: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe") {
success
error
membersSynced
}
}

The guildId must be the internal UUID (database ID).

Response:

{
"data": {
"syncDiscordMembers": {
"success": true,
"error": null,
"membersSynced": 1500
}
}
}
Large Guilds

Member sync can be slow for large guilds. The operation has a 60-second timeout.

Moderate Discord Member

Execute a moderation action on a Discord member (warn, kick, ban, timeout).

Authentication: Required Permission: Action-specific (discord:guild.warn, discord:guild.kick, discord:guild.ban, discord:guild.timeout)

mutation {
moderateDiscordMember(input: {
guildId: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe"
userId: "987654321098765432"
action: TIMEOUT
reason: "Spamming in general chat"
durationSeconds: 3600 # 1 hour timeout
}) {
success
error
action
}
}

Input Fields:

FieldTypeRequiredDescription
guildIdStringYesInternal guild UUID (database ID)
userIdStringYesTarget Discord user ID (snowflake)
actionEnumYesModeration action to perform
reasonStringNoReason for the action
durationSecondsIntNoDuration for timeout (required for TIMEOUT action)
deleteMessageDaysIntNoDays of messages to delete for ban (0-7)

Action Types:

ActionDescriptionRequired Permission
WARNSend a warning DM to the userdiscord:guild.warn
KICKKick the user from the guilddiscord:guild.kick
BANBan the user from the guilddiscord:guild.ban
TIMEOUTTimeout the user (requires durationSeconds)discord:guild.timeout
REMOVE_TIMEOUTRemove an existing timeoutdiscord:guild.timeout

Response:

{
"data": {
"moderateDiscordMember": {
"success": true,
"error": null,
"action": "TIMEOUT"
}
}
}

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.

Authentication: Required Permission: discord:delete

mutation {
deleteDiscordGuild(guildId: "5543dd84-9fe3-4331-b071-f2e7e39c8bbe")
}

Response:

{
"data": {
"deleteDiscordGuild": true
}
}

Get Discord Bot Settings

Get global Discord bot settings.

Authentication: Required Permission: discord:read

query {
discordBotSettings {
id
autoSyncEnabled
syncIntervalSeconds
initialDelaySeconds
defaultLanguage
notifyOnGuildJoin
notifyOnGuildLeave
commandCooldownSeconds
updatedAt
}
}

Response:

{
"data": {
"discordBotSettings": {
"id": "00000000-0000-0000-0000-000000000001",
"autoSyncEnabled": true,
"syncIntervalSeconds": 86400,
"initialDelaySeconds": 300,
"defaultLanguage": "en",
"notifyOnGuildJoin": true,
"notifyOnGuildLeave": true,
"commandCooldownSeconds": 3,
"updatedAt": "2025-01-15T10:30:00Z"
}
}
}

Update Discord Bot Settings

Update global Discord bot settings.

Authentication: Required Permission: discord:edit

mutation UpdateDiscordBotSettings($input: UpdateDiscordBotSettingsInput!) {
updateDiscordBotSettings(input: $input) {
success
error
}
}

Variables:

{
"input": {
"autoSyncEnabled": true,
"syncIntervalSeconds": 86400,
"defaultLanguage": "en"
}
}

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

Update Discord Guild Settings

Update per-guild settings for a specific Discord guild.

Authentication: Required Permission: discord:edit

mutation UpdateDiscordGuildSettings($guildId: String!, $input: UpdateDiscordGuildSettingsInput!) {
updateDiscordGuildSettings(guildId: $guildId, input: $input) {
success
error
}
}

Variables:

{
"guildId": "123456789012345678",
"input": {
"defaultLanguage": "de"
}
}

Set defaultLanguage to null to use the global default.

Admin Actions

Administrative actions for user management.

User Bans

Get User Ban Status

Check if a user is currently banned.

query {
userBanStatus(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
isBanned
activeBan {
id
reason
bannedAt
expiresAt
isPermanent
}
}
}

Get User Ban History

Get the complete ban history for a user.

Authentication: Required Permission: users:read

query {
userBanHistory(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
id
reason
bannedBy
bannedAt
expiresAt
isPermanent
unbannedAt
unbannedBy
}
}

Ban User

Ban a user from the platform.

Authentication: Required Permission: users:ban

mutation {
banUser(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
reason: "Violation of terms of service"
durationSeconds: 604800 # 7 days, null for permanent
}) {
id
userId
reason
bannedAt
expiresAt
isPermanent
}
}

Unban User

Remove a ban from a user.

Authentication: Required Permission: users:ban

mutation {
unbanUser(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8")
}

Assign Role to User

Assign a role to a user. Invalidates permission caches and broadcasts updates via WebSocket.

Authentication: Required Permission: users:write

mutation {
assignUserRole(
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
roleId: "role_editor"
)
}

Remove Role from User

Remove a role from a user. Invalidates permission caches and broadcasts updates via WebSocket.

Authentication: Required Permission: users:write

mutation {
removeUserRole(
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
roleId: "role_editor"
)
}

User Deletion

Get User Deletion Status (Admin)

Get deletion status for any user including deleted status.

Authentication: Required Permission: users:read

query {
userDeletionStatus(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
isScheduledForDeletion
scheduledDeletionAt
isDeleted
deletedAt
}
}

Force Delete User

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

Authentication: Required Permission: users:delete

mutation {
adminForceDeleteUser(
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
reason: "GDPR urgent request"
) {
success
message
}
}
Use with Caution

Force deletion is immediate and irreversible. Use only for:

  • GDPR urgent requests
  • Compromised accounts
  • Legal requirements

Schedule User Deletion

Schedule a user for deletion with 7-day grace period.

Authentication: Required Permission: users:delete

mutation {
adminScheduleUserDeletion(
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
reason: "Policy violation"
) {
success
message
}
}

Cancel User Deletion (Admin)

Cancel scheduled deletion for a user.

Authentication: Required Permission: users:write

mutation {
adminCancelUserDeletion(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
success
message
}
}

User Data Export (GDPR/DSGVO)

Export user data for GDPR/DSGVO compliance. The GraphQL API provides access to export logs, while the actual data export is available via the REST API endpoint GET /v1/user/export.

Queries

Get Data Export Logs

Retrieve the history of data exports for the current user.

Authentication: Required

query {
dataExportLogs {
id
exportedAt
format
fileSize
}
}

Response:

{
"data": {
"dataExportLogs": [
{
"id": "f0eebc99-9c0b-4ef8-bb6d-6bb9bd380a16",
"exportedAt": "2025-01-03T12:00:00Z",
"format": "zip",
"fileSize": 45678
}
]
}
}
Export via REST API

To download the actual data export (ZIP file with JSON + PDF), use the REST API endpoint:

GET /v1/user/export
Authorization: Bearer YOUR_TOKEN

This returns a ZIP archive containing:

  • data.json - Machine-readable export
  • report.pdf - Human-readable PDF report
  • README.txt - Explanation file

See the REST API documentation for full details.

Data Included in Export:

  • User profile (ID, username, privacy mode, avatars enabled, preferred locale, dates)
  • Platform accounts (connected providers)
  • Connected apps (OAuth consents)
  • Owned apps (developer applications)
  • API keys (names, prefixes, scopes)
  • Roles and permissions
  • Two-factor authentication status
  • Account deletion status
  • Export history
  • Active sessions (device info, IP addresses, authentication provider used)
GDPR Compliance

This feature complies 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 structured, machine-readable format

Activity Log (Audit Events)

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

Queries

Get My Activity Log

Get the activity log for the authenticated user.

Authentication: Required

query {
myAuditLog(page: 1, limit: 20, eventType: "login") {
events {
id
userId
eventType
resourceType
resourceId
actorId
ipAddress
userAgent
description
metadata
status
errorMessage
countryCode
countryName
city
region
sourceService
createdAt
}
totalCount
page
limit
totalPages
}
}

Parameters:

ParameterTypeRequiredDescription
pageIntNoPage number (default: 1)
limitIntNoItems per page (default: 20, max: 100)
eventTypeStringNoFilter by event type

Response:

{
"data": {
"myAuditLog": {
"events": [
{
"id": "81eebc99-9c0b-4ef8-bb6d-6bb9bd380a25",
"userId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"eventType": "login",
"resourceType": "session",
"resourceId": "e0eebc99-9c0b-4ef8-bb6d-6bb9bd380a15",
"actorId": null,
"ipAddress": "192.168.1.100",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
"description": "Signed in via Twitch",
"metadata": { "provider": "twitch" },
"status": "success",
"errorMessage": null,
"countryCode": "DE",
"countryName": "Germany",
"city": "Berlin",
"region": "Berlin",
"sourceService": "id",
"createdAt": "2025-01-05T10:30:00Z"
}
],
"totalCount": 150,
"page": 1,
"limit": 20,
"totalPages": 8
}
}
}
Location Data

Location fields (countryCode, countryName, city, region) are populated automatically from the IP address using the GeoIP database. If GeoIP is not configured, these fields will be null.

Event Types:

Event TypeDescription
loginSuccessful sign-in
login_failedFailed sign-in attempt
logoutUser signed out
session_createdNew session created
session_revokedSession revoked
all_sessions_revokedAll other sessions 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
deletion_scheduledAccount deletion scheduled
deletion_cancelledScheduled deletion cancelled
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
token_createdOAuth token created
token_revokedOAuth token revoked
account_linkedPlatform account linked
account_unlinkedPlatform account unlinked
account_reconnectedPlatform account reconnected
primary_account_changedPrimary account changed
data_exportedUser data exported
api_key_createdAPI key created
api_key_revokedAPI key revoked
user_bannedUser banned (admin action)
user_unbannedUser unbanned (admin action)
bot_command_executedBot command executed (Discord/Twitch)
bot_guild_configuredBot guild/server settings configured

Source Services:

The sourceService field identifies which application or service created the audit event:

ValueDescription
apiDirect API calls
idHeimdall ID webapp (account management)
backendBackend dashboard (admin panel)
policiesPolicies webapp
discord_botDiscord bot
twitch_botTwitch bot

Get User Audit Log (Admin)

Get the activity log for a specific user.

Authentication: Required Permission: audit:read

query {
userAuditLog(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", page: 1, limit: 20) {
events {
id
eventType
description
status
createdAt
}
totalCount
page
limit
totalPages
}
}

Get All Audit Events (Admin)

Get all audit events across the system.

Authentication: Required Permission: audit:read

query {
allAuditEvents(page: 1, limit: 50, eventType: "login", userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
events {
id
userId
eventType
description
metadata
ipAddress
countryCode
countryName
city
status
sourceService
createdAt
}
totalCount
page
limit
totalPages
}
}

Parameters:

ParameterTypeRequiredDescription
pageIntNoPage number (default: 1)
limitIntNoItems per page (default: 50, max: 100)
eventTypeStringNoFilter by event type
userIdStringNoFilter by user ID
Activity Log in Data Export

Audit events are also included in the GDPR data export. When users download their data export, their complete activity log is included with anonymized IP addresses.

Report Suspicious Activity

Users can report suspicious activity in their audit log if they notice something they don't recognize.

Report an Audit Event

Report an audit event as suspicious activity.

Authentication: Required

mutation {
reportAuditEvent(input: {
auditEventId: "81eebc99-9c0b-4ef8-bb6d-6bb9bd380a25"
reason: "not_me"
description: "I didn't make this login"
}) {
success
report {
id
auditEventId
reason
description
status
createdAt
}
error
}
}

Input Parameters:

ParameterTypeRequiredDescription
auditEventIdStringYesThe audit event ID to report
reasonStringYesReason for reporting (see table below)
descriptionStringNoAdditional details

Report Reasons:

ReasonDescription
not_meThe activity wasn't performed by the user
suspiciousGeneral suspicious activity
unknown_deviceActivity from an unknown device
unknown_locationActivity from an unknown location
otherOther concern

Response:

{
"data": {
"reportAuditEvent": {
"success": true,
"report": {
"id": "91eebc99-9c0b-4ef8-bb6d-6bb9bd380a26",
"auditEventId": "81eebc99-9c0b-4ef8-bb6d-6bb9bd380a25",
"reason": "not_me",
"description": "I didn't make this login",
"status": "pending",
"createdAt": "2025-01-05T12:00:00Z"
},
"error": null
}
}
}
Email Confirmation

Users receive a confirmation email when they submit a report, and another email when an admin updates the report status.

Get My Audit Reports

Get all audit reports submitted by the current user.

Authentication: Required

mutation {
myAuditReports {
id
auditEventId
reason
description
status
reviewedBy
reviewedAt
resolutionNotes
createdAt
updatedAt
}
}

Response:

{
"data": {
"myAuditReports": [
{
"id": "91eebc99-9c0b-4ef8-bb6d-6bb9bd380a26",
"auditEventId": "81eebc99-9c0b-4ef8-bb6d-6bb9bd380a25",
"reason": "not_me",
"description": "I didn't make this login",
"status": "resolved",
"reviewedBy": "admin_123",
"reviewedAt": "2025-01-05T14:00:00Z",
"resolutionNotes": "Confirmed unauthorized access. Password reset initiated.",
"createdAt": "2025-01-05T12:00:00Z",
"updatedAt": "2025-01-05T14:00:00Z"
}
]
}
}

Admin: Manage Audit Reports

Administrators can view and manage audit reports submitted by users.

Get All Audit Reports (Admin)

Get all audit reports across the system.

Authentication: Required Permission: audit:read

mutation {
allAuditReports(page: 1, limit: 20, status: "pending") {
reports {
id
auditEventId
userId
reason
description
status
reviewedBy
reviewedAt
resolutionNotes
createdAt
updatedAt
}
totalCount
page
limit
totalPages
}
}

Parameters:

ParameterTypeRequiredDescription
pageIntNoPage number (default: 1)
limitIntNoItems per page (default: 20, max: 100)
statusStringNoFilter by status (pending, reviewed, resolved, dismissed)

Update Audit Report (Admin)

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

Authentication: Required Permission: audit:write

mutation {
updateAuditReport(input: {
reportId: "91eebc99-9c0b-4ef8-bb6d-6bb9bd380a26"
status: "resolved"
resolutionNotes: "Confirmed unauthorized access. Password reset initiated and all sessions revoked."
}) {
success
report {
id
status
reviewedBy
reviewedAt
resolutionNotes
updatedAt
}
error
}
}

Input Parameters:

ParameterTypeRequiredDescription
reportIdStringYesThe report ID to update
statusStringYesNew status (reviewed, resolved, dismissed)
resolutionNotesStringNoNotes about the resolution

Report Statuses:

StatusDescription
pendingReport submitted, awaiting review
reviewedReport is being investigated
resolvedInvestigation complete, action taken
dismissedReport determined to be legitimate activity

Response:

{
"data": {
"updateAuditReport": {
"success": true,
"report": {
"id": "91eebc99-9c0b-4ef8-bb6d-6bb9bd380a26",
"status": "resolved",
"reviewedBy": "admin_123",
"reviewedAt": "2025-01-05T14:00:00Z",
"resolutionNotes": "Confirmed unauthorized access. Password reset initiated and all sessions revoked.",
"updatedAt": "2025-01-05T14:00:00Z"
},
"error": null
}
}
}
Email Notifications

When an admin updates a report status, the user automatically receives an email notification with the new status and any resolution notes.

Devices

Manage GPS tracking devices.

Queries

List Devices

Get a paginated list of all devices.

Authentication: Required Permission: devices:read

query {
devices(page: 1, limit: 20) {
data {
id
name
description
deviceType
isActive
createdAt
updatedAt
apiKey {
id
keyPrefix
name
isActive
}
}
total
page
limit
totalPages
}
}

Response:

{
"data": {
"devices": {
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "GPS Tracker 1",
"description": "Main vessel tracker",
"deviceType": "gps_tracker",
"isActive": true,
"createdAt": "2026-01-15T10:00:00Z",
"updatedAt": "2026-01-15T10:00:00Z",
"apiKey": {
"id": "ak_123",
"keyPrefix": "hm_abc12345...",
"name": "Tracker Key",
"isActive": true
}
}
],
"total": 5,
"page": 1,
"limit": 20,
"totalPages": 1
}
}
}

Get Device by ID

Get a specific device by its ID.

Authentication: Required Permission: devices:read

query {
device(id: "550e8400-e29b-41d4-a716-446655440000") {
id
name
description
deviceType
isActive
createdAt
updatedAt
apiKey {
id
keyPrefix
name
}
}
}

Mutations

Create Device

Create a new device. Optionally link an existing API key.

Authentication: Required Permission: devices:write

mutation {
createDevice(input: {
name: "GPS Tracker 1"
description: "Main vessel tracker"
deviceType: "gps_tracker"
apiKeyId: "ak_550e8400-e29b-41d4-a716-446655440000"
}) {
id
name
description
deviceType
isActive
apiKey {
id
keyPrefix
name
}
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringYesDevice name
descriptionStringNoDevice description
deviceTypeStringNoDevice type identifier
apiKeyIdUUIDNoLink an existing API key to this device

Update Device

Update an existing device.

Authentication: Required Permission: devices:write

mutation {
updateDevice(id: "550e8400-e29b-41d4-a716-446655440000", input: {
name: "Updated Tracker"
description: "Updated description"
isActive: false
apiKeyId: "ak_new-key-id"
}) {
id
name
description
deviceType
isActive
updatedAt
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringNoNew device name
descriptionStringNoNew description
deviceTypeStringNoNew device type
isActiveBooleanNoEnable or disable the device
apiKeyIdUUIDNoReassign API key to this device

Delete Device

Delete a device permanently.

Authentication: Required Permission: devices:delete

mutation {
deleteDevice(id: "550e8400-e29b-41d4-a716-446655440000")
}

Response:

{
"data": {
"deleteDevice": true
}
}

Trips

Manage GPS tracking trips with status lifecycle, GPS data association, and location references.

Queries

List Trips

Get a paginated list of trips, optionally filtered by status.

Authentication: Required Permission: trips:read

query {
trips(page: 1, limit: 20, status: "active") {
data {
id
name
description
status
gpsLoggingEnabled
deviceId
categoryId
originId
destinationId
weightKg
weightTons
notes
tags
startLatitude
startLongitude
endLatitude
endLongitude
startedAt
endedAt
createdAt
updatedAt
gpsPointCount
category {
id
name
color
}
origin {
id
name
city
}
destination {
id
name
city
}
device {
id
name
}
}
total
page
limit
totalPages
}
}

Parameters:

ParameterTypeDefaultDescription
pageInt1Page number
limitInt20Items per page
statusStringnullFilter by status (draft, active, paused, completed)

Response:

{
"data": {
"trips": {
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Hamburg to Rotterdam",
"description": "Weekly cargo run",
"status": "active",
"gpsLoggingEnabled": true,
"weightKg": 25000.0,
"weightTons": 25.0,
"tags": ["cargo", "weekly"],
"gpsPointCount": 1450,
"startedAt": "2026-06-20T08:00:00Z",
"category": {
"id": "cat-123",
"name": "Cargo Transport",
"color": "#3B82F6"
},
"origin": {
"id": "loc-1",
"name": "Hamburg Port",
"city": "Hamburg"
},
"destination": {
"id": "loc-2",
"name": "Rotterdam Port",
"city": "Rotterdam"
},
"device": {
"id": "dev-1",
"name": "GPS Tracker 1"
}
}
],
"total": 42,
"page": 1,
"limit": 20,
"totalPages": 3
}
}
}

Get Trip by ID

Get a single trip with all resolved relations.

Authentication: Required Permission: trips:read

query {
trip(id: "550e8400-e29b-41d4-a716-446655440000") {
id
name
description
status
gpsLoggingEnabled
weightKg
weightTons
notes
tags
startLatitude
startLongitude
endLatitude
endLongitude
startedAt
endedAt
gpsPointCount
category { id name color }
origin { id name city country }
destination { id name city country }
device { id name isActive }
}
}

Get Trip GPS Data

Get paginated GPS data points for a specific trip (queries TimescaleDB). Page 1 results are cached in Redis.

Authentication: Required Permission: trips:read

query {
tripGpsData(tripId: "550e8400-e29b-41d4-a716-446655440000", page: 1, limit: 100) {
data {
id
deviceId
tripId
latitude
longitude
altitude
heading
timestamp
speedKmh
speedMps
speedMph
speedKnots
pdop
hdop
vdop
createdAt
}
total
page
limit
totalPages
}
}

Parameters:

ParameterTypeDefaultDescription
tripIdUUID-Trip ID (required)
pageInt1Page number
limitInt20Items per page (max 100)

Get Trip Status History

Get the status transition history for a trip, including coordinates where status changes occurred.

Authentication: Required Permission: trips:read

query {
tripStatusHistory(tripId: "550e8400-e29b-41d4-a716-446655440000") {
id
tripId
oldStatus
newStatus
changedBy
latitude
longitude
changedAt
}
}

Response:

{
"data": {
"tripStatusHistory": [
{
"id": "hist-1",
"tripId": "550e8400-e29b-41d4-a716-446655440000",
"oldStatus": "draft",
"newStatus": "active",
"changedBy": "user-123",
"latitude": 53.5511,
"longitude": 9.9937,
"changedAt": "2026-06-20T08:00:00Z"
},
{
"id": "hist-2",
"tripId": "550e8400-e29b-41d4-a716-446655440000",
"oldStatus": "active",
"newStatus": "completed",
"changedBy": "user-123",
"latitude": 51.9225,
"longitude": 4.4792,
"changedAt": "2026-06-21T16:30:00Z"
}
]
}
}

Mutations

Create Trip

Create a new trip (status defaults to draft).

Authentication: Required Permission: trips:write

mutation {
createTrip(input: {
name: "Hamburg to Rotterdam"
description: "Weekly cargo run"
gpsLoggingEnabled: true
deviceId: "dev-1"
categoryId: "cat-123"
originId: "loc-1"
destinationId: "loc-2"
weightKg: 25000.0
notes: "Fragile cargo"
tags: ["cargo", "weekly"]
}) {
id
name
status
gpsLoggingEnabled
device { id name }
category { id name }
origin { id name }
destination { id name }
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringYesTrip name
descriptionStringNoTrip description
gpsLoggingEnabledBooleanNoEnable GPS logging (default: false)
deviceIdUUIDNoDevice to associate with this trip
categoryIdUUIDNoTrip category
originIdUUIDNoOrigin location
destinationIdUUIDNoDestination location
weightKgFloatNoCargo weight in kg
notesStringNoAdditional notes
tags[String]NoTags for filtering

Update Trip

Update a trip including status transitions. When changing status, coordinates can be recorded for the status history.

Authentication: Required Permission: trips:write

mutation {
updateTrip(id: "550e8400-e29b-41d4-a716-446655440000", input: {
status: "active"
startLatitude: 53.5511
startLongitude: 9.9937
historyLatitude: 53.5511
historyLongitude: 9.9937
}) {
id
name
status
startedAt
startLatitude
startLongitude
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringNoNew trip name
descriptionStringNoNew description
statusStringNoNew status (triggers transition validation)
gpsLoggingEnabledBooleanNoToggle GPS logging
deviceIdUUIDNoReassign device
categoryIdUUIDNoChange category
originIdUUIDNoChange origin location
destinationIdUUIDNoChange destination location
weightKgFloatNoUpdate cargo weight
notesStringNoUpdate notes
tags[String]NoUpdate tags
startLatitudeFloatNoStart position latitude
startLongitudeFloatNoStart position longitude
endLatitudeFloatNoEnd position latitude
endLongitudeFloatNoEnd position longitude
historyLatitudeFloatNoLatitude for status history entry
historyLongitudeFloatNoLongitude for status history entry

Status Transitions:

FromToNotes
draftactiveSets startedAt timestamp
activepaused-
pausedactive-
activecompletedSets endedAt timestamp
pausedcompletedSets endedAt timestamp
GPS Logging Conflict

When activating a trip with gpsLoggingEnabled: true, no other active trip for the same device can have GPS logging enabled. The mutation will return an error if a conflict exists.

Delete Trip

Delete a trip. Only trips with status draft or completed can be deleted. For completed trips, associated GPS data is unlinked (not deleted).

Authentication: Required Permission: trips:delete

mutation {
deleteTrip(id: "550e8400-e29b-41d4-a716-446655440000")
}

Response:

{
"data": {
"deleteTrip": true
}
}
Deletion Rules
  • draft trips: deleted immediately
  • completed trips: GPS data is unlinked (trip_id set to NULL), then the trip is deleted
  • active or paused trips: cannot be deleted (complete them first)

Locations

Manage locations (ports, warehouses, etc.) with optional contacts.

Queries

List Locations

Get a paginated list of locations (ordered by name).

Authentication: Required Permission: locations:read

query {
locations(page: 1, limit: 20) {
data {
id
name
street
zip
city
country
latitude
longitude
notes
isActive
createdAt
updatedAt
contacts {
id
name
phone
email
role
}
}
total
page
limit
totalPages
}
}

Response:

{
"data": {
"locations": {
"data": [
{
"id": "loc-1",
"name": "Hamburg Port",
"street": "Hafenstrasse 1",
"zip": "20457",
"city": "Hamburg",
"country": "DE",
"latitude": 53.5411,
"longitude": 9.9837,
"notes": "Gate 3 for cargo",
"isActive": true,
"contacts": [
{
"id": "contact-1",
"name": "John Doe",
"phone": "+49 40 12345678",
"email": "john@port.example.com",
"role": "Dock Manager"
}
]
}
],
"total": 15,
"page": 1,
"limit": 20,
"totalPages": 1
}
}
}

Get Location by ID

Get a specific location with its contacts.

Authentication: Required Permission: locations:read

query {
location(id: "loc-1") {
id
name
street
zip
city
country
latitude
longitude
notes
isActive
contacts {
id
name
phone
email
role
}
}
}

Mutations

Create Location

Create a new location. Can include inline contacts.

Authentication: Required Permission: locations:write

mutation {
createLocation(input: {
name: "Hamburg Port"
street: "Hafenstrasse 1"
zip: "20457"
city: "Hamburg"
country: "DE"
latitude: 53.5411
longitude: 9.9837
notes: "Gate 3 for cargo"
contacts: [
{
name: "John Doe"
phone: "+49 40 12345678"
email: "john@port.example.com"
role: "Dock Manager"
}
]
}) {
id
name
city
contacts {
id
name
role
}
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringYesLocation name
streetStringNoStreet address
zipStringNoPostal code
cityStringNoCity
countryStringNoCountry code
latitudeFloatNoGPS latitude
longitudeFloatNoGPS longitude
notesStringNoAdditional notes
contacts[ContactInput]NoInline contacts to create

Contact Input Fields:

FieldTypeRequiredDescription
nameStringYesContact name
phoneStringNoPhone number
emailStringNoEmail address
roleStringNoContact role/title

Update Location

Update an existing location.

Authentication: Required Permission: locations:write

mutation {
updateLocation(id: "loc-1", input: {
name: "Hamburg Port - Terminal A"
notes: "Updated gate info"
isActive: true
}) {
id
name
notes
updatedAt
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringNoNew location name
streetStringNoNew street address
zipStringNoNew postal code
cityStringNoNew city
countryStringNoNew country code
latitudeFloatNoNew latitude
longitudeFloatNoNew longitude
notesStringNoNew notes
isActiveBooleanNoEnable or disable the location

Delete Location

Delete a location (cascades to contacts via database FK).

Authentication: Required Permission: locations:delete

mutation {
deleteLocation(id: "loc-1")
}

Response:

{
"data": {
"deleteLocation": true
}
}

Add Location Contact

Add a contact to an existing location.

Authentication: Required Permission: locations:write

mutation {
addLocationContact(locationId: "loc-1", input: {
name: "Jane Smith"
phone: "+49 40 87654321"
email: "jane@port.example.com"
role: "Operations Lead"
}) {
id
locationId
name
phone
email
role
createdAt
}
}

Update Location Contact

Update an existing location contact.

Authentication: Required Permission: locations:write

mutation {
updateLocationContact(id: "contact-1", input: {
phone: "+49 40 99999999"
role: "Senior Dock Manager"
}) {
id
name
phone
role
}
}

Delete Location Contact

Remove a contact from a location.

Authentication: Required Permission: locations:write

mutation {
deleteLocationContact(id: "contact-1")
}

Response:

{
"data": {
"deleteLocationContact": true
}
}

Geofences

Manage circular geofences for proximity-based alerts. When a GPS data point is created, the system checks all active geofences and broadcasts enter/exit events via WebSocket.

Queries

List Geofences

Get a paginated list of geofences.

Authentication: Required Permission: geofences:read

query {
geofences(page: 1, limit: 20) {
data {
id
name
description
latitude
longitude
radiusKm
isActive
metadata
createdAt
updatedAt
}
total
page
limit
totalPages
}
}

Response:

{
"data": {
"geofences": {
"data": [
{
"id": "fence-1",
"name": "Hamburg Port Zone",
"description": "Alert when entering Hamburg port area",
"latitude": 53.5411,
"longitude": 9.9837,
"radiusKm": 2.5,
"isActive": true,
"metadata": { "type": "port", "berth": "A3" },
"createdAt": "2026-01-10T08:00:00Z",
"updatedAt": "2026-01-10T08:00:00Z"
}
],
"total": 8,
"page": 1,
"limit": 20,
"totalPages": 1
}
}
}

Get Geofence by ID

Get a specific geofence by its ID.

Authentication: Required Permission: geofences:read

query {
geofence(id: "fence-1") {
id
name
description
latitude
longitude
radiusKm
isActive
metadata
createdAt
updatedAt
}
}

Mutations

Create Geofence

Create a new geofence.

Authentication: Required Permission: geofences:write

mutation {
createGeofence(input: {
name: "Hamburg Port Zone"
description: "Alert when entering Hamburg port area"
latitude: 53.5411
longitude: 9.9837
radiusKm: 2.5
metadata: { "type": "port", "berth": "A3" }
}) {
id
name
latitude
longitude
radiusKm
isActive
metadata
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringYesGeofence name
descriptionStringNoDescription
latitudeFloatYesCenter latitude
longitudeFloatYesCenter longitude
radiusKmFloatYesRadius in kilometers
metadataJSONNoArbitrary JSONB metadata

Update Geofence

Update an existing geofence.

Authentication: Required Permission: geofences:write

mutation {
updateGeofence(id: "fence-1", input: {
radiusKm: 3.0
isActive: false
metadata: { "type": "port", "berth": "A3", "note": "expanded zone" }
}) {
id
name
radiusKm
isActive
metadata
updatedAt
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringNoNew name
descriptionStringNoNew description
latitudeFloatNoNew center latitude
longitudeFloatNoNew center longitude
radiusKmFloatNoNew radius in km
isActiveBooleanNoEnable or disable
metadataJSONNoReplace metadata

Delete Geofence

Delete a geofence permanently.

Authentication: Required Permission: geofences:delete

mutation {
deleteGeofence(id: "fence-1")
}

Response:

{
"data": {
"deleteGeofence": true
}
}
WebSocket Events

When a GPS point enters or exits an active geofence, a WebSocket event is broadcast on the geofence channel:

{
"type": "geofence_event",
"data": {
"geofence_id": "fence-1",
"geofence_name": "Hamburg Port Zone",
"event": "enter",
"device_id": "tracker-1",
"distance_km": 1.2,
"metadata": { "type": "port", "berth": "A3" }
}
}

Trip Categories

Manage hierarchical categories for organizing trips.

Queries

List Trip Categories

Get all trip categories as a flat list. The client builds the tree structure using parentId.

Authentication: Required Permission: trip_categories:read

query {
tripCategories {
id
parentId
name
description
color
isActive
sortOrder
createdAt
updatedAt
}
}

Response:

{
"data": {
"tripCategories": [
{
"id": "cat-1",
"parentId": null,
"name": "Cargo Transport",
"description": "All cargo-related trips",
"color": "#3B82F6",
"isActive": true,
"sortOrder": 0,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
},
{
"id": "cat-2",
"parentId": "cat-1",
"name": "Container Shipping",
"description": "Container cargo trips",
"color": "#10B981",
"isActive": true,
"sortOrder": 1,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
}
]
}
}

Get Trip Category by ID

Get a specific trip category.

Authentication: Required Permission: trip_categories:read

query {
tripCategory(id: "cat-1") {
id
parentId
name
description
color
isActive
sortOrder
}
}

Mutations

Create Trip Category

Create a new trip category. Use parentId to create subcategories.

Authentication: Required Permission: trip_categories:write

mutation {
createTripCategory(input: {
name: "Cargo Transport"
description: "All cargo-related trips"
color: "#3B82F6"
parentId: null
sortOrder: 0
}) {
id
name
color
isActive
sortOrder
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringYesCategory name
descriptionStringNoCategory description
colorStringNoHex color code for UI display
parentIdUUIDNoParent category ID (for subcategories)
sortOrderIntNoSort order (default: 0)

Update Trip Category

Update an existing trip category.

Authentication: Required Permission: trip_categories:write

mutation {
updateTripCategory(id: "cat-1", input: {
name: "Cargo & Freight"
color: "#2563EB"
isActive: true
sortOrder: 1
}) {
id
name
color
isActive
sortOrder
updatedAt
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringNoNew name
descriptionStringNoNew description
colorStringNoNew color
isActiveBooleanNoEnable or disable
sortOrderIntNoNew sort order

Delete Trip Category

Delete a trip category. Child categories are cascaded via database FK.

Authentication: Required Permission: trip_categories:delete

mutation {
deleteTripCategory(id: "cat-1")
}

Response:

{
"data": {
"deleteTripCategory": true
}
}

Trip Templates

Manage reusable trip templates for quick trip creation with pre-filled values.

Queries

List Trip Templates

Get all trip templates with resolved relations.

Authentication: Required Permission: trips:read

query {
tripTemplates {
id
name
description
gpsLoggingEnabled
defaultWeightKg
notes
tags
createdAt
updatedAt
category {
id
name
color
}
origin {
id
name
city
}
destination {
id
name
city
}
device {
id
name
}
}
}

Response:

{
"data": {
"tripTemplates": [
{
"id": "tmpl-1",
"name": "Hamburg-Rotterdam Standard",
"description": "Standard cargo run template",
"gpsLoggingEnabled": true,
"defaultWeightKg": 20000.0,
"notes": "Use gate 3",
"tags": ["cargo", "standard"],
"category": {
"id": "cat-1",
"name": "Cargo Transport",
"color": "#3B82F6"
},
"origin": {
"id": "loc-1",
"name": "Hamburg Port",
"city": "Hamburg"
},
"destination": {
"id": "loc-2",
"name": "Rotterdam Port",
"city": "Rotterdam"
},
"device": {
"id": "dev-1",
"name": "GPS Tracker 1"
}
}
]
}
}

Get Trip Template by ID

Get a specific trip template with resolved relations.

Authentication: Required Permission: trips:read

query {
tripTemplate(id: "tmpl-1") {
id
name
description
categoryId
originId
destinationId
deviceId
gpsLoggingEnabled
defaultWeightKg
notes
tags
category { id name }
origin { id name city }
destination { id name city }
device { id name }
}
}

Mutations

Create Trip Template

Create a new trip template.

Authentication: Required Permission: trips:write

mutation {
createTripTemplate(input: {
name: "Hamburg-Rotterdam Standard"
description: "Standard cargo run template"
categoryId: "cat-1"
originId: "loc-1"
destinationId: "loc-2"
deviceId: "dev-1"
gpsLoggingEnabled: true
defaultWeightKg: 20000.0
notes: "Use gate 3"
tags: ["cargo", "standard"]
}) {
id
name
category { id name }
origin { id name }
destination { id name }
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringYesTemplate name
descriptionStringNoTemplate description
categoryIdUUIDNoDefault trip category
originIdUUIDNoDefault origin location
destinationIdUUIDNoDefault destination location
deviceIdUUIDNoDefault device
gpsLoggingEnabledBooleanNoDefault GPS logging (default: false)
defaultWeightKgFloatNoDefault cargo weight in kg
notesStringNoDefault notes
tags[String]NoDefault tags

Update Trip Template

Update an existing trip template.

Authentication: Required Permission: trips:write

mutation {
updateTripTemplate(id: "tmpl-1", input: {
name: "Hamburg-Rotterdam Express"
defaultWeightKg: 25000.0
tags: ["cargo", "express"]
}) {
id
name
defaultWeightKg
tags
updatedAt
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringNoNew name
descriptionStringNoNew description
categoryIdUUIDNoNew category
originIdUUIDNoNew origin
destinationIdUUIDNoNew destination
deviceIdUUIDNoNew device
gpsLoggingEnabledBooleanNoToggle GPS logging
defaultWeightKgFloatNoNew default weight
notesStringNoNew notes
tags[String]NoNew tags

Delete Trip Template

Delete a trip template permanently.

Authentication: Required Permission: trips:delete

mutation {
deleteTripTemplate(id: "tmpl-1")
}

Response:

{
"data": {
"deleteTripTemplate": true
}
}

Create Trip from Template

Create a new trip pre-filled from a template. The weightKg parameter is required and overrides the template's default weight.

Authentication: Required Permission: trips:write

mutation {
createTripFromTemplate(templateId: "tmpl-1", weightKg: 22000.0) {
id
name
status
weightKg
gpsLoggingEnabled
category { id name }
origin { id name }
destination { id name }
device { id name }
}
}

Response:

{
"data": {
"createTripFromTemplate": {
"id": "trip-new-123",
"name": "Hamburg-Rotterdam Standard (from template)",
"status": "draft",
"weightKg": 22000.0,
"gpsLoggingEnabled": true,
"category": {
"id": "cat-1",
"name": "Cargo Transport"
},
"origin": {
"id": "loc-1",
"name": "Hamburg Port"
},
"destination": {
"id": "loc-2",
"name": "Rotterdam Port"
},
"device": {
"id": "dev-1",
"name": "GPS Tracker 1"
}
}
}
}
Template Workflow

Templates are ideal for recurring routes. Create a template once with default values (device, category, origin, destination, GPS settings), then use createTripFromTemplate to quickly spawn new trips with just the weight specified.

System Config

Pre-seeded configuration values (e.g. GPS settings). Update-only — no creation or deletion.

Queries

List System Configs

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

Authentication: Required Permission: settings:read

query {
systemConfigs(prefix: "gps.") {
key
value
description
updatedAt
}
}

Get System Config by Key

Authentication: Required Permission: settings:read

query {
systemConfig(key: "gps.update_interval_seconds") {
key
value
description
updatedAt
}
}

Mutations

Update System Config

Authentication: Required Permission: settings:write

mutation {
updateSystemConfig(key: "gps.update_interval_seconds", value: 60) {
key
value
description
updatedAt
}
}

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.

Queries

Get Nearest Station

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

Authentication: Required Permission: pegel:read

query {
pegelNearest(water: "RHEIN", radius: 50) {
stationId
shortname
longname
waterShortname
waterLongname
latitude
longitude
km
agency
levelCm
trend
distanceKm
}
}

Get Stations

List water level stations, optionally filtered by water body.

Authentication: Required Permission: pegel:read

query {
pegelStations(water: "RHEIN") {
stationId
shortname
waterShortname
levelCm
trend
distanceKm
}
}

Returns an empty array if the Pegelonline service is not configured.

Additional Mutations

Resend the verification email for a pending email account link.

mutation {
resendLinkEmailVerification(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
message
expiresAt
}
}

Generated Schema

The GraphQL schema is automatically generated using the generate-schema binary:

cargo run --bin generate-schema

This generates schema.graphql in the project root, which can be used for:

  • Client-side code generation
  • Schema validation
  • Documentation
  • IDE auto-completion

Next Steps