{"record":{"id":"ef75dbd2176f5194","repo":"calcom/cal.diy","slug":"invalid-access-token-ef75db","errorCode":null,"errorMessage":"Invalid Access token.","messagePattern":"Invalid Access token\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"apps/api/v2/src/modules/stripe/controllers/stripe.controller.ts","lineNumber":136,"sourceCode":"          const fallbackUrl = decodedCallbackState.onErrorReturnTo || \"\";\n          return { url: fallbackUrl };\n        }\n      }\n\n      // user-level fallback\n      const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);\n\n      // user cancels flow\n      if (error === \"access_denied\") {\n        return { url: getOnErrorReturnToValueFromQueryState(state) };\n      }\n\n      if (error) {\n        throw new BadRequestException(stringify({ error, error_description }));\n      }\n\n      if (!userId) {\n        throw new BadRequestException(\"Invalid Access token.\");\n      }\n\n      return await this.stripeService.saveStripeAccount(decodedCallbackState, code, userId);\n    } catch (error) {\n      if (error instanceof Error) {\n        console.error(error.message);\n      }\n      return {\n        url: decodedCallbackState.onErrorReturnTo ?? \"\",\n      };\n    }\n  }\n\n  @Get(\"/check\")\n  @UseGuards(ApiAuthGuard)\n  @HttpCode(HttpStatus.OK)\n  @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)\n  @ApiOperation({ summary: \"Check Stripe connection\" })","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/stripe/controllers/stripe.controller.ts#L118-L154","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Have the user re-authenticate to obtain a fresh access token before starting the Stripe connect flow.","Restart the connect flow from /v2/stripe/redirect with a current Bearer token so state contains a valid accessToken.","If persisting state across the redirect, ensure the token has a lifetime longer than the OAuth round-trip."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"async function ensureFreshToken(api) {\n  const me = await api.getMe().catch(() => null);\n  if (!me) {\n    // re-authenticate before starting the Stripe flow\n    redirectToLogin();\n  }\n}","typeGuard":"function hasUsableToken(token: string | null | undefined): token is string {\n  return typeof token === 'string' && token.length > 0 && !isExpired(token);\n}\nfunction isExpired(jwt: string) {\n  const payload = JSON.parse(atob(jwt.split('.')[1] ?? ''));\n  return Date.now() >= (payload.exp ?? 0) * 1000;\n}","tryCatchPattern":"try {\n  await api.completeStripeSave();\n} catch (e) {\n  if (e.status === 400 && /Invalid Access token/.test(e.message)) {\n    await refreshToken();\n    await api.completeStripeSave(); // retry once with fresh token\n  } else throw e;\n}","preventionTips":["Ensure the access token has a lifetime longer than the OAuth round-trip before starting the flow.","Re-authenticate the user if their session is near expiry before launching Stripe connect.","Do not reuse state JSON built with a stale token; regenerate state via /redirect."],"tags":["stripe","oauth","auth","api-v2","access-token"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}