GraphQL API
Heimdall provides a flexible GraphQL API for querying GPS data and system information.
Endpoints
The GraphQL API is available at multiple endpoints:
| Endpoint | Method | Auth Required | Availability | Description |
|---|---|---|---|---|
/v1/gql | POST/GET | Yes | All environments | GraphQL queries and mutations |
/v1/graphiql | GET | No | Development only | Interactive GraphiQL IDE |
/v1/schema | GET | No | All environments | GraphQL schema in SDL format |
GraphiQL Interactive IDE
Interactive GraphiQL IDE is available in development environments only:
Development: http://localhost:3000/v1/graphiql
GraphiQL is only accessible when APP_ENV is not set to production. In production environments, accessing this endpoint returns a 404 error. This is a security measure to prevent exposing the interactive IDE in production.
The GraphiQL IDE provides:
- Schema documentation browser
- Auto-completion for queries and fields
- Query validation and syntax highlighting
- Real-time query execution
- Query history
- Schema introspection
Note: To execute queries in GraphiQL, you'll need to add your Bearer token in the HTTP HEADERS section since all GraphQL queries require authentication.
Authentication
The /v1/gql endpoint requires Bearer token authentication for all queries and mutations. Include the Authorization header with your requests:
curl -X POST https://api.elcto.com/v1/gql \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ me { tokenId description role } }"}'
Authentication is required - Unlike some GraphQL APIs that allow unauthenticated access to certain queries, the Heimdall GraphQL endpoint requires authentication for all operations.
Queries
Get Current User Info
Retrieve information about the authenticated API token.
Authentication: Required
query {
me {
tokenId
description
role
isAdmin
}
}
Response:
{
"data": {
"me": {
"tokenId": "550e8400-e29b-41d4-a716-446655440000",
"description": "My API Token",
"role": "ADMIN",
"isAdmin": true
}
}
}
Get Current GPS Location
Retrieve the most recent GPS data point.
query {
currentGps {
id
latitude
longitude
altitude
timestamp
speed
createdAt
}
}
Response:
{
"data": {
"currentGps": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 11.0,
"timestamp": 1700000000,
"speed": 5.5,
"createdAt": "2025-11-24T10:00:00Z"
}
}
}
List GPS Data
Retrieve a paginated list of GPS data points.
query {
gpsList(page: 1, limit: 10) {
data {
id
latitude
longitude
altitude
timestamp
speed
createdAt
}
pagination {
page
limit
total
totalPages
}
}
}
Response:
{
"data": {
"gpsList": {
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 11.0,
"timestamp": 1700000000,
"speed": 5.5,
"createdAt": "2025-11-24T10:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 100,
"totalPages": 10
}
}
}
}
Get GPS by ID
Retrieve a specific GPS data point by ID.
query {
gpsById(id: "550e8400-e29b-41d4-a716-446655440000") {
id
latitude
longitude
altitude
timestamp
speed
createdAt
}
}
List Users
Retrieve a paginated list of all users.
Authentication: Required
Permission: users:read
query {
users(page: 1, limit: 20, search: "john") {
users {
id
username
email
avatarUrl
platform
createdAt
updatedAt
lastLoginAt
privacyMode
}
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",
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-01-20T15:30:00Z",
"lastLoginAt": "2025-01-25T09:00:00Z",
"privacyMode": false
}
],
"totalCount": 150,
"page": 1,
"limit": 20,
"totalPages": 8,
"hasNextPage": true,
"hasPreviousPage": false
}
}
}
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
}
}
Get User Profile
Retrieve a user's profile with login provider information.
Authentication: Required
Permission: users:read (or self)
query {
userProfile(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
name
displayName
picture
provider
primaryProvider
lastLoginAt
createdAt
privacyMode
}
}
Get User Permissions
Retrieve a user's effective permissions.
Authentication: Required
Permission: users:read (or self)
query {
userPermissions(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8")
}
Response:
{
"data": {
"userPermissions": ["users:read", "users:write", "gps:read", "*:*"]
}
}
Get User Accounts (Platform Accounts)
Retrieve all platform accounts linked to a user.
Authentication: Required
Permission: users:read (or self)
query {
userAccounts(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
id
platformId
platformName
platformSlug
platformUserId
isOauth
isPrimary
username
email
avatarUrl
createdAt
updatedAt
}
}
Response:
{
"data": {
"userAccounts": [
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"platformId": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"platformName": "Twitch",
"platformSlug": "twitch",
"platformUserId": "12345678",
"isOauth": true,
"isPrimary": true,
"username": "johndoe",
"email": "john@example.com",
"avatarUrl": "https://cdn.example.com/avatar.png",
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-01-20T15:30:00Z"
}
]
}
}
Get User Ban Status
Check if a user is currently banned.
Authentication: Required
Permission: users:read (or self)
query {
userBanStatus(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
isBanned
activeBan {
id
reason
bannedAt
expiresAt
isPermanent
}
}
}
Mutations
Create GPS Data
Create a new GPS data point.
Authentication: Required (Admin only)
mutation {
createGps(input: {
latitude: 51.5074
longitude: -0.1278
altitude: 11.0
timestamp: 1700000000
speed: 5.5
}) {
id
latitude
longitude
altitude
timestamp
speed
createdAt
}
}
Response:
{
"data": {
"createGps": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"latitude": 51.5074,
"longitude": -0.1278,
"altitude": 11.0,
"timestamp": 1700000000,
"speed": 5.5,
"createdAt": "2025-11-24T10:00:00Z"
}
}
}
Update User
Update a user's information.
Authentication: Required
Permission: users:write (or self)
mutation {
updateUser(
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
username: "newusername"
email: "newemail@example.com"
avatarUrl: "https://cdn.example.com/new-avatar.png"
) {
id
username
email
avatarUrl
preferredLocale
updatedAt
}
}
All fields except userId are optional. Only provided fields will be updated.
Note: User objects include preferredLocale (e.g., "en", "de") for email communication preferences.
Update Display Name
Update only the user's display name (username).
Authentication: Required (self or system API key)
mutation {
updateDisplayName(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", newUsername: "mynewname") {
id
username
}
}
Update Privacy Mode
Toggle the user's privacy mode setting.
Authentication: Required (self or system API key)
mutation {
updatePrivacyMode(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", privacyMode: true) {
success
privacyMode
}
}
Update 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 OAuth Account
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 Platform Account
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
}
}
- 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"
}
}
}
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.
Request Link Email Account
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"
}
}
}
The password must be at least 8 characters long. This password will be used to authenticate with the email account after verification.
Verify Link Email Account
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 Pending Email Link Status
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 Link Email Account
Cancel a pending email link request.
mutation CancelLinkEmailAccount {
cancelLinkEmailAccount
}
Response:
{
"data": {
"cancelLinkEmailAccount": true
}
}
Email accounts are a type of platform account that allows users to authenticate with email/password instead of OAuth. This is useful for users who don't want to link their social accounts or for platforms that don't support OAuth.
Schema Introspection
You can introspect the GraphQL schema to discover available types, queries, and mutations:
query {
__schema {
types {
name
kind
description
}
}
}
Get Type Details
query {
__type(name: "GpsData") {
name
kind
fields {
name
type {
name
kind
}
}
}
}
Variables
Use variables for dynamic queries:
query GetGpsById($id: String!) {
gpsById(id: $id) {
id
latitude
longitude
altitude
}
}
Variables:
{
"id": "550e8400-e29b-41d4-a716-446655440000"
}
Error Handling
GraphQL errors follow the standard format:
{
"errors": [
{
"message": "GPS data not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["gpsById"]
}
],
"data": null
}
Best Practices
- Request only needed fields - GraphQL allows you to select exactly what you need
- Use variables - For dynamic values, use variables instead of string interpolation
- Batch queries - Combine multiple queries in a single request
- Handle partial errors - GraphQL can return partial data with errors
- 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:
| Field | Type | Description |
|---|---|---|
id | String! | Session ID |
ipAddress | String | IP address used to create the session |
userAgent | String | Full user agent string |
deviceType | String | Parsed device type (desktop, mobile, tablet) |
browserName | String | Parsed browser name |
osName | String | Parsed operating system name |
provider | String | Auth provider slug (twitch, discord, email, etc.) |
providerName | String | Auth provider display name (Twitch, Discord, Email) |
lastActivityAt | DateTime | Last activity timestamp |
createdAt | DateTime! | Session creation time |
expiresAt | DateTime! | Session expiration time |
isCurrent | Boolean! | 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
}
}
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.
Use this when you suspect unauthorized access to your account. It will sign out all other devices while keeping you logged in.
Two-Factor Authentication
Manage two-factor authentication (2FA) using TOTP (Time-based One-Time Password).
Queries
Get 2FA Status
Check if 2FA is enabled for a user.
Authentication: Required
query {
twoFactorStatus(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
enabled
enabledAt
backupCodesRemaining
platformSlug
}
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | String | No | User ID (defaults to current user) |
Response:
{
"data": {
"twoFactorStatus": {
"enabled": true,
"enabledAt": "2025-01-15T10:00:00Z",
"backupCodesRemaining": 8,
"platformSlug": "email"
}
}
}
Check 2FA Requirement
Check if 2FA is required for a user based on their roles.
Authentication: Required
query {
userRequiresTwoFactor(userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8") {
required
enabled
reason
}
}
Response:
{
"data": {
"userRequiresTwoFactor": {
"required": true,
"enabled": false,
"reason": "Your role requires two-factor authentication"
}
}
}
Mutations
Setup 2FA
Initiate 2FA setup. Returns a TOTP secret and QR code for authenticator apps.
Authentication: Required
mutation {
setupTwoFactor(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
platformId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
}) {
secret
qrCodeUrl
qrCodeImage
expiresAt
platformSlug
}
}
Input:
| Field | Type | Required | Description |
|---|---|---|---|
userId | String | No | User ID (defaults to current user) |
platformId | String | No | Platform 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"
}
}
}
The setup expires after 10 minutes. Verify the code before then, or initiate setup again.
Verify 2FA
Verify a TOTP code to complete 2FA setup.
Authentication: Required
mutation {
verifyTwoFactor(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
code: "123456"
}) {
success
backupCodes
message
}
}
Input:
| Field | Type | Required | Description |
|---|---|---|---|
userId | String | No | User ID (defaults to current user) |
code | String | Yes | 6-digit TOTP code |
Response:
{
"data": {
"verifyTwoFactor": {
"success": true,
"backupCodes": [
"ABCD-EFGH-IJKL",
"MNOP-QRST-UVWX",
"1234-5678-9012",
"3456-7890-1234",
"5678-9012-3456",
"7890-1234-5678",
"9012-3456-7890",
"1234-5678-9012"
],
"message": "Two-factor authentication enabled successfully"
}
}
}
Backup codes are only shown once. Store them in a secure location.
Disable 2FA
Disable two-factor authentication.
Authentication: Required
mutation {
disableTwoFactor(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
code: "123456"
}) {
success
message
}
}
Input:
| Field | Type | Required | Description |
|---|---|---|---|
userId | String | No | User ID (defaults to current user) |
code | String | Yes | TOTP code or backup code |
Response:
{
"data": {
"disableTwoFactor": {
"success": true,
"message": "Two-factor authentication disabled"
}
}
}
Regenerate Backup Codes
Generate new backup codes (invalidates all previous codes).
Authentication: Required
mutation {
regenerateBackupCodes(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
code: "123456"
}) {
success
backupCodes
message
}
}
Input:
| Field | Type | Required | Description |
|---|---|---|---|
userId | String | No | User ID (defaults to current user) |
code | String | Yes | TOTP code to confirm |
Response:
{
"data": {
"regenerateBackupCodes": {
"success": true,
"backupCodes": [
"ABCD-EFGH-IJKL",
"MNOP-QRST-UVWX",
"..."
],
"message": "Backup codes regenerated successfully"
}
}
}
Regenerating backup codes invalidates all previous backup codes immediately.
Verify 2FA During Login
Verify 2FA code during login flow (for auth systems).
Authentication: Required (System API key)
mutation {
verifyTwoFactorLogin(input: {
userId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
code: "123456"
}) {
success
message
}
}
This mutation verifies a TOTP code during login without modifying 2FA settings.
Permissions
| Action | Self | Other Users |
|---|---|---|
| Get status | ✅ No special permission | two_factor:manage |
| Setup | ✅ No special permission | two_factor:manage |
| Verify | ✅ No special permission | two_factor:manage |
| Disable | ✅ No special permission | two_factor:manage |
| Regenerate codes | ✅ No special permission | two_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
}
}
The clientSecret is only returned once at creation time. Store it securely!
Input Fields:
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Client application name |
description | String | No | Application description |
logoUrl | String | No | URL to client logo |
homepageUrl | String | No | Client homepage URL |
privacyPolicyUrl | String | No | Privacy policy URL |
termsOfServiceUrl | String | No | Terms of service URL |
redirectUris | [String!]! | Yes | Allowed redirect URIs |
allowedScopes | [String!]! | Yes | Scopes client can request |
clientType | String | Yes | "confidential" or "public" |
isFirstParty | Boolean | No | First-party apps skip consent (default: false) |
Update OAuth Client
Update an existing OAuth client.
mutation {
updateOauthClient(
id: "client-123"
input: {
name: "Updated Name"
description: "Updated description"
redirectUris: ["https://example.com/new-callback"]
}
) {
id
name
description
updatedAt
}
}
Regenerate Client Secret
Regenerate the client secret for a confidential client.
mutation {
regenerateOauthClientSecret(id: "client-123") {
success
clientSecret
error
}
}
Response:
| Field | Type | Description |
|---|---|---|
success | Boolean! | Whether the operation succeeded |
clientSecret | String | The new client secret (only shown once!) |
error | String | Error message if operation failed |
Delete OAuth Client
Delete an OAuth client.
mutation {
deleteOauthClient(id: "client-123") {
success
error
}
}
Response:
| Field | Type | Description |
|---|---|---|
success | Boolean! | Whether the deletion succeeded |
error | String | Error message if deletion failed |
Revoke OAuth Consent
Revoke user consent for an OAuth client (disconnect the app).
mutation {
revokeOauthConsent(clientId: "client-123")
}
Revoke OAuth Token
Revoke a specific OAuth access token.
mutation {
revokeOauthToken(tokenId: "token-123")
}
API Key Management
Manage API keys for programmatic access to the Heimdall API.
Queries
List API Keys
Get all API keys for the authenticated user.
Authentication: Required
Permission: api_keys:read
query {
apiKeys {
id
keyPrefix
name
description
scopes
isActive
isSystem
expiresAt
lastUsedAt
createdAt
updatedAt
}
}
Response:
{
"data": {
"apiKeys": [
{
"id": "ak_550e8400-e29b-41d4-a716-446655440000",
"keyPrefix": "hm_abc12345...",
"name": "My API Key",
"description": "Key for CI/CD pipeline",
"scopes": ["gps:read", "gps:write"],
"isActive": true,
"isSystem": false,
"expiresAt": "2025-12-31T23:59:59Z",
"lastUsedAt": "2025-01-25T10:30:00Z",
"createdAt": "2025-01-01T00:00:00Z",
"updatedAt": "2025-01-15T12:00:00Z"
}
]
}
}
API Key Fields:
| Field | Type | Description |
|---|---|---|
id | String! | Unique API key identifier |
keyPrefix | String! | Visible prefix of the key (e.g., hm_abc12345...) |
name | String! | Name/label for the key |
description | String | Optional description |
scopes | [String!]! | Permission scopes granted to this key |
isActive | Boolean! | Whether the key is currently active |
isSystem | Boolean! | Whether this is a system-managed key |
expiresAt | DateTime | Optional expiration date |
lastUsedAt | DateTime | Last time the key was used |
createdAt | DateTime! | Creation timestamp |
updatedAt | DateTime! | 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
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:
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Name/label for the key |
description | String | No | Optional description |
scopes | [String!] | No | Permission scopes (defaults to empty) |
expiresAt | DateTime | No | Optional expiration date |
Response:
{
"data": {
"createApiKey": {
"id": "ak_550e8400-e29b-41d4-a716-446655440000",
"key": "hm_abcdefghijklmnopqrstuvwxyz123456",
"keyPrefix": "hm_abc12345...",
"name": "CI/CD Pipeline Key",
"description": "Key for automated deployments",
"scopes": ["gps:read", "gps:write"],
"isActive": true,
"isSystem": false,
"expiresAt": "2025-12-31T23:59:59Z",
"createdAt": "2025-01-25T14:00:00Z",
"updatedAt": "2025-01-25T14:00:00Z"
}
}
}
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.
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:
| Field | Type | Required | Description |
|---|---|---|---|
name | String | No | New name for the key |
description | String | No | New description |
scopes | [String!] | No | New permission scopes |
isActive | Boolean | No | Enable or disable the key |
expiresAt | DateTime | No | New expiration date |
All fields are optional - only include fields you want to update.
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 API keys cannot be deleted. Attempting to delete a system key will return an error.
Users can only manage their own API keys. Super admins can manage all API keys.
Platform Management
Manage authentication platform providers.
Queries
List All Platforms
Get all authentication platforms with their status.
query {
platforms {
id
name
slug
isOauth
enabled
twoFactorSupported
twoFactorRequired
createdAt
updatedAt
}
}
List Enabled Platforms
Get only enabled platforms (for login/registration UI).
query {
enabledPlatforms {
id
name
slug
isOauth
}
}
Get Platform by ID or Slug
Get a specific platform.
query {
platform(idOrSlug: "twitch") {
id
name
slug
enabled
twoFactorSupported
twoFactorRequired
}
}
Mutations
Enable Platform
Enable a platform for authentication.
Authentication: Required
Permission: settings:write
mutation {
enablePlatform(idOrSlug: "discord") {
success
message
}
}
Disable Platform
Disable a platform for authentication.
Authentication: Required
Permission: settings:write
mutation {
disablePlatform(idOrSlug: "discord") {
success
message
}
}
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:
| Field | Type | Description |
|---|---|---|
id | String! | Unique role identifier |
name | String! | Role display name |
description | String | Role description |
isSystem | Boolean! | Whether this is a system-managed role |
requiresTwoFactor | Boolean! | Whether users with this role must have 2FA enabled |
permissions | [Permission!]! | Permissions granted by this role |
createdAt | DateTime! | Creation timestamp |
updatedAt | DateTime! | 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:
| Field | Type | Description |
|---|---|---|
id | String! | Unique permission identifier |
name | String! | Permission name in resource:action format |
description | String | Permission description |
resource | String! | Resource this permission applies to |
action | String! | Action this permission allows |
createdAt | DateTime! | Creation timestamp |
Common Permissions:
| Permission | Description |
|---|---|
users:read | Read user profiles |
users:write | Update user data |
users:delete | Delete users |
users:ban | Ban/unban users |
roles:read | View roles |
roles:write | Create/update roles |
permissions:read | View permissions |
audit:read | View audit logs |
audit:write | Manage audit reports |
discord:read | View Discord data |
discord:sync | Sync Discord data |
api_keys:read | View API keys |
api_keys:write | Create/update API keys |
api_keys:delete | Delete API keys |
two_factor:manage | Manage other users' 2FA |
settings:read | View system settings |
settings:write | Update 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:
| Field | Type | Description |
|---|---|---|
id | String! | Internal UUID primary key |
guildId | String! | Discord guild ID (snowflake) |
name | String! | Guild name |
icon | String | Guild icon hash |
iconUrl | String | Full URL to guild icon |
ownerId | String! | Discord user ID of guild owner |
memberCount | Int | Approximate member count |
boostCount | Int | Number of server boosts |
premiumTier | Int | Premium tier (0=none, 1=tier1, 2=tier2, 3=tier3) |
botJoinedAt | DateTime | When the bot joined this guild |
lastSyncedAt | DateTime! | 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:
| Field | Type | Description |
|---|---|---|
id | String! | Internal UUID primary key |
channelId | String! | Discord channel ID (snowflake) |
guildId | String! | Discord guild ID (snowflake) |
name | String! | Channel name |
channelType | Enum | Channel type (Text, Voice, Category, etc.) |
channelTypeRaw | Int! | Raw channel type value |
parentId | String | Parent category channel ID |
position | Int! | Position in channel list |
topic | String | Channel topic/description |
nsfw | Boolean! | Whether channel is NSFW |
isTextBased | Boolean! | Whether channel supports text messages |
isVoice | Boolean! | Whether channel is a voice channel |
Channel Types:
| Type | Name | Description |
|---|---|---|
| 0 | Text | Text channel |
| 2 | Voice | Voice channel |
| 4 | Category | Channel category |
| 5 | Announcement | Announcement channel |
| 10 | Announcement Thread | Thread in announcement channel |
| 11 | Public Thread | Public thread |
| 12 | Private Thread | Private thread |
| 13 | Stage | Stage channel |
| 15 | Forum | Forum channel |
| 16 | Media | Media 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
guildId | String | Yes | Internal guild UUID (database ID) |
page | Int | No | Page number (default: 1) |
limit | Int | No | Items per page (default: 50, max: 100) |
filter | Object | No | Filter options |
Filter Options:
| Field | Type | Description |
|---|---|---|
search | String | Search by username, global name, or nickname |
botOnly | Boolean | Only show bots |
humansOnly | Boolean | Only show humans (non-bots) |
hasRole | String | Filter 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:
| Field | Type | Description |
|---|---|---|
id | String! | Internal UUID primary key |
userId | String! | Discord user ID (snowflake) |
username | String! | Discord username |
discriminator | String | Legacy discriminator (may be "0") |
globalName | String | Global display name |
nickname | String | Server-specific nickname |
displayName | String! | Computed: nickname > globalName > username |
formattedUsername | String! | Formatted: @username or username#discriminator |
avatar | String | Avatar hash |
avatarUrl | String | Full URL to avatar |
bot | Boolean! | Whether this is a bot account |
system | Boolean! | Whether this is a system account |
joinedAt | DateTime | When the member joined |
premiumSince | DateTime | When the member started boosting |
roles | [String!]! | List of role IDs |
isTimedOut | Boolean! | Whether member is currently timed out |
communicationDisabledUntil | DateTime | Timeout 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
}
}
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:
| Field | Type | Description |
|---|---|---|
success | Boolean | Whether the sync was successful |
error | String | Error message if failed |
guildsSynced | Int | Number of guilds synced |
channelsSynced | Int | Number of channels synced |
rolesSynced | Int | Number of roles synced |
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
}
}
}
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:
| Field | Type | Required | Description |
|---|---|---|---|
guildId | String | Yes | Internal guild UUID (database ID) |
userId | String | Yes | Target Discord user ID (snowflake) |
action | Enum | Yes | Moderation action to perform |
reason | String | No | Reason for the action |
durationSeconds | Int | No | Duration for timeout (required for TIMEOUT action) |
deleteMessageDays | Int | No | Days of messages to delete for ban (0-7) |
Action Types:
| Action | Description | Required Permission |
|---|---|---|
WARN | Send a warning DM to the user | discord:guild.warn |
KICK | Kick the user from the guild | discord:guild.kick |
BAN | Ban the user from the guild | discord:guild.ban |
TIMEOUT | Timeout the user (requires durationSeconds) | discord:guild.timeout |
REMOVE_TIMEOUT | Remove an existing timeout | discord: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")
}
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
}
}
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
}
]
}
}
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 exportreport.pdf- Human-readable PDF reportREADME.txt- Explanation file
See the REST API documentation for full details.
Data Included in Export:
- User profile (ID, username, privacy mode, 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)
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:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | Int | No | Page number (default: 1) |
limit | Int | No | Items per page (default: 20, max: 100) |
eventType | String | No | Filter 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 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 Type | Description |
|---|---|
login | Successful sign-in |
login_failed | Failed sign-in attempt |
logout | User signed out |
session_created | New session created |
session_revoked | Session revoked |
all_sessions_revoked | All other sessions revoked |
2fa_enabled | Two-factor authentication enabled |
2fa_disabled | Two-factor authentication disabled |
2fa_verified | Two-factor code verified |
2fa_backup_codes_regenerated | Backup codes regenerated |
password_changed | Password was changed |
password_reset_requested | Password reset requested |
user_created | Account created |
user_updated | Account details updated |
user_deleted | Account deleted |
deletion_scheduled | Account deletion scheduled |
deletion_cancelled | Scheduled deletion cancelled |
client_created | OAuth client application created |
client_updated | OAuth client application updated |
client_secret_regenerated | OAuth client secret regenerated |
client_deleted | OAuth client application deleted |
consent_granted | OAuth consent granted |
consent_revoked | OAuth consent revoked |
token_created | OAuth token created |
token_revoked | OAuth token revoked |
account_linked | Platform account linked |
account_unlinked | Platform account unlinked |
primary_account_changed | Primary account changed |
data_exported | User data exported |
api_key_created | API key created |
api_key_revoked | API key revoked |
user_banned | User banned (admin action) |
user_unbanned | User unbanned (admin action) |
bot_command_executed | Bot command executed (Discord/Twitch) |
bot_guild_configured | Bot guild/server settings configured |
Source Services:
The sourceService field identifies which application or service created the audit event:
| Value | Description |
|---|---|
api | Direct API calls |
id | Heimdall ID webapp (account management) |
backend | Backend dashboard (admin panel) |
policies | Policies webapp |
discord_bot | Discord bot |
twitch_bot | Twitch 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | Int | No | Page number (default: 1) |
limit | Int | No | Items per page (default: 50, max: 100) |
eventType | String | No | Filter by event type |
userId | String | No | Filter by user ID |
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:
| Parameter | Type | Required | Description |
|---|---|---|---|
auditEventId | String | Yes | The audit event ID to report |
reason | String | Yes | Reason for reporting (see table below) |
description | String | No | Additional details |
Report Reasons:
| Reason | Description |
|---|---|
not_me | The activity wasn't performed by the user |
suspicious | General suspicious activity |
unknown_device | Activity from an unknown device |
unknown_location | Activity from an unknown location |
other | Other 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
}
}
}
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:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | Int | No | Page number (default: 1) |
limit | Int | No | Items per page (default: 20, max: 100) |
status | String | No | Filter 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
reportId | String | Yes | The report ID to update |
status | String | Yes | New status (reviewed, resolved, dismissed) |
resolutionNotes | String | No | Notes about the resolution |
Report Statuses:
| Status | Description |
|---|---|
pending | Report submitted, awaiting review |
reviewed | Report is being investigated |
resolved | Investigation complete, action taken |
dismissed | Report 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
}
}
}
When an admin updates a report status, the user automatically receives an email notification with the new status and any resolution notes.
Additional Mutations
Resend Email Link Verification
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