{"record":{"id":"7cce9ded77c4a557","repo":"calcom/cal.diy","slug":"responsebody-error-7cce9d","errorCode":null,"errorMessage":"${responseBody.error}","messagePattern":"\\$\\{responseBody\\.error\\}","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"apps/api/v2/src/modules/conferencing/services/zoom-video.service.ts","lineNumber":89,"sourceCode":"        },\n      }\n    );\n\n    if (result.status !== 200) {\n      let errorMessage = \"Something is wrong with Zoom API\";\n      try {\n        const responseBody = await result.json();\n        errorMessage = responseBody.error;\n      } catch (e) {\n        errorMessage = await result.clone().text();\n      }\n      throw new BadRequestException(errorMessage);\n    }\n\n    const responseBody = await result.json();\n\n    if (responseBody.error) {\n      throw new BadRequestException(responseBody.error);\n    }\n\n    responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);\n    delete responseBody.expires_in;\n\n    if (!userId) {\n      throw new UnauthorizedException(\"Invalid Access token.\");\n    }\n\n    const existingCredentialZoomVideo = teamId\n      ? await this.credentialsRepository.findAllCredentialsByTypeAndTeamId(ZOOM_TYPE, teamId)\n      : await this.credentialsRepository.findAllCredentialsByTypeAndUserId(ZOOM_TYPE, userId);\n\n    const credentialIdsToDelete = existingCredentialZoomVideo.map((item) => item.id);\n    if (credentialIdsToDelete.length > 0) {\n      teamId\n        ? await this.appsRepository.deleteTeamAppCredentials(credentialIdsToDelete, teamId)\n        : await this.appsRepository.deleteAppCredentials(credentialIdsToDelete, userId);","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/conferencing/services/zoom-video.service.ts#L71-L107","documentation":"Thrown in connectZoomApp in two spots when Zoom returns a body containing an `error` field. First: after a non-200 status where the JSON parse succeeds, errorMessage is set to responseBody.error and thrown as BadRequestException (HTTP 400). Second: after a 200 response, `if (responseBody.error)` re-checks and throws. The surfaced message is whatever Zoom put in `error` (e.g. invalid_grant, invalid_client).","triggerScenarios":"Reusing an authorization code, expired code, wrong redirect_uri, or bad client credentials in the Zoom token exchange. Zoom responds with an `error` field and this code forwards it verbatim.","commonSituations":"User reloads the OAuth callback URL (code already consumed); redirect_uri mismatch with Zoom Marketplace app config; client_secret rotated on Zoom but not in app keys; clock skew causing code expiry.","solutions":["Read the forwarded error value: invalid_grant -> re-auth (new code); invalid_client -> fix app keys.","Restart the OAuth flow from generateZoomAuthUrl to get a fresh code and complete it in a single pass.","Ensure redirect_uri in the token request matches the Zoom Marketplace allow-list exactly."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await zoomService.connectZoomApp(state, code, userId, teamId);\n} catch (e) {\n  if (e instanceof BadRequestException) {\n    const msg = String(e.message);\n    if (msg.includes('invalid_grant')) {\n      // restart OAuth: code reused/expired\n    } else if (msg.includes('invalid_client')) {\n      // fix Zoom app keys\n    }\n  }\n  throw e;\n}","preventionTips":["Never reuse an authorization code; complete the OAuth flow in one request.","Keep the redirect_uri identical between auth URL and token exchange.","Map known Zoom error strings to user-facing guidance."],"tags":["conferencing","zoom","oauth","external-api","token-exchange","nestjs"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}