# Revenue attribution

URL: https://pieowl.com/docs/revenue-attribution





[Revenue attribution](/features/revenue-attribution) shows which sources, campaigns, pages and countries make you money, not just which bring visits. You send each purchase to Pieowl, and Pieowl credits it to the visit where it happened.

This page covers how to send purchases, how they are credited, and where to find revenue in the dashboard. For step-by-step checkout flows with Stripe and other payment providers, see [Revenue tracking](/docs/revenue-tracking).

Send purchases [#send-purchases]

**Settings → Site → Revenue** in your dashboard has everything in one place: your currency, ready code to copy for your site or your server, and the last payment Pieowl received, so you can see it works.

Call `purchase()` when your site learns the payment went through. The SDKs send it as an event named `revenue`.

```js
pieowl.purchase({ id: 'A-1001', amount: 49.99, currency: 'EUR', plan: 'pro' });
```

* **`id`** is your order number. See [Order numbers](#order-numbers).
* **`amount`** is in normal units, like `49.99`, not cents. It must be zero or more.
* **`currency`** is an ISO 4217 code, like `EUR`. Leave it out and Pieowl uses your [site currency](#currency).
* Add any other properties you want to break revenue down by, like `plan` or `product`.

From your payment provider, no code [#from-your-payment-provider-no-code]

Stripe, Paddle, Lemon Squeezy and Polar can send your payments to Pieowl by themselves. In Pieowl, open **Settings → Site → Revenue → Your payment provider** and pick yours. You get your own webhook address and the steps for that provider:

| Provider      | Where to add the webhook                                                     | Events                                                                                                                     | Secret                                              |
| ------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| Stripe        | **Workbench → Webhooks → Create an event destination**, payload **Snapshot** | `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `invoice.paid`, `charge.refunded`                | Copy the **Signing secret** (`whsec_…`) into Pieowl |
| Paddle        | **Events → Notifications → New destination**, type **URL**                   | `transaction.completed`, `adjustment.created`, `adjustment.updated`                                                        | Copy the destination's **Secret key** into Pieowl   |
| Lemon Squeezy | **Settings → Webhooks → +**                                                  | `order_created`, `order_refunded`, `subscription_created`, `subscription_payment_success`, `subscription_payment_refunded` | Paste the secret Pieowl shows into Lemon Squeezy    |
| Polar         | **Settings → Webhooks → Add Endpoint**, format **Raw**                       | `order.paid`, `refund.created`, `refund.updated`                                                                           | Copy the endpoint's secret (`whsec_…`) into Pieowl  |

From then on:

* One-off payments, subscription payments and renewals are counted. Each event counts once, even when the provider sends it again.
* Refunds are subtracted from the visit the order was paid in.
* Test-mode payments show that the connection works, but they aren't counted.
* The card shows when the provider last sent an event, and what to fix if something is wrong (for example, a wrong secret).

**Who paid.** The Pieowl script adds the visitor to checkout links by itself: Stripe payment links and buy buttons, Lemon Squeezy and Polar checkout links, and Paddle.js buttons. If your own code creates the checkout, pass the visitor yourself:

* Stripe: `client_reference_id: pieowl.getDeviceId()` (or `metadata.pieowl_device_id`)
* Paddle: `customData: { pieowl_device_id: pieowl.getDeviceId() }`
* Lemon Squeezy: `checkout_data.custom.pieowl_device_id`
* Polar: `metadata.pieowl_device_id`

Without it the payment counts in your total, without a source. If you also send `purchase()` from your site for a Stripe payment, use the PaymentIntent id (`pi_…`) as `id`, so the payment counts once.

From the browser [#from-the-browser]

Call `purchase()` with the [Web SDK](/docs/sdks/web) or the [script tag](/docs/sdks/script). The purchase is credited to the visit that is going on in that browser.

```js
pieowl.purchase({ id: 'A-1001', amount: 49.99, currency: 'EUR', plan: 'pro' });

// Script tag
window.pieowl('purchase', { id: 'A-1001', amount: 49.99, currency: 'EUR', plan: 'pro' });
```

Payment in a modal or an embedded card form [#payment-in-a-modal-or-an-embedded-card-form]

If people pay without leaving the page, there is no success page. Send the purchase where your code learns that the payment went through. For example, with Stripe's Payment Element in a modal, `confirmPayment` with `redirect: 'if_required'` returns the paid PaymentIntent for card payments:

```js
const { paymentIntent } = await stripe.confirmPayment({
  elements,
  redirect: 'if_required',
});
if (paymentIntent?.status === 'succeeded') {
  window.pieowl('purchase', {
    id: paymentIntent.id,
    amount: paymentIntent.amount / 100, // Stripe sends cents
    currency: paymentIntent.currency.toUpperCase(),
  });
}
```

Some currencies, like JPY, have no decimals. Stripe sends `1500` for ¥1,500, so don't divide those by 100. If your payment provider gives you the amount with decimals, send that.

The visitor is still on the same visit, so the purchase gets that visit's source, pages and country. Payment methods that redirect to a bank come back to your `return_url`: send the purchase there, as on a success page.

Payment on another site [#payment-on-another-site]

If the visitor pays on another site, like a hosted checkout page, the simplest way is your payment provider's webhook: [no code](#from-your-payment-provider-no-code) or [from your server](#from-your-server). Pieowl stores nothing in the browser to carry the purchase across the redirect.

To send it from the browser instead, call `purchase()` on your success page. If the page doesn't know the order, look it up on your server, for example with the checkout session ID your payment provider adds to the success address:

```js
// Success address: /thanks?session_id={CHECKOUT_SESSION_ID}
const sessionId = new URLSearchParams(location.search).get('session_id');
const order = await fetch(`/api/orders/${sessionId}`).then((res) => res.json());

pieowl.purchase({ id: order.id, amount: order.total, currency: order.currency });
```

<Callout type="warning">
  Pieowl rejects purchases and refunds sent from the browser by default, because anyone can send browser events. Turn on **Accept revenue sent from the browser** in Settings → Site → Revenue to accept them. Sending from your server is more reliable.
</Callout>

From your server [#from-your-server]

Send the purchase when your server learns the payment went through, for example in your payment provider's webhook. Use the [JavaScript SDK](/docs/sdks/javascript) with your client secret, or the [HTTP API](/docs/api/track) from any language.

```js
import { Pieowl } from '@pieowl/sdk';

const pieowl = new Pieowl({
  clientId: 'YOUR_CLIENT_ID',
  clientSecret: 'YOUR_CLIENT_SECRET',
});

// When the payment went through
await pieowl.purchase({
  id: order.number,
  amount: order.total, // 49.99
  currency: order.currency, // 'EUR'
  deviceId: order.deviceId, // from pieowl.getDeviceId() in the browser
  plan: 'pro',
});
```

```bash
curl -X POST 'https://api.pieowl.com/track' \
  -H 'Content-Type: application/json' \
  -H 'pieowl-client-id: YOUR_CLIENT_ID' \
  -H 'pieowl-client-secret: YOUR_CLIENT_SECRET' \
  -d '{
    "type": "track",
    "payload": {
      "name": "revenue",
      "properties": {
        "__amount": 49.99,
        "__currency": "EUR",
        "__orderId": "A-1001",
        "__deviceId": "DEVICE_ID",
        "plan": "pro"
      }
    }
  }'
```

A server has no visit of its own, so Pieowl needs to know who paid. Send one of these, or both (the `deviceId` is tried first):

* **`deviceId`**, when you can. In the browser, read it with `pieowl.getDeviceId()` before checkout and keep it with the order, for example in your payment provider's metadata. Send it as `deviceId` with the SDK, or as the `__deviceId` property over HTTP.
* **`profileId`**. In the browser, call [`identify()`](/docs/get-started/identify-users) with the visitor's `profileId` before they pay, for example when they log in or start checkout. Send the same `profileId` with the purchase.

Pieowl credits the purchase to that visitor's current visit. If the visit has already ended (a visit ends after 30 minutes without activity), for example because the payment webhook came hours later, the purchase goes to the visitor's most recent visit that started up to 7 days before the purchase. A purchase without a `deviceId` or `profileId`, or with no visit in those 7 days, is still saved as a `revenue` event, but it isn't credited to a visit, so it doesn't show up in the revenue of sources, pages or countries.

Order numbers [#order-numbers]

Send your order number as `id`. Pieowl counts each order once, so the same order sent twice adds nothing the second time. This happens more than you'd think: a visitor reloads the thank-you page, or your payment provider retries a webhook.

Use an ID that is unique on your site, like your order number or your payment provider's payment ID. For subscription renewals, use the ID of each invoice, not of the subscription.

Refunds [#refunds]

Call `refund()` with the order number when you give money back. Pieowl subtracts the order from your revenue.

```js
// The whole order
pieowl.refund({ id: 'A-1001' });

// Part of it
pieowl.refund({ id: 'A-1001', amount: 10, currency: 'EUR' });

// Script tag
window.pieowl('refund', { id: 'A-1001' });
```

* Without `amount`, Pieowl refunds what was paid for that order. The order must have been sent with `purchase()`.
* Each refund counts once. For several partial refunds of one order, give each a `refundId` (refunds without one are told apart by amount).
* A refund lowers the revenue of the visit the order was paid in, so its source, pages and country lose it too.

Over HTTP, send the same `revenue` event with `"__refund": true` and the `__orderId`. `__amount` and `__currency` are optional.

Currency [#currency]

Pieowl shows all revenue in your site currency. Set it in Settings → Site → **Revenue** → **Your currency**. It's USD until you change it.

* You can send purchases in any of these currencies: USD, EUR, GBP, PLN, CHF, CZK, DKK, HUF, NOK, RON, SEK, ISK, TRY, AUD, BRL, CAD, CNY, HKD, IDR, ILS, INR, JPY, KRW, MXN, MYR, NZD, PHP, SGD, THB and ZAR. Any other code is rejected with an error, so a wrong currency never gets counted.
* A purchase in another currency is converted at the European Central Bank reference rate of the payment's day. On weekends and holidays, Pieowl uses the last published rate.
* Pieowl keeps the original amount and currency of each purchase.
* If you change your site currency, past purchases are recalculated in the background. It takes a few minutes.

The older revenue() call [#the-older-revenue-call]

Before `purchase()`, the SDKs had `pieowl.revenue(4900)`. It still works. It takes a whole number in the smallest unit of your site currency, like cents. It has no order number, so a resent purchase counts twice. Use `purchase()` for new code.

How a purchase is credited [#how-a-purchase-is-credited]

Every purchase is credited to **the visit where it happened**. That visit's details become the purchase's details:

* Source and referrer
* UTM campaign, source and medium
* Entry page (the first page of the visit)
* Country
* Device, browser and OS

Other visits by the same visitor get nothing, even if they came first.

Example [#example]

A visitor comes to your site twice:

| Visit    | Came from     | Entry page           | Bought  |
| -------- | ------------- | -------------------- | ------- |
| Monday   | Google search | `/blog/pricing-tips` | Nothing |
| Thursday | Instagram     | `/products`          | $49     |

The $49 is credited to **Instagram*&#x2A; and to the entry page &#x2A;*`/products`**. Google and `/blog/pricing-tips` get no revenue. Both visits still count as visits for their own source and page.

If the visitor had come back on Thursday by typing your address, the $49 would go to **Direct**.

Where to see revenue [#where-to-see-revenue]

In the overview, switch the metric to **Revenue** in any of these places:

* **Sources**: referrers, UTM sources, mediums and campaigns
* **Entry pages**: revenue by the page each visit started on. Each purchase belongs to one entry page, so the shares add up to 100%.
* **Viewed pages**: revenue of the visits that viewed each page. A visit that viewed three pages counts for all three, so the shares overlap and add up to more than 100%. Use it to see which pages are part of buying visits.
* **Countries and the map**
* **Devices, browsers and OS**
* **Events**: the revenue of visits that had each event. For example, revenue of visits with `signup_clicked`.

Revenue also shows up in:

* **People**: each person's lifetime revenue, from all of their visits
* **Groups** and **cohorts**: revenue for each group and each cohort

Metrics [#metrics]

| Metric                  | What it means                                                  |
| ----------------------- | -------------------------------------------------------------- |
| **Revenue**             | The sum of purchases credited to visits in the selected period |
| **Paying visits**       | Visits with at least one purchase                              |
| **Conversion**          | Paying visits divided by all visits, as a percentage           |
| **Revenue per visitor** | Revenue divided by unique visitors                             |

Revenue is shown in your [site currency](#currency).

Filters [#filters]

Add the **Bought** filter to the overview:

* **Bought: yes** shows only visits with a purchase. Use it to see where buyers come from and what they view.
* **Bought: no** shows only visits without a purchase.

All other filters work with revenue too. Filter by a UTM campaign and every revenue number on the page is for that campaign.

Reports [#reports]

In [reports](/features/data-visualization), revenue works with any event:

* **Revenue (visits with this event)**: pick it as the metric for any event to get the revenue of the visits that had that event.
* **Breakdowns by visit**: break revenue down by entry page, referrer or UTM fields.
* **Funnels and conversion**: see the revenue of the visits that completed.
* **Formulas**: combine revenue with other series.

Example: revenue per visitor over time [#example-revenue-per-visitor-over-time]

1. Series **A**: pick `screen_view` with the metric &#x2A;*Revenue (visits with this event)**.
2. Series **B**: pick `screen_view` with the metric **Unique users**.
3. Click **Add formula** and enter `A / B`.

The formula line is revenue per visitor for each day, week or month.

Privacy [#privacy]

You don't need personal data for revenue attribution. Send the order number and the amount and, from a server, a `deviceId` or `profileId`. The `profileId` can be your internal user ID, it doesn't need to be an email address. Don't put names, emails or card details in the purchase properties.

Pieowl sets no cookies, stores nothing on the visitor's device and doesn't store IP addresses. See [How it works](/docs/how-it-works).

Revenue by source is conversion measurement. In some countries that needs your visitors' consent, and France's exemption for audience measurement doesn't cover it. Sending purchases from your server or with webhooks keeps payment data out of the browser. See [Do you need a cookie banner?](/docs/consent-and-cookie-banners).

FAQ [#faq]

<Faqs>
  <FaqItem question="Which currency should I send?">
    The currency the customer paid in. Pieowl converts it to your site currency at the European Central Bank rate of the payment's day, and keeps the original. See [Currency](#currency).
  </FaqItem>

  <FaqItem question="Can I import payments from Stripe?">
    Yes, from the moment you connect it: see [From your payment provider, no code](#from-your-payment-provider-no-code). Stripe, Paddle, Lemon Squeezy and Polar then send every payment, renewal and refund by themselves. Payments made before the connection aren't imported.
  </FaqItem>

  <FaqItem question="How do I handle refunds?">
    Call `refund()` with the order number. Pass `amount` for a partial refund. See [Refunds](#refunds).
  </FaqItem>

  <FaqItem question="Can I track subscription renewals?">
    Yes, send a purchase for each renewal, with the invoice ID as `id`. A renewal goes to the customer's most recent visit if it started up to 7 days before the renewal. Otherwise there is no visit to credit it to: it's still saved as a `revenue` event, but it doesn't add to the revenue of sources or pages.
  </FaqItem>

  <FaqItem question="Why doesn't the first visit get any credit?">
    Pieowl credits each purchase to the visit where it happened. It doesn't split a purchase across earlier visits. Without cookies, Pieowl can't reliably link an anonymous visitor's visits on different days, so crediting the visit where the purchase happened gives numbers you can trust.
  </FaqItem>

  <FaqItem question="Why is my server-side purchase not credited to a source?">
    Check that you send the visitor's `deviceId` (from `pieowl.getDeviceId()`) or a `profileId` that the browser also passed to `identify()`, and that the visitor had a visit that started up to 7 days before the purchase.
  </FaqItem>
</Faqs>
