Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
On this page

Polar

Configure the Polar provider — organization access token, the separate sandbox host, webhook signing secret, limitations, and the traps worth knowing.

Polar is a merchant of record for digital products. Import the factory from revenue-sdk/polar.

import { createClient } from 'revenue-sdk';
import { polar } from 'revenue-sdk/polar';

const client = createClient({
  provider: polar({ accessToken: process.env.POLAR_ACCESS_TOKEN! }),
});

Factory options

PropType
accessToken?string

Organization access token (polar_oat_…). Sent as a Bearer credential.

Typestring
server?'production' | 'sandbox'

Selects api.polar.sh or sandbox-api.polar.sh.

Type'production' | 'sandbox'
Defaultproduction
baseUrl?string

Overrides server; used verbatim. Polar collection paths keep a trailing slash (/v1/checkouts/), so point it at a host, not a rewritten path.

Typestring
fetch?typeof fetch

Custom fetch implementation.

Typetypeof fetch

Authentication

Create an organization access token in the Polar dashboard under Settings → Developers → Access tokens. Organization tokens are scoped to a single organization, which is why the factory needs no organization ID. Grant the token the scopes for what you use: products, checkouts, customers, subscriptions, and customer sessions.

Sandbox & test mode

Polar’s sandbox is a separate environment on a separate host (sandbox-api.polar.sh) with its own organization, its own products, and its own tokens — a production token will not authenticate against it.

polar({
  accessToken: process.env.POLAR_SANDBOX_ACCESS_TOKEN!,
  server: 'sandbox',
});

Limitations

  • No checkout expiry override. Polar fixes it at 24 hours server-side, so checkouts.create({ expiresAt }) throws unsupported (checkoutExpiresAt: false). Checkout.expiresAt is still populated when you read a checkout back.
  • Pause is period-end only. subscriptions.pause({ behavior: 'immediately' }) throws unsupported, and a resumesAt must fall after the current period end.
  • No none proration. next_period defers the plan change itself and reset restarts the billing anchor — neither means “switch now, bill nothing extra”, so the SDK refuses rather than pick a lookalike.
  • No item quantities. A checkout item or a plan change with quantity other than 1 throws unsupported.
  • customAmount applies to pay-what-you-want prices only. Polar ignores it for fixed and free prices rather than failing, and the accepted range comes from the price’s own minimum_amount/maximum_amount.
  • No idempotency keys, apart from event ingestion — retrying a checkouts.create may create a second checkout. That is why the client only replays a write Polar rejected with a rate limit, and never one that failed in transport.
  • Price.trialDays covers day- and week-based trials only. Month- and year-based trials have no exact day count and are left undefined — read raw.

Full values for every capability: capability matrix.

Webhooks

Create the endpoint in the Polar dashboard under Settings → Webhooks, choose the Raw payload format, and copy the generated signing secret. Verification and parsing come from the subpath:

import { parseWebhookEvent, verifyWebhook } from 'revenue-sdk/polar';

See Handle webhooks for the full handler.

Events worth subscribing to: subscription.created, subscription.updated, subscription.active, subscription.canceled, subscription.uncanceled, subscription.past_due, subscription.paused, subscription.resumed, subscription.revoked, subscription.cycled, order.paid, checkout.updated, and — if you sell license keys — benefit_grant.created.

Polar is the only provider that names all five transitions subscriptionChange covers — see Webhook events for the mapping and Webhooks for the dedupe key.

License keys

Validating, activating, and deactivating a key needs no credential, so those three functions are standalone exports rather than client methods — safe to ship inside a desktop, mobile, or CLI app. See License keys for the shared signatures and return types.

Polar-specific options — the rest (key, activationId, label, fetch, signal) are the same on every provider:

PropType
organizationId?string

Required on all three calls. It scopes the check server-side and is a public identifier, safe to ship inside an application.

Typestring
server?'production' | 'sandbox'

Same hosts as the factory.

Type'production' | 'sandbox'
Defaultproduction
baseUrl?string

Overrides server; used verbatim.

Typestring
  • These calls must be unauthenticated. Polar’s customer-portal license routes answer 401 invalid_token when any Authorization header is present, which is why they are standalone subpath exports; organizationId takes the place of the credential.
  • A 404 from validate means every kind of rejection — unknown key, revoked, disabled, expired, or an activation that doesn’t match — and comes back as valid: false. Any other error status still throws.
  • Activate returns 403 both when the activation limit is reached and when the key has no limit configured at all, so the SDK maps it to RevenueError { code: 'validation' } rather than forbidden.
  • The license.issued webhook carries no key. Polar’s benefit_grant.created includes only the masked display_key, so event.licenseKey is undefined; event.licenseKeyId is always set — read the real key with client.licenseKeys.get({ id: event.licenseKeyId }).
  • LicenseKey.productId is never set. A Polar key hangs off a benefit, not a product — raw carries benefit_id, which products.get does not accept. activationCount is only populated by licenseKeys.get; list and update responses carry no activations.
  • update({ disabled: true }) writes Polar’s disabled status, not revoked. revoked belongs to the benefit lifecycle and flips back to granted on the next grant cycle. disabled: false writes granted.

Orders

See Orders for the model. Polar specifics:

  • Invoice URLs live for 10 minutes. orders.getInvoiceUrl returns an S3 presign valid for 600 seconds — mint one per click, never store it.
  • A missing invoice is a not_found. Polar generates invoices through an asynchronous 202 job that the SDK deliberately does not trigger or poll, so an order whose invoice was never generated throws instead of blocking.

Provider notes

  • Price.checkoutRef is the product ID, not a price ID — Polar checkouts take products: string[].
  • Resume takes effect immediately. subscriptions.resume starts a new billing period and charges the customer — it is not a “continue where we left off” operation.
  • A customer email must be unique within the organization. customers.create, and an email change through customers.update, fails with RevenueError { code: 'validation' } when the address is already taken.
  • external_customer_id links your own user IDs to Polar customers and lives on the raw payload. Event ingestion accepts it as an alternative customer key, but usage.report always sends the Polar customer ID — external keying needs Polar’s native API.
  • Usage events are deduplicated permanently. idempotencyKey becomes the event’s external_id, which Polar enforces with a permanent unique index, so replaying the same key is safe. A backdated timestamp is accepted, but events are attributed to billing periods by receipt time and Polar never issues a retroactive invoice — backdating affects reporting only. metadata is capped at 50 pairs, keys at 40 characters, and string values at 500. See Usage-based billing.
  • Meters are returned inline on the subscription. Polar is the only provider that populates Subscription.meters with the current period’s consumed units, credited units, and accrued amount.

For an end-to-end walkthrough — token setup, products, checkout, subscriptions, portal, and webhooks — see How to Use the Polar API from TypeScript.

Last updated on August 8, 2026

Was this page helpful?