Revenue overview
Connect a payment provider with a read-only key, see which payments count, and read revenue and subscription metrics.
PilotFox reads a site's payments from Stripe, Dodo Payments or Polar with a read-only key you give it, links each payment to the visit that led to it, and reports revenue next to the traffic: by channel, referrer, campaign, landing page, country, device and every other dimension, with conversion rates. On any other billing system, your server records each payment through the payments API instead.
Stripe
A restricted key with Read access. Checkout Sessions, Payment Links and subscription invoices.
Dodo Payments
An API key with Read access to Payments and Refunds. Checkout sessions, payment links, renewals.
Polar
An Organization Access Token with read scopes. Orders, Checkout Links, subscriptions.
Payments API
Paddle, Lemon Squeezy, Gumroad, crypto or your own billing: record payments from your server.
Attribution
How a payment is linked to a visit, and which of the buyer's visits gets the credit.
Connect a payment provider
A site can connect one account of each provider, and several providers at once: their payments add up in the same reports.
Create a read-only key at the provider. Each provider's page names the exact menu and the access the key needs: Stripe, Dodo Payments, Polar.
Paste it in PilotFox. Open Site settings → Revenue, choose the provider, paste the key and choose how many days of
past payments to import: 1 to 365, 90 by default. This needs the site:update permission (the editor role and
above).
PilotFox checks the key before it saves it. It makes a read request for each access the key needs. A rejected
key is refused with 400 and a message saying so, a missing permission with 400 and a message that names it; a
provider that cannot be reached gives 503. Nothing is stored then.
The key is stored encrypted and never returned. The settings (and the API) show only a hint, the key's type prefix
when the provider has one and its last four characters (for example rk_live_…wxyz), and whether it is a live or a
test key. A test key imports test-mode payments only.
The same works over the API with PUT /api/v1/sites/{siteId}/integrations/{provider}, where {provider} is
stripe, dodo or polar:
curl -X PUT "https://pilotfox.app/api/v1/sites/SITE_ID/integrations/stripe" \
-H "Authorization: Bearer $PILOTFOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"secretKey": "rk_live_...", "backfillDays": 90}'Self-hosted instances use their own PUBLIC_URL instead of https://pilotfox.app in every example.
How syncing works
PilotFox only reads: no webhook is needed and nothing is written to your provider account.
- It first imports the chosen history, then checks the provider for new payments and refunds. While it is still catching up it runs every minute; once caught up it waits 10 minutes between checks for Stripe and 5 minutes for Dodo Payments and Polar, to stay well inside the read allowance each provider gives your account (which your own integration shares).
- Sync now (
POST /api/v1/sites/{siteId}/integrations/{provider}/sync) asks for a check right away. It also restarts a connection that stopped because the provider rejected its key. - Match again (
POST /api/v1/sites/{siteId}/integrations/{provider}/rematch) imports the connection's whole history window again with the same key and links every payment with today's data. Use it after you start callingidentify, sending thecheckoutevent or sending your users' emails from your server, so payments imported before are linked too. Reports keep their numbers meanwhile; links improve as the import goes.
Both answer 202 with {"data": {"queued": true}}, and 404 when the provider is not connected.
What counts as a payment
- Money the customer actually paid, with an amount above zero. Unpaid, failed, cancelled or pending payments and free checkouts are not counted. Each provider's page lists exactly which of its payments count.
- Full and partial refunds are subtracted from the payment they refund. A refund counts against the payment's date, not the refund's.
- A payment's time is when it was paid, as near as the provider says: each provider's page names the timestamp it uses (Dodo Payments and Polar only give the time the payment or order was created). Amounts include tax, as the customer paid them.
- Amounts are kept in the payment's own currency, as integers in the currency's smallest unit (
4900is 49.00 USD,1200is 1,200 JPY). Reports show one currency at a time, with no conversion: the site's default currency (Site settings → Revenue;usduntil you change it), or the one a report asks for withcurrency=. - Payments that could not be linked to a visit still count in the totals (
unattributed_paymentssays how many). When a report is filtered or segmented, a payment counts only when the visit it is credited to matches (see credit models).
The numbers
| Field | Definition |
|---|---|
gross | Sum of the amounts paid. |
refunded | Sum of the amounts refunded. |
revenue | gross − refunded. |
payments | Number of payments. |
refunds | Number of payments with a refunded amount. |
aov | Average order value: gross ÷ payments, rounded. |
paying_visitors | Distinct people behind the payments linked to a visit. |
visitors | Visitors of the same period and filters. |
conversion_rate | paying_visitors ÷ visitors × 100. |
revenue_per_visitor | revenue ÷ visitors, in the smallest unit, not rounded. |
unattributed_payments | Payments linked to no visit. |
Every amount is in the report's currency and its smallest unit.
Subscription metrics
When the provider's subscriptions are synced (Stripe and Dodo Payments with Read access to Subscriptions, Polar with
the subscriptions:read scope), the Revenue page shows a Subscriptions section for the selected period and currency.
Filters and segments do not apply to it. The definitions are
Stripe's Billing analytics ones, for every provider:
- MRR (monthly recurring revenue): the plan price of every
activeandpast_duesubscription, normalized to a month (a yearly price ÷ 12, a weekly one × 52 ÷ 12, a daily one × 365 ÷ 12), at the end of the period or now. Trials, paused, unpaid and canceled subscriptions count 0. The plan price is the recurring price times the quantity, before tax and discounts; usage-based (metered) parts are left out. Each provider's page notes where its prices differ. - MRR movements in the period: new, expansion (upgrades), reactivation, contraction (downgrades) and churned (canceled, unpaid or paused). Net new MRR = new + expansion + reactivation − contraction − churned.
- Subscribers: customers with a paying subscription; a customer with several subscriptions counts once.
- Churn rate: subscribers who stopped paying in the 30 days to the end of the period ÷ (subscribers 30 days before
- new subscribers of those 30 days).
- ARPU: MRR ÷ subscribers. LTV: ARPU ÷ churn rate, shown once the churn rate is above 0.
- Trials: subscriptions in a free trial at the end of the period, and trials that started in it.
- MRR by source: the MRR at the end of the period (or now) by the channel, source or campaign of the visit each subscription is credited to: the visit its first linked payment is credited to under the credit model.
PilotFox keeps each subscription's history from the moment it is first synced. Starts, trial ends and cancellations come from the provider's own dates, so the history before you connected is rebuilt from them; earlier plan changes are not known.
Payments, journeys and customer emails
- The payments list (
GET /api/v1/sites/{siteId}/payments) shows each payment's amount, status (paid,partially_refunded,refunded), provider, how it was linked and the credited visit's source. It can be searched by payment id,utm_sourceor referrer domain. It is never cached. - A payment's journey (
GET /api/v1/sites/{siteId}/payments/{paymentId}/journey) lists the buyer's visits that started up to the payment, oldest first: the latest 50, from the 365 days before it (or further back for a renewal credited to an older visit), each with its pages (up to 200) and the credit models that credit it. A payment linked to no visit has none. - Store customer emails (Site settings → Revenue, off by default) keeps the email the provider has for each buyer with the payment, so the list can be searched by email. Turn it on only if your privacy policy covers it.
Who sees what:
| Data | Needs |
|---|---|
| Revenue reports, the payments list, subscription metrics, connection status | revenue:read (every organization role has it) |
A payment's session_id, visitor_id, user_id, credited_session_id and customer_email, search by email | revenue:read and sessions:read |
| A payment's journey | revenue:read and sessions:read (else 403) |
| Connecting, replacing or disconnecting a provider, Sync now, Match again | site:update |
Share links and public dashboards never see revenue.
Disconnecting or changing the key
- Disconnect (
DELETE /api/v1/sites/{siteId}/integrations/{provider}) deletes the provider's key and stops its syncing. Its imported payments (and subscriptions) stay in your reports unless you also delete them (?deletePayments=true); other providers' payments are never touched. - Replace key checks the new key the same way. If it cannot read the site's latest payment imported from that provider (a different account, or test instead of live), that provider's imported payments are deleted first, so accounts and modes never mix.
- Erasing a user's data keeps their payments in the totals but removes the link to the user, their sessions and their email.
API endpoints
All under /api/v1/sites/{siteId}. Revenue reports take the usual
time window, filters and segment, plus currency= and model= (a
credit model other than the site's).
| Endpoint | Permission | Reference |
|---|---|---|
GET /revenue/summary | revenue:read | summary |
GET /revenue/timeseries | revenue:read | timeseries |
GET /revenue/breakdown?dim= | revenue:read | breakdown |
GET /revenue/subscriptions/summary, /timeseries, /breakdown | revenue:read | summary, timeseries, breakdown |
GET /payments | revenue:read | list |
GET /payments/{paymentId}/journey | revenue:read and sessions:read | journey |
GET /revenue/connections | revenue:read | connections |
GET, PUT, DELETE /integrations/{provider} | revenue:read to read, site:update to change | Stripe, Dodo Payments, Polar |
POST /integrations/{provider}/sync, /rematch | site:update | sync, rematch |
POST /payments, POST /payments/{paymentId}/refund, DELETE /payments/{paymentId} | revenue:write, API keys only | Payments API |
curl "https://pilotfox.app/api/v1/sites/SITE_ID/revenue/summary?last=30d" \
-H "Authorization: Bearer $PILOTFOX_API_KEY"Revenue and subscription reports go through the query cache, so a new payment can take
up to its time to appear in them: 30 seconds by default (QUERY_CACHE_TTL_SECONDS on a self-hosted instance, at most
300), or at once with Cache-Control: no-cache from an organization member or an API key with site:read. The
payments list and journeys are never cached, so they show it at once.