Skip to main content
This endpoint wraps the official @polar-sh/nextjs Checkout helper. It validates the query string, redirects users to Polar’s hosted checkout, and returns them to /success when payment completes.
  • Endpoint: GET /api/checkout
  • File Location: src/app/api/checkout/route.ts
  • Authentication: Pricing UI enforces Supabase auth before building the checkout URL

Query Parameters

products
string
required
Comma-separated Polar product IDs. Usually a single ID such as prod_monthly_... or prod_yearly_....Source: Polar Dashboard → Products → Copy product ID (or use NEXT_PUBLIC_POLAR_PRODUCT_ID_*).
customerEmail
string
Prefills the email field in Polar Checkout. The pricing component passes the Supabase user’s email.
metadata
string
JSON string forwarded to Polar and later surfaced in webhook payloads. Sabo stores { "user_id": "uuid" } so the webhook can link the Polar subscription back to the Supabase user.

Example Request

Because the endpoint performs a redirect, you typically build a URL and assign it to window.location.href:
const checkoutUrl = new URL('/api/checkout', window.location.origin);
checkoutUrl.searchParams.set('products', planId);
checkoutUrl.searchParams.set('customerEmail', user.email ?? '');
checkoutUrl.searchParams.set('metadata', JSON.stringify({ user_id: user.id }));

window.location.href = checkoutUrl.toString();

Example Response

HTTP/1.1 307 Temporary Redirect
Location: https://polar.sh/checkout/checkout_123...
Polar returns 307/308 redirects rather than JSON payloads. The browser navigates to the hosted checkout UI automatically.

How It Works

1

Proxy to Polar

@polar-sh/nextjs generates a handler that validates query parameters, injects your POLAR_ACCESS_TOKEN, and negotiates the redirect with Polar.
2

Success redirect

successUrl is set to ${NEXT_PUBLIC_SITE_URL}/success. After payment, Polar sends the buyer there. The success page is purely informational—the webhook activates the subscription.
3

Webhook metadata

The metadata string travels with the subscription so /api/webhooks/polar can read user_id and associate the subscription + orders with the right Supabase user.

Implementation

The default boilerplate uses "sandbox" for safety during development:
src/app/api/checkout/route.ts
import { Checkout } from "@polar-sh/nextjs";

export const GET = Checkout({
  accessToken: process.env.POLAR_ACCESS_TOKEN!,
  successUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/success`,
  server: "sandbox",
});
Production-ready version: To switch dynamically based on environment, update the handler:
src/app/api/checkout/route.ts (environment-driven)
import { Checkout } from "@polar-sh/nextjs";

export const GET = Checkout({
  accessToken: process.env.POLAR_ACCESS_TOKEN!,
  successUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/success`,
  server: process.env.POLAR_SANDBOX === "true" ? "sandbox" : "production",
});
Then set POLAR_SANDBOX=false in production so Polar serves live checkout URLs.

Frontend Integration

The pricing component handles authentication, reads the plan IDs from src/lib/payments/plans.ts, and then calls the endpoint:
src/components/marketing/pricing.tsx (excerpt)
const handleCheckout = async (productId: string | null) => {
  if (!productId) return;

  const {
    data: { user },
  } = await supabase.auth.getUser();
  if (!user) {
    sessionStorage.setItem('signin-redirect-to', '/pricing');
    router.push('/sign-in');
    return;
  }

  const checkoutUrl = new URL('/api/checkout', window.location.origin);
  checkoutUrl.searchParams.set('products', productId);
  checkoutUrl.searchParams.set('customerEmail', user.email ?? '');
  checkoutUrl.searchParams.set(
    'metadata',
    JSON.stringify({ user_id: user.id })
  );

  window.location.href = checkoutUrl.toString();
};

Testing

  1. Set POLAR_SANDBOX=true and ensure ngrok/HTTPS tunneling is running for webhooks.
  2. Sign in, visit /pricing, and click Upgrade to Pro.
  3. Complete checkout using 4242 4242 4242 4242.
  4. Confirm /success loads and webhook logs show subscription.created followed by order.paid.
Do not rely on the success page alone. Always keep /api/webhooks/polar configured so database records stay in sync even if the buyer closes the browser early.