Skip to main content
This endpoint lets authenticated users open Polar’s self-serve billing portal to update payment methods, download invoices, or cancel subscriptions.
  • 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.
curl -X GET 'http://localhost:3000/api/portal' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: supabase-auth-cookie'

Response

{
  "url": "https://polar.sh/customer-portal/session_123..."
}
Use window.location.href = url to redirect the browser.

Error Responses

StatusBodyDescription
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

1

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".
2

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

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.

Implementation

src/app/api/portal/route.ts
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

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

Ensure the webhook has created a user_subscriptions row with polar_customer_id. Run a test checkout if the row is missing.
The request must include Supabase auth cookies. If you call this from the client, pass credentials: 'include'.