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).
| # | Question | Answer | Source |
|---|---|---|---|
| 1 | Merchant of record? | Yes. | https://polar.sh/docs/merchant-of-record/introduction |
| 2 | Native licence keys? | Yes, as the License Keys benefit on a product. | https://polar.sh/docs/features/benefits/license-keys |
| 3 | Activation limits? | Yes, per benefit (properties.activations.limit). Set in the dashboard or the Benefits API. | same; OpenAPI BenefitLicenseKeyActivationProperties |
| 4 | Licence endpoints | Server: 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 |
| 5 | Refusal statuses | validate: 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 |
| 6 | Credential statuses | 401 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 |
| 7 | Webhook signature | Standard 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 |
| 8 | Webhook events | order.paid (payment received; also each subscription renewal); order.refunded (status refunded or partially_refunded). No dispute event. | https://polar.sh/docs/integrate/webhooks/events |
| 9 | Test vs live | Separate 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 |
| 10 | Checkout links | Persistent 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 |
| 11 | Return verification | Unsigned; look up GET /v1/checkouts/{id} and require status: "succeeded" and our organization. | OpenAPI CheckoutStatus |
| 12 | Recovery by email | GET /v1/customers/?organization_id&email → GET /v1/customers/{id}/state → granted_benefits[].properties.license_key_id → GET /v1/license-keys/{id}. | OpenAPI |
| 13 | Prices | GET /v1/products/{id} → prices[] with amount_type: "fixed", price_amount (cents), price_currency. | OpenAPI ProductPriceFixed |
| 14 | Custom fields | Yes (Custom Fields, attached per product/checkout). | https://polar.sh/docs/features/custom-fields |
| 15 | Refund behaviour | Full refunds revoke benefits by default; Polar may refund within 60 days to pre-empt chargebacks. | https://polar.sh/docs/features/refunds |
| 16 | Legal name | Polar 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.