Payments
Polar checkout, signed webhooks, and the three parts you cannot ship two of.
Payments are Polar.sh, acting as merchant of record so VAT and sales tax are their problem rather than yours.
The integration has three parts, and all three are required. Ship two and either money arrives with nobody credited, or users are charged and see no change. This is rule #7 on the rules page for that reason.
Checkout
POST /api/v1/checkout creates a session and returns { checkoutUrl }. The
body picks a plan:
{ "plan": "monthly" }monthly and annual map to POLAR_PRODUCT_ID_PRO_MONTHLY and
POLAR_PRODUCT_ID_PRO_ANNUAL. An unmapped plan fails with a BAD_REQUEST
naming the plan rather than creating a session Polar cannot fulfil.
The caller's userId goes into the checkout metadata and into
customer_external_id. Both, deliberately: that is the only thread connecting
a payment back to an account, and the webhook needs it to resolve who to
credit.
Webhook
POST /api/v1/webhooks/polar verifies the
Standard Webhooks signature with
POLAR_WEBHOOK_SECRET, then upserts a subscriptions row keyed on the Polar
subscription id and linked to the resolved user. It handles
subscription.created, subscription.updated and subscription.canceled.
Crediting is immediate. Nothing polls.
The signature check uses real secrets in development too, and disabling it "just to test locally" is how an unauthenticated endpoint that grants subscriptions reaches production. Use a tunnel instead: smee.io, ngrok or cloudflared, pointed at the API's dev port.
Gate
GET /api/v1/subscription/status returns the authenticated user's current
subscription, and the server enforces the same thing independently. Client
state decides what to render; it never decides what is allowed.
In practice you gate on entitlements rather than on a subscription row directly, because a plan is a set of capabilities and a subscription is a payment fact.
Unconfigured is a supported state
Without POLAR_ACCESS_TOKEN and the product ids, checkout is unavailable and
every user resolves to the free plan. Gates still work, the app still runs,
and you can build the whole product before opening a Polar account. See
getting started.
Grace, cancellation and the states in between
A subscription is not a boolean. packages/entitlements/src/plan.ts resolves:
{ plan, status, cancelAtPeriodEnd, currentPeriodEnd, inGracePeriod }A past_due subscription keeps its access for ENTITLEMENTS_GRACE_DAYS
(default 7) while the provider retries the charge. Locking somebody out on the
first decline turns a bank hiccup into a cancellation.
cancelAtPeriodEnd means a user who cancelled today still has paid time left.
Revoking immediately is a refund request waiting to happen.
Surface both in the UI. A user whose card failed should see a banner, and one who cancelled should see when access ends.
Testing the whole loop
The reference caller is apps/web/app/[locale]/pricing/page.tsx, and the
demo runs it against a live
Polar account. Polar's dashboard has a webhook tester for replaying events
without repeating a purchase, which is the fastest way to exercise
subscription.updated and subscription.canceled — the two you will otherwise
never see until a real customer hits them.