Tutorials / Backend integrations / Take payments with Polar
πŸ“ Written ● Intermediate Updated 2026-05-13

Take payments with Polar (merchant of record)

Polar handles sales tax, VAT, GST, and chargebacks for you. Stripe doesn't. If you're shipping a global SaaS solo and don't want to register in 50 jurisdictions, this is the trade-off worth knowing.

Why merchant-of-record matters

0
  • Stripe processes the card and hands you the money. You are the merchant β€” you owe sales tax in California, VAT in Germany, GST in India, etc. EU VAT alone has 27 country-specific rates.
  • Polar (and Lemon Squeezy, Paddle) are merchants of record β€” they sell the product to your customer, you sell to them. They handle every tax compliance obligation; you get a single payout in your currency.
  • Cost of MoR β€” ~5–6% per transaction vs. Stripe's ~2.9% + 30Β’. You're paying for tax-compliance-as-a-service. For an indie SaaS doing <$1M/yr globally, this is the right trade.
  • Pick Polar when β€” you want the most developer-first MoR (open source, GitHub-style auth, ergonomic APIs). Pick Lemon Squeezy if you want more polish; Paddle if you want enterprise features.
  • Sign up β€” polar.sh/login. GitHub login. Approval takes 1–3 business days (KYC).

Create a product

1

Dashboard β†’ Products β†’ New product. Pick:

  • Type β€” one-time (license, lifetime deal) or recurring (subscription)
  • Billing interval β€” monthly, yearly, both
  • Price β€” USD; Polar converts at checkout
  • Benefits β€” Polar's killer feature. Attach automated entitlements: GitHub repo access, Discord role, file download, license key, ad-hoc webhook. Triggered automatically on successful purchase + revoked on cancellation.

Save. Copy the product_id β€” you'll need it for the checkout URL.

Get your API keys

2

Settings β†’ API Keys β†’ Create API key. Pick scopes:

  • checkouts:write β€” to create checkout sessions
  • customers:read β€” to query customer state
  • subscriptions:read β€” to check entitlement

Add to .env:

POLAR_ACCESS_TOKEN=polar_oat_…
POLAR_WEBHOOK_SECRET=polar_whs_…  # from next step
POLAR_PRODUCT_ID=prod_…           # from step 1
Sandbox vs production. Polar has a sandbox at sandbox.polar.sh β€” use it for development. The token prefixes differ; don't mix them.

Create a checkout from your app

3

Install the SDK:

npm install @polar-sh/sdk

Server route β€” when user clicks "Upgrade":

import { Polar } from "@polar-sh/sdk";

const polar = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN,
  server: "production", // or "sandbox"
});

// POST /api/upgrade
app.post("/api/upgrade", async (req, res) => {
  const checkout = await polar.checkouts.create({
    products: [process.env.POLAR_PRODUCT_ID],
    successUrl: "https://yourapp.com/welcome?checkout_id={CHECKOUT_ID}",
    customerEmail: req.user.email,
    metadata: { userId: req.user.id },   // critical β€” see step 5
  });
  res.json({ url: checkout.url });
});

Client redirects to checkout.url. Polar handles card entry, tax calculation per the customer's country, receipt email.

Alternative: Polar Checkout Links β€” no API, just a hosted URL like polar.sh/checkout/abc. Drop into a button. Great for landing pages.

Wire up webhooks for fulfillment

4

Polar fires events on your server when state changes. Don't trust the success-URL redirect β€” users close tabs, cards retry async.

Dashboard β†’ Settings β†’ Webhooks β†’ Add endpoint:

  • URL β€” https://yourapp.com/api/webhooks/polar
  • Events β€” subscription.created, subscription.updated, subscription.canceled, order.created, order.refunded
  • Copy the signing secret into POLAR_WEBHOOK_SECRET

Verify + handle:

import { validateEvent } from "@polar-sh/sdk/webhooks";

app.post(
  "/api/webhooks/polar",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    let event;
    try {
      event = validateEvent(
        req.body,
        req.headers,
        process.env.POLAR_WEBHOOK_SECRET,
      );
    } catch {
      return res.status(401).send("bad signature");
    }

    switch (event.type) {
      case "subscription.created":
      case "subscription.updated": {
        const userId = event.data.metadata.userId;
        await db.users.update(userId, {
          plan: event.data.product.name,
          polarSubscriptionId: event.data.id,
          subscriptionStatus: event.data.status, // active | past_due | …
        });
        break;
      }
      case "subscription.canceled": {
        const userId = event.data.metadata.userId;
        await db.users.update(userId, { plan: "free" });
        break;
      }
    }
    res.json({ ok: true });
  },
);

Same shape as the Stripe webhooks tutorial. Same rules apply: respond 2xx fast, idempotent handler, signature verification before any DB write.

Test locally with the CLI

5

Polar doesn't have a webhook-forwarder CLI like Stripe's. Two options:

  • ngrok β€” ngrok http 3000, set the public URL as your sandbox webhook endpoint
  • Svix Play β€” captures + replays webhooks so you don't need to recreate test orders

Make a sandbox purchase with Polar's test card numbers β€” same as Stripe (4242 4242 4242 4242). Tail your server logs, verify the webhook arrived signed, verify DB updated.

Check entitlement on every request

6

Don't query Polar's API per request β€” too slow + rate-limited. Read from your DB:

// middleware
function requirePlan(...plans) {
  return (req, res, next) => {
    if (!plans.includes(req.user.plan)) {
      return res.status(402).json({ error: "Upgrade required" });
    }
    next();
  };
}

app.post("/api/pro-only", requirePlan("pro", "max_pro"), handler);

The webhook keeps the DB in sync. Polar's server is authoritative; your DB is the cache.

Customer portal (manage subscription)

7

Don't build cancel/upgrade UI yourself. Polar hosts a Customer Portal.

app.post("/api/billing/portal", async (req, res) => {
  const session = await polar.customerPortal.sessions.create({
    customerId: req.user.polarCustomerId,
  });
  res.redirect(session.customerPortalUrl);
});

Users get: change card, upgrade/downgrade plan, cancel, view invoices, download receipts (with correct tax info). Polar handles proration.

Common failures

8
  • Test webhooks not firing β€” sandbox endpoints are configured separately from production. Two webhook endpoints to manage. Don't confuse them.
  • Metadata not flowing through β€” metadata on the checkout flows to the order and subscription objects, but you have to read it from event.data.metadata (not event.metadata). Easy to typo.
  • Higher fees than Stripe β€” yes, ~5%. The mental model: that 2% extra is your tax compliance team. EU VAT registration alone is ~$2K/yr per country.
  • Payouts in USD only β€” Polar pays out in USD via wire/ACH. If you need local-currency payouts, Paddle is more flexible (but more expensive and enterprise-y).
  • Some regions blocked β€” Polar can't accept payments from sanctioned countries. Same for all MoRs. If you need 100% global, you may need both Stripe and an MoR.

When NOT to use Polar

9
  • You already have entities + tax registrations β€” if you have a US LLC + a EU VAT registration + sales-tax software (TaxJar, Anrok), Stripe + your accountant is cheaper.
  • You're >$1M ARR and growing β€” at scale, the 2% MoR premium turns into real money. Migrate to Stripe + dedicated tax tooling.
  • You sell physical goods β€” Polar is digital-only (SaaS, downloads, licenses). For physical, use Shopify or Stripe.
  • B2B with invoicing/POs β€” Polar supports it but is less mature than Paddle for enterprise invoicing.

Pricing reality

10
  • Polar β€” 4% + 40Β’ per transaction. No monthly fee, no minimum.
  • Lemon Squeezy β€” 5% + 50Β’. Acquired by Stripe in 2024 β€” still operates standalone but future is uncertain.
  • Paddle β€” 5% + 50Β’ on standard; enterprise rates negotiable.
  • Stripe + DIY tax β€” 2.9% + 30Β’, plus your time + tax software (~$500/mo Anrok, ~$200/mo TaxJar entry tier) + accountant + filings.

Pricing pages: polar.sh/pricing, lemonsqueezy.com/pricing, paddle.com/pricing.

Official references

11
Trade-off framing. Stripe = lower fees, you handle compliance. MoR = higher fees, they handle compliance. Solo founder shipping globally? Pay the 2%. Series A with a finance team? Switch to Stripe.