PilotFoxDocs

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:

ScopeNeeded for
orders:readRequired: the payments
refunds:readRequired: refunds
subscriptions:readOptional: the subscription metrics
organizations:readOptional: 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 has subscriptions:read and 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

CountedNot counted
Paid orders of one-time purchasesPending orders, until they are paid
The first order of a subscription and every renewalVoid orders, and draft orders not charged yet
Plan changes and usage charges that Polar bills as ordersOrders 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_refunded or refunded.
  • 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).

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.

server.js (Node.js, @polar-sh/sdk)
// 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

FieldUsed as
metadata.pilotfox_refCheckout reference, any format
metadata.reference_idCheckout 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_idUser id
checkout_idReturn-page id, matched up to an hour after the order
customer.emailCustomer email
customer_idSame-customer link for later orders
subscription_idThe subscription link of renewals

Metadata values are read as strings, or as whole numbers (a numeric user id), up to 500 characters.

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-Version header). 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/sdk package; check its documentation for the current method names.

On this page