calcom/cal.diy · error · BadRequestException

Invalid Access token.

Error message

Invalid Access token.

What it means

Thrown by StripeController.save on the user-level fallback path. The controller resolves the owner of the access token embedded in state via tokensRepository.getAccessTokenOwnerId; if no token owner is found, it throws 400 BadRequest 'Invalid Access token.' Note this is a BadRequest in the controller, distinct from the UnauthorizedException with the same message thrown later inside StripeService.saveStripeAccount.

Source

Thrown at apps/api/v2/src/modules/stripe/controllers/stripe.controller.ts:136

          const fallbackUrl = decodedCallbackState.onErrorReturnTo || "";
          return { url: fallbackUrl };
        }
      }

      // user-level fallback
      const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);

      // user cancels flow
      if (error === "access_denied") {
        return { url: getOnErrorReturnToValueFromQueryState(state) };
      }

      if (error) {
        throw new BadRequestException(stringify({ error, error_description }));
      }

      if (!userId) {
        throw new BadRequestException("Invalid Access token.");
      }

      return await this.stripeService.saveStripeAccount(decodedCallbackState, code, userId);
    } catch (error) {
      if (error instanceof Error) {
        console.error(error.message);
      }
      return {
        url: decodedCallbackState.onErrorReturnTo ?? "",
      };
    }
  }

  @Get("/check")
  @UseGuards(ApiAuthGuard)
  @HttpCode(HttpStatus.OK)
  @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
  @ApiOperation({ summary: "Check Stripe connection" })

View on GitHub (pinned to 176037d0af)

Solutions

  1. Have the user re-authenticate to obtain a fresh access token before starting the Stripe connect flow.
  2. Restart the connect flow from /v2/stripe/redirect with a current Bearer token so state contains a valid accessToken.
  3. If persisting state across the redirect, ensure the token has a lifetime longer than the OAuth round-trip.
Defensive patterns

Strategy: validation

Validate before calling

async function ensureFreshToken(api) {
  const me = await api.getMe().catch(() => null);
  if (!me) {
    // re-authenticate before starting the Stripe flow
    redirectToLogin();
  }
}

Type guard

function hasUsableToken(token: string | null | undefined): token is string {
  return typeof token === 'string' && token.length > 0 && !isExpired(token);
}
function isExpired(jwt: string) {
  const payload = JSON.parse(atob(jwt.split('.')[1] ?? ''));
  return Date.now() >= (payload.exp ?? 0) * 1000;
}

Try / catch

try {
  await api.completeStripeSave();
} catch (e) {
  if (e.status === 400 && /Invalid Access token/.test(e.message)) {
    await refreshToken();
    await api.completeStripeSave(); // retry once with fresh token
  } else throw e;
}

Prevention

When it happens

Trigger: The OAuth callback's state.accessToken is expired, revoked, belongs to a different environment, or was never issued, so getAccessTokenOwnerId returns null/falsy. teamId/orgId are absent so the user-level fallback is taken.

Common situations: The user's session expired during the Stripe OAuth round-trip. The access token was issued by a different API deployment. The token was manually crafted or corrupted in the state JSON. The token row was purged.

Related errors


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