# Multi-User API Guide

import { Steps } from '@astrojs/starlight/components'

## Overview

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 vs Multi-User Mode

**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

## Authentication (Multi-User Mode Only)

### Authentication Flow

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:**

<Steps>
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
</Steps>

:::note[Single-User Mode]
When running in single-user mode (`multiuser: false`), authentication endpoints are not available and authentication headers are not required.
:::

### Login Endpoint

**Endpoint:** `POST /api/v1/auth/login`

**Request:**

```json
{
  "email": "user@example.com",
  "password": "SecurePassword123",
  "remember_me": false
}
```

**Response (Success):**

```json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "user_id": "abc123",
    "email": "user@example.com",
    "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):**

```json
{
  "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

### Using the Token

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

**HTTP Header:**

```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**Example HTTP Request:**

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

### Token Expiration

Tokens have a limited lifetime:

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

**Handling Expiration:**

```python
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
```

### Logout Endpoint

**Endpoint:** `POST /api/v1/auth/logout`

**Request:**

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

**Response:**

```json
{
  "success": true
}
```

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

## Code Examples

### Python

**Using `requests` library:**

```python
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("user@example.com", "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:**

```python
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}")
```

### JavaScript/TypeScript

**Using `fetch` API:**

```javascript
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('user@example.com', '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:**

```typescript
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();
  }
}
```

### cURL

**Login:**

```bash
# Login and extract token
TOKEN=$(curl -X POST http://localhost:9090/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "SecurePassword123",
    "remember_me": false
  }' | jq -r '.token')

echo "Token: $TOKEN"
```

**Get Boards:**

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

**Create Board:**

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

## API Endpoint Changes

### Authentication Required

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.

### User-Scoped Resources

Resources are now filtered by the authenticated user:

**Boards:**

```python
# 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, boards they are a member of and boards with general access; admins can see all boards
```

**Images:**

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

**Workflows:**

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

**Queue:**

```python
# 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

# With origin_prefix, item_ids, current, next and status cover only the caller's own items
GET /api/v1/queue/{queue_id}/status?origin_prefix=webv2:p:<project_id>:q:

# Administrators see all queue items in full
GET /api/v1/queue/...  # Full queue data
```

### Administrator Endpoints

Some endpoints require administrator privileges:

**User Management:**

```python
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):**

```python
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):**

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

### Error Responses

**401 Unauthorized:**

```json
{
  "detail": "Invalid authentication credentials"
}
```

Occurs when:

- Token is missing
- Token is invalid
- Token is expired
- Token signature is invalid

**403 Forbidden:**

```json
{
  "detail": "Admin privileges required"
}
```

Occurs when:

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

**404 Not Found:**

```json
{
  "detail": "Resource not found"
}
```

Occurs when:

- Resource doesn't exist
- User doesn't have access to resource

## Multiuser API Endpoints

### Authentication Endpoints

#### 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):**
```json
{
  "setup_required": true,
  "multiuser_enabled": true,
  "strict_password_checking": true,
  "admin_email": "admin@example.com"
}
```

**Response (setup already complete, or multiuser disabled):**
```json
{
  "setup_required": false,
  "multiuser_enabled": true,
  "strict_password_checking": true,
  "admin_email": null
}
```

:::note
`admin_email` is only populated while `setup_required` is `true` (to help locate the pre-seeded
administrator account during initial setup). Once an admin has been created — and whenever
multiuser mode is disabled — it is returned as `null` to avoid leaking administrator identity on
public deployments.
:::


#### Setup Administrator

**Endpoint:** `POST /api/v1/auth/setup`

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

**Request:**

```json
{
  "email": "admin@example.com",
  "display_name": "Administrator",
  "password": "SecureAdminPass123"
}
```

**Response:**

```json
{
  "success": true,
  "user": {
    "user_id": "abc123",
    "email": "admin@example.com",
    "display_name": "Administrator",
    "is_admin": true,
    "is_active": true
  }
}
```


#### Get Current User

**Endpoint:** `GET /api/v1/auth/me`

**Description:** Get currently authenticated user's information

**Request:**

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

**Response:**

```json
{
  "user_id": "abc123",
  "email": "user@example.com",
  "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"
}
```

### User Management Endpoints (Admin Only)

#### List Users

**Endpoint:** `GET /api/v1/auth/users`

**Request:**

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

**Response:**

```json
[
  {
    "user_id": "abc123",
    "email": "user@example.com",
    "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"
  }
]
```

#### Create User

**Endpoint:** `POST /api/v1/auth/users`

**Request:**

```json
{
  "email": "newuser@example.com",
  "display_name": "New User",
  "password": "TempPassword123",
  "is_admin": false
}
```

**Response:**

```json
{
  "user_id": "xyz789",
  "email": "newuser@example.com",
  "display_name": "New User",
  "is_admin": false,
  "is_active": true,
  "created_at": "2024-01-15T16:00:00Z"
}
```

#### Update User

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

**Request:**

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

**Response:**

```json
{
  "user_id": "xyz789",
  "email": "newuser@example.com",
  "display_name": "Updated Name",
  "is_admin": false,
  "is_active": true
}
```

#### Delete User

**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:

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

### Board Endpoints

Boards are available to every authenticated user; what each caller may do to one is decided by their role on it.

#### List Image Boards

**Endpoint:** `GET /api/v1/boards/`

**Response:**

```json
{
  "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,
      "general_access": "restricted",
      "board_visibility": "private",
      "my_role": "owner",
      "allowed_actions": ["read", "add_media", "manage_items", "rename", "set_cover", "archive", "set_general_access", "move", "delete"],
      "admin_override": false,
      "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.
Each board describes the caller's access to it:

- `general_access` -- the role every other user gets: `restricted` (none), `viewer` or `contributor`. Only the owner or an administrator may change it, with `PATCH /api/v1/boards/{board_id}`.
- `my_role` -- the caller's own role: `viewer`, `contributor`, `editor` or `owner`; `null` when only administrator rights give access.
- `allowed_actions` -- the board actions the caller may take. Clients should decide what to offer from this list rather than from `my_role`.
- `admin_override` -- `true` when administrator rights give the caller more than `my_role` does.
- `owner_username` -- the owner's display name. Administrators see the email of an owner without one; other users see `Unknown user`.
- `project_id` -- shown only to the board's owner and administrators. An image or video DTO's `project_id` likewise names the project it was made in only to that project's owner and to administrators; everyone else reads `null`.

Only a board that does not exist answers `404`. A board the caller cannot open answers `403` with the detail `{"code": "board_unreadable", "message": "Not authorized to access this board"}`, and an action the caller's role does not allow answers `403` with the code `board_action_denied` and the message `Not authorized to change this board`: a refusal can be reversed by the board's owner, so clients must not treat it as the board being gone, and the code tells lost access from a stale role without matching the message. A project's inbox refuses renaming, archiving, moving, deleting and a change of general access with `409`; those follow its project.

:::caution[Deprecated]
`board_visibility` is `general_access` under its pre-7.0 name (`private` = `restricted`, `shared` = `viewer`, `public` = `contributor`). It is still returned and accepted on update, but sending it together with `general_access` is refused with `422`. It will be removed in 8.0.
:::

#### Get One Board

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

**Response:**

```json
{
  "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,
  "general_access": "viewer",
  "board_visibility": "shared",
  "my_role": "viewer",
  "allowed_actions": ["read"],
  "admin_override": false,
  "image_count": 55,
  "asset_count": 0,
  "owner_username": "Sam"
}
```

### Project Endpoints

Projects are private to the account that created them. The full save protocol, including the content and workspace halves and the schema floors, is described in [Project Files (.invk)](/development/front-end/project-files/).

#### Create a Project

**Endpoint:** `POST /api/v1/projects`

Creates the project and its inbox board and answers `201` with the record and `workspace`. The optional `workspace` field is the caller's first workspace in the project, the workspace document itself (a create's workspace is always the first row, so it carries no `expected_revision`), stored in the same transaction as the content, so no project is created without its creator's workspace. It is checked as on `PUT /api/v1/projects/{project_id}/workspace`, except that the create's `max_canvas_schema_version` covers it, and it needs `minimum_document_schema_version` at 4 or above: beside a whole document it is refused with `400` and the code `workspace_needs_split_document`, and nothing is created. The response's `workspace` is the stored workspace (`data`, `revision` 1, `updated_at`), or `null` when the request carried none.

#### Save a Project

**Endpoint:** `PUT /api/v1/projects/{project_id}`

Saves the project's content and answers with the record and `workspace`. The optional `workspace` field (`{data, expected_revision}`) stores the caller's workspace in the same transaction and is for the save that splits a whole project (document schema 3 or older) into content and workspace: the raised document floor, the content and the first workspace row then land together or not at all. Its revision is checked as on `PUT /api/v1/projects/{project_id}/workspace`, and a mismatch refuses the whole save with `409` and the workspace's `current_revision`. Omit it on every other save: a `workspace` sent with a save that leaves the document floor below 4 is refused with `400` and the code `workspace_needs_split_document`. The split cannot go without it either: a save that raises the floor from below 4 to 4 or above without `workspace` is refused with `400` and the code `split_needs_workspace`, so the split of a whole project never lands without the caller's workspace.

Workspace saves declare `max_canvas_schema_version` (default 2) as well as `max_document_schema_version`; a client below the project's canvas floor gets `412` with the code `canvas_schema_unsupported`.


## Best Practices

### Token Storage

**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

### Error Handling

Always handle authentication errors:

```python
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
```

### Rate Limiting

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

### Connection Management

```python
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
```

## Migration Guide

### Updating Existing Code

**Before (single-user mode):**

```python
import requests

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

**After (multi-user mode):**

```python
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("user@example.com", "password")
boards = client.get_boards()
```

### Backward Compatibility

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

**Configuration:**

```yaml
# invokeai.yaml

# Single-user mode (no authentication)
multiuser: false  # or omit the option entirely

# Multi-user mode (authentication required)
multiuser: true
```

**Checking Mode Programmatically:**

```python
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:**

```python
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()
```

## Programmatic Graph Execution

If you are building an external client against the HTTP API, see the dedicated [Workflow Execution guide](/development/guides/workflow-api/).

In multi-user mode, remember to include your `Authorization: Bearer <token>` header on queue and workflow requests.

## OpenAPI/Swagger Documentation

InvokeAI provides OpenAPI documentation for all endpoints.

**Access Swagger UI:**

```
http://localhost:9090/docs
```

**Download OpenAPI Schema:**

```bash
curl http://localhost:9090/openapi.json > invokeai_openapi.json
```

**Generate Client Code:**

Use tools like `openapi-generator` to generate client libraries:

```bash
# 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
```

## Security Considerations

### HTTPS

Always use HTTPS in production:

```python
# Development
client = InvokeAIClient("http://localhost:9090")

# Production
client = InvokeAIClient("https://invoke.example.com")
```

### Token Security

Protect JWT tokens:

```python
# 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")
```

### Input Validation

Always validate user input:

```python
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, ""
```

## Troubleshooting

### Common Issues

**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:

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

## Additional Resources

- [User Guide](/configuration/multi-user-mode/user-guide/) — For end users
- [Administrator Guide](/configuration/multi-user-mode/admin-guide/) — For administrators
- [GitHub Repository](https://github.com/invoke-ai/InvokeAI) — Source code

---

**Questions?** Visit the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy) or check the [FAQ](/troubleshooting/faq/).