immich-app/immich · error · ForbiddenException
Sync endpoints cannot be used with API keys
Error message
Sync endpoints cannot be used with API keys
What it means
Thrown (as ForbiddenException) by SyncService.throwSessionRequired whenever a sync endpoint is invoked by a request whose AuthDto has no session id, i.e. it was authenticated with an API key instead of a user session. The sync protocol relies on a persistent server-side session to store ack checkpoints, so API keys are explicitly rejected.
Source
Thrown at server/src/services/sync.service.ts:84
SyncRequestType.AlbumsV2,
SyncRequestType.AlbumUsersV1,
SyncRequestType.AlbumToAssetsV1,
SyncRequestType.AssetExifsV1,
SyncRequestType.AlbumAssetExifsV1,
SyncRequestType.AssetOcrV1,
SyncRequestType.PartnerAssetExifsV1,
SyncRequestType.MemoriesV1,
SyncRequestType.MemoryToAssetsV1,
SyncRequestType.PeopleV1,
SyncRequestType.AssetFacesV1,
SyncRequestType.AssetFacesV2,
SyncRequestType.UserMetadataV1,
SyncRequestType.AssetMetadataV1,
SyncRequestType.AssetEditsV1,
];
const throwSessionRequired = () => {
throw new ForbiddenException('Sync endpoints cannot be used with API keys');
};
@Injectable()
export class SyncService extends BaseService {
getAcks(auth: AuthDto) {
const sessionId = auth.session?.id;
if (!sessionId) {
return throwSessionRequired();
}
return this.syncCheckpointRepository.getAll(sessionId);
}
async setAcks(auth: AuthDto, dto: SyncAckSetDto) {
const sessionId = auth.session?.id;
if (!sessionId) {
return throwSessionRequired();
}View on GitHub (pinned to 199723261c)
Solutions
- Authenticate sync requests with a session token (cookie or Authorization: Bearer <jwt>), not x-api-key.
- Re-login the user to obtain a fresh session, then retry.
- For automation that needs sync data, obtain a session token via the /auth/login endpoint first.
- If you must use a long-lived credential, request a non-expiring session from the auth API instead of an API key.
Example fix
// before
fetch('/sync/stream', { headers: { 'x-api-key': KEY } });
// after
const { accessToken } = await login(email, password);
fetch('/sync/stream', { headers: { Authorization: `Bearer ${accessToken}` } }); Defensive patterns
Strategy: type-guard
Validate before calling
function isSessionAuth(auth: { session?: { id?: string } | null }): boolean {
return Boolean(auth?.session?.id);
}
if (!isSessionAuth(auth)) { /* use /auth/login instead of x-api-key */ } Type guard
function isSessionAuth(auth: unknown): auth is { session: { id: string } } {
return typeof auth === 'object' && !!auth
&& typeof (auth as any).session?.id === 'string';
} Try / catch
try { await syncApi.stream(req); }
catch (e) {
if (e instanceof ForbiddenException && /cannot be used with API keys/.test(e.message)) {
// re-login to obtain a session token, then retry once
}
} Prevention
- Never reuse API keys for sync endpoints; keep a separate session token in the client.
- In tests, authenticate via /auth/login rather than the admin API key.
- Detect 403 on sync and prompt the user to re-authenticate.
When it happens
Trigger: Calling GET /sync/acks, POST /sync/acks, DELETE /sync/acks, or the /sync/stream SSE endpoint with an x-api-key header (or any auth flow that yields an API-key AuthDto) instead of a session cookie or Bearer JWT.
Common situations: Scripts/CLI tools reused an API key against the sync API; mobile client fell back to API-key auth after session expiry; integration tests reused the admin API key for sync fixtures.
Related errors
- This endpoint can only be used with a session token
- Not authenticated with an API Key
- Invalid logout token: it must contain either a sub or a sid
- Unauthorized
- Missing required permission: ${requestedPermission}
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/d63c8a88ceabe569.
Report an issue: GitHub.