How the Immich API Is Structured and API Key Authentication Works

Immich organizes its REST API under a /api prefix using NestJS controllers, and secures access via SHA‑256 hashed API keys that must be sent in the x-api-key header or apiKey query parameter for every request.

The Immich backend, located in the immich-app/immich repository, is built with NestJS and exposes a conventional REST API grouped by functional domain. Understanding the Immich API structure and API key authentication mechanisms is essential for building secure integrations, whether using direct HTTP calls or the official TypeScript SDK.

NestJS API Architecture and Routing Structure

All public endpoints live under the /api prefix and are grouped by functional domain—users, assets, albums, and authentication. Each domain is implemented by a controller class decorated with @Controller('<route>'), with individual methods annotated using @Get, @Post, @Put, or @Delete.

Common route patterns include:

  • /api/usersUserController
  • /api/assetsAssetController
  • /api/albumsAlbumController
  • /api/api-keysApiKeyController
  • /api/authAuthController

To protect a route, developers apply the custom @Authenticated decorator. This decorator tells the AuthGuard—implemented in server/src/middleware/auth.guard.ts—which permission is required and whether the route is admin-only or shared-link-aware.

API Key Authentication Workflow

API keys in Immich follow a strict creation and validation pipeline that ensures secrets are never stored in plain text.

Creating API Keys

A client initiates access by sending a POST /api/api-keys request with a JSON body describing the key name and desired permissions. The ApiKeyController.createApiKey method forwards this to ApiKeyService.create in server/src/controllers/api-key.controller.ts.

In server/src/services/api-key.service.ts, the service generates a random 32-byte token, stores only its SHA‑256 hash in the api_key table, and returns the plain token once in the response (APIKeyCreateResponseDto). The clear-text secret is never persisted to the database.

Sending API Keys in Requests

Subsequent API calls must include the key once per request using one of two methods:

  • As the header x-api-key (preferred and case-insensitive), defined in ImmichHeader.ApiKey in server/src/enum.ts
  • Or as the query parameter apiKey

Validation and Authorization

The AuthGuard.canActivate method extracts the candidate value from the header or query string and passes it to AuthService.authenticate. Inside server/src/services/auth.service.ts, the validate method invokes validateApiKey when an apiKey is present.

The validation process:

  1. Hashes the supplied token using SHA‑256
  2. Looks up the hash in the api_key repository
  3. If found, builds an AuthDto containing the owning user and the permission list stored with the key

Permission Enforcement

When a route requires a specific permission, AuthService.validate calls isGranted from server/src/utils/access.ts. This helper checks whether the key’s permission set contains the requested permission or the special wildcard Permission.All.

Inside controller methods, the current user and key are available via the @Auth() parameter decorator. For example:

@Get('me')
@Authenticated({ permission: false })
async getMyApiKey(@Auth() auth: AuthDto): Promise<APIKeyResponseDto> {
  return this.service.getMine(auth);
}

Using the Official TypeScript SDK

Immich provides a TypeScript SDK that mirrors the server API structure. The SDK handles authentication globally via the init function:

import { init } from 'immich-openapi-typescript-sdk';

init({
  baseUrl: 'https://my-immich-instance/api',
  apiKey: '<your-x-api-key>',
});

As implemented in open-api/typescript-sdk/src/index.ts, the init function stores the key in default fetch options as the x-api-key header, automatically injecting it into all subsequent requests. The SDK rejects any attempt to set that header manually on individual calls.

Practical Code Examples

Creating an API Key via cURL

curl -X POST https://my-immich/api/api-keys \
  -H "Authorization: Bearer <user-jwt>" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "mobile-app",
        "permissions": ["asset.read","asset.upload"]
      }'

The response returns the secret only once:

{
  "secret": "a1b2c3d4…",
  "apiKey": {
    "id": "c5d8e9f0-1234-5678-abcd-ef0123456789",
    "name": "mobile-app",
    "createdAt": "2024-05-01T12:34:56Z",
    "updatedAt": "2024-05-01T12:34:56Z",
    "permissions": ["asset.read","asset.upload"]
  }
}

Authenticating Requests with cURL

curl -X GET https://my-immich/api/assets \
  -H "x-api-key: a1b2c3d4…"

The AuthGuard validates the key, loads the permission list, and allows the request because Permission.AssetRead is present.

Using the TypeScript SDK

import {
  init,
  getAssetOriginalPath,
  listAssets,
} from 'immich-openapi-typescript-sdk';

// Initialise SDK with base URL and API key
init({
  baseUrl: 'https://my-immich/api',
  apiKey: process.env.IMMICH_API_KEY!, // store secret in env, never hard-code
});

// List assets (the SDK automatically adds the x-api-key header)
const assets = await listAssets({});

// Download a specific asset
const downloadUrl = `${init.getBaseUrl()}${getAssetOriginalPath('abcd1234')}`;
const data = await fetch(downloadUrl);

Server-Side Permission Checks

In server/src/controllers, protected routes use the @Authenticated decorator with specific permissions:

@Delete(':id')
@Authenticated({ permission: Permission.AssetDelete })
async deleteAsset(
  @Auth() auth: AuthDto,
  @Param('id') id: string,
) {
  // auth.apiKey?.permissions already guaranteed to contain AssetDelete
  return this.assetService.delete(auth.user.id, id);
}

Key Implementation Files

Understanding the Immich API structure requires familiarity with these core files:

Summary

  • Immich API structure: NestJS controllers group endpoints under /api by domain (users, assets, albums), protected by the @Authenticated decorator and AuthGuard.
  • API key creation: Generated as 32-byte random tokens, stored as SHA‑256 hashes only, with the plain secret returned once during creation in ApiKeyService.create.
  • Authentication methods: Send the key via x-api-key header (preferred) or apiKey query parameter on every request.
  • Validation flow: AuthGuard extracts the key, AuthService.validateApiKey hashes and lookups the token, returning an AuthDto with user and permissions.
  • Permission model: Fine-grained access control via isGranted in server/src/utils/access.ts, supporting specific permissions or the Permission.All wildcard.
  • SDK integration: The TypeScript SDK handles x-api-key header injection automatically through the init() function.

Frequently Asked Questions

How are API keys stored securely in Immich?

API keys are never stored in plain text. According to the source code in server/src/services/api-key.service.ts, Immich generates a random 32-byte token, calculates its SHA‑256 hash, and persists only the hash to the api_key database table. The original secret is returned to the client exactly once during creation and then discarded.

Can I restrict an API key to specific permissions?

Yes. When creating a key via POST /api/api-keys, you specify a permissions array in the request body (e.g., ["asset.read", "asset.upload"]). The AuthService.validate method in server/src/services/auth.service.ts enforces these restrictions via the isGranted helper, which checks if the key's permission set includes the required action or the Permission.All wildcard.

What header should I use to send an API key?

Use the x-api-key header, defined in server/src/enum.ts as ImmichHeader.ApiKey. This is the preferred, case-insensitive method. Alternatively, you may include the key as an apiKey query parameter, though the header approach is recommended for better security and logging practices.

How does the TypeScript SDK handle authentication?

The SDK exports an init function from open-api/typescript-sdk/src/index.ts that accepts an apiKey parameter. This value is stored in the default fetch configuration as the x-api-key header, ensuring all generated client functions automatically include authentication without manual header manipulation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →