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": "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": "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: "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": "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: "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: "newemail@example.com"
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: "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: "user-123")
}
Link OAuth Account
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: "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: "user-123", 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: "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": "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": "pa-789"
}
}
}
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": "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
- 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 } }"}'
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