Skip to main content

GraphQL API

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

Endpoints

The GraphQL API is available at multiple endpoints:

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

GraphiQL Interactive IDE

Interactive GraphiQL IDE is available in development environments only:

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

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

The GraphiQL IDE provides:

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

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

Authentication

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

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

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

Queries

Get Current User Info

Retrieve information about the authenticated API token.

Authentication: Required

query {
me {
tokenId
description
role
isAdmin
}
}

Response:

{
"data": {
"me": {
"tokenId": "abc-123",
"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": "abc-123",
"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": "abc-123",
"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: "abc-123") {
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": "user-123",
"username": "johndoe",
"email": "[email protected]",
"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: "user-123") {
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: "user-123")
}

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: "user-123") {
id
platformId
platformName
platformSlug
platformUserId
isOauth
isPrimary
username
email
avatarUrl
createdAt
updatedAt
}
}

Response:

{
"data": {
"userAccounts": [
{
"id": "pa-123",
"platformId": "platform-twitch",
"platformName": "Twitch",
"platformSlug": "twitch",
"platformUserId": "12345678",
"isOauth": true,
"isPrimary": true,
"username": "johndoe",
"email": "[email protected]",
"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: "user-123") {
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": "abc-123",
"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: "user-123"
username: "newusername"
email: "[email protected]"
avatarUrl: "https://cdn.example.com/new-avatar.png"
) {
id
username
email
avatarUrl
updatedAt
}
}

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

Update Display Name

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

Authentication: Required (self or system API key)

mutation {
updateDisplayName(userId: "user-123", newUsername: "mynewname") {
id
username
}
}

Update Privacy Mode

Toggle the user's privacy mode setting.

Authentication: Required (self or system API key)

mutation {
updatePrivacyMode(userId: "user-123", privacyMode: true) {
success
privacyMode
}
}

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: "user-123") {
isScheduledForDeletion
scheduledDeletionAt
}
}

Cancel Account Deletion

Cancel a scheduled account deletion.

Authentication: Required (self or system API key)

mutation {
cancelAccountDeletion(userId: "user-123")
}

Request Email Change

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

Authentication: Required (self or system API key)

mutation {
requestEmailChange(userId: "user-123", newEmail: "[email protected]") {
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: "user-123")
}

Link an OAuth provider account to a user.

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

mutation {
linkOauthAccount(
userId: "user-123"
input: {
oauthProvider: "twitch"
oauthProviderId: "12345678"
username: "johndoe"
email: "[email protected]"
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: "user-123", 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: "user-123", accountId: "pa-456") {
id
platformId
platformName
platformSlug
platformUserId
isOauth
isPrimary
username
email
avatarUrl
createdAt
updatedAt
}
}

Response:

{
"data": {
"setPrimaryPlatformAccount": {
"id": "pa-456",
"platformId": "platform-discord",
"platformName": "Discord",
"platformSlug": "discord",
"platformUserId": "98765432",
"isOauth": true,
"isPrimary": true,
"username": "johndoe#1234",
"email": "[email protected]",
"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": "[email protected]",
"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": "pa-789"
}
}
}

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": "[email protected]",
"expiresAt": "2025-01-25T15:30:00Z"
}
}
}

Response (no pending link):

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

Cancel a pending email link request.

mutation CancelLinkEmailAccount {
cancelLinkEmailAccount
}

Response:

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

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

Schema Introspection

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

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

Get Type Details

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

Variables

Use variables for dynamic queries:

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

Variables:

{
"id": "abc-123"
}

Error Handling

GraphQL errors follow the standard format:

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

Best Practices

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

Code Examples

JavaScript (Fetch)

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

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

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

Python (requests)

import requests

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

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

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

cURL

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

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