{"record":{"id":"c67a4fcb828456a9","repo":"immich-app/immich","slug":"invalid-api-key","errorCode":null,"errorMessage":"Invalid API key","messagePattern":"Invalid API key","errorType":"http","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":533,"sourceCode":"  }\n\n  private isValidSharedLink(\n    sharedLink?: AuthSharedLink & { user: AuthUser | null },\n  ): sharedLink is AuthSharedLink & { user: AuthUser } {\n    return !!sharedLink?.user && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date());\n  }\n\n  private async validateApiKey(key: string): Promise<AuthDto> {\n    const hashed = this.cryptoRepository.hashSha256(key);\n    const apiKey = await this.apiKeyRepository.getKey(hashed);\n    if (apiKey?.user) {\n      return {\n        user: apiKey.user,\n        apiKey,\n      };\n    }\n\n    throw new UnauthorizedException('Invalid API key');\n  }\n\n  private validateSecret(inputSecret: string, existingHash?: string | null): boolean {\n    if (!existingHash) {\n      return false;\n    }\n\n    return this.cryptoRepository.compareBcrypt(inputSecret, existingHash);\n  }\n\n  private async validateSession(token: string, headers: IncomingHttpHeaders): Promise<AuthDto> {\n    const hashed = this.cryptoRepository.hashSha256(token);\n    const session = await this.sessionRepository.getByToken(hashed);\n    if (session?.user) {\n      const { appVersion, deviceOS, deviceType } = getUserAgentDetails(headers);\n      const now = DateTime.now();\n      const updatedAt = DateTime.fromJSDate(session.updatedAt);\n      const diff = now.diff(updatedAt, ['hours']);","sourceCodeStart":515,"sourceCodeEnd":551,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/auth.service.ts#L515-L551","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Regenerate the API key from the user's API Keys settings and copy it without trailing whitespace.","Confirm the key belongs to this server instance/database (api_keys table is present and populated).","Send the key in the header the guard expects (commonly x-api-key), not as a bearer token.","If integrating programmatically, validate key presence and non-empty length before the request and fail fast client-side."],"exampleFix":"// before\nconst res = await fetch(url, { headers: { 'x-api-key': keyFromConfig } });\n\n// after\nif (!keyFromConfig || keyFromConfig.trim().length < 16) {\n  throw new Error('API key is missing or malformed');\n}\nconst res = await fetch(url, { headers: { 'x-api-key': keyFromConfig.trim() } });\nif (res.status === 401) throw new Error('API key rejected by server (revoked or wrong instance)');","handlingStrategy":"validation","validationCode":"function isValidApiKeyFormat(key: unknown): boolean {\n  return typeof key === 'string' && key.trim().length >= 16 && /^[A-Za-z0-9_-]+$/.test(key);\n}\nif (!isValidApiKeyFormat(process.env.IMMICH_API_KEY)) {\n  throw new Error('IMMICH_API_KEY is missing or malformed');\n}","typeGuard":"function isApiKey(v: unknown): v is string {\n  return typeof v === 'string' && v.trim().length >= 16;\n}","tryCatchPattern":null,"preventionTips":["Store API keys in a secrets manager, never hard-coded, and copy them without whitespace.","Issue one key per integration so revocation does not affect others.","Tie API keys to the specific instance/database that issued them."],"tags":["auth","api-key","unauthorized","hashing"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}