immich-app/immich · error · UnauthorizedException

Invalid API key

Error message

Invalid API key

What it means

API-key authentication hashes the presented key and compares it (via validateSecret) against the stored hash. If no API key record matches the provided secret, an UnauthorizedException('Invalid API key') is thrown.

Solutions

  1. Regenerate or re-copy the API key from user settings and update the client
  2. Ensure the header value is the secret key with no surrounding whitespace or quotes
  3. Confirm the key is still active and was not deleted/rotated
  4. Verify the client sends the key on the expected header for this endpoint

Example fix

// before
const key = process.env.IMMICH_API_KEY.trim().split(' ')[0];
// after
const key = process.env.IMMICH_API_KEY;
Defensive patterns

Strategy: validation

Validate before calling

if (!/^[A-Za-z0-9]{20,}$/.test(apiKey)) throw new Error('malformed api key');

Try / catch

catch (e) { if (e.status === 401 && e.message === 'Invalid API key') { /* refresh key from settings */ } }

Prevention

When it happens

Trigger: Request with x-api-key header whose value does not match any stored key hash, or validate() routed to API-key auth with an empty/malformed key.

Common situations: Key rotated/revoked server-side while client still uses the old value; key truncated or whitespace-padded in env config; copying the key name/label instead of the secret; regenerating the key without updating integrations.

Understand the failure class

Related errors


AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15). Data as JSON: /api/errors/c67a4fcb828456a9. Report an issue: GitHub.

Appendix: source

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

  }

  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 f48d4b3321)