calcom/cal.diy · error · ForbiddenException

User with ID=${userId} does not own schedule with ID=${sched

Error message

User with ID=${userId} does not own schedule with ID=${schedule.id}

What it means

Thrown by checkUserOwnsSchedule in the 2024-04-15 SchedulesService (also called from the 2024-06-11 service) when the authenticated user's ID does not match the schedule's userId field. This is an authorization guard that prevents users from accessing, updating, or deleting schedules owned by other users. Results in HTTP 403 Forbidden.

Source

Thrown at apps/api/v2/src/platform/schedules/schedules_2024_04_15/services/schedules.service.ts:146

      prisma: this.dbWrite.prisma as unknown as PrismaClient,
    });
  }

  async deleteUserSchedule(userId: number, scheduleId: number) {
    const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);

    if (!existingSchedule) {
      throw new BadRequestException(`Schedule with ID=${scheduleId} does not exist.`);
    }

    this.checkUserOwnsSchedule(userId, existingSchedule);

    return this.schedulesRepository.deleteScheduleById(scheduleId);
  }

  checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
    if (userId !== schedule.userId) {
      throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
    }
  }

  getDefaultAvailabilityInput(): CreateAvailabilityInput_2024_04_15 {
    const startTime = new Date(new Date().setUTCHours(9, 0, 0, 0));
    const endTime = new Date(new Date().setUTCHours(17, 0, 0, 0));

    return {
      days: [1, 2, 3, 4, 5],
      startTime,
      endTime,
    };
  }
}

View on GitHub (pinned to 176037d0af)

Solutions

  1. List the authenticated user's own schedules via GET /v2/schedules and only use IDs from that response.
  2. Verify the API key belongs to the same user who owns the target schedule.
  3. For team schedules, use the appropriate team scheduling endpoints rather than personal schedule endpoints.
Defensive patterns

Strategy: validation

Validate before calling

// Before calling any schedule mutation endpoint, verify the user owns the schedule
async function verifyOwnership(api: ApiClient, scheduleId: number): Promise<void> {
  const { data } = await api.get(`/v2/schedules`);
  const owned = data.data.schedules.some((s: { id: number }) => s.id === scheduleId);
  if (!owned) {
    throw new Error(`User does not own schedule ${scheduleId}`);
  }
}

Try / catch

try {
  await api.patch(`/v2/schedules/${scheduleId}`, payload);
} catch (error) {
  if (error.response?.status === 403) {
    // User doesn't own this schedule — use a different API key or schedule ID
    console.error('Access denied: schedule belongs to another user.');
  }
  throw error;
}

Prevention

When it happens

Trigger: Calling any schedule endpoint (GET/PATCH/DELETE) with a scheduleId that belongs to a different user; using a team member's API key to access another member's personal schedule; schedule ownership was transferred but the old owner's integration still references it.

Common situations: Integration hardcodes a schedule ID from one user but runs with another user's API key; multi-tenant confusion where IDs are shared across accounts; attempting to access a managed/team schedule through the personal schedule API.

Related errors


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