{"record":{"id":"5869ff026a9fbc04","repo":"calcom/cal.diy","slug":"appslug-not-connected","errorCode":null,"errorMessage":"${appSlug} not connected.","messagePattern":"(.+?) not connected\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"apps/api/v2/src/modules/conferencing/services/conferencing.service.ts","lineNumber":101,"sourceCode":"  }\n\n  async getUserDefaultConferencingApp(userId: number) {\n    const user = await this.usersRepository.findById(userId);\n    return userMetadata.parse(user?.metadata)?.defaultConferencingApp;\n  }\n\n  async checkAppIsValidAndConnected(user: UserWithProfile, appSlug: string) {\n    if (!CONFERENCING_APPS.includes(appSlug)) {\n      throw new BadRequestException(\"Invalid app, available apps are: \", CONFERENCING_APPS.join(\", \"));\n    }\n    const credentials = await getUsersCredentialsIncludeServiceAccountKey(user);\n\n    const foundApp = getApps(credentials, true).filter((app) => app.slug === appSlug)[0];\n\n    const appLocation = foundApp?.appData?.location;\n\n    if (!foundApp || !appLocation) {\n      throw new BadRequestException(`${appSlug} not connected.`);\n    }\n    return foundApp.credential;\n  }\n\n  async disconnectConferencingApp(user: UserWithProfile, app: string) {\n    const credential = await this.checkAppIsValidAndConnected(user, app);\n    return handleDeleteCredential({\n      userId: user.id,\n      userMetadata: user?.metadata,\n      credentialId: credential.id,\n    });\n  }\n\n  async setDefaultConferencingApp(user: UserWithProfile, app: string) {\n    // cal-video is global, so we can skip this check\n    if (app !== CAL_VIDEO) {\n      await this.checkAppIsValidAndConnected(user, app);\n    }","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/conferencing/services/conferencing.service.ts#L83-L119","documentation":"Thrown by ConferencingService.checkAppIsValidAndConnected when the slug is valid but no connected credential with an app location is found for the user. Returns HTTP 400. Used by disconnectConferencingApp (cannot disconnect what is not connected) and setDefaultConferencingApp (cannot default to a non-connected app, except cal_video which is global).","triggerScenarios":"Calling DELETE /v2/conferencing/zoom/disconnect when the user never connected Zoom, or connected then deleted it; calling POST /v2/conferencing/zoom/default without a Zoom credential; the credential exists but its appData.location is missing (incomplete install).","commonSituations":"Stale UI showing an app as connected; user connected in a different account/tenant; app-store credential row missing location metadata after a partial install.","solutions":["Refresh the installed-apps list via GET /v2/conferencing before offering disconnect/set-default actions.","If the user expects it connected, have them reconnect via the OAuth flow.","For setDefaultConferencingApp, cal_video bypasses this check — use cal_video if no app is connected.","Investigate any credential rows missing appData.location as a data-integrity bug."],"exampleFix":"// before\nawait api.setDefaultConferencing('zoom');\n\n// after\nconst installed = await api.listConferencingApps();\nconst slugs = installed.data.map(a => a.slug);\nif (!slugs.includes('zoom')) {\n  showToast('Connect Zoom first.'); return;\n}\nawait api.setDefaultConferencing('zoom');","handlingStrategy":"validation","validationCode":"async function isAppConnected(conferencingService: ConferencingService, user: UserWithProfile, appSlug: string): Promise<boolean> {\n  try {\n    await conferencingService.checkAppIsValidAndConnected(user, appSlug);\n    return true;\n  } catch (e) {\n    if (e instanceof BadRequestException && /not connected/.test(e.message)) return false;\n    throw e;\n  }\n}\n\nif (!(await isAppConnected(conferencingService, user, app))) {\n  return { code: 'app_not_connected', app };\n}","typeGuard":"interface InstalledApp { slug: string; credential?: { id: number } }\n\nfunction isConnectedApp<T extends InstalledApp>(apps: T[], slug: string): apps is (T & { credential: { id: number } })[] {\n  return apps.some(a => a.slug === slug && Boolean(a.credential));\n}","tryCatchPattern":"try {\n  await conferencingService.setDefaultConferencingApp(user, app);\n} catch (e) {\n  if (e instanceof BadRequestException && /not connected/.test(e.message)) {\n    return res.status(400).json({ code: 'app_not_connected', app });\n  }\n  throw e;\n}","preventionTips":["Refresh the installed-apps list before exposing disconnect/set-default actions.","Disable those actions in the UI when the app is not in the installed list.","Use cal_video as a safe default-app fallback since it bypasses the connected check."],"tags":["conferencing","credentials","validation","nestjs","connection"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}