calcom/cal.diy · error · ForbiddenException

authenticated user is not owner of event type, does not have

Error message

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.

What it means

A NestJS ForbiddenException (HTTP 403) from SlotsService_2024_09_04.reserveSlot. The caller is authenticated (authUserId present) and requested a custom reservationDuration, but canSpecifyCustomReservationDuration returned false. That helper grants permission only if the user owns the event type, shares a membership with the owner (individual event), or has an accepted team/org membership (team event).

Source

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

  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."
        );
      }
    }

    const startDate = DateTime.fromISO(input.slotStart, { zone: "utc" });
    if (!startDate.isValid) {
      throw new BadRequestException("Invalid start date");
    }

    if (input.slotDuration) {
      this.validateSlotDuration(eventType, input.slotDuration);
    }

    const endDate = startDate.plus({ minutes: input.slotDuration ?? eventType.length });
    if (!endDate.isValid) {
      throw new BadRequestException("Invalid end date");
    }

View on GitHub (pinned to 176037d0af)

Solutions

  1. Only send reservationDuration when the caller is the event type owner or a teammate.
  2. Use the owner's API key / OAuth credentials for privileged booking flows.
  3. For end-user-driven flows, omit reservationDuration and use the default hold.
  4. If a team admin should have access, ensure their team membership is accepted in the DB.

Example fix

// before — using attendee token
fetch('/v2/slots/reserve', { headers:{ Authorization:`Bearer ${attendeeToken}` }, body: JSON.stringify({ eventTypeId, slotStart, reservationDuration: 10 }) });

// after — owner credentials for custom hold, or drop the field
fetch('/v2/slots/reserve', { headers:{ 'cal-api-key': ownerApiKey }, body: JSON.stringify({ eventTypeId, slotStart, reservationDuration: 10 }) });
Defensive patterns

Strategy: validation

Validate before calling

// Mirror the server's ownership check before adding reservationDuration.
function canUseCustomReservationDuration(authUser, eventType) {
  if (eventType.userId) return authUser.id === eventType.userId; // owner or peer membership (approx)
  if (eventType.teamId) return authUser.teamMemberships?.some(m => m.teamId === eventType.teamId && m.accepted);
  return false;
}
const body = { eventTypeId, slotStart };
if (canUseCustomReservationDuration(authUser, eventType)) body.reservationDuration = duration;

Type guard

function isEventTypeOwner(authUserId: number, eventType: { userId?: number | null }): boolean {
  return eventType.userId != null && authUserId === eventType.userId;
}

Try / catch

try {
  await cal.slots.reserve({ eventTypeId, slotStart, reservationDuration });
} catch (e) {
  if (e instanceof HttpError && e.statusCode === 403) {
    // drop the privileged field and retry with default hold
    return cal.slots.reserve({ eventTypeId, slotStart });
  }
  throw e;
}

Prevention

When it happens

Trigger: An authenticated user who is neither the event type owner, nor a membership peer of the owner, nor a member of the event type's team/org, sends a reserve request with reservationDuration set.

Common situations: Using an end-user's OAuth token instead of the event-type owner's API key; an org admin expecting team permissions but their membership is not yet accepted; reservationDuration set globally on all requests regardless of caller.

Understand the failure class

Related errors


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