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
accessToken?string
Organization access token (polar_oat_…). Sent as a Bearer credential.
stringserver?'production' | 'sandbox'
Selects api.polar.sh or sandbox-api.polar.sh.
'production' | 'sandbox'productionbaseUrl?string
Overrides server; used verbatim. Polar collection paths keep a trailing slash (/v1/checkouts/), so point it at a host, not a rewritten path.
stringfetch?typeof fetch
Custom fetch implementation.
typeof fetchAuthentication
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 })throwsunsupported(checkoutExpiresAt: false).Checkout.expiresAtis still populated when you read a checkout back. - Pause is period-end only.
subscriptions.pause({ behavior: 'immediately' })throwsunsupported, and aresumesAtmust fall after the current period end. - No
noneproration.next_perioddefers the plan change itself andresetrestarts 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
quantityother than1throwsunsupported. customAmountapplies 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 ownminimum_amount/maximum_amount.- No idempotency keys, apart from event ingestion — retrying a
checkouts.createmay 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.trialDayscovers day- and week-based trials only. Month- and year-based trials have no exact day count and are leftundefined— readraw.
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:
organizationId?string
Required on all three calls. It scopes the check server-side and is a public identifier, safe to ship inside an application.
stringserver?'production' | 'sandbox'
Same hosts as the factory.
'production' | 'sandbox'productionbaseUrl?string
Overrides server; used verbatim.
string- These calls must be unauthenticated. Polar’s customer-portal license routes answer
401 invalid_tokenwhen anyAuthorizationheader is present, which is why they are standalone subpath exports;organizationIdtakes 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 thanforbidden. - The
license.issuedwebhook carries no key. Polar’sbenefit_grant.createdincludes only the maskeddisplay_key, soevent.licenseKeyisundefined;event.licenseKeyIdis always set — read the real key withclient.licenseKeys.get({ id: event.licenseKeyId }). LicenseKey.productIdis never set. A Polar key hangs off a benefit, not a product —rawcarriesbenefit_id, whichproducts.getdoes not accept.activationCountis only populated bylicenseKeys.get; list and update responses carry no activations.update({ disabled: true })writes Polar’sdisabledstatus, notrevoked.revokedbelongs to the benefit lifecycle and flips back tograntedon the next grant cycle.disabled: falsewritesgranted.
Orders
See Orders for the model. Polar specifics:
- Invoice URLs live for 10 minutes.
orders.getInvoiceUrlreturns 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 asynchronous202job that the SDK deliberately does not trigger or poll, so an order whose invoice was never generated throws instead of blocking.
Provider notes
Price.checkoutRefis the product ID, not a price ID — Polar checkouts takeproducts: string[].- Resume takes effect immediately.
subscriptions.resumestarts 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 throughcustomers.update, fails withRevenueError { code: 'validation' }when the address is already taken. external_customer_idlinks your own user IDs to Polar customers and lives on the raw payload. Event ingestion accepts it as an alternative customer key, butusage.reportalways sends the Polar customer ID — external keying needs Polar’s native API.- Usage events are deduplicated permanently.
idempotencyKeybecomes the event’sexternal_id, which Polar enforces with a permanent unique index, so replaying the same key is safe. A backdatedtimestampis accepted, but events are attributed to billing periods by receipt time and Polar never issues a retroactive invoice — backdating affects reporting only.metadatais 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.meterswith 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.