> ## 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.

# Polar Customer Portal

> Create a Polar Customer Portal session for subscription management

<Info>
  This endpoint lets authenticated users open Polar's self-serve billing portal to update payment methods, download invoices, or cancel subscriptions.
</Info>

* **Endpoint**: `GET /api/portal`
* **File Location**: `src/app/api/portal/route.ts`
* **Authentication**: Required (Supabase session)

## Request

No body or query parameters. The request must include the Supabase auth cookies.

```bash theme={null}
curl -X GET 'http://localhost:3000/api/portal' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: supabase-auth-cookie'
```

## Response

```json theme={null}
{
  "url": "https://polar.sh/customer-portal/session_123..."
}
```

Use `window.location.href = url` to redirect the browser.

### Error Responses

| Status | Body                                   | Description                                          |
| ------ | -------------------------------------- | ---------------------------------------------------- |
| `401`  | `{ "error": "Unauthorized" }`          | User not signed in                                   |
| `500`  | `{ "error": "Something went wrong" }`  | Unexpected failure (see server logs)                 |
| `500`  | `{ "error": "No subscription found" }` | No `polar_customer_id` saved in `user_subscriptions` |

## How It Works

<Steps>
  <Step title="Fetch Supabase user">
    `CustomerPortal` receives a `getCustomerId` callback. The route reuses `createClient()` to read the current Supabase user. If no session exists, it throws `"Unauthorized"`.
  </Step>

  <Step title="Lookup Polar customer">
    After getting the user, the route queries `user_subscriptions` for `polar_customer_id`. Webhooks populate this field when subscriptions are created or updated.
  </Step>

  <Step title="Create portal session">
    The helper calls Polar's API with your `POLAR_ACCESS_TOKEN` and returns a portal URL scoped to that customer. The response is serialized as `{ url }` so the client can redirect.
  </Step>
</Steps>

## Implementation

```ts src/app/api/portal/route.ts theme={null}
import { CustomerPortal } from "@polar-sh/nextjs";
import { createClient } from "@/lib/supabase/server";

export const GET = CustomerPortal({
  accessToken: process.env.POLAR_ACCESS_TOKEN!,
  getCustomerId: async () => {
    const supabase = await createClient();
    const {
      data: { user },
      error: authError,
    } = await supabase.auth.getUser();

    if (authError || !user) {
      throw new Error("Unauthorized");
    }

    const { data: subscription, error: subscriptionError } = await supabase
      .from("user_subscriptions")
      .select("polar_customer_id")
      .eq("user_id", user.id)
      .single();

    if (subscriptionError || !subscription?.polar_customer_id) {
      throw new Error("No subscription found");
    }

    return subscription.polar_customer_id;
  },
  server: process.env.NODE_ENV === "production" ? "production" : "sandbox",
});
```

## Frontend Integration

```ts theme={null}
const openPortal = async () => {
  const res = await fetch('/api/portal', {
    method: 'GET',
    credentials: 'include',
  });

  if (!res.ok) {
    // Handle errors (show toast, etc.)
    return;
  }

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

## Troubleshooting

<AccordionGroup>
  <Accordion title="No subscription found">
    Ensure the webhook has created a `user_subscriptions` row with `polar_customer_id`. Run a test checkout if the row is missing.
  </Accordion>

  <Accordion title="401 Unauthorized">
    The request must include Supabase auth cookies. If you call this from the client, pass `credentials: 'include'`.
  </Accordion>
</AccordionGroup>
