/api/v1/payment/create-checkout-sessionCreate a Stripe-hosted Checkout session for a subscription upgrade.
Create a Stripe-hosted Checkout session for a subscription upgrade. EXEMPT (`cost: 0`). Returns the Stripe URL — frontend MUST redirect via `window.location.href`.
Why use this
Common use case
Stripe-hosted subscription Checkout session for plan upgrades. EXEMPT (cost: 0). Wraps stripe.checkout.Session.create(mode='subscription'). The frontend MUST redirect (not iframe-embed) — Stripe blocks framing. On successful payment Stripe webhooks checkout.session.completed to /api/v1/payments/webhook → _handle_checkout_completed activates the subscription on user_plans (sets plan, stripe_subscription_id, current_period_end). The Free plan cannot be checked out (no stripe_price_id) — gate the 'Upgrade' button server-side rather than rendering for Free. To list available plans use GET /api/v1/plans/; to check post-upgrade state use GET /api/v1/billing/subscription; for self-serve subscription management (cancel, change card) once on a paid plan use POST /api/v1/account/billing-portal. For one-time USD wallet top-ups (NOT subscription upgrades) use POST /api/v1/payments/create-intent.
Parameters
| Name | In | Required | Default | Allowed | Description | Example |
|---|---|---|---|---|---|---|
| plan_id | query | required | — | — | Plan database ID (NOT name) — the integer `id` from [GET /api/v1/plans/](/docs/account/billing-and-subscription/get-plans). Returns 400 `BAD_REQUEST` if missing or non-numeric, 404 `NOT_FOUND` if the plan does not exist or is inactive, 400 `BAD_REQUEST` if the plan has no `stripe_price_id` (Free plan). | 2 |
Response schema
| Field | Type | Nullable | Description |
|---|---|---|---|
| status | string | no | ApiResponse envelope status — `success` on 200, `error` on 4xx/5xx. |
| request_id | string | yes | Per-request correlation ID. |
| timestamp | string | no | ISO-8601 UTC timestamp. |
| data | object | no | Checkout-session payload. |
| data.url | string | no | Stripe Checkout-hosted page URL in `https://checkout.stripe.com/c/pay/cs_XXXXXXX#YYYYY` format. REDIRECT the browser via `window.location.href = response.data.url` — do NOT iframe-embed (Stripe blocks framing of checkout pages). On success, Stripe redirects back to `${FRONTEND_URL}/account/subscription?session_id={CHECKOUT_SESSION_ID}`; on cancel, to `${FRONTEND_URL}/account/subscription?cancelled=true`. The Checkout session embeds metadata (`user_uuid`, `plan_id`, `plan_name`) so the `checkout.session.completed` webhook handler can activate the right subscription on the right user. |
Sample response
- "status": "success"
- "request_id": "req_3OqK2jK9L8pQ4xZ6"
- "timestamp": "2026-05-02T15:51:00.000Z"
- "data":
- "url": "https://checkout.stripe.com/c/pay/cs_test_b1Yq8nL7aP2kT6sB1mDfHj9vC3xRtKzE5wQ8jN4uM2pX7yI6oA9hT0gU1vF#fidkdWxOYHwnPyd1blpxYHZxWjA0SF9MUFc2dVZxYG1QSXNqcGhJQDR9aGhNNXxPSk09Q1xLTHN1MnxwfFBIYDU8YzVtXX1HSUtAaD1KaHM3RkB"
Errors
| Status | Label | Description |
|---|---|---|
| 200 | OK | Request succeeded. |
| 400 | Bad Request | Invalid query, body, or path parameter. |
| 401 | Unauthorized | Missing or invalid Authorization header / api_Token. |
| 402 | Payment Required | Insufficient token balance for this call. Top up |
| 429 | Too Many Requests | Rate limit exceeded for your tier (see /pricing for tier limits). Tier limits |
| 500 | Server Error | Unexpected server-side failure. Retry with backoff; report if persistent. |
Code samples
curl -X POST "https://api.finradar.ai/api/v1/payment/create-checkout-session" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"Generate an API key in /account/credentials to run live queries (literal YOUR_API_KEY placeholder shown until then).