> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getsabo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe Checkout

> Create a Stripe Checkout session for recurring subscriptions

<Info>
  This endpoint creates a Stripe Checkout session, redirecting the authenticated user to Stripe's hosted checkout page to complete their subscription payment.
</Info>

* **Endpoint**: `POST /api/checkout_sessions`
* **File Location**: `src/app/api/checkout_sessions/route.ts`
* **Authentication**: Required (uses Supabase session)

## Parameters

<ParamField body="price_id" type="string" required>
  The Stripe Price ID for the subscription plan. Get this from your Stripe Dashboard or environment variables.

  **Format:** `price_1234567890abcdef`

  **Where to find:**

  * Stripe Dashboard → Products → Select product → Copy Price ID
  * Or use environment variables: `NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_MONTHLY`
</ParamField>

## Request/Response

#### Example Request (cURL)

```bash theme={null}
curl -X POST 'http://localhost:3000/api/checkout_sessions' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Cookie: your-auth-cookie' \
  -d 'price_id=price_1234567890abcdef'
```

#### Example Request (Fetch API)

```javascript theme={null}
const response = await fetch('/api/checkout_sessions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams({
    price_id: 'price_1234567890abcdef'
  })
});

// Stripe Checkout redirects automatically (303)
// No JSON response to handle
```

#### Example Response (303 Redirect)

```http theme={null}
HTTP/1.1 303 See Other
Location: https://checkout.stripe.com/c/pay/cs_test_abc123...
```

The endpoint redirects to Stripe Checkout. Users complete payment there and return to your success URL.

#### Example Response (401 Unauthorized)

```json theme={null}
{
  "error": "Authentication required"
}
```

User is not authenticated. They must sign in first.

#### Example Response (400 Bad Request)

```json theme={null}
{
  "error": "Valid price ID is required"
}
```

Missing or invalid `price_id` parameter.

#### Example Response (500 Server Error)

```json theme={null}
{
  "error": "Error message from Stripe"
}
```

Stripe API error (invalid price ID, network issue, etc.)

## How It Works

<Steps>
  <Step title="Authenticate user">
    The endpoint checks for an active Supabase session using `supabase.auth.getUser()`.

    If no session exists, returns `401 Unauthorized`.
  </Step>

  <Step title="Validate price ID">
    Extracts `price_id` from form data and validates it's not empty or "undefined".

    Invalid price IDs return `400 Bad Request`.
  </Step>

  <Step title="Create Checkout session">
    Calls Stripe API to create a Checkout session with:

    * **Line items**: Subscription with selected price
    * **Mode**: `"subscription"` (recurring payment)
    * **Success URL**: `{SITE_URL}/success?session_id={CHECKOUT_SESSION_ID}`
    * **Cancel URL**: `{SITE_URL}/pricing?canceled=true`
    * **Customer email**: Pre-filled with user's email
    * **Metadata**: Includes `user_id` and `user_email` for webhook processing
    * **Features**:
      * Automatic tax calculation
      * Promotion code support
      * Customer email pre-filled
  </Step>

  <Step title="Redirect to Stripe">
    Returns a `303 See Other` redirect to the Stripe Checkout URL.

    User completes payment on Stripe's secure hosted page.
  </Step>

  <Step title="Handle return">
    After payment:

    * **Success**: User redirected to `/success?session_id=cs_...`
    * **Cancel**: User redirected to `/pricing?canceled=true`

    The success page verifies payment and shows confirmation.
  </Step>
</Steps>

## Implementation

The endpoint is implemented in `src/app/api/checkout_sessions/route.ts`:

```ts src/app/api/checkout_sessions/route.ts theme={null}
import { NextResponse } from "next/server";
import { headers } from "next/headers";
import { stripe } from "@/lib/payments/stripe";
import { createClient } from "@/lib/supabase/server";

export async function POST(request: Request) {
  try {
    const headersList = await headers();
    const origin = headersList.get("origin");

    const supabase = await createClient();
    const {
      data: { user },
      error: authError,
    } = await supabase.auth.getUser();

    if (authError || !user) {
      return NextResponse.json(
        { error: "Authentication required" },
        { status: 401 }
      );
    }

    const formData = await request.formData();
    const price_id = formData.get("price_id") as string;

    if (!price_id || price_id === "undefined") {
      return NextResponse.json(
        { error: "Valid price ID is required" },
        { status: 400 }
      );
    }

    const session = await stripe.checkout.sessions.create({
      line_items: [
        {
          price: price_id,
          quantity: 1,
        },
      ],
      mode: "subscription",
      success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${origin}/pricing?canceled=true`,
      automatic_tax: { enabled: true },
      allow_promotion_codes: true,
      customer_email: user.email,
      metadata: {
        user_id: user.id,
        user_email: user.email || "",
      },
      subscription_data: {
        metadata: {
          user_id: user.id,
          user_email: user.email || "",
        },
      },
    });

    return NextResponse.redirect(session.url!, 303);
  } catch (err) {
    const error = err as Error;
    console.error("Checkout session creation error:", error);
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}
```

## Frontend Integration

Typically called from a pricing page or "Upgrade" button:

```tsx src/components/marketing/pricing.tsx theme={null}
"use client";

export function PricingCard({ priceId, planName }) {
  const handleSubscribe = async () => {
    // Create form and submit to trigger checkout
    const form = document.createElement('form');
    form.method = 'POST';
    form.action = '/api/checkout_sessions';
    
    const input = document.createElement('input');
    input.type = 'hidden';
    input.name = 'price_id';
    input.value = priceId;
    
    form.appendChild(input);
    document.body.appendChild(form);
    form.submit();
  };

  return (
    <button onClick={handleSubscribe}>
      Subscribe to {planName}
    </button>
  );
}
```

## Success Page Verification

After Stripe redirects to `/success`, verify the session:

```tsx src/app/success/page.tsx theme={null}
import { stripe } from "@/lib/payments/stripe";
import { createClient } from "@/lib/supabase/server";

export default async function SuccessPage({ searchParams }) {
  const sessionId = searchParams.session_id;
  
  if (!sessionId) {
    return <div>No session ID provided</div>;
  }

  // Verify session with Stripe
  const session = await stripe.checkout.sessions.retrieve(sessionId);
  
  if (session.status === "complete") {
    // Payment successful!
    return <div>Thank you for subscribing!</div>;
  }
  
  if (session.status === "open") {
    // Payment still pending
    return <div>Payment in progress...</div>;
  }
  
  return <div>Payment failed</div>;
}
```

## Webhook Integration

This endpoint only starts the checkout flow—**subscription activation and billing history are synced by `/api/webhooks/stripe`**.

The webhook handler listens to these events:

| Event                           | Purpose                                                                                     |
| ------------------------------- | ------------------------------------------------------------------------------------------- |
| `customer.subscription.created` | Upserts the initial record in `user_subscriptions` using metadata from the Checkout session |
| `customer.subscription.updated` | Handles plan changes, cancellations, renewals, and billing-cycle changes                    |
| `customer.subscription.deleted` | Marks subscriptions as canceled in the database                                             |
| `invoice.payment_succeeded`     | Inserts a payment record into `payment_history`                                             |
| `invoice.payment_failed`        | Logs failed payments so you can notify the customer                                         |

Because the handler reads the `user_id` from metadata you set in `checkout_sessions`, **webhooks must run even if the user never returns to `/success`**.

See [Stripe Webhook Handler](/api-reference/endpoint/stripe-webhooks) for the full implementation details.

<Warning>
  Never rely solely on the success page to activate subscriptions. Users can close the browser before returning. Always use webhooks for reliable subscription management.
</Warning>

## Testing

### Test with Stripe Test Cards

Use Stripe's test cards to simulate payments:

```bash theme={null}
# Successful payment
Card: 4242 4242 4242 4242
Expiry: Any future date
CVC: Any 3 digits
ZIP: Any 5 digits
```

### Test Workflow

<Steps>
  <Step title="Ensure Stripe CLI is running">
    ```bash theme={null}
    stripe listen --forward-to localhost:3000/api/webhooks/stripe
    ```

    This forwards webhook events to your local server.
  </Step>

  <Step title="Start dev server">
    ```bash theme={null}
    pnpm dev
    ```
  </Step>

  <Step title="Create checkout session">
    1. Sign in to your app
    2. Go to `/pricing`
    3. Click "Subscribe" on any plan
    4. You'll be redirected to Stripe Checkout (test mode)
  </Step>

  <Step title="Complete test payment">
    1. Fill form with test card `4242 4242 4242 4242`
    2. Click "Subscribe"
    3. Stripe processes payment and redirects to `/success`
  </Step>

  <Step title="Verify webhook received">
    Check your terminal running Stripe CLI. You should see one of the subscription or invoice events:

    ```
    ✔ Received event: customer.subscription.created
    ✔ Forwarded to http://localhost:3000/api/webhooks/stripe
    ✔ Response: 200
    ```

    For invoice flows, you can also trigger:

    ```bash theme={null}
    stripe trigger invoice.payment_succeeded
    ```
  </Step>

  <Step title="Check database">
    Verify `user_subscriptions` and `payment_history` tables updated:

    ```sql theme={null}
    SELECT * FROM user_subscriptions WHERE user_id = 'your-user-id';
    SELECT * FROM payment_history WHERE user_id = 'your-user-id';
    ```
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="401 Unauthorized error">
    **Cause:** User not authenticated or session expired.

    **Fix:**

    1. Verify user is signed in
    2. Check Supabase session cookie exists
    3. Test with `supabase.auth.getUser()` in browser console
  </Accordion>

  <Accordion title="400 Bad Request - 'Valid price ID is required'">
    **Cause:** Missing or invalid `price_id` parameter.

    **Fix:**

    1. Verify form includes `price_id` field
    2. Check Price ID format: `price_...` (not `prod_...`)
    3. Ensure Price ID exists in Stripe Dashboard
    4. Test with hardcoded Price ID first
  </Accordion>

  <Accordion title="Redirect to Stripe fails">
    **Cause:** Stripe API error, network issue, or invalid configuration.

    **Fix:**

    1. Check `STRIPE_SECRET_KEY` environment variable
    2. Verify Stripe key matches mode (test vs live)
    3. Check browser console for errors
    4. Inspect server logs for Stripe API errors
  </Accordion>

  <Accordion title="Checkout works but subscription not created">
    **Cause:** Webhook not received or webhook handler error.

    **Fix:**

    1. Verify Stripe CLI running: `stripe listen --forward-to localhost:3000/api/webhooks/stripe`
    2. Check webhook endpoint: `POST /api/webhooks/stripe`
    3. Verify `STRIPE_WEBHOOK_SECRET` environment variable
    4. Check webhook handler logs for errors
    5. Test webhook: `stripe trigger checkout.session.completed`
  </Accordion>

  <Accordion title="'origin' header missing">
    **Cause:** Request made without origin header (e.g., from Postman).

    **Fix:**
    Use browser fetch or add origin header manually:

    ```bash theme={null}
    curl -X POST 'http://localhost:3000/api/checkout_sessions' \
      -H 'Origin: http://localhost:3000' \
      -H 'Content-Type: application/x-www-form-urlencoded' \
      -d 'price_id=price_...'
    ```
  </Accordion>
</AccordionGroup>

## Security Considerations

<AccordionGroup>
  <Accordion title="Authentication Required">
    The endpoint checks for an active Supabase session. Unauthenticated requests return `401`.

    **Why:** Prevents anonymous users from creating checkout sessions and ensures proper user-subscription linking.
  </Accordion>

  <Accordion title="Metadata Tracking">
    User ID and email are stored in Stripe metadata:

    ```ts theme={null}
    metadata: {
      user_id: user.id,
      user_email: user.email || "",
    }
    ```

    **Purpose:** Webhooks use this to update the correct `user_subscriptions` record.

    **Privacy:** Email stored in Stripe only, not exposed publicly.
  </Accordion>

  <Accordion title="Success URL Protection">
    The success page should verify the session ID with Stripe:

    ```ts theme={null}
    const session = await stripe.checkout.sessions.retrieve(sessionId);
    ```

    **Why:** Prevents users from forging success URLs to fake payments.
  </Accordion>

  <Accordion title="No Sensitive Data Exposure">
    The endpoint doesn't return the Checkout session object (contains sensitive data). It only redirects to Stripe.

    **Best practice:** Stripe handles all payment details securely on their PCI-compliant infrastructure.
  </Accordion>
</AccordionGroup>

## Related Documentation

<CardGroup cols={2}>
  <Card title="Payments with Stripe" icon="credit-card" href="/core-features/payments-with-stripe">
    Complete Stripe integration guide including webhooks, plans, and testing.
  </Card>

  <Card title="Customer Portal" icon="user" href="/api-reference/endpoint/customer-portal">
    Allow users to manage their subscriptions, update payment methods, and view invoices.
  </Card>

  <Card title="Stripe Webhooks" icon="webhook" href="/core-features/payments-with-stripe#webhook-handler">
    Handle subscription lifecycle events and keep your database in sync.
  </Card>

  <Card title="Environment Variables" icon="code" href="/core-features/environment-variables">
    Set up Stripe API keys and webhook secrets for local and production environments.
  </Card>
</CardGroup>
