calcom/cal.diy · error · BadRequestException

Missing `state` query param

Error message

Missing `state` query param

What it means

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.

Source

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

  @UseGuards()
  @Redirect(undefined, 301)
  @ApiOperation({ summary: "Save Stripe credentials" })
  /**
   * Handles saving Stripe credentials.
   * If both orgId and teamId are present in the callback state, the request is proxied to the organization/team-level endpoint;
   * otherwise, credentials are saved at the user level.
   *
   * Proxying ensures that permission checks—such as whether the user is allowed to install Stripe for a team or organization—
   * are enforced via controller route guards, avoiding duplication of this logic within the service layer.
   */
  async save(
    @Query("state") state: string,
    @Query("code") code: string,
    @Query("error") error: string | undefined,
    @Query("error_description") error_description: string | undefined
  ): Promise<StripCredentialsSaveOutputResponseDto> {
    if (!state) {
      throw new BadRequestException("Missing `state` query param");
    }

    const decodedCallbackState: OAuthCallbackState = JSON.parse(state);
    try {
      // If teamId is present, proxy to team endpoint
      if (decodedCallbackState.teamId && decodedCallbackState.orgId) {
        let url = "";
        const apiUrl = this.config.get("api.url");
        url = `${apiUrl}/organizations/${decodedCallbackState.orgId}/teams/${decodedCallbackState.teamId}/stripe/save`;

        const params: Record<string, string | undefined> = { state, code, error, error_description };
        const headers = {
          Authorization: `Bearer ${decodedCallbackState.accessToken}`,
        };
        try {
          const response = await this.httpService.axiosRef.get(url, { params, headers });
          const redirectUrl = response.data?.url || decodedCallbackState.onErrorReturnTo || "";
          return { url: redirectUrl };

View on GitHub (pinned to 176037d0af)

Solutions

  1. 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.
  2. In the Stripe dashboard, set the redirect URI to the exact /v2/stripe/save endpoint so Stripe appends state and code.
  3. Ensure no proxy/CDN rule strips query strings from the callback URL.
Defensive patterns

Strategy: validation

Validate before calling

// This error is a server-side OAuth callback contract: clients should never
craft /save URLs. Always start via /redirect, which builds state.
async function startStripeConnect(api) {
  const { url } = await api.post('/v2/stripe/redirect', { returnTo: window.location.origin });
  window.location.href = url; // Stripe appends state & code on return
}

Type guard

type OAuthCallbackState = {
  accessToken: string;
  teamId?: string;
  orgId?: string;
  onErrorReturnTo?: string;
};
function isCallbackState(v: unknown): v is OAuthCallbackState {
  return !!v && typeof (v as any).accessToken === 'string';
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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