Build Mac AppDocsMenu

Features

Provider research: Polar

The worked example for docs/payments/provider-research.template.md. The adapter built from it is site/lib/payments/polar/adapter.ts.

Read on: 2026-09-25. API version: 2026-04 (https://polar.sh/docs/api-reference).

#QuestionAnswerSource
1Merchant of record?Yes.https://polar.sh/docs/merchant-of-record/introduction
2Native licence keys?Yes, as the License Keys benefit on a product.https://polar.sh/docs/features/benefits/license-keys
3Activation limits?Yes, per benefit (properties.activations.limit). Set in the dashboard or the Benefits API.same; OpenAPI BenefitLicenseKeyActivationProperties
4Licence endpointsServer: POST /v1/license-keys/{activate,validate,deactivate}, organization token with license_keys:write, body needs organization_id. Public twins under /v1/customer-portal/license-keys/* (not used: the app talks only to our routes).OpenAPI 2026-04
5Refusal statusesvalidate: 404 for unknown, revoked, disabled, expired or wrong activation. activate: 403 NotPermitted for revoked / expired / at the limit; 404 unknown. deactivate: 404 = already gone.OpenAPI response descriptions
6Credential statuses401 for a missing or wrong token. A token lacking a scope can be a 403, but without error: "NotPermitted" — so the adapter reads the error code, not just the status.OpenAPI Unauthorized, NotPermitted
7Webhook signatureStandard Webhooks: webhook-id, webhook-timestamp, webhook-signature (v1,<base64>), HMAC-SHA256 over id.timestamp.body. Secrets from 2026-09-08 on: key = base64 decode of what follows whsec_; older: key = UTF-8 of the whole string. Replay window: ours, 5 minutes.https://polar.sh/docs/integrate/webhooks/delivery
8Webhook eventsorder.paid (payment received; also each subscription renewal); order.refunded (status refunded or partially_refunded). No dispute event.https://polar.sh/docs/integrate/webhooks/events
9Test vs liveSeparate hosts: api.polar.sh / sandbox-api.polar.sh; tokens look alike, so POLAR_SERVER=sandbox states it. Payloads carry no mode; a sandbox delivery fails a production secret.https://polar.sh/docs/integrate/sandbox
10Checkout linksPersistent Checkout Links; query accepts product_id, customer_email, reference_id, utm_* — no arbitrary metadata. Success URL supports checkout_id={CHECKOUT_ID}.https://polar.sh/docs/features/checkout/links
11Return verificationUnsigned; look up GET /v1/checkouts/{id} and require status: "succeeded" and our organization.OpenAPI CheckoutStatus
12Recovery by emailGET /v1/customers/?organization_id&email → GET /v1/customers/{id}/state → granted_benefits[].properties.license_key_id → GET /v1/license-keys/{id}.OpenAPI
13PricesGET /v1/products/{id} → prices[] with amount_type: "fixed", price_amount (cents), price_currency.OpenAPI ProductPriceFixed
14Custom fieldsYes (Custom Fields, attached per product/checkout).https://polar.sh/docs/features/custom-fields
15Refund behaviourFull refunds revoke benefits by default; Polar may refund within 60 days to pre-empt chargebacks.https://polar.sh/docs/features/refunds
16Legal namePolar Software, Inc.https://polar.sh/legal/terms

The path this puts us on

  • Adapter only.

Differences from the contract worth recording

  • Refusals are unambiguous (no prose matching), except the 403 scope case.
  • The return URL is unsigned: a server lookup is the proof.
  • Links take no metadata; attribution rides on checkout_id (DataFast reads it).
  • No dispute event; chargebacks surface as refunds.
  • Environment is stated (POLAR_SERVER), not derived.

This page is docs/payments/providers/polar.md in the repository, copied 2026-09-25.