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
- Only send reservationDuration when the caller is the event type owner or a teammate.
- Use the owner's API key / OAuth credentials for privileged booking flows.
- For end-user-driven flows, omit reservationDuration and use the default hold.
- 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
- Use the event-type owner's credentials for flows that set reservationDuration.
- Gate reservationDuration behind a local ownership/membership check.
- For end-user tokens, omit reservationDuration.
- Ensure team memberships are accepted to grant team-event permission.
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- reservationDuration can only be used for authenticated reque
- Event Type with ID=${input.eventTypeId} not found
- Invalid start date
- Invalid end date
- Booking with id=${input.eventTypeId} at ${input.slotStart} h
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/2487ca1d67e9fd1f.
Report an issue: GitHub.