immich-app/immich · error · BadRequestException

This endpoint can only be used with a session token

Error message

This endpoint can only be used with a session token

What it means

SessionService.create() mints a new (child) session token derived from the caller's existing session. It requires auth.session to be populated, which only happens when the request was authenticated with a session JWT — not an API key. A BadRequestException (HTTP 400) is thrown at session.service.ts:32 when auth.session is absent.

Source

Thrown at server/src/services/session.service.ts:32

import { BaseService } from 'src/services/base.service';

@Injectable()
export class SessionService extends BaseService {
  @OnJob({ name: JobName.SessionCleanup, queue: QueueName.BackgroundTask })
  async handleCleanup(): Promise<JobStatus> {
    const sessions = await this.sessionRepository.cleanup();
    for (const session of sessions) {
      this.logger.verbose(`Deleted expired session token: ${session.deviceOS}/${session.deviceType}`);
    }

    this.logger.log(`Deleted ${sessions.length} expired session tokens`);

    return JobStatus.Success;
  }

  async create(auth: AuthDto, dto: SessionCreateDto): Promise<SessionCreateResponseDto> {
    if (!auth.session) {
      throw new BadRequestException('This endpoint can only be used with a session token');
    }

    const token = this.cryptoRepository.randomBytesAsText(32);
    const hashed = this.cryptoRepository.hashSha256(token);
    const session = await this.sessionRepository.create({
      parentId: auth.session.id,
      userId: auth.user.id,
      expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null,
      deviceType: dto.deviceType,
      deviceOS: dto.deviceOS,
      token: hashed,
    });

    return { ...mapSession(session), token };
  }

  async getAll(auth: AuthDto): Promise<SessionResponseDto[]> {
    const sessions = await this.sessionRepository.getByUserId(auth.user.id);

View on GitHub (pinned to 199723261c)

Solutions

  1. Authenticate the request with a valid session JWT (cookie or Authorization: Bearer) instead of an API key.
  2. If programmatic long-lived access is needed, use API keys directly rather than creating child sessions.
  3. Confirm the session token hasn't expired by calling GET /sessions before creating a child.
  4. Regenerate/refresh the session token if it is malformed or stale.

Example fix

// before - API key cannot create a session
fetch('/sessions', { headers: { 'x-api-key': apiKey } });
// after - session token required
fetch('/sessions', { headers: { Authorization: `Bearer ${sessionToken}` } });
Defensive patterns

Strategy: validation

Validate before calling

// Only create a child session when authenticated with a session token.
if (!hasSessionToken(authContext)) {
  throw new Error('Use a session token, not an API key, to create a session.');
}
await sessionApi.create(authContext, dto);

function hasSessionToken(auth: AuthDto): boolean {
  return !!auth?.session;
}

Type guard

const hasSession = (a: AuthDto | undefined): a is AuthDto & { session: NonNullable<AuthDto['session']> } =>
  !!a && !!a.session;

Try / catch

try {
  await sessionApi.create(auth, dto);
} catch (e) {
  if (e instanceof BadRequestException && /session token/i.test(e.message)) {
    redirectToLogin();
  } else throw e;
}

Prevention

When it happens

Trigger: POST /sessions (session creation) authenticated with an API key (auth.session is null for API-key requests) or with no/invalid session token. The endpoint cannot derive a parentId without an existing session.

Common situations: Scripts or integrations using API keys to programmatically create sessions, calling the endpoint with a bearer token that expired or was malformed, or migrating an integration that assumed API-key auth covers session creation.

Related errors


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