calcom/cal.diy · error · ForbiddenException

Access denied. Either the team with ID=${teamId} does not ow

Error message

Access denied. Either the team with ID=${teamId} does not own the event type with ID=${eventTypeId}, or your MEMBER role does not have permission to access this resource.

What it means

Thrown by EventTypesAtomService.checkTeamOwnsEventType when the querying user is not an ADMIN or OWNER of the team that owns the event type, OR the team does not own the event type at all. The method queries membership with an OR filter for ADMIN/OWNER roles and accepted status, then checks if the eventTypeId appears in the team's eventTypes relation. If either condition fails, ForbiddenException (HTTP 403) is thrown.

Source

Thrown at apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts:186

  async checkTeamOwnsEventType(userId: number, eventTypeId: number, teamId: number) {
    const membership = await this.dbRead.prisma.membership.findFirst({
      where: {
        userId,
        teamId,
        accepted: true,
        OR: [{ role: "ADMIN" }, { role: "OWNER" }],
      },
      select: {
        team: {
          select: {
            eventTypes: true,
          },
        },
      },
    });
    if (!membership?.team?.eventTypes?.some((item) => item.id === eventTypeId)) {
      throw new ForbiddenException(
        `Access denied. Either the team with ID=${teamId} does not own the event type with ID=${eventTypeId}, or your MEMBER role does not have permission to access this resource.`
      );
    }
  }

  async getEventTypesAppIntegration(slug: string, user: UserWithProfile, teamId?: number) {
    let credentials = await this.credentialsRepository.getAllUserCredentialsById(user.id);
    let userTeams: TeamQuery[] = [];
    if (teamId) {
      const teamsQuery = await this.atomsRepository.getUserTeams(user.id);
      // If a team is a part of an org then include those apps
      // Don't want to iterate over these parent teams
      const filteredTeams: TeamQuery[] = [];
      const parentTeams: TeamQuery[] = [];
      // Only loop and grab parent teams if a teamId was given. If not then all teams will be queried
      if (teamId) {
        teamsQuery.forEach((team) => {
          if (team?.parent) {

View on GitHub (pinned to 176037d0af)

Solutions

  1. Promote the user to ADMIN or OWNER role for the team, or have them request elevation from a team owner.
  2. Verify the eventTypeId actually belongs to the team the user is an admin of — check the team-event-type association.
  3. Ensure the user's membership is accepted (accepted=true) — pending invitations do not grant access.
  4. If the user is an organization admin, verify isUserOrganizationAdmin returns true to bypass this check.

Example fix

// before: MEMBER role user calls team event-type endpoint
await api.get(`/v2/event-types/${teamEventTypeId}`); // 403

// after: verify role and team ownership before calling
const membership = await api.get(`/v2/teams/${teamId}/membership/me`);
if (!['ADMIN', 'OWNER'].includes(membership.role) || !membership.accepted) {
  throw new Error('Insufficient role: ADMIN or OWNER membership required for team event types.');
}
if (!teamEventTypes.some(et => et.id === teamEventTypeId)) {
  throw new Error('Event type does not belong to this team.');
}
await api.get(`/v2/event-types/${teamEventTypeId}`);
Defensive patterns

Strategy: validation

Validate before calling

// Verify user's role and team ownership before accessing team event types
const verifyTeamAccess = async (
  api: ApiClient,
  userId: number,
  teamId: number,
  eventTypeId: number
): Promise<void> => {
  const membership = await api.get(`/v2/teams/${teamId}/membership`);
  const isPrivileged = ['ADMIN', 'OWNER'].includes(membership.role) && membership.accepted;
  if (!isPrivileged) {
    throw new Error(`User ${userId} is not ADMIN/OWNER of team ${teamId}.`);
  }
  const teamEventTypes = await api.get(`/v2/teams/${teamId}/event-types`);
  if (!teamEventTypes.some((et: { id: number }) => et.id === eventTypeId)) {
    throw new Error(`Event type ${eventTypeId} does not belong to team ${teamId}.`);
  }
};

Type guard

type PrivilegedRole = 'ADMIN' | 'OWNER';
const isPrivilegedMember = (m: { role: string; accepted: boolean }): m is { role: PrivilegedRole; accepted: true } =>
  (m.role === 'ADMIN' || m.role === 'OWNER') && m.accepted === true;

Try / catch

// Handle 403 on team event type access
try {
  return await api.get(`/v2/event-types/${id}`);
} catch (err: any) {
  if (err?.response?.status === 403) {
    throw new Error('Access denied. Ask a team OWNER/ADMIN to elevate your role or share the event type.');
  }
  throw err;
}

Prevention

When it happens

Trigger: A MEMBER-role user (not ADMIN/OWNER) attempts to access a team event type they don't individually own. A user who is ADMIN/OWNER of Team A tries to access an event type belonging to Team B. The user's membership has accepted=false (pending invitation). The eventTypeId doesn't belong to any team the user administrates.

Common situations: A newly added team member with MEMBER role trying to manage team event types. A user belonging to multiple teams using the wrong team context. A membership that was created but not yet accepted (invitation pending). Organization admins bypass this check (line 109: isUserOrganizationAdmin skips checkTeamOwnsEventType), but non-admin org members hit it.

Understand the failure class

Related errors


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