> ## 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 & Stripe

> Create Stripe checkout sessions and manage customer billing portals

## POST /api/stripe/create-checkout

Create a Stripe Checkout session for one-time payments or subscriptions. This endpoint is triggered by the `ButtonCheckout` component.

### Request

<ParamField body="priceId" type="string" required>
  Stripe Price ID for the product or subscription plan.
</ParamField>

<ParamField body="successUrl" type="string" required>
  URL to redirect to after successful payment. Must be a valid URI.
</ParamField>

<ParamField body="cancelUrl" type="string" required>
  URL to redirect to if user cancels the checkout. Must be a valid URI.
</ParamField>

<ParamField body="mode" type="string" required>
  Checkout mode. Either `payment` for one-time payments or `subscription` for recurring subscriptions.
</ParamField>

### Response

<ResponseField name="url" type="string">
  The Stripe Checkout session URL to redirect the user to.
</ResponseField>

<ResponseField name="error" type="string">
  Error message returned when request fails.
</ResponseField>

### Code Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://8space.app/api/stripe/create-checkout \
    -H "Content-Type: application/json" \
    -d '{
      "priceId": "price_1234567890",
      "successUrl": "https://8space.app/success",
      "cancelUrl": "https://8space.app/cancel",
      "mode": "subscription"
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://8space.app/api/stripe/create-checkout', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      priceId: 'price_1234567890',
      successUrl: 'https://8space.app/success',
      cancelUrl: 'https://8space.app/cancel',
      mode: 'subscription'
    })
  });

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

  ```typescript TypeScript theme={null}
  interface CheckoutRequest {
    priceId: string;
    successUrl: string;
    cancelUrl: string;
    mode: 'payment' | 'subscription';
  }

  const response = await fetch('https://8space.app/api/stripe/create-checkout', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      priceId: 'price_1234567890',
      successUrl: 'https://8space.app/success',
      cancelUrl: 'https://8space.app/cancel',
      mode: 'subscription'
    } as CheckoutRequest)
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error);
  }

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

### Request Validation

<Accordion title="Validation Code">
  ```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) {
      return NextResponse.json(
        { error: "Price ID is required" },
        { status: 400 }
      );
    } else if (!body.successUrl || !body.cancelUrl) {
      return NextResponse.json(
        { error: "Success and cancel URLs are required" },
        { status: 400 }
      );
    } else if (!body.mode) {
      return NextResponse.json(
        {
          error:
            "Mode is required (either 'payment' for one-time payments or 'subscription' for recurring subscription)",
        },
        { status: 400 }
      );
    }

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

      const { priceId, mode, successUrl, cancelUrl } = body;

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

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

### Error Handling

<ResponseField name="400" type="error">
  **Validation Error**

  Returned when:

  * `priceId` is missing: `"Price ID is required"`
  * `successUrl` or `cancelUrl` is missing: `"Success and cancel URLs are required"`
  * `mode` is missing: `"Mode is required (either 'payment' for one-time payments or 'subscription' for recurring subscription)"`
</ResponseField>

<ResponseField name="500" type="error">
  **Internal Server Error**

  Returned when Stripe API call fails or unexpected server error occurs.

  ```json theme={null}
  {
    "error": "Error message from Stripe or internal error"
  }
  ```
</ResponseField>

### Success Response

**Status Code:** `200 OK`

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

***

## POST /api/stripe/create-portal

Create a Stripe Customer Portal session for managing billing and subscriptions. This endpoint is triggered by the `ButtonAccount` component and requires an authenticated user.

### Authentication

This endpoint requires a valid Supabase user session. Users must be signed in with a session cookie.

### Request

<ParamField body="returnUrl" type="string" required>
  URL to return to after the user completes their session in the billing portal. Must be a valid URI.
</ParamField>

### Response

<ResponseField name="url" type="string">
  The Stripe Customer Portal URL to redirect the user to.
</ResponseField>

<ResponseField name="error" type="string">
  Error message returned when request fails.
</ResponseField>

### Code Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://8space.app/api/stripe/create-portal \
    -H "Content-Type: application/json" \
    -H "Cookie: sb-access-token=..." \
    -d '{
      "returnUrl": "https://8space.app/account"
    }'
  ```

  ```javascript JavaScript theme={null}
  // Requires authenticated session cookie
  const response = await fetch('https://8space.app/api/stripe/create-portal', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    credentials: 'include',
    body: JSON.stringify({
      returnUrl: 'https://8space.app/account'
    })
  });

  const { url } = await response.json();
  window.location.href = url;
  ```

  ```typescript TypeScript theme={null}
  interface PortalRequest {
    returnUrl: string;
  }

  const response = await fetch('https://8space.app/api/stripe/create-portal', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    credentials: 'include',
    body: JSON.stringify({
      returnUrl: 'https://8space.app/account'
    } as PortalRequest)
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error);
  }

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

### Request Validation

<Accordion title="Validation Code">
  ```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) {
      try {
        const body = await req.json();

        if (!body.returnUrl) {
          return NextResponse.json(
            { error: "Return URL is required" },
            { status: 400 }
          );
        }

        // TODO: Look up customerId from your database (e.g., a Supabase 'customers' table)
        // For now, this is a placeholder
        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,
        });
      } catch (e: any) {
        console.error(e);
        return NextResponse.json({ error: e?.message }, { status: 500 });
      }
    } else {
      return NextResponse.json({ error: "Not signed in" }, { status: 401 });
    }
  }
  ```
</Accordion>

### Error Handling

<ResponseField name="400" type="error">
  **Validation Error**

  Returned when:

  * `returnUrl` is missing: `"Return URL is required"`
  * User has no Stripe customer account: `"You don't have a billing account yet. Make a purchase first."`
</ResponseField>

<ResponseField name="401" type="error">
  **Unauthorized**

  Returned when user is not authenticated.

  ```json theme={null}
  {
    "error": "Not signed in"
  }
  ```
</ResponseField>

<ResponseField name="500" type="error">
  **Internal Server Error**

  Returned when Stripe API call fails or unexpected server error occurs.

  ```json theme={null}
  {
    "error": "Error message from Stripe or internal error"
  }
  ```
</ResponseField>

### Success Response

**Status Code:** `200 OK`

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

### Implementation Notes

* The endpoint includes a TODO for looking up the Stripe customer ID from the database
* Currently uses a placeholder `customerId` that is always `null`
* Users must make a purchase first to create a Stripe customer account before accessing the portal
* All authentication is handled via Supabase session cookies
