API Reference
Every Heimdall endpoint documented with request and response examples. All paths are relative to the base URL: https://heimdall.productcraft.co/api
Common headers
Authorization: Bearer YOUR_TOKENfor authenticated operationsContent-Type: application/jsonfor all request bodies
Error responses
All errors return a consistent shape:
{
"statusCode": 409,
"message": "Email already in use",
"error": "CONFLICT"
}HTTP status codes follow standard conventions: 400 for validation errors, 401 for missing or invalid credentials, 403 for insufficient permissions, 404 for not found, 409 for conflicts, 410 for expired invites, and 429 for rate limiting.
Permission format
Permissions follow the resource.action format. All permissions are scoped to the App in which the role carrying them is assigned.
Authentication
Register, sign in, refresh tokens, and logout. These endpoints are public and do not require an existing token.
/v1/auth/signupCreate a new account
/v1/auth/signinSign in with email/username and password
/v1/auth/refreshRefresh an access token using a refresh token
/v1/auth/logoutRevoke a session by refresh token
/v1/auth/password/request-resetRequest a password reset code
/v1/auth/password/resetReset password with a verification code
Example: Sign up
Request
POST /v1/auth/signup
Content-Type: application/json
{
"email": "jane@example.com",
"username": "jane_doe",
"password": "SecurePassword123",
"displayName": "Jane Doe"
}Response
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}Apps
Manage tenants. Each App is an isolated namespace containing its own roles, members, and permissions. The App slug is used as the identifier in URL paths.
/v1/appsCreate a new App (caller becomes owner)
/v1/appsList Apps the current user belongs to
/v1/apps/:appIdGet App details (requires tenant.read)
/v1/apps/:appIdUpdate App name and metadata (requires tenant.update)
/v1/apps/:appId/statusUpdate App status (requires tenant.update)
/v1/apps/:appIdDelete an App permanently (requires tenant.delete)
/v1/apps/:appId/membersList members of an App (requires user.list)
Example: Create an App
Request
POST /v1/apps
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
{
"slug": "acme-corp",
"displayName": "Acme Corporation"
}Response
{
"id": "550e8400-e29b-41d4-a716-...",
"slug": "acme-corp",
"display_name": "Acme Corporation",
"status": "active",
"metadata": {},
"created_by": "...",
"created_at": "2026-02-20T10:00:00Z",
"updated_at": "2026-02-20T10:00:00Z"
}Roles
Define named collections of permissions within an App. Three system roles (owner, admin, member) are created automatically. You can create additional custom roles.
/v1/apps/:appId/rolesCreate a custom role (requires role.create)
/v1/apps/:appId/rolesList all roles in an App (requires role.read)
/v1/apps/:appId/roles/permissionsList all available permissions (requires role.read)
/v1/apps/:appId/roles/:roleNameGet role with its permissions (requires role.read)
/v1/apps/:appId/roles/:roleNameUpdate role name or description (requires role.update)
/v1/apps/:appId/roles/:roleNameDelete a custom role (requires role.delete)
/v1/apps/:appId/roles/:roleName/permissionsSet permissions for a role (requires role.update)
/v1/apps/:appId/roles/assignAssign a role to a user (requires role.assign)
Example: Create a role
Request
POST /v1/apps/acme-corp/roles
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
{
"name": "editor",
"description": "Can manage content"
}Response
{
"id": "...",
"tenant_id": "...",
"name": "editor",
"description": "Can manage content",
"is_system": false,
"created_at": "2026-02-20T10:01:00Z",
"updated_at": "2026-02-20T10:01:00Z"
}Invites
Generate invite codes to onboard new users into an App with a predefined role. Invites can be restricted to a specific email, limited in uses, and set to expire.
/v1/apps/:appId/invitesCreate an invite (requires invite.create)
/v1/apps/:appId/invitesList invites for an App (requires invite.read)
/v1/apps/:appId/invites/:inviteIdRevoke an invite (requires invite.revoke)
/v1/invites/acceptAccept an invite using its code (auth required)
Example: Create an invite
Request
POST /v1/apps/acme-corp/invites
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
{
"role": "admin",
"email": "bob@contractor.io",
"expiresInHours": 72,
"maxUses": 1
}Response
{
"id": "...",
"tenant_id": "...",
"email": "bob@contractor.io",
"code": "dGhpcyBpcyBhIHRlc3Q",
"role_id": "...",
"max_uses": 1,
"use_count": 0,
"expires_at": "2026-02-23T10:00:00Z",
"created_by": "...",
"created_at": "2026-02-20T10:10:00Z"
}Service Credentials (M2M)
Create and manage service accounts for machine-to-machine authentication. Each credential set is scoped to specific permissions within an App.
/v1/apps/:appId/credentialsCreate service credentials (requires m2m.create)
/v1/apps/:appId/credentialsList service credentials (requires m2m.read)
/v1/apps/:appId/credentials/:clientIdGet credential details (requires m2m.read)
/v1/apps/:appId/credentials/:clientId/rotateRotate the client secret (requires m2m.rotate_secret)
/v1/apps/:appId/credentials/:clientId/scopesSet permission scopes (requires m2m.update)
/v1/apps/:appId/credentials/:clientIdUpdate client status (requires m2m.update)
/v1/apps/:appId/credentials/:clientIdDelete credentials (requires m2m.delete)
/v1/oauth/tokenExchange client credentials for an access token (public)
Example: Create service credentials
Request
POST /v1/apps/acme-corp/credentials
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
{
"name": "billing-service"
}Response
{
"id": "...",
"client_id": "m2m_a1b2c3d4e5f6...",
"client_secret": "base64url-encoded-secret",
"name": "billing-service",
"created_at": "2026-02-20T10:05:00Z"
}Audit Logs
Query the immutable audit trail for any App. Every permission change, role assignment, and management action is recorded.
/v1/apps/:appId/audit-logsList audit log entries (requires audit.read, supports cursor pagination)
Example: Query audit logs
Request
GET /v1/apps/acme-corp/audit-logs?limit=10&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wMi0yMFQxMDoxNTowMFoiLCJpZCI6ImF1ZGl0XzEyMyJ9
Authorization: Bearer YOUR_TOKENResponse
[
{
"id": "...",
"tenant_id": "...",
"actor_id": "...",
"actor_type": "user",
"action": "role.permissions_changed",
"resource": "role",
"resource_id": "...",
"metadata": {},
"ip": "203.0.113.42",
"created_at": "2026-02-20T10:15:00Z"
}
]Token Exchange
Exchange a session token for a tenant-scoped token. The returned token includes the user's role and permissions for the specified App, enabling local authorization checks.
/v1/apps/:appId/tokenExchange session token for a tenant-scoped token (auth required)
Example: Get a tenant-scoped token
Request
POST /v1/apps/acme-corp/token
Authorization: Bearer YOUR_SESSION_TOKENResponse
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}Introspection
Inspect the current user's identity, App memberships, and permissions within a specific App.
/v1/introspectCurrent user identity and list of Apps
/v1/introspect/apps/:appIdRole and permissions within a specific App
Example: Introspect permissions
Request
GET /v1/introspect/apps/acme-corp
Authorization: Bearer YOUR_TOKENResponse
{
"isMember": true,
"role": { "id": "...", "name": "owner" },
"permissions": [
"user.create",
"user.read",
"user.update",
"user.delete",
"user.list",
"role.create",
"role.read",
"..."
]
}JWKS Endpoint
Verify tokens locally by fetching the public signing keys:
GET /v1/.well-known/jwks.jsonKeys are cached for 1 hour (Cache-Control: public, max-age=3600). Use any JWT library (jose, jsonwebtoken, PyJWT, etc.) to verify the RS256 signature.