calcom/cal.diy · error · Error
Failed to get access token
Error message
Failed to get access token
What it means
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.
Source
Thrown at packages/app-store/closecom/api/callback.ts:59
return res.status(400).json({ message: "Close.com client_secret missing." });
try {
const response = await fetch("https://api.close.com/oauth2/token/", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id,
client_secret,
grant_type: "authorization_code",
code: code as string,
redirect_uri: `${WEBAPP_URL}/api/integrations/closecom/callback`,
}),
});
if (!response.ok) {
throw new Error("Failed to get access token");
}
const responseJson = await response.json();
const { access_token, refresh_token, expires_in } = responseJson;
const expires_at = Date.now() + expires_in * 1000;
await prisma.credential.create({
data: {
type: "closecom_crm",
key: {
access_token,
refresh_token,
expires_at,
},
userId: req.session.user.id,
appId: "closecom",
},View on GitHub (pinned to 176037d0af)
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.
Example fix
// before
if (!response.ok) {
throw new Error("Failed to get access token");
}
// after
if (!response.ok) {
const body = await response.text().catch(() => "");
throw new Error(`Failed to get access token (HTTP ${response.status}): ${body}`);
} Defensive patterns
Strategy: try-catch
Type guard
function isOAuthTokenError(e: unknown, status?: number): boolean {
return !!e && status !== undefined && status >= 400 && status < 500;
} Try / catch
try {
await exchangeCodeForToken(code);
} catch (e) {
if (e instanceof Error && /access token/.test(e.message)) {
// restart the OAuth flow; do not replay the same code
return res.redirect(`${WEBAPP_URL}/integrations/closecom?error=token_exchange`);
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: User clicked the callback link twice (code reuse); Close OAuth app credentials rotated; redirect URI mismatch; clock skew; network blip during exchange.
Related errors
- {responseBody.error}
- ${responseBody.error}
- Failed to fetch project details
- Failed to fetch Basecamp projects
- `code` must be a string
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/656c61266690ed68.
Report an issue: GitHub.