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

  1. Capture and log `await response.text()` before throwing to expose Close's `error`/`error_description`.
  2. Restart the OAuth flow from the beginning (do not replay the same `code`).
  3. Verify `client_id`/`client_secret` from `getAppKeysFromSlug("closecom")` match the Close OAuth app.
  4. 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

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


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/656c61266690ed68. Report an issue: GitHub.