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

# Billing Configuration

> Set up Stripe payment integration for subscriptions and one-time payments

8Space includes optional Stripe integration for payment processing. Configure Stripe to enable billing features in the landing site.

<Note>
  Billing is optional. If Stripe is not configured, payment features will be disabled.
</Note>

## Overview

The Stripe integration supports:

* One-time payments (Stripe Checkout in `payment` mode)
* Recurring subscriptions (Stripe Checkout in `subscription` mode)
* Customer portal for subscription management
* Webhook handling for payment events
* Tax ID collection
* Promotional codes

## Environment Variables

Add Stripe credentials to `packages/landing/.env.local`:

<ParamField path="STRIPE_SECRET_KEY" type="string" required>
  Stripe secret key for API authentication.

  **Test mode:** `sk_test_...`

  **Live mode:** `sk_live_...`

  Get from: [Stripe Dashboard → Developers → API keys](https://dashboard.stripe.com/apikeys)
</ParamField>

<ParamField path="STRIPE_WEBHOOK_SECRET" type="string" required>
  Webhook signing secret for verifying Stripe webhook events.

  **Format:** `whsec_...`

  Get from: [Stripe Dashboard → Developers → Webhooks](https://dashboard.stripe.com/webhooks)
</ParamField>

### Example Configuration

```bash packages/landing/.env.local theme={null}
STRIPE_SECRET_KEY=sk_test_51Abc123...
STRIPE_WEBHOOK_SECRET=whsec_xyz789...
```

<Warning>
  Never commit Stripe secrets to version control. Add `.env.local` to `.gitignore`.
</Warning>

## Stripe Setup

<Steps>
  <Step title="Create Stripe Account">
    Sign up at [stripe.com](https://stripe.com) if you don't have an account.
  </Step>

  <Step title="Get API Keys">
    1. Navigate to [Developers → API keys](https://dashboard.stripe.com/apikeys)
    2. Copy the **Secret key** (starts with `sk_test_` or `sk_live_`)
    3. Add to `.env.local` as `STRIPE_SECRET_KEY`
  </Step>

  <Step title="Create Products & Prices">
    1. Navigate to [Products](https://dashboard.stripe.com/products)
    2. Click "Add product"
    3. Set name, description, and pricing
    4. Copy the **Price ID** (starts with `price_`)

    Use this Price ID in your checkout calls.
  </Step>

  <Step title="Configure Webhooks">
    1. Navigate to [Developers → Webhooks](https://dashboard.stripe.com/webhooks)
    2. Click "Add endpoint"
    3. Set endpoint URL:
       * **Local:** Use [Stripe CLI](#local-testing-with-stripe-cli)
       * **Production:** `https://yourdomain.com/api/stripe/webhook`
    4. Select events to listen for
    5. Copy the **Signing secret** (starts with `whsec_`)
    6. Add to `.env.local` as `STRIPE_WEBHOOK_SECRET`
  </Step>
</Steps>

## API Implementation

The Stripe integration is implemented in `packages/landing/libs/stripe.ts` and used by Next.js API routes.

### Stripe Client

```typescript packages/landing/libs/stripe.ts theme={null}
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2023-08-16',
  typescript: true,
});
```

### Checkout Session

Create a checkout session for payment or subscription:

```typescript packages/landing/libs/stripe.ts theme={null}
export const createCheckout = async ({
  priceId,
  mode,
  successUrl,
  cancelUrl,
  clientReferenceId,
  user,
  couponId,
}: CreateCheckoutParams): Promise<string> => {
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
    apiVersion: '2023-08-16',
    typescript: true,
  });

  const extraParams: any = {};

  if (user?.customerId) {
    extraParams.customer = user.customerId;
  } else {
    if (mode === 'payment') {
      extraParams.customer_creation = 'always';
      extraParams.payment_intent_data = {
        setup_future_usage: 'on_session'
      };
    }
    if (user?.email) {
      extraParams.customer_email = user.email;
    }
    extraParams.tax_id_collection = { enabled: true };
  }

  const stripeSession = await stripe.checkout.sessions.create({
    mode,
    allow_promotion_codes: true,
    client_reference_id: clientReferenceId,
    line_items: [{ price: priceId, quantity: 1 }],
    discounts: couponId ? [{ coupon: couponId }] : [],
    success_url: successUrl,
    cancel_url: cancelUrl,
    ...extraParams,
  });

  return stripeSession.url;
};
```

### Customer Portal

Create a customer portal session for subscription management:

```typescript packages/landing/libs/stripe.ts theme={null}
export const createCustomerPortal = async ({
  customerId,
  returnUrl,
}: CreateCustomerPortalParams): Promise<string> => {
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
    apiVersion: '2023-08-16',
    typescript: true,
  });

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: returnUrl,
  });

  return portalSession.url;
};
```

## API Routes

The landing package includes API routes for Stripe operations.

### Create Checkout Route

**Endpoint:** `POST /api/stripe/create-checkout`

**Location:** `packages/landing/app/api/stripe/create-checkout/route.ts`

**Request body:**

```json theme={null}
{
  "priceId": "price_1Abc123",
  "mode": "subscription",
  "successUrl": "https://example.com/success",
  "cancelUrl": "https://example.com/cancel"
}
```

**Response:**

```json theme={null}
{
  "url": "https://checkout.stripe.com/c/pay/cs_..."
}
```

**Implementation:**

```typescript packages/landing/app/api/stripe/create-checkout/route.ts theme={null}
export async function POST(req: NextRequest) {
  const body = await req.json();

  if (!body.priceId || !body.successUrl || !body.cancelUrl || !body.mode) {
    return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
  }

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

    const stripeSessionURL = await createCheckout({
      priceId: body.priceId,
      mode: body.mode,
      successUrl: body.successUrl,
      cancelUrl: body.cancelUrl,
      clientReferenceId: user?.id,
      user: user ? { email: user.email, customerId: null } : null,
    });

    return NextResponse.json({ url: stripeSessionURL });
  } catch (e: any) {
    return NextResponse.json({ error: e?.message }, { status: 500 });
  }
}
```

### Create Portal Route

**Endpoint:** `POST /api/stripe/create-portal`

**Location:** `packages/landing/app/api/stripe/create-portal/route.ts`

**Request body:**

```json theme={null}
{
  "returnUrl": "https://example.com/billing"
}
```

**Response:**

```json theme={null}
{
  "url": "https://billing.stripe.com/p/session/..."
}
```

<Info>
  The portal route currently returns an error if no `customerId` is found. You'll need to implement customer lookup from your database.
</Info>

**Implementation:**

```typescript packages/landing/app/api/stripe/create-portal/route.ts theme={null}
export async function POST(req: NextRequest) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    return NextResponse.json({ error: 'Not signed in' }, { status: 401 });
  }

  const body = await req.json();
  if (!body.returnUrl) {
    return NextResponse.json({ error: 'Return URL is required' }, { status: 400 });
  }

  // TODO: Look up customerId from database
  const customerId: string | null = null;

  if (!customerId) {
    return NextResponse.json(
      { error: "You don't have a billing account yet. Make a purchase first." },
      { status: 400 }
    );
  }

  const stripePortalUrl = await createCustomerPortal({
    customerId,
    returnUrl: body.returnUrl,
  });

  return NextResponse.json({ url: stripePortalUrl });
}
```

## Customer Data Storage

<Warning>
  The current implementation does not store Stripe customer IDs in the database. You'll need to add this functionality.
</Warning>

### Recommended Schema

Add a table to store Stripe customer data:

```sql theme={null}
create table public.stripe_customers (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references public.profiles (id) on delete cascade,
  stripe_customer_id text not null unique,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create unique index stripe_customers_user_id_idx
  on public.stripe_customers (user_id);
```

### Update Customer Portal

Modify the portal route to look up the customer ID:

```typescript theme={null}
// Look up customerId from database
const { data: customer } = await supabase
  .from('stripe_customers')
  .select('stripe_customer_id')
  .eq('user_id', user.id)
  .single();

const customerId = customer?.stripe_customer_id;
```

## Webhook Handling

Webhooks notify your application of Stripe events (payment success, subscription canceled, etc.).

### Webhook Route

Create `packages/landing/app/api/stripe/webhook/route.ts`:

```typescript theme={null}
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-08-16',
});

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
  } catch (err: any) {
    console.error('Webhook signature verification failed:', err.message);
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  // Handle the event
  switch (event.type) {
    case 'checkout.session.completed':
      const session = event.data.object as Stripe.Checkout.Session;
      // TODO: Save customer ID, grant access, send confirmation email
      console.log('Checkout completed:', session.id);
      break;

    case 'customer.subscription.updated':
      const subscription = event.data.object as Stripe.Subscription;
      // TODO: Update subscription status in database
      console.log('Subscription updated:', subscription.id);
      break;

    case 'customer.subscription.deleted':
      const deletedSubscription = event.data.object as Stripe.Subscription;
      // TODO: Revoke access
      console.log('Subscription canceled:', deletedSubscription.id);
      break;

    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  return NextResponse.json({ received: true });
}
```

### Important Webhook Events

| Event                           | Description                      |
| ------------------------------- | -------------------------------- |
| `checkout.session.completed`    | Payment successful, grant access |
| `customer.subscription.created` | New subscription started         |
| `customer.subscription.updated` | Subscription plan changed        |
| `customer.subscription.deleted` | Subscription canceled            |
| `invoice.payment_succeeded`     | Recurring payment successful     |
| `invoice.payment_failed`        | Payment failed, notify user      |

## Local Testing with Stripe CLI

Test webhooks locally using the Stripe CLI.

<Steps>
  <Step title="Install Stripe CLI">
    Download from [stripe.com/docs/stripe-cli](https://stripe.com/docs/stripe-cli)

    Or install via package manager:

    ```bash theme={null}
    # macOS
    brew install stripe/stripe-cli/stripe

    # Linux/WSL
    curl -L https://github.com/stripe/stripe-cli/releases/latest/download/stripe_linux_x86_64.tar.gz | tar -xz
    ```
  </Step>

  <Step title="Authenticate">
    ```bash theme={null}
    stripe login
    ```
  </Step>

  <Step title="Forward Webhooks">
    ```bash theme={null}
    stripe listen --forward-to localhost:3000/api/stripe/webhook
    ```

    Copy the webhook signing secret (starts with `whsec_`) and add to `.env.local`:

    ```bash theme={null}
    STRIPE_WEBHOOK_SECRET=whsec_...
    ```
  </Step>

  <Step title="Trigger Test Events">
    ```bash theme={null}
    stripe trigger checkout.session.completed
    stripe trigger customer.subscription.deleted
    ```
  </Step>
</Steps>

## Testing Payments

### Test Cards

Stripe provides test cards for different scenarios:

| Card Number           | Scenario                            |
| --------------------- | ----------------------------------- |
| `4242 4242 4242 4242` | Successful payment                  |
| `4000 0000 0000 0002` | Card declined                       |
| `4000 0025 0000 3155` | Requires authentication (3D Secure) |
| `4000 0000 0000 9995` | Insufficient funds                  |

Use any future expiry date and any 3-digit CVC.

### Test Checkout Flow

<Tabs>
  <Tab title="One-time Payment">
    ```typescript theme={null}
    const response = await fetch('/api/stripe/create-checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        priceId: 'price_1Abc123',
        mode: 'payment',
        successUrl: window.location.origin + '/success',
        cancelUrl: window.location.origin + '/cancel',
      }),
    });

    const { url } = await response.json();
    window.location.href = url; // Redirect to Stripe Checkout
    ```
  </Tab>

  <Tab title="Subscription">
    ```typescript theme={null}
    const response = await fetch('/api/stripe/create-checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        priceId: 'price_1Xyz789',
        mode: 'subscription',
        successUrl: window.location.origin + '/welcome',
        cancelUrl: window.location.origin + '/pricing',
      }),
    });

    const { url } = await response.json();
    window.location.href = url;
    ```
  </Tab>
</Tabs>

## Production Checklist

<Steps>
  <Step title="Switch to Live Mode">
    * Get live API keys from Stripe Dashboard
    * Update `STRIPE_SECRET_KEY` with `sk_live_...`
    * Update webhook secret with live endpoint secret
  </Step>

  <Step title="Configure Webhook Endpoint">
    * Add production URL to Stripe Dashboard
    * Select events to listen for
    * Test webhook delivery
  </Step>

  <Step title="Test Payment Flow">
    * Use real credit cards in test mode first
    * Verify webhooks are received
    * Check customer data is saved
  </Step>

  <Step title="Security Review">
    * Verify webhook signature validation
    * Ensure secrets are not committed
    * Enable HTTPS only
    * Review Stripe security best practices
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhook signature verification failed">
    * Verify `STRIPE_WEBHOOK_SECRET` matches the endpoint secret
    * For local testing, use Stripe CLI's secret (starts with `whsec_`)
    * Check that you're passing the raw request body to `constructEvent`
  </Accordion>

  <Accordion title="Customer portal returns 'No billing account'">
    Implement customer ID storage:

    1. Create `stripe_customers` table
    2. Save customer ID on checkout completion
    3. Update portal route to look up customer ID
  </Accordion>

  <Accordion title="Checkout session creation fails">
    * Verify `STRIPE_SECRET_KEY` is set correctly
    * Check that Price ID exists in Stripe Dashboard
    * Ensure `mode` matches price type (`payment` for one-time, `subscription` for recurring)
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Environment Variables" icon="key" href="/configuration/environment-variables">
    Configure all required Stripe variables
  </Card>

  <Card title="Database Setup" icon="database" href="/configuration/database-setup">
    Add customer table for Stripe IDs
  </Card>
</CardGroup>
