{"record":{"id":"813183e78cbb7a7f","repo":"calcom/cal.diy","slug":"missing-state-query-param-813183","errorCode":null,"errorMessage":"Missing `state` query param","messagePattern":"Missing `state` query param","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"apps/api/v2/src/modules/stripe/controllers/stripe.controller.ts","lineNumber":98,"sourceCode":"  @UseGuards()\n  @Redirect(undefined, 301)\n  @ApiOperation({ summary: \"Save Stripe credentials\" })\n  /**\n   * Handles saving Stripe credentials.\n   * If both orgId and teamId are present in the callback state, the request is proxied to the organization/team-level endpoint;\n   * otherwise, credentials are saved at the user level.\n   *\n   * Proxying ensures that permission checks—such as whether the user is allowed to install Stripe for a team or organization—\n   * are enforced via controller route guards, avoiding duplication of this logic within the service layer.\n   */\n  async save(\n    @Query(\"state\") state: string,\n    @Query(\"code\") code: string,\n    @Query(\"error\") error: string | undefined,\n    @Query(\"error_description\") error_description: string | undefined\n  ): Promise<StripCredentialsSaveOutputResponseDto> {\n    if (!state) {\n      throw new BadRequestException(\"Missing `state` query param\");\n    }\n\n    const decodedCallbackState: OAuthCallbackState = JSON.parse(state);\n    try {\n      // If teamId is present, proxy to team endpoint\n      if (decodedCallbackState.teamId && decodedCallbackState.orgId) {\n        let url = \"\";\n        const apiUrl = this.config.get(\"api.url\");\n        url = `${apiUrl}/organizations/${decodedCallbackState.orgId}/teams/${decodedCallbackState.teamId}/stripe/save`;\n\n        const params: Record<string, string | undefined> = { state, code, error, error_description };\n        const headers = {\n          Authorization: `Bearer ${decodedCallbackState.accessToken}`,\n        };\n        try {\n          const response = await this.httpService.axiosRef.get(url, { params, headers });\n          const redirectUrl = response.data?.url || decodedCallbackState.onErrorReturnTo || \"\";\n          return { url: redirectUrl };","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/stripe/controllers/stripe.controller.ts#L80-L116","documentation":"Thrown by StripeController.save, the OAuth callback handler that Stripe redirects to after the user authorizes the connect flow. It requires a `state` query parameter, which is a JSON-encoded OAuthCallbackState (built earlier by the /redirect endpoint and round-tripped through Stripe). If `state` is absent or empty, the controller rejects with 400 BadRequest before attempting to parse it.","triggerScenarios":"A GET to the /v2/stripe/save callback URL with no ?state= query parameter, e.g. someone visiting the callback URL directly, or Stripe being configured with a redirect URI that drops the state.","commonSituations":"The Stripe app's redirect URI in the Stripe dashboard points to the wrong path. A reverse proxy or CDN strips query parameters. A developer manually tests the callback URL in a browser without reconstructing state. The flow was started by something other than /v2/stripe/redirect.","solutions":["Always start the connect flow via POST /v2/stripe/redirect, which builds the state and sends the user to Stripe; do not call /save directly.","In the Stripe dashboard, set the redirect URI to the exact /v2/stripe/save endpoint so Stripe appends state and code.","Ensure no proxy/CDN rule strips query strings from the callback URL."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// This error is a server-side OAuth callback contract: clients should never\ncraft /save URLs. Always start via /redirect, which builds state.\nasync function startStripeConnect(api) {\n  const { url } = await api.post('/v2/stripe/redirect', { returnTo: window.location.origin });\n  window.location.href = url; // Stripe appends state & code on return\n}","typeGuard":"type OAuthCallbackState = {\n  accessToken: string;\n  teamId?: string;\n  orgId?: string;\n  onErrorReturnTo?: string;\n};\nfunction isCallbackState(v: unknown): v is OAuthCallbackState {\n  return !!v && typeof (v as any).accessToken === 'string';\n}","tryCatchPattern":null,"preventionTips":["Never link users directly to /v2/stripe/save; always go through /v2/stripe/redirect.","Register the exact /v2/stripe/save URL in the Stripe dashboard so Stripe appends state and code.","Ensure proxies/CDNs preserve query parameters on the callback path."],"tags":["stripe","oauth","callback","api-v2","query-param"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}