Skip to content

Multi-User API Guide

This guide explains how to interact with InvokeAI’s API in both single-user and multi-user modes. The API behavior depends on the multiuser configuration setting.

Single-User Mode (multiuser: false or option absent):

  • No authentication required
  • All API endpoints accessible without tokens
  • Direct API access like previous InvokeAI versions
  • All content visible in unified view

Multi-User Mode (multiuser: true):

  • JWT token authentication required
  • User-scoped access to resources
  • Role-based authorization (admin vs regular user)
  • Data isolation between users

When multi-user mode is enabled, most API endpoints require authentication using JWT bearer tokens. The unauthenticated authentication endpoints are GET /api/v1/auth/status, POST /api/v1/auth/setup, and POST /api/v1/auth/login.

Authentication Process:

  1. Obtain Token: POST credentials to /api/v1/auth/login
  2. Store Token: Save the JWT token securely
  3. Use Token: Include token in Authorization header for all requests
  4. Refresh: Re-authenticate when token expires

Endpoint: POST /api/v1/auth/login

Request:

{
"email": "[email protected]",
"password": "SecurePassword123",
"remember_me": false
}

Response (Success):

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"user_id": "abc123",
"email": "[email protected]",
"display_name": "John Doe",
"is_admin": false,
"is_active": true,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"last_login_at": "2024-01-15T15:30:00Z"
},
"expires_in": 86400
}

Response (Error):

{
"detail": "Incorrect email or password"
}

Status Codes:

  • 200 OK — Authentication successful
  • 401 Unauthorized — Invalid credentials
  • 403 Forbidden — Account disabled
  • 422 Unprocessable Entity — Invalid request format

Include the JWT token in the Authorization header with the Bearer scheme:

HTTP Header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Example HTTP Request:

GET /api/v1/boards HTTP/1.1
Host: localhost:9090
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

Tokens have a limited lifetime:

  • Default: 24 hours (86400 seconds)
  • Remember Me: 7 days (604800 seconds)

Handling Expiration:

import requests
import time
def api_request(url, token, max_retries=1):
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
if response.status_code == 401: # Token expired
# Re-authenticate and retry
new_token = login()
headers = {"Authorization": f"Bearer {new_token}"}
response = requests.get(url, headers=headers)
return response

Endpoint: POST /api/v1/auth/logout

Request:

POST /api/v1/auth/logout HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response:

{
"success": true
}

Note: With JWT tokens, logout is primarily client-side (delete token). Server-side session invalidation may be added in future releases.

Using requests library:

import requests
import json
class InvokeAIClient:
def __init__(self, base_url="http://localhost:9090"):
self.base_url = base_url
self.token = None
def login(self, email, password, remember_me=False):
"""Authenticate and store token."""
url = f"{self.base_url}/api/v1/auth/login"
payload = {
"email": email,
"password": password,
"remember_me": remember_me
}
response = requests.post(url, json=payload)
response.raise_for_status()
data = response.json()
self.token = data["token"]
return data["user"]
def _get_headers(self):
"""Get headers with authentication token."""
if not self.token:
raise Exception("Not authenticated. Call login() first.")
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
def get_boards(self):
"""Get user's boards."""
url = f"{self.base_url}/api/v1/boards/"
response = requests.get(url, headers=self._get_headers())
response.raise_for_status()
return response.json()
def create_board(self, board_name):
"""Create a new board."""
url = f"{self.base_url}/api/v1/boards/"
response = requests.post(
url,
params={"board_name": board_name},
headers=self._get_headers()
)
response.raise_for_status()
return response.json()
def logout(self):
"""Logout and clear token."""
url = f"{self.base_url}/api/v1/auth/logout"
response = requests.post(url, headers=self._get_headers())
self.token = None
return response.json()
# Usage
client = InvokeAIClient()
user = client.login("[email protected]", "SecurePassword123")
print(f"Logged in as: {user['display_name']}")
boards = client.get_boards()
print(f"User has {len(boards['items'])} boards")
new_board = client.create_board("My New Board")
print(f"Created board: {new_board['board_name']}")
client.logout()

Error Handling:

import requests
from requests.exceptions import HTTPError
def safe_api_call(client, method, *args, **kwargs):
"""Make API call with error handling."""
try:
func = getattr(client, method)
return func(*args, **kwargs)
except HTTPError as e:
if e.response.status_code == 401:
print("Authentication failed or token expired")
# Re-authenticate
client.login(email, password)
# Retry
return func(*args, **kwargs)
elif e.response.status_code == 403:
print("Permission denied")
elif e.response.status_code == 404:
print("Resource not found")
else:
print(f"API error: {e.response.status_code}")
print(e.response.text)
raise
# Usage
try:
boards = safe_api_call(client, "get_boards")
except Exception as e:
print(f"Failed to get boards: {e}")

Using fetch API:

class InvokeAIClient {
constructor(baseUrl = 'http://localhost:9090') {
this.baseUrl = baseUrl;
this.token = null;
}
async login(email, password, rememberMe = false) {
const response = await fetch(`${this.baseUrl}/api/v1/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
remember_me: rememberMe,
}),
});
if (!response.ok) {
throw new Error(`Login failed: ${response.statusText}`);
}
const data = await response.json();
this.token = data.token;
// Store token in localStorage
localStorage.setItem('invokeai_token', data.token);
return data.user;
}
getHeaders() {
if (!this.token) {
throw new Error('Not authenticated. Call login() first.');
}
return {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
};
}
async getBoards() {
const response = await fetch(`${this.baseUrl}/api/v1/boards/`, {
headers: this.getHeaders(),
});
if (!response.ok) {
throw new Error(`Failed to get boards: ${response.statusText}`);
}
return response.json();
}
async createBoard(boardName) {
const url = new URL(`${this.baseUrl}/api/v1/boards/`);
url.searchParams.set('board_name', boardName);
const response = await fetch(url, {
method: 'POST',
headers: this.getHeaders(),
});
if (!response.ok) {
throw new Error(`Failed to create board: ${response.statusText}`);
}
return response.json();
}
async logout() {
const response = await fetch(`${this.baseUrl}/api/v1/auth/logout`, {
method: 'POST',
headers: this.getHeaders(),
});
this.token = null;
localStorage.removeItem('invokeai_token');
return response.json();
}
}
// Usage
(async () => {
const client = new InvokeAIClient();
try {
const user = await client.login('[email protected]', 'SecurePassword123');
console.log(`Logged in as: ${user.display_name}`);
const boards = await client.getBoards();
console.log(`User has ${boards.items.length} boards`);
const newBoard = await client.createBoard('My New Board');
console.log(`Created board: ${newBoard.board_name}`);
await client.logout();
} catch (error) {
console.error('Error:', error.message);
}
})();

TypeScript with Types:

interface LoginRequest {
email: string;
password: string;
remember_me?: boolean;
}
interface User {
user_id: string;
email: string;
display_name: string;
is_admin: boolean;
is_active: boolean;
created_at: string;
}
interface LoginResponse {
token: string;
user: User;
expires_in: number;
}
interface Board {
board_id: string;
board_name: string;
created_at: string;
updated_at: string;
deleted_at?: string;
cover_image_name?: string;
}
class InvokeAIClient {
private baseUrl: string;
private token: string | null = null;
constructor(baseUrl: string = 'http://localhost:9090') {
this.baseUrl = baseUrl;
}
async login(
email: string,
password: string,
rememberMe: boolean = false
): Promise<User> {
const response = await fetch(`${this.baseUrl}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, remember_me: rememberMe }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Login failed');
}
const data: LoginResponse = await response.json();
this.token = data.token;
return data.user;
}
private getHeaders(): HeadersInit {
if (!this.token) {
throw new Error('Not authenticated');
}
return {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
};
}
async getBoards(): Promise<{ items: Board[] }> {
const response = await fetch(`${this.baseUrl}/api/v1/boards/`, {
headers: this.getHeaders(),
});
if (!response.ok) {
throw new Error('Failed to get boards');
}
return response.json();
}
}

Login:

Terminal window
# Login and extract token
TOKEN=$(curl -X POST http://localhost:9090/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "SecurePassword123",
"remember_me": false
}' | jq -r '.token')
echo "Token: $TOKEN"

Get Boards:

Terminal window
curl -X GET http://localhost:9090/api/v1/boards/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"

Create Board:

Terminal window
curl -X POST "http://localhost:9090/api/v1/boards/?board_name=My%20API%20Board" \
-H "Authorization: Bearer $TOKEN"

All endpoints now require authentication except:

  • GET /api/v1/auth/status — Check whether multi-user setup is required
  • POST /api/v1/auth/setup — Initial admin setup
  • POST /api/v1/auth/login — User login
  • GET /api/v1/images/i/{image_name}/full — Full-resolution image file
  • GET /api/v1/images/i/{image_name}/thumbnail — Image thumbnail
  • GET /api/v1/workflows/i/{workflow_id}/thumbnail — Workflow thumbnail

The image and thumbnail endpoints are intentionally unauthenticated because browsers load these resources via <img src> tags, which cannot send Authorization headers. Security relies on the fact that image and workflow IDs are UUIDs and therefore unguessable.

Resources are now filtered by the authenticated user:

Boards:

# Before (single-user)
GET /api/v1/boards/?all=true # Returns all boards
# After (multi-user)
GET /api/v1/boards/?all=true # Returns boards the current user can access, including their own boards plus shared/public boards; admins can see all boards

Images:

# Images are filtered by board ownership
GET /api/v1/images/ # Only shows images on user's boards

Workflows:

# Returns user's workflows + public workflows
GET /api/v1/workflows/

Queue:

# Regular users see their own queue items in full and may see redacted details for other users' items on queue-status endpoints
GET /api/v1/queue/... # Queue data, sanitized for non-admin viewers
# Administrators see all queue items in full
GET /api/v1/queue/... # Full queue data

Some endpoints require administrator privileges:

User Management:

GET /api/v1/auth/users # List users (admin only)
POST /api/v1/auth/users # Create user (admin only)
GET /api/v1/auth/users/{id} # Get user (admin only)
PATCH /api/v1/auth/users/{id} # Update user (admin only)
DELETE /api/v1/auth/users/{id} # Delete user (admin only)

Model Management (Write Operations):

POST /api/v2/models/install # Install model (admin only)
DELETE /api/v2/models/i/{key} # Delete model (admin only)
PATCH /api/v2/models/i/{key} # Update model (admin only)
PUT /api/v2/models/convert/{key} # Convert model (admin only)

Model Management (Read Operations):

GET /api/v2/models/ # List models (all users)
GET /api/v2/models/i/{key} # Get model details (all users)

401 Unauthorized:

{
"detail": "Invalid authentication credentials"
}

Occurs when:

  • Token is missing
  • Token is invalid
  • Token is expired
  • Token signature is invalid

403 Forbidden:

{
"detail": "Admin privileges required"
}

Occurs when:

  • User attempts admin-only operation
  • Account is disabled
  • Insufficient permissions

404 Not Found:

{
"detail": "Resource not found"
}

Occurs when:

  • Resource doesn’t exist
  • User doesn’t have access to resource

Check if initial administrator setup is required

Section titled “Check if initial administrator setup is required”

Endpoint: GET /api/v1/auth/status

Description: Returns a SetupStatusResponse indicating whether setup is needed and multiuser mode status.

Request: No parameters

Response (initial setup not yet complete):

{
"setup_required": true,
"multiuser_enabled": true,
"strict_password_checking": true,
"admin_email": "[email protected]"
}

Response (setup already complete, or multiuser disabled):

{
"setup_required": false,
"multiuser_enabled": true,
"strict_password_checking": true,
"admin_email": null
}

Endpoint: POST /api/v1/auth/setup

Description: Create initial administrator account (only works if no admin exists)

Request:

{
"email": "[email protected]",
"display_name": "Administrator",
"password": "SecureAdminPass123"
}

Response:

{
"success": true,
"user": {
"user_id": "abc123",
"email": "[email protected]",
"display_name": "Administrator",
"is_admin": true,
"is_active": true
}
}

Endpoint: GET /api/v1/auth/me

Description: Get currently authenticated user’s information

Request:

GET /api/v1/auth/me
Authorization: Bearer <token>

Response:

{
"user_id": "abc123",
"email": "[email protected]",
"display_name": "John Doe",
"is_admin": false,
"is_active": true,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"last_login_at": "2024-01-15T15:30:00Z"
}

Endpoint: GET /api/v1/auth/users

Request:

GET /api/v1/auth/users
Authorization: Bearer <admin_token>

Response:

[
{
"user_id": "abc123",
"email": "[email protected]",
"display_name": "John Doe",
"is_admin": false,
"is_active": true,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-04-25T17:23:00Z",
"last_login_at": "2024-01-15T15:30:00Z"
}
]

Endpoint: POST /api/v1/auth/users

Request:

{
"email": "[email protected]",
"display_name": "New User",
"password": "TempPassword123",
"is_admin": false
}

Response:

{
"user_id": "xyz789",
"email": "[email protected]",
"display_name": "New User",
"is_admin": false,
"is_active": true,
"created_at": "2024-01-15T16:00:00Z"
}

Endpoint: PATCH /api/v1/auth/users/{user_id}

Request:

{
"display_name": "Updated Name",
"is_active": true,
"is_admin": false
}

Response:

{
"user_id": "xyz789",
"email": "[email protected]",
"display_name": "Updated Name",
"is_admin": false,
"is_active": true
}

Endpoint: DELETE /api/v1/auth/users/{user_id}

Response:

Returns 204 No Content on success.

On an error, it returns 422 Unprocessable Content and the following JSON:

{
"detail": [
{
"loc": [
"string",
0
],
"msg": "string",
"type": "string"
}
]
}

Endpoint: GET /api/v1/boards/

Response:

{
"limit": 0,
"offset": 0,
"total": 0,
"items": [
{
"board_id": "8b31a33d-0acb-46fe-8612-83601481cf2c",
"board_name": "Testing Board",
"user_id": "string",
"created_at": "2026-05-07T03:04:00.738Z",
"updated_at": "2026-05-07T03:04:00.738Z",
"deleted_at": "2026-05-07T03:04:00.738Z",
"cover_image_name": "string",
"archived": false,
"board_visibility": "private",
"image_count": 0,
"asset_count": 0,
"owner_username": "string"
}
]
}

This returns a paged response. See the swagger page (http://localhost:9090/docs#/boards/list_boards) for details. The board_visibility field will be one of:

  • private — private to the owner and administrator
  • shared — read/write to the owner and administrator, read-only to everyone else
  • public — read/write by everyone

Endpoint: GET /api/v1/boards/{board_id}

Response:

{
"board_id": "8b31a33d-0acb-46fe-8612-83601481cf2c",
"board_name": "Testing Board",
"user_id": "3c59a0ba-f4c7-4275-b96f-82179e8aaff8",
"created_at": "2026-03-09 16:10:47.095",
"updated_at": "2026-03-09 16:10:55",
"deleted_at": null,
"cover_image_name": "08689e4b-f084-4c49-83a8-4fc1edb167c4.png",
"archived": false,
"board_visibility": "shared",
"image_count": 55,
"asset_count": 0,
"owner_username": null
}

Do:

  • Store tokens securely (keychain, secure storage)
  • Use HTTPS to transmit tokens
  • Clear tokens on logout
  • Handle token expiration gracefully

Don’t:

  • Store tokens in URL parameters
  • Log tokens in plain text
  • Share tokens between users
  • Store tokens in version control

Always handle authentication errors:

def make_request(client, func, *args, **kwargs):
max_retries = 3
retry_count = 0
while retry_count < max_retries:
try:
return func(*args, **kwargs)
except AuthenticationError:
if retry_count >= max_retries - 1:
raise
# Re-authenticate
client.login(email, password)
retry_count += 1
except Exception as e:
logger.error(f"Request failed: {e}")
raise

Be mindful of API rate limits:

  • Implement exponential backoff for retries
  • Cache frequently accessed data
  • Batch requests when possible
  • Don’t hammer the login endpoint
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_session():
"""Create session with retry logic."""
session = requests.Session()
retry = Retry(
total=3,
backoff_factor=0.3,
status_forcelist=[500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session

Before (single-user mode):

import requests
def get_boards():
response = requests.get("http://localhost:9090/api/v1/boards/")
return response.json()

After (multi-user mode):

import requests
class APIClient:
def __init__(self):
self.token = None
def login(self, email, password):
response = requests.post(
"http://localhost:9090/api/v1/auth/login",
json={"email": email, "password": password}
)
self.token = response.json()["token"]
def get_boards(self):
headers = {"Authorization": f"Bearer {self.token}"}
response = requests.get(
"http://localhost:9090/api/v1/boards/",
headers=headers
)
return response.json()
# Usage
client = APIClient()
client.login("[email protected]", "password")
boards = client.get_boards()

InvokeAI supports both single-user and multi-user modes via the multiuser configuration option.

Configuration:

invokeai.yaml
# Single-user mode (no authentication)
multiuser: false # or omit the option entirely
# Multi-user mode (authentication required)
multiuser: true

Checking Mode Programmatically:

def is_multiuser_enabled(base_url):
response = requests.get(f"{base_url}/api/v1/auth/status")
response.raise_for_status()
return response.json()["multiuser_enabled"]
# Example usage
base_url = "http://localhost:9090"
if is_multiuser_enabled(base_url):
print("Multi-user mode: authentication required")
# Use authenticated API calls
else:
print("Single-user mode: no authentication needed")
# Use direct API calls

Adaptive Client:

class AdaptiveInvokeAIClient:
def __init__(self, base_url="http://localhost:9090"):
self.base_url = base_url
self.token = None
self.multiuser_mode = self._check_multiuser_mode()
def _check_multiuser_mode(self):
"""Detect if multi-user mode is enabled."""
try:
response = requests.get(f"{self.base_url}/api/v1/boards/")
return response.status_code == 401
except:
return False
def login(self, email, password):
"""Login (only needed in multi-user mode)."""
if not self.multiuser_mode:
print("Single-user mode: login not required")
return
response = requests.post(
f"{self.base_url}/api/v1/auth/login",
json={"email": email, "password": password}
)
self.token = response.json()["token"]
def _get_headers(self):
"""Get headers (with auth token if in multi-user mode)."""
if self.multiuser_mode and self.token:
return {"Authorization": f"Bearer {self.token}"}
return {}
def get_boards(self):
"""Get boards (works in both modes)."""
response = requests.get(
f"{self.base_url}/api/v1/boards/",
headers=self._get_headers()
)
return response.json()

InvokeAI provides OpenAPI documentation for all endpoints.

Access Swagger UI:

http://localhost:9090/docs

Download OpenAPI Schema:

Terminal window
curl http://localhost:9090/openapi.json > invokeai_openapi.json

Generate Client Code:

Use tools like openapi-generator to generate client libraries:

Terminal window
# Generate Python client
openapi-generator generate \
-i http://localhost:9090/openapi.json \
-g python \
-o ./invokeai-client
# Generate TypeScript client
openapi-generator generate \
-i http://localhost:9090/openapi.json \
-g typescript-fetch \
-o ./invokeai-client-ts

Always use HTTPS in production:

# Development
client = InvokeAIClient("http://localhost:9090")
# Production
client = InvokeAIClient("https://invoke.example.com")

Protect JWT tokens:

# Never log tokens
logger.info(f"User logged in") # Good
logger.info(f"Token: {token}") # Bad!
# Use environment variables for credentials
import os
email = os.environ.get("INVOKEAI_EMAIL")
password = os.environ.get("INVOKEAI_PASSWORD")

Always validate user input:

import re
def validate_email(email):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
def validate_password(password):
"""Check password meets requirements."""
if len(password) < 8:
return False, "Password must be at least 8 characters"
if not any(c.isupper() for c in password):
return False, "Password must contain uppercase letters"
if not any(c.islower() for c in password):
return False, "Password must contain lowercase letters"
if not any(c.isdigit() for c in password):
return False, "Password must contain numbers"
return True, ""

Issue: “Invalid authentication credentials”

  • Token expired — re-authenticate
  • Token malformed — check token string
  • Token signature invalid — check secret key hasn’t changed

Issue: “Admin privileges required”

  • User is not an administrator
  • Use admin account for this operation

Issue: Token not being sent

  • Check Authorization header is present
  • Verify Bearer prefix is included
  • Check token isn’t truncated

Issue: CORS errors

Configure CORS in InvokeAI:

invokeai.yaml
allow_origins:
- "http://localhost:3000"
- "https://myapp.example.com"
allow_credentials: true
allow_methods:
- "*"
allow_headers:
- "*"

Questions? Visit the InvokeAI Discord or check the FAQ.

This site was designed and developed by Aether Fox Studio.