This document describes Portal's REST API endpoints, request/response formats, and authentication requirements.
All API endpoints are prefixed with /api:
https://portal.atl.dev/api
Most endpoints require authentication. Portal uses BetterAuth for session management.
-
Session Cookie (Browser)
- Automatically sent with requests from the browser
- Set via
/api/auth/sign-inendpoint
-
API Key (Programmatic)
- Include in
Authorizationheader:Authorization: Bearer <api-key> - Create API keys via admin panel or API
- Include in
requireAuth(): Requires any authenticated userrequireAdmin(): Requires admin rolerequireAdminOrStaff(): Requires admin or staff role
{
"ok": true,
"data": { ... }
}Some endpoints may return data directly without the ok wrapper:
{
"user": { ... }
}{
"ok": false,
"error": "Error message description",
"details": {
"issues": [
{
"code": "too_small",
"minimum": 3,
"type": "string",
"inclusive": true,
"exact": false,
"message": "String must contain at least 3 character(s)",
"path": ["name"]
}
],
"flattened": {
"formErrors": [],
"fieldErrors": {
"name": ["String must contain at least 3 character(s)"]
}
}
}
}Example with zod-validation-error:
{
"ok": false,
"error": "Validation error: Name must contain at least 3 character(s) at 'name'",
"details": { ... }
}Note: The details field is optional and typically populated for 400 Bad Request (Validation) errors.
200 OK- Successful request201 Created- Resource created successfully400 Bad Request- Invalid request parameters401 Unauthorized- Authentication required403 Forbidden- Insufficient permissions404 Not Found- Resource not found500 Internal Server Error- Server error
BetterAuth catch-all endpoint for authentication operations.
Endpoints handled:
/api/auth/sign-in- Sign in/api/auth/sign-up- Sign up/api/auth/sign-out- Sign out/api/auth/session- Get session/api/auth/forgot-password- Request password reset/api/auth/reset-password- Reset password/api/auth/verify-email- Verify email address
See BetterAuth Documentation for detailed request/response formats.
Get current authenticated user's profile.
Authentication: Required (requireAuth)
Response:
{
"user": {
"id": "string",
"name": "string",
"email": "string",
"image": "string | null",
"role": "user" | "staff" | "admin",
"emailVerified": boolean,
"createdAt": "ISO 8601 date"
}
}Update current authenticated user's profile.
Authentication: Required (requireAuth)
Request Body:
{
"name": "string",
"image": "string (url)"
}Response:
{
"user": {
"id": "string",
"name": "string",
"email": "string",
"image": "string | null",
"role": "user" | "staff" | "admin",
"emailVerified": boolean,
"createdAt": "ISO 8601 date"
}
}Get current authenticated user's sessions.
Authentication: Required (requireAuth)
Query Parameters:
active(optional): Filter active sessions (true/false)limit(optional): Maximum results (default: 100)offset(optional): Pagination offset (default: 0)
Response:
{
"sessions": [
{
"id": "string",
"userId": "string",
"expiresAt": "ISO 8601 date",
"ipAddress": "string | null",
"userAgent": "string | null",
"createdAt": "ISO 8601 date"
}
]
}All admin endpoints require admin or staff role (requireAdminOrStaff).
List users with filtering and pagination.
Authentication: Required (requireAdminOrStaff)
Query Parameters:
role(optional): Filter by role (user,staff,admin)banned(optional): Filter by banned status (true/false)search(optional): Search by email or namelimit(optional): Maximum results (default: 50)offset(optional): Pagination offset (default: 0)
Response:
{
"users": [
{
"id": "string",
"name": "string",
"email": "string",
"role": "user" | "staff" | "admin",
"banned": boolean,
"emailVerified": boolean,
"createdAt": "ISO 8601 date",
"updatedAt": "ISO 8601 date"
}
],
"pagination": {
"total": number,
"limit": number,
"offset": number,
"hasMore": boolean
}
}Get a specific user by ID.
Authentication: Required (requireAdminOrStaff)
Response:
{
"user": {
"id": "string",
"name": "string",
"email": "string",
"role": "user" | "staff" | "admin",
"banned": boolean,
"emailVerified": boolean,
"createdAt": "ISO 8601 date",
"updatedAt": "ISO 8601 date"
}
}Update a user by ID.
Authentication: Required (requireAdminOrStaff)
Request Body:
{
"name": "string",
"role": "user" | "staff" | "admin",
"banned": boolean,
"banReason": "string",
"banExpires": "ISO 8601 date"
}Response:
{
"user": {
"id": "string",
"name": "string",
"email": "string",
"role": "user" | "staff" | "admin",
"banned": boolean,
"emailVerified": boolean,
"createdAt": "ISO 8601 date",
"updatedAt": "ISO 8601 date"
}
}List all sessions with filtering.
Authentication: Required (requireAdminOrStaff)
Query Parameters:
userId(optional): Filter by user IDactive(optional): Filter active sessions (true/false)limit(optional): Maximum results (default: 100)offset(optional): Pagination offset (default: 0)
Response:
{
"sessions": [
{
"id": "string",
"userId": "string",
"expiresAt": "ISO 8601 date",
"ipAddress": "string | null",
"userAgent": "string | null",
"createdAt": "ISO 8601 date"
}
]
}Get a specific session by ID.
Authentication: Required (requireAdminOrStaff)
Response:
{
"session": {
"id": "string",
"userId": "string",
"expiresAt": "ISO 8601 date",
"ipAddress": "string | null",
"userAgent": "string | null",
"createdAt": "ISO 8601 date"
}
}Revoke a session by ID.
Authentication: Required (requireAdminOrStaff)
Response:
{
"ok": true
}List API keys with filtering.
Authentication: Required (requireAdminOrStaff)
Query Parameters:
userId(optional): Filter by user IDenabled(optional): Filter by enabled status (true/false)limit(optional): Maximum results (default: 100)offset(optional): Pagination offset (default: 0)
Response:
{
"apiKeys": [
{
"id": "string",
"userId": "string",
"name": "string",
"enabled": boolean,
"lastUsedAt": "ISO 8601 date | null",
"createdAt": "ISO 8601 date",
"user": {
"id": "string",
"email": "string",
"name": "string"
}
}
]
}Get a specific API key by ID.
Authentication: Required (requireAdminOrStaff)
Response:
{
"apiKey": {
"id": "string",
"userId": "string",
"name": "string",
"enabled": boolean,
"lastUsedAt": "ISO 8601 date | null",
"createdAt": "ISO 8601 date",
"user": {
"id": "string",
"email": "string",
"name": "string"
}
}
}Delete an API key by ID.
Authentication: Required (requireAdminOrStaff)
Response:
{
"ok": true
}List OAuth clients.
Authentication: Required (requireAdminOrStaff)
Query Parameters:
limit(optional): Maximum results (default: 100)offset(optional): Pagination offset (default: 0)
Response:
{
"oauthClients": [
{
"id": "string",
"name": "string",
"clientId": "string",
"redirectUris": ["string"],
"disabled": boolean,
"createdAt": "ISO 8601 date"
}
]
}Get a specific OAuth client by ID.
Authentication: Required (requireAdminOrStaff)
Response:
{
"oauthClient": {
"id": "string",
"name": "string",
"clientId": "string",
"redirectUris": ["string"],
"disabled": boolean,
"createdAt": "ISO 8601 date"
}
}Get admin dashboard statistics.
Authentication: Required (requireAdminOrStaff)
Response:
{
"users": {
"total": number,
"admins": number,
"staff": number,
"banned": number,
"regular": number
},
"sessions": {
"total": number,
"active": number
},
"apiKeys": {
"total": number,
"enabled": number
},
"oauthClients": {
"total": number,
"disabled": number
}
}List available integrations.
Authentication: Required (requireAuth)
Response:
{
"ok": true,
"integrations": [
{
"id": "string",
"name": "string",
"description": "string",
"enabled": boolean,
"icon": "string | null"
}
]
}List integration accounts for the current user.
Authentication: Required (requireAuth)
Response:
{
"ok": true,
"accounts": [
{
"id": "string",
"userId": "string",
"integrationId": "string",
"data": {},
"createdAt": "ISO 8601 date"
}
]
}Create an integration account for the current user.
Authentication: Required (requireAuth)
Request Body:
{
"key1": "value1",
"key2": "value2"
}Response:
{
"ok": true,
"account": {
"id": "string",
"userId": "string",
"integrationId": "string",
"data": {},
"createdAt": "ISO 8601 date"
}
}Status Code: 201 Created
Get a specific integration account.
Authentication: Required (requireAuth - owner or admin)
Response:
{
"ok": true,
"account": {
"id": "string",
"userId": "string",
"integrationId": "string",
"data": {},
"createdAt": "ISO 8601 date"
}
}Update an integration account.
Authentication: Required (requireAuth - owner or admin)
Request Body:
{
"key1": "new-value1"
}Response:
{
"ok": true,
"account": {
"id": "string",
"userId": "string",
"integrationId": "string",
"data": {},
"createdAt": "ISO 8601 date"
}
}Delete an integration account.
Authentication: Required (requireAuth - owner or admin)
Response:
{
"ok": true
}Sentry tunnel endpoint for forwarding error reports.
Authentication: Not required (public endpoint)
Request: Sentry envelope format
Response: Forwarded Sentry response
Health check endpoint.
Authentication: Not required
Response:
{
"status": "ok",
"service": "sentry-tunnel"
}Common error messages:
"Unauthorized"(401) - Authentication required"Forbidden - Admin access required"(403) - Admin role required"Forbidden - Admin or Staff access required"(403) - Admin or staff role required"User not found"(404) - User does not exist"Unknown integration"(404) - Integration not found"Integration is disabled"(403) - Integration not available"Invalid request body"(400) - Request validation failed"Internal server error"(500) - Server error (details logged to Sentry)
{
"ok": false,
"error": "Error message",
"details": { ... } // Optional Zod validation issues
}Dynamic segments ([id], [integration]) are user input and are validated before use:
[id]: Validated withparseRouteId()from@/shared/api/utils. Invalid format (empty, oversize, or non-string) returns400with error"Invalid id format".[integration]: Validated via the integration registry; unknown or disabled integration returns404or403as appropriate.
Rate limiting may be applied to prevent abuse. Check response headers:
X-RateLimit-Limit: Maximum requests per windowX-RateLimit-Remaining: Remaining requests in current windowX-RateLimit-Reset: Unix timestamp when limit resets
Many list endpoints support pagination via query parameters:
limit: Maximum number of results (default varies by endpoint)offset: Number of results to skip (default: 0)
Example:
GET /api/admin/users?limit=20&offset=40
- Always check response status codes before processing data
- Handle errors gracefully - show user-friendly messages
- Use pagination for large datasets
- Cache responses when appropriate (respect cache headers)
- Include proper authentication headers for protected endpoints
- Validate request data before sending
- Handle rate limiting with exponential backoff
const response = await fetch('/api/user/me', {
credentials: 'include', // Include session cookie
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
const data = await response.json();
console.log(data.user);const response = await fetch('/api/integrations/xmpp/accounts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
username: 'user@atl.chat',
password: 'secure-password',
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
const data = await response.json();
console.log(data.account);const response = await fetch('/api/admin/stats', {
headers: {
'Authorization': `Bearer ${apiKey}`,
},
});
const data = await response.json();
console.log(data.users.total);