pieowlDocs
Revenue tracking

Revenue tracking

Learn how to track revenue with Pieowl, link each payment to the visitor who made it, and see which sources and campaigns bring in money.

Revenue attribution shows you which sources, campaigns, pages and countries bring in money. This page walks through checkout flows step by step. For how purchases are credited to visits and where to see revenue in the dashboard, see Revenue attribution.

First, a few basics about how Pieowl and your payment provider work, and how to link a payment to a visitor.

Payment providers

Usually, you create your checkout from your backend, which then returns a payment link that your visitor will be redirected to. When creating the checkout link, you usually add additional fields such as metadata, customer information, or order details. We'll add the device ID information in this metadata field to be able to link your payment to a visitor.

Pieowl

Pieowl doesn't use cookies. It identifies visitors with a device_id. To link a payment to a visitor, you need to capture their device_id before they complete checkout. This device_id will be stored in your payment provider's metadata, and when the payment webhook arrives, you'll use it to associate the revenue with the correct visitor.

Some typical flows

In the backend examples, pieowl is a server-side instance created with your client secret (see the JavaScript SDK). In the browser examples, it's your Web SDK instance or window.pieowl.

Revenue tracking from your backend (webhook)

This is the most common flow and most secure one. Your backend receives webhooks from your payment provider, and here is the best opportunity to do revenue tracking.

Pieowl credits the purchase to the visit of that deviceId: the visit going on when the webhook arrives or, if it has already ended, the visitor's most recent visit that started up to 7 days before the purchase. If there is no such visit, the purchase is still saved as a revenue event, but it doesn't show up in the revenue of sources, pages or countries.

1
Visitor: Visits your website
2
Visitor: Makes a purchase
3
Your website: Does a POST request to get the checkout URL

When you create the checkout, first call pieowl.getDeviceId(). It returns the visitor's current deviceId. Pass it to your checkout endpoint.

javascript
fetch('https://domain.com/api/checkout', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    deviceId: pieowl.getDeviceId(), // keep the visitor's deviceId with the payment
    // ... other checkout data
  }),
})
  .then(response => response.json())
  .then(data => {
    // Handle checkout response, e.g., redirect to payment link
    window.location.href = data.paymentUrl;
  })
4
Your backend: Will generate and return the checkout URL
javascript
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function POST(req: Request) {
  const { deviceId, amount, currency } = await req.json();
  
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [
      {
        price_data: {
          currency: currency,
          product_data: { name: 'Product Name' },
          unit_amount: amount * 100, // Convert to cents
        },
        quantity: 1,
      },
    ],
    mode: 'payment',
    metadata: {
      deviceId: deviceId, // keep the deviceId with the payment
    },
    success_url: 'https://domain.com/success',
    cancel_url: 'https://domain.com/cancel',
  });
  
  return Response.json({
    paymentUrl: session.url,
  });
} 
5
Visitor: Gets redirected to payment link
6
Visitor: Pays on your payment provider
7
Your backend: Receives a webhook for a successful payment
javascript
export async function POST(req: Request) {
  const event = await req.json();
  
  // Stripe sends events with type and data.object structure
  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;

    pieowl.purchase({
      id: session.id, // a resent webhook counts once
      amount: session.amount_total / 100, // Stripe sends cents
      currency: session.currency.toUpperCase(),
      deviceId: session.metadata.deviceId, // credited to this visitor's visit
    });
  }
  
  return Response.json({ received: true });
}

Stripe sends amounts in the smallest currency unit. For currencies without decimals, like JPY, send session.amount_total as it is. If your payment provider gives you the amount with decimals, send that.

8
Visitor: Redirected to your website with payment confirmation

Revenue tracking from your backend (webhook) - Identified users

If your visitors are identified (meaning you have called identify with a profileId), this process gets a bit easier. You don't need to pass the deviceId when creating your checkout, and you only need to provide the profileId (in backend) to the purchase() call.

Pieowl credits the purchase to the visitor's current visit, so its source, entry page and country get the revenue. If the visit has already ended (a visit ends after 30 minutes without activity), the purchase goes to the visitor's most recent visit that started up to 7 days before it. If you also have the visitor's deviceId, send it too: Pieowl tries it first. See Revenue attribution.

1
Visitor: Visits your website
2
Your website: Identifies the visitor

When a visitor logs in or is identified, call pieowl.identify() with their unique profileId.

javascript
pieowl.identify({
  profileId: 'user-123', // Unique identifier for this user
  email: 'user@example.com',
  firstName: 'John',
  lastName: 'Doe',
});
3
Visitor: Makes a purchase
4
Your website: Does a POST request to get the checkout URL

Since the visitor is already identified, you don't need to fetch or pass the deviceId. Just send the checkout data.

javascript
fetch('https://domain.com/api/checkout', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    // No deviceId needed - user is already identified
    // ... other checkout data
  }),
})
  .then(response => response.json())
  .then(data => {
    // Handle checkout response, e.g., redirect to payment link
    window.location.href = data.paymentUrl;
  })
5
Your backend: Will generate and return the checkout URL

Since the user is authenticated, you can get their profileId from the session and store it in metadata for easy retrieval in the webhook.

javascript
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function POST(req: Request) {
  const { amount, currency } = await req.json();
  
  // Get profileId from authenticated session
  const profileId = req.session.userId; // or however you get the user ID
  
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [
      {
        price_data: {
          currency: currency,
          product_data: { name: 'Product Name' },
          unit_amount: amount * 100, // Convert to cents
        },
        quantity: 1,
      },
    ],
    mode: 'payment',
    metadata: {
      profileId: profileId, // Store profileId instead of deviceId
    },
    success_url: 'https://domain.com/success',
    cancel_url: 'https://domain.com/cancel',
  });
  
  return Response.json({
    paymentUrl: session.url,
  });
} 
6
Visitor: Gets redirected to payment link
7
Visitor: Pays on your payment provider
8
Your backend: Receives a webhook for a successful payment

In the webhook handler, retrieve the profileId from the session metadata.

javascript
export async function POST(req: Request) {
  const event = await req.json();
  
  // Stripe sends events with type and data.object structure
  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;

    pieowl.purchase({
      id: session.id,
      amount: session.amount_total / 100, // Stripe sends cents
      currency: session.currency.toUpperCase(),
      profileId: session.metadata.profileId, // Use profileId instead of deviceId
    });
  }
  
  return Response.json({ received: true });
}
9
Visitor: Redirected to your website with payment confirmation

Revenue tracking from your frontend

This flow sends the purchase from your frontend. Call pieowl.purchase() on your success page, or where your code learns that the payment went through. If your success 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. Pieowl stores nothing in the browser between checkout and the success page.

1
Visitor: Visits your website
2
Visitor: Clicks to purchase
3
Your website: Sends the visitor to checkout

Create the checkout with a success address that tells your page which order was paid. With Stripe Checkout, add session_id={CHECKOUT_SESSION_ID} to success_url.

4
Visitor: Pays on your payment provider
5
Visitor: Redirected back to your success page
6
Your website: Sends the purchase on the success page

Look up the paid order and send it with purchase(). The same order sent twice counts once.

javascript
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, // in normal units, like 49.99
  currency: order.currency,
});

Pros:

  • Quick way to get going
  • No webhook required

Cons:

  • Missed if the visitor closes the tab before the success page loads
  • Less "secure": anyone could post revenue data
  • Needs Accept revenue sent from the browser turned on in Settings → Site → Revenue
  • Revenue sent from the browser is conversion measurement. In some countries it needs your visitors' consent. See Do you need a cookie banner?

Revenue tracking without linking it to an identity or device

If you simply want to track revenue totals without linking payments to specific visitors or devices, you can call pieowl.purchase() directly from your backend without providing a deviceId or profileId. This is the simplest approach and works well when you only need aggregate revenue data.

1
Visitor: Makes a purchase
2
Visitor: Pays on your payment provider
3
Your backend: Receives a webhook for a successful payment

Call pieowl.purchase() with the order number and amount. No deviceId or profileId is needed.

javascript
export async function POST(req: Request) {
  const event = await req.json();
  
  // Stripe sends events with type and data.object structure
  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;

    // Simple revenue tracking without linking to a visitor
    pieowl.purchase({
      id: session.id,
      amount: session.amount_total / 100,
      currency: session.currency.toUpperCase(),
    });
  }
  
  return Response.json({ received: true });
}

Pros:

  • Simplest implementation
  • No need to capture or pass device IDs
  • Works well for aggregate revenue tracking

Cons:

  • You can't dive deeper into where this revenue came from. For instance, you won't be able to see which source generates the best revenue, which campaigns are most profitable, or which visitors are your highest-value customers.
  • Revenue events won't be linked to specific user journeys or sessions
  • The overview's revenue only counts purchases credited to a visit, so these purchases don't show up there. You can still chart the revenue event in reports.

Available methods

Purchase

Sends a paid order. For security, Pieowl rejects purchases that aren't sent with your client secret. To send them from the browser, turn on Accept revenue sent from the browser in Settings → Site → Revenue.

  • id: your order number. The same order sent twice counts once.
  • amount: in normal units, like 49.99, not cents.
  • currency: an ISO 4217 code, like EUR. Optional, your site currency by default. See Currency.
  • deviceId: from a server, the visitor's device ID from pieowl.getDeviceId().
  • Any other properties, like plan.
javascript
pieowl.purchase({ id: string, amount: number, currency?: string, deviceId?: string, ...properties }): Promise<void>

Refund

Subtracts an order from your revenue. Without amount, the whole order is refunded. Pass amount (and currency) for a partial refund, and a refundId to count several partial refunds of one order. The visit the order was paid in loses the amount. Browser refunds need the same setting as purchases.

javascript
pieowl.refund({ id: string, amount?: number, currency?: string, deviceId?: string }): Promise<void>

Revenue (older)

The older way to send a purchase. The amount is a whole number in the smallest unit of your site currency, like cents (4900 for 49.00). It has no order number, so a resent purchase counts twice. Use purchase() for new code.

javascript
pieowl.revenue(amount: number, properties?: Record<string, unknown>): Promise<void>

Fetch your current users device id

javascript
pieowl.getDeviceId(): string