immich-app/immich · error · UnauthorizedException

Invalid API key

Error message

Invalid API key

What it means

Thrown by AuthService.validateApiKey. The supplied key is SHA-256 hashed and looked up in apiKeyRepository.getKey; the lookup must return a record with a non-null user. Because keys are stored only as hashes, this fires both for malformed keys and for valid-looking keys that are simply not registered.

Source

Thrown at server/src/services/auth.service.ts:533

  }

  private isValidSharedLink(
    sharedLink?: AuthSharedLink & { user: AuthUser | null },
  ): sharedLink is AuthSharedLink & { user: AuthUser } {
    return !!sharedLink?.user && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date());
  }

  private async validateApiKey(key: string): Promise<AuthDto> {
    const hashed = this.cryptoRepository.hashSha256(key);
    const apiKey = await this.apiKeyRepository.getKey(hashed);
    if (apiKey?.user) {
      return {
        user: apiKey.user,
        apiKey,
      };
    }

    throw new UnauthorizedException('Invalid API key');
  }

  private validateSecret(inputSecret: string, existingHash?: string | null): boolean {
    if (!existingHash) {
      return false;
    }

    return this.cryptoRepository.compareBcrypt(inputSecret, existingHash);
  }

  private async validateSession(token: string, headers: IncomingHttpHeaders): Promise<AuthDto> {
    const hashed = this.cryptoRepository.hashSha256(token);
    const session = await this.sessionRepository.getByToken(hashed);
    if (session?.user) {
      const { appVersion, deviceOS, deviceType } = getUserAgentDetails(headers);
      const now = DateTime.now();
      const updatedAt = DateTime.fromJSDate(session.updatedAt);
      const diff = now.diff(updatedAt, ['hours']);

View on GitHub (pinned to 199723261c)

Solutions

  1. Regenerate the API key from the user's API Keys settings and copy it without trailing whitespace.
  2. Confirm the key belongs to this server instance/database (api_keys table is present and populated).
  3. Send the key in the header the guard expects (commonly x-api-key), not as a bearer token.
  4. If integrating programmatically, validate key presence and non-empty length before the request and fail fast client-side.

Example fix

// before
const res = await fetch(url, { headers: { 'x-api-key': keyFromConfig } });

// after
if (!keyFromConfig || keyFromConfig.trim().length < 16) {
  throw new Error('API key is missing or malformed');
}
const res = await fetch(url, { headers: { 'x-api-key': keyFromConfig.trim() } });
if (res.status === 401) throw new Error('API key rejected by server (revoked or wrong instance)');
Defensive patterns

Strategy: validation

Validate before calling

function isValidApiKeyFormat(key: unknown): boolean {
  return typeof key === 'string' && key.trim().length >= 16 && /^[A-Za-z0-9_-]+$/.test(key);
}
if (!isValidApiKeyFormat(process.env.IMMICH_API_KEY)) {
  throw new Error('IMMICH_API_KEY is missing or malformed');
}

Type guard

function isApiKey(v: unknown): v is string {
  return typeof v === 'string' && v.trim().length >= 16;
}

Prevention

When it happens

Trigger: Any request carrying an x-api-key header (or equivalent) whose value does not hash to a known, user-backed API key record: revoked keys, mistyped keys, keys from a different environment/database, or an empty header.

Common situations: Using a key issued against a different Immich instance or after a DB restore that lost the api_keys table; sending the key in the wrong header; whitespace/newline copied into the key; key was deleted by the owner.

Understand the failure class

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/c67a4fcb828456a9. Report an issue: GitHub.