Polar
Connect Polar with an access token, which Polar orders count, and linking orders and checkout links to visits.
PilotFox reads a site's Polar orders and refunds with an Organization Access Token that has read scopes, and links
each order to the visit that led to it. Connect it in Site settings → Revenue → Polar, or with
PUT /api/v1/sites/{siteId}/integrations/polar. The
revenue overview explains connecting, syncing, Match again and disconnecting for every provider.
Create the access token
In the Polar dashboard, open Settings → Access Tokens for your organization and click Create token. Give it a name, an expiration date and these scopes:
| Scope | Needed for |
|---|---|
orders:read | Required: the payments |
refunds:read | Required: refunds |
subscriptions:read | Optional: the subscription metrics |
organizations:read | Optional: the settings show your organization's name |
The token starts with polar_oat_; only Organization Access Tokens are accepted. PilotFox lists orders and refunds
once when you connect; a token without one of the required scopes is refused with a message that names it.
- Sandbox. Polar's sandbox (sandbox.polar.sh) is a separate server with its own tokens. A sandbox token works the same way and imports sandbox orders only: PilotFox tries the token on production first and the sandbox second, and the settings show a sandbox token as a test key.
- Subscriptions. Without
subscriptions:read, payments and renewals still sync and the connection shows a notice. To add the subscription metrics, create a token that also hassubscriptions:readand use Replace key. PilotFox also asks the connected token again every 6 hours, and at once on Sync now. - Expiry. A Polar token stops working on the expiration date you chose, or when it is revoked (Polar also revokes a token reported as leaked). The sync then stops and the settings say the key was rejected: create a new token and use Replace key.
PilotFox checks Polar every 5 minutes once it has caught up, far below the rate limit Polar gives your organization, which your own integration shares.
Which Polar orders count
| Counted | Not counted |
|---|---|
| Paid orders of one-time purchases | Pending orders, until they are paid |
| The first order of a subscription and every renewal | Void orders, and draft orders not charged yet |
| Plan changes and usage charges that Polar bills as orders | Orders of 0 (free products, 100% discounts, totals covered by a credit) |
| Full and partial refunds, tax included |
- An order counts when its status is
paid,partially_refundedorrefunded. - The amount is the order's total with tax, adjusted by the customer's balance applied to it (less a credit, plus an earlier amount owed): what the customer was charged. Refunds count with their tax, never more than the amount.
- A Polar payment's time is when the order was created, so a renewal paid after a failed attempt keeps its renewal date. Orders still pending, for example a renewal Polar is retrying, are checked again until they are paid or given up (at most 45 days).
Link Polar orders to visits
When your server creates the checkout, put the checkout reference
in metadata.pilotfox_ref, in any format: Polar copies the checkout's metadata to the order. For a signed-in buyer,
set the checkout's external_customer_id to the id you pass to identify, or put that id in
metadata.pilotfox_user_id. Renewals keep their subscription's metadata and customer, so they are linked to the same
user.
// With a token that can create checkouts (not the read-only one you gave PilotFox):
import { createPolar } from "@polar-sh/sdk/2026-10";
const polar = createPolar({ accessToken: process.env.POLAR_ACCESS_TOKEN });
app.post("/create-checkout", async (req, res) => {
const ref = String(req.body.ref ?? "");
const checkout = await polar.checkouts.create({
products: ["<your product id>"],
success_url: "https://example.com/thanks?checkout_id={CHECKOUT_ID}",
metadata: ref ? { pilotfox_ref: ref.slice(0, 500) } : {}, // Polar refuses an empty value
// A signed-in buyer: external_customer_id: user.id,
});
res.json({ url: checkout.url });
});The checkout_id={CHECKOUT_ID} in the success URL is optional: it lets the script link the order from your
return page when nothing else does.
What PilotFox reads from a Polar order
| Field | Used as |
|---|---|
metadata.pilotfox_ref | Checkout reference, any format |
metadata.reference_id | Checkout reference only in the PilotFox format (pf and digits); any other value is a user id |
metadata.pilotfox_user_id, then the customer's external_id | User id |
checkout_id | Return-page id, matched up to an hour after the order |
customer.email | Customer email |
customer_id | Same-customer link for later orders |
subscription_id | The subscription link of renewals |
Metadata values are read as strings, or as whole numbers (a numeric user id), up to 500 characters.
Polar Checkout Links
With Payment links and return pages on (the default), the script adds a fresh PilotFox reference to Checkout Links
as they are clicked (links to https://buy.polar.sh/, and the API's https://api.polar.sh/v1/checkout-links/…
form), in their reference_id parameter, and sends the matching checkout event
(payment links).
To do it yourself: a Checkout Link accepts the reference as its reference_id URL parameter, which Polar adds to the
checkout's metadata. PilotFox reads it as a reference only in the PilotFox format, pf and digits, and as a user id
otherwise (references and your own ids):
// Mark your checkout link anchors, e.g. <a href="…" data-checkout-link>:
document.querySelectorAll("a[data-checkout-link]").forEach((a) => {
a.addEventListener("click", () => {
const ref = "pf" + crypto.getRandomValues(new Uint32Array(4)).join(""); // a PilotFox reference
window.pilotfox?.event("checkout", { ref });
const url = new URL(a.href);
url.searchParams.set("reference_id", ref);
a.href = url.toString();
});
});Polar subscriptions
Each renewal is its own order and counts as a payment. A subscription's renewals are credited to the visit of its first linked order, so a renewal months later never needs a reference of its own; a later order of the same Polar customer that nothing else links is credited like their earlier one.
With subscriptions:read, PilotFox reads every subscription once, then checks them again every hour (or when you
click Sync now): new ones, plan changes, trials, cancellations and ends show in the subscription metrics within
the hour. Polar's statuses are used as they are (incomplete_expired counts as incomplete). The plan price is the
subscription's price before its discount, with its seats and units; usage-based parts are left out.
Known limits
- A subscription's price is as listed in Polar: a tax-inclusive price (Polar's default outside the United States, Canada and India) counts in MRR with its tax, since Polar gives no net price for a subscription.
- If Polar takes back a refund after it succeeded (the refund later fails or is cancelled), PilotFox only notices when it reads that order again, for example after another refund of it.
- PilotFox uses version 2026-10 of Polar's API (the
Polar-Versionheader). If Polar ever stops accepting it, the settings say so and a PilotFox update is needed. - The Node.js snippet uses Polar's own
@polar-sh/sdkpackage; check its documentation for the current method names.