This endpoint receives Polar subscription/order lifecycle events and keeps
user_subscriptions and payment_history in sync. It must be publicly accessible and secured with POLAR_WEBHOOK_SECRET.- Endpoint:
POST /api/webhooks/polar - File Location:
src/app/api/webhooks/polar/route.ts - Authentication: Polar webhook signature (
POLAR_WEBHOOK_SECRET) - Raw body: Next.js automatically provides the raw request body to
@polar-sh/nextjs
Supported Events
| Event | Purpose |
|---|---|
subscription.created | Upsert subscription row with product/billing info |
subscription.updated | Keep status, plan name, cancellation dates in sync |
subscription.active | Force status back to active after payment |
subscription.canceled | Record cancel_at, cancel_at_period_end, canceled_at |
subscription.revoked | Immediate cancellation |
order.created | Logged for observability (no DB write) |
order.paid | Insert payment record + fetch invoice URL |
order.refunded | Insert negative payment record and mark as refunded |
Events are forwarded as soon as Polar receives an update. Always keep this endpoint live—even if the user closes the checkout success page, webhooks complete the subscription sync.
Implementation Highlights
Metadata fallback
Metadata fallback
The handler first looks for
subscription.metadata.user_id, falling back to subscription.customer.metadata.user_id. This guards against legacy customers that do not include metadata on the subscription itself.Invoice helper
Invoice helper
For
order.paid / order.refunded, the helper calls POST /v1/orders/{id}/invoice followed by GET /v1/orders/{id}/invoice to fetch a hosted invoice URL. The link is stored in payment_history.invoice_url and surfaced in the billing UI.Service client + envs
Service client + envs
createServiceClient() uses SUPABASE_SECRET_KEY to bypass Row Level Security. API base URLs respect POLAR_SANDBOX; set it to false in production.Code (abridged)
src/app/api/webhooks/polar/route.ts (excerpt)
Testing
1
Expose local server
Run
ngrok http 3000 (or similar) and configure the URL in Polar Dashboard → Settings → Webhooks. Copy the signing secret to POLAR_WEBHOOK_SECRET.2
Trigger events
Complete a sandbox checkout or use Polar’s dashboard to simulate subscription updates. Watch your terminal—the handler logs every event (
Subscription created, Order paid, etc.).3
Verify database
Check
user_subscriptions and payment_history in Supabase to confirm new rows were inserted/updated.Troubleshooting
No user ID found
No user ID found
Ensure checkout metadata includes
user_id. The pricing component does this automatically—if you customize it, keep the metadata intact.Invoice URL null
Invoice URL null
Polar may return
409 if an invoice already exists. The helper handles this, but if invoice_url is still null, try re-fetching after a short delay.401 or 400 from Supabase
401 or 400 from Supabase
Verify
SUPABASE_SECRET_KEY is present so the service client can bypass RLS. Missing keys cause insert/update failures.