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/graphiqlGETNo[graphql].playgroundInteractive GraphiQL IDE
/v1/schemaGETNo[graphql].schema_sdl (or playground)GraphQL schema in SDL format

GraphiQL Interactive IDE​

The interactive GraphiQL IDE is gated by the [graphql].playground config flag (env override HEIMDALL__GRAPHQL__PLAYGROUND):

Development: http://localhost:3000/v1/graphiql
Config-gated

GraphiQL is served only when [graphql].playground = true. The shipped default.toml (dev) enables it; production.toml disables it. When disabled the endpoint returns 404 Not Found. See Configuration → [graphql].

GraphQL Schema (SDL)​

The /v1/schema endpoint returns the schema in SDL format (text/plain). It is gated by [graphql].schema_sdl (env override HEIMDALL__GRAPHQL__SCHEMA_SDL) and is also served whenever playground = true (GraphiQL fetches it to build its schema). Disabled → 404 Not Found. Shipped production.toml/staging config disables the SDL export unless playground is on.

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 { userId apiKeyId roles permissions isSuperAdmin } }"}'

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 {
userId
apiKeyId
roles
permissions
isSuperAdmin
}
}

Response:

{
"data": {
"me": {
"userId": "550e8400-e29b-41d4-a716-446655440000",
"apiKeyId": null,
"roles": ["Admin"],
"permissions": ["*:*"],
"isSuperAdmin": 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 {
listGpsData(page: 1, limit: 10) {
data {
id
deviceId
tripId
latitude
longitude
altitude
heading
timestamp
speedKmh
speedMps
speedMph
speedKnots
pdop
hdop
vdop
createdAt
}
total
page
limit
totalPages
}
}

Response:

{
"data": {
"listGpsData": {
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"deviceId": "my-tracker-1",
"tripId": null,
"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,
"pdop": 1.2,
"hdop": 0.8,
"vdop": 0.9,
"createdAt": "2025-11-24T10:00:00Z"
}
],
"total": 100,
"page": 1,
"limit": 10,
"totalPages": 10
}
}
}

Get GPS by ID​

Retrieve a specific GPS data point by ID.

query {
gpsData(id: "550e8400-e29b-41d4-a716-446655440000") {
id
deviceId
tripId
latitude
longitude
altitude
heading
timestamp
speedKmh
speedMps
speedMph
speedKnots
pdop
hdop
vdop
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
roleIdStringnullOnly return users holding this role id (e.g. "role_developer"). Useful for populating a developer-user picker when assigning API keys / OAuth clients.
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
}
}

List Developer-Role Users Only​

query {
users(roleId: "role_developer") {
users {
id
username
}
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") {
roles
roleIds
permissions
isSuperAdmin
}
}

Response:

{
"data": {
"userPermissions": {
"roles": ["Admin"],
"roleIds": ["role_admin"],
"permissions": ["users:read", "users:write", "gps:read", "*:*"],
"isSuperAdmin": true
}
}
}

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 Permission: gps:write

The input timestamp is a Unix epoch integer (seconds). The returned timestamp is an ISO 8601 DateTime. All four speedKmh/speedMps/speedMph/speedKnots fields are required on create.

mutation {
createGpsData(input: {
latitude: 51.5074
longitude: -0.1278
altitude: 11.0
heading: 270.0
timestamp: 1700000000
speedKmh: 72.4
speedMps: 20.1
speedMph: 45.0
speedKnots: 39.1
}) {
id
deviceId
tripId
latitude
longitude
altitude
heading
timestamp
speedKmh
speedMps
speedMph
speedKnots
pdop
hdop
vdop
createdAt
}
}

Response:

{
"data": {
"createGpsData": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"deviceId": null,
"tripId": null,
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 11.0,
"heading": 270.0,
"timestamp": "2023-11-14T22:13:20Z",
"speedKmh": 72.4,
"speedMps": 20.1,
"speedMph": 45.0,
"speedKnots": 39.1,
"pdop": null,
"hdop": null,
"vdop": null,
"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!) {
gpsData(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": ["gpsData"]
}
],
"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
setupPlatform
backupCodesRemaining
}
}

Parameters:

ParameterTypeRequiredDescription
userIdStringNoUser ID (defaults to current user)

Response:

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

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"
}) {
backupCodes
message
platformSlug
}
}

Input:

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

Response:

{
"data": {
"verifyTwoFactor": {
"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 has been enabled successfully.",
"platformSlug": "email"
}
}
}
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"
})
}

This mutation returns a Boolean (true on success).

Input:

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

Response:

{
"data": {
"disableTwoFactor": true
}
}

Regenerate Backup Codes​

Generate new backup codes (invalidates all previous codes).

Authentication: Required

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

Input:

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

Response:

{
"data": {
"regenerateBackupCodes": {
"backupCodes": [
"ABCD-EFGH-IJKL",
"MNOP-QRST-UVWX",
"..."
],
"message": "Backup codes have been regenerated. Your old codes are no longer valid.",
"platformSlug": "email"
}
}
}
Previous Codes Invalidated

Regenerating backup codes invalidates all previous backup codes immediately.

Verify 2FA During Login​

Verify a 2FA code during the login flow and complete authentication. Called after an initial login returns requires_2fa = true. Does not require authentication — it is authorized by the temporary token (temp_token, prefixed 2fa_) issued by the initial login.

Authentication: Not required (uses temp_token from initial login)

mutation {
verifyTwoFactorLogin(input: {
tempToken: "2fa_abcdef..."
code: "123456"
}) {
userId
email
username
sessionToken
sessionId
expiresAt
provider
preferredLocale
}
}

Input:

FieldTypeRequiredDescription
tempTokenString!YesTemporary token from initial login (starts with 2fa_)
codeString!YesTOTP code from authenticator app or a backup code

On success this returns a full session token (sessionToken) for authenticated requests; the temporary token is consumed.

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)
ownerUserIdIDNoOptional target owner. Super admin only — a super admin may assign the client to another user, but that user must hold the role_developer role. Errors with "Only super admins can assign to another user" (non-super-admin) or "Target user must have the developer role" otherwise. Defaults to the creator when omitted.

Update OAuth Client​

Update an existing OAuth client. A super admin may reassign ownership via ownerUserId (same developer-role requirement and error messages as create); omitting it leaves the owner unchanged.

mutation {
updateOauthClient(
id: "client-123"
input: {
name: "Updated Name"
description: "Updated description"
redirectUris: ["https://example.com/new-callback"]
ownerUserId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
) {
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). Revokes that client's access + refresh tokens for the current user and deletes the consent record — the user must re-authorize before the app can be used again. Broadcasts an oAuthConsentRevoked WebSocket message.

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

Returns: Boolean! — true on success.

Auth: requires an authenticated user session.

Logout vs. Disconnect

revokeOauthConsent deletes consent (re-authorization required). To sign a user out of one app or everywhere while keeping consent (silent re-login, no re-consent), use logoutApp / logoutEverywhere below. See the Global SSO Logout kit for the client-side helpers.

Logout Everywhere (Global SSO Logout)​

Revoke all of the current user's OAuth access + refresh tokens across all clients. Consent is kept (no forced re-consent). Broadcasts a forceLogout WebSocket message so every open client app signs out instantly.

mutation {
logoutEverywhere(reason: "Signed out from account settings")
}

Arguments:

ArgumentTypeRequiredDescription
reasonStringNoHuman-readable reason forwarded in the forceLogout WS message. Defaults to sso_logout in the audit log.

Returns: Boolean! — true on success.

Auth: requires an authenticated user session.

REST equivalent: POST /v1/auth/logout-everywhere.

Logout App (Per-Client SSO Logout)​

Revoke the current user's OAuth access + refresh tokens for a single client. Consent is kept — distinct from revokeOauthConsent, which deletes consent. Broadcasts an appLogout WebSocket message so that open client app signs out instantly.

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

Arguments:

ArgumentTypeRequiredDescription
clientIdString!YesThe OAuth client id (UUID) to log the current user out of.

Returns: Boolean! — true on success.

Auth: requires an authenticated user session.

REST equivalent: POST /v1/auth/logout-app.

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
isSystemBooleanNoCreate a system-level key (super admin only)
userIdIDNoOptional target owner. Super admin only — a super admin may assign the key to another user, but that user must hold the role_developer role. Errors with "Only super admins can assign to another user" (non-super-admin) or "Target user must have the developer role" otherwise. Defaults to the creator when omitted.

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
userIdIDNoReassign the key to another user. Super admin only, and the target must hold the role_developer role (same error messages as create). Omitting it leaves the owner unchanged.

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
}
}

Check Platform User Permission​

Look up a Heimdall user by their linked platform account and check whether they have a specific permission. Designed to be called by bots and integrations.

Authentication: Required (system API key) Permission: platforms:read

Arguments:

ArgumentTypeDescription
platformSlugString!Platform identifier (e.g., discord, twitch, github)
platformUserIdString!User's ID on that platform (e.g., a Discord snowflake)
permissionString!Permission to check (e.g., discord:bot.admin)
query {
checkPlatformUserPermission(
platformSlug: "discord"
platformUserId: "123456789012345678"
permission: "discord:bot.admin"
) {
isLinked
hasPermission
userId
}
}

Returns isLinked (whether the platform account is linked to a Heimdall user), hasPermission (false if not linked; supports *:* and resource wildcards such as discord:*), and userId (null if not linked).

Get Platform User Info​

Get basic info about a platform user's linked Heimdall account.

Authentication: Required (system API key) Permission: platforms:read

Arguments:

ArgumentTypeDescription
platformSlugString!Platform identifier (e.g., discord, twitch, github)
platformUserIdString!User's ID on that platform
query {
platformUserInfo(platformSlug: "twitch", platformUserId: "123456789") {
userId
username
avatarUrl
platformName
}
}

Returns null if the platform account is not linked to any Heimdall user.

Find User by Platform​

Find a Heimdall user ID by their platform account.

Authentication: Required (system API key) Permission: platforms:read

Arguments:

ArgumentTypeDescription
platformSlugString!Platform identifier (e.g., discord, twitch, github)
platformUserIdString!User's ID on that platform
query {
findUserByPlatform(platformSlug: "discord", platformUserId: "123456789012345678")
}

Returns the Heimdall user ID as a string, or null if not found.

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

Arguments: page (Int), limit (Int), and an optional category (String) that filters to a single device category server-side, before pagination (e.g. "ais_receiver", "gps_tracker"); omit it to list all categories. deviceType is a free label, no longer a filter.

query {
devices(page: 1, limit: 20, category: "ais_receiver") {
data {
id
name
description
deviceType
category
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",
"category": "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
category
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"
category: "gps_tracker"
apiKeyId: "ak_550e8400-e29b-41d4-a716-446655440000"
}) {
id
name
description
deviceType
category
isActive
apiKey {
id
keyPrefix
name
}
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringYesDevice name
descriptionStringNoDevice description
deviceTypeStringNoFree-text type label (e.g. hardware/model) — not a discriminator
categoryStringNoControlled discriminator (e.g. "gps_tracker", "ais_receiver", extensible); defaults to gps_tracker when omitted
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
category
isActive
updatedAt
}
}

Input Fields:

FieldTypeRequiredDescription
nameStringNoNew device name
descriptionStringNoNew description
deviceTypeStringNoNew free-text type label — not a discriminator
categoryStringNoNew discriminator (e.g. "gps_tracker", "ais_receiver")
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.

AIS Vessels​

Query the AIS vessel directory, live vessel detail, and per-MMSI static-history change-log. Read-only, gated ais:read. Full reference (data sources, field mapping, error codes): Vessels API.

Queries​

List Vessels​

Paginated vessel directory (static Postgres registry — position fields always null).

query {
vessels(filter: { name: "rena", shipTypeCode: 70 }, page: 1, limit: 20) {
data {
id
mmsi
name
callsign
shipType
navStatus
length
beam
lat
lng
heading
course
speedKmh
source
lastSeen
firstSeen
}
total
page
limit
totalPages
}
}

Get Vessel​

Vessel detail by MMSI. Redis hot-read first (live position), falls back to the static registry on a cache miss. Returns null if neither source has it.

query {
vessel(mmsi: 211234560) {
id
mmsi
name
lat
lng
navStatus
speedKmh
lastSeen
firstSeen
}
}

Get Vessel Static History​

Paginated static-data change-log for a single MMSI (TimescaleDB hypertable "VesselStaticHistory", DESC by recordedAt). Logs changes to name/callsign/ship-type/length/beam — not position history (positions are never persisted). Returns an empty page (not an error) for an unknown/never-changed MMSI.

query {
vesselHistory(mmsi: 211234560, page: 1, limit: 20) {
data {
id
mmsi
name
callsign
shipTypeCode
length
beam
source
deviceId
recordedAt
}
total
page
limit
totalPages
}
}

Arguments:

ArgumentTypeDefaultDescription
mmsiInt!—Vessel MMSI
pageInt1Page number (1-indexed)
limitInt20Items per page

GqlVesselHistoryEntry fields:

FieldTypeDescription
idUUID!History entry ID
mmsiInt!Vessel MMSI
nameStringVessel name at the time of the change
callsignString
shipTypeCodeIntExact numeric ITU ship-type code
shipTypeCodeMinIntInclusive lower bound of a ship-type code range (base-category filter, e.g. Cargo = 70)
shipTypeCodeMaxIntInclusive upper bound of the ship-type code range (e.g. Cargo = 79)
lengthIntMeters
beamIntMeters
sourceString!Data source identifier
deviceIdStringReporting device ID, if known
recordedAtDateTime!Vessel/event time of the change

PaginatedVesselHistory mirrors PaginatedVessel: data, total, page, limit, totalPages.

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​