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
- 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.
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
- 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.
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
- error=${error}&error_description=${error_description}
- Invalid Access token.
- ApiAuthStrategy - access token - Invalid Access Token.
- ApiAuthStrategy - access token - OAuth client not found give
- Missing `state` query param
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/813183e78cbb7a7f.
Report an issue: GitHub.