{"record":{"id":"656c61266690ed68","repo":"calcom/cal.diy","slug":"failed-to-get-access-token","errorCode":null,"errorMessage":"Failed to get access token","messagePattern":"Failed to get access token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/app-store/closecom/api/callback.ts","lineNumber":59,"sourceCode":"    return res.status(400).json({ message: \"Close.com client_secret missing.\" });\n\n  try {\n    const response = await fetch(\"https://api.close.com/oauth2/token/\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n      },\n      body: new URLSearchParams({\n        client_id,\n        client_secret,\n        grant_type: \"authorization_code\",\n        code: code as string,\n        redirect_uri: `${WEBAPP_URL}/api/integrations/closecom/callback`,\n      }),\n    });\n\n    if (!response.ok) {\n      throw new Error(\"Failed to get access token\");\n    }\n\n    const responseJson = await response.json();\n\n    const { access_token, refresh_token, expires_in } = responseJson;\n    const expires_at = Date.now() + expires_in * 1000;\n\n    await prisma.credential.create({\n      data: {\n        type: \"closecom_crm\",\n        key: {\n          access_token,\n          refresh_token,\n          expires_at,\n        },\n        userId: req.session.user.id,\n        appId: \"closecom\",\n      },","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/closecom/api/callback.ts#L41-L77","documentation":"After POSTing the authorization code to `https://api.close.com/oauth2/token/`, the handler checks `response.ok`; any non-2xx response from Close's token endpoint throws this generic Error. The actual Close error body (invalid_grant, invalid_client, etc.) is discarded.","triggerScenarios":"Token exchange fails because: the authorization `code` is expired or already used (Close codes are single-use and short-lived), `client_id`/`client_secret` are wrong, the `redirect_uri` differs from the one used to start the flow, or Close's token endpoint is erroring.","commonSituations":"User clicked the callback link twice (code reuse); Close OAuth app credentials rotated; redirect URI mismatch; clock skew; network blip during exchange.","solutions":["Capture and log `await response.text()` before throwing to expose Close's `error`/`error_description`.","Restart the OAuth flow from the beginning (do not replay the same `code`).","Verify `client_id`/`client_secret` from `getAppKeysFromSlug(\"closecom\")` match the Close OAuth app.","Ensure the `redirect_uri` in the token request exactly matches the one in the authorize request and the Close app config."],"exampleFix":"// before\nif (!response.ok) {\n  throw new Error(\"Failed to get access token\");\n}\n\n// after\nif (!response.ok) {\n  const body = await response.text().catch(() => \"\");\n  throw new Error(`Failed to get access token (HTTP ${response.status}): ${body}`);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isOAuthTokenError(e: unknown, status?: number): boolean {\n  return !!e && status !== undefined && status >= 400 && status < 500;\n}","tryCatchPattern":"try {\n  await exchangeCodeForToken(code);\n} catch (e) {\n  if (e instanceof Error && /access token/.test(e.message)) {\n    // restart the OAuth flow; do not replay the same code\n    return res.redirect(`${WEBAPP_URL}/integrations/closecom?error=token_exchange`);\n  }\n  throw e;\n}","preventionTips":["Capture and log Close's error body for diagnosis.","Never replay a used/expired authorization code — restart the flow.","Verify client_id/secret and redirect_uri before each exchange.","Handle the OAuth round-trip within the code's short lifetime."],"tags":["oauth","token-exchange","closecom","swallowed-error","upstream-api"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}