calcom/cal.diy · error · BadRequestException
${responseBody.error}
Error message
${responseBody.error} What it means
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).
Source
Thrown at apps/api/v2/src/modules/conferencing/services/zoom-video.service.ts:89
},
}
);
if (result.status !== 200) {
let errorMessage = "Something is wrong with Zoom API";
try {
const responseBody = await result.json();
errorMessage = responseBody.error;
} catch (e) {
errorMessage = await result.clone().text();
}
throw new BadRequestException(errorMessage);
}
const responseBody = await result.json();
if (responseBody.error) {
throw new BadRequestException(responseBody.error);
}
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
delete responseBody.expires_in;
if (!userId) {
throw new UnauthorizedException("Invalid Access token.");
}
const existingCredentialZoomVideo = teamId
? await this.credentialsRepository.findAllCredentialsByTypeAndTeamId(ZOOM_TYPE, teamId)
: await this.credentialsRepository.findAllCredentialsByTypeAndUserId(ZOOM_TYPE, userId);
const credentialIdsToDelete = existingCredentialZoomVideo.map((item) => item.id);
if (credentialIdsToDelete.length > 0) {
teamId
? await this.appsRepository.deleteTeamAppCredentials(credentialIdsToDelete, teamId)
: await this.appsRepository.deleteAppCredentials(credentialIdsToDelete, userId);View on GitHub (pinned to 176037d0af)
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.
Defensive patterns
Strategy: try-catch
Try / catch
try {
await zoomService.connectZoomApp(state, code, userId, teamId);
} catch (e) {
if (e instanceof BadRequestException) {
const msg = String(e.message);
if (msg.includes('invalid_grant')) {
// restart OAuth: code reused/expired
} else if (msg.includes('invalid_client')) {
// fix Zoom app keys
}
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Missing `state` query param
- {error_description}
- Invalid conferencing app, available apps are:
- {responseBody.error}
- Something is wrong with Zoom API
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/7cce9ded77c4a557.
Report an issue: GitHub.