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

  1. Authenticate sync requests with a session token (cookie or Authorization: Bearer <jwt>), not x-api-key.
  2. Re-login the user to obtain a fresh session, then retry.
  3. For automation that needs sync data, obtain a session token via the /auth/login endpoint first.
  4. 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

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


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