calcom/cal.diy · error · UnauthorizedException

reservationDuration can only be used for authenticated reque

Error message

reservationDuration can only be used for authenticated requests - use access token, api key or OAuth credentials

What it means

A NestJS UnauthorizedException (HTTP 401) from SlotsService_2024_09_04.reserveSlot. The request included a `reservationDuration` (custom hold time) but no authenticated user was resolved (authUserId is undefined). Custom reservation duration is a privileged feature that requires an API key, OAuth access token, or OAuth client credentials.

Source

Thrown at apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts:97

        }
      }
      throw error;
    }
  }

  async getAvailableSlots(query: GetSlotsInput_2024_09_04) {
    const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query);
    return this.fetchAndFormatSlots(queryTransformed, query.format);
  }

  async getAvailableSlotsWithRouting(query: GetSlotsInputWithRouting_2024_09_04) {
    const queryTransformed = await this.slotsInputService.transformRoutingGetSlotsQuery(query);
    return this.fetchAndFormatSlots(queryTransformed, query.format);
  }

  async reserveSlot(input: ReserveSlotInput_2024_09_04, authUserId?: number) {
    if (input.reservationDuration && !authUserId) {
      throw new UnauthorizedException(
        "reservationDuration can only be used for authenticated requests - use access token, api key or OAuth credentials"
      );
    }

    const eventType = await this.eventTypeRepository.getEventTypeWithHosts(input.eventTypeId);
    if (!eventType) {
      throw new NotFoundException(`Event Type with ID=${input.eventTypeId} not found`);
    }

    if (input.reservationDuration && authUserId) {
      const canSpecifyCustomReservationDuration = await this.canSpecifyCustomReservationDuration(
        authUserId,
        eventType
      );
      if (!canSpecifyCustomReservationDuration) {
        throw new ForbiddenException(
          "authenticated user is not owner of event type, does not have memberships in common with owner of the event type, nor does belong to event type's team or org."
        );

View on GitHub (pinned to 176037d0af)

Solutions

  1. Send a cal-api-key header, an Authorization: Bearer <accessToken>, or OAuth client credentials with the request.
  2. If the caller is anonymous, remove the reservationDuration field and accept the default 5-minute hold.
  3. Refresh expired OAuth tokens before retrying.
  4. Verify the API key is valid and belongs to the event type owner's app.

Example fix

// before
fetch('/v2/slots/reserve', { method:'POST', body: JSON.stringify({ eventTypeId, slotStart, reservationDuration: 10 }) });

// after — authenticate, or drop reservationDuration
fetch('/v2/slots/reserve', {
  method: 'POST',
  headers: { 'cal-api-key': apiKey, 'content-type': 'application/json' },
  body: JSON.stringify({ eventTypeId, slotStart, reservationDuration: 10 }),
});
Defensive patterns

Strategy: validation

Validate before calling

function assertAuthForReservationDuration(input: { reservationDuration?: number }, auth: { apiKey?: string; accessToken?: string }) {
  if (input.reservationDuration != null && !auth.apiKey && !auth.accessToken) {
    throw new Error('reservationDuration requires an API key or OAuth access token');
  }
}
assertAuthForReservationDuration(input, { apiKey, accessToken });

Type guard

function isAuthorized(auth: { apiKey?: string; accessToken?: string }): boolean {
  return typeof auth.apiKey === 'string' && auth.apiKey.length > 0
      || typeof auth.accessToken === 'string' && auth.accessToken.length > 0;
}

Try / catch

try {
  await cal.slots.reserve({ eventTypeId, slotStart, reservationDuration });
} catch (e) {
  if (e instanceof HttpError && e.statusCode === 401) {
    // either authenticate or drop reservationDuration and use default hold
    return cal.slots.reserve({ eventTypeId, slotStart });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling POST /v2/slots/2024-09-04/reserve (or the booking-flow reserve) with a reservationDuration field while unauthenticated — i.e. no cal-api-key header, no Authorization: Bearer, and no OAuth client-credentials grant.

Common situations: Embed booking widget calling reserve without passing the API key; using the public endpoint by mistake; OAuth token expired and was silently dropped; reservationDuration field left over from a copy-paste in an anonymous flow.

Understand the failure class

Related errors


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/e8e1cb841dc6d971. Report an issue: GitHub.