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
- Revenue tracking from your backend (not identified)
- Revenue tracking from your backend (identified)
- Revenue tracking from your frontend
- Revenue tracking without linking it to a identity or device
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.
When you create the checkout, first call pieowl.getDeviceId(). It returns the visitor's current deviceId. Pass it to your checkout endpoint.
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;
})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,
});
} 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.
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.
When a visitor logs in or is identified, call pieowl.identify() with their unique profileId.
pieowl.identify({
profileId: 'user-123', // Unique identifier for this user
email: 'user@example.com',
firstName: 'John',
lastName: 'Doe',
});Since the visitor is already identified, you don't need to fetch or pass the deviceId. Just send the checkout data.
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;
})Since the user is authenticated, you can get their profileId from the session and store it in metadata for easy retrieval in the webhook.
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,
});
} In the webhook handler, retrieve the profileId from the session metadata.
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 });
}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.
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.
Look up the paid order and send it with purchase(). The same order sent twice counts once.
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.
Call pieowl.purchase() with the order number and amount. No deviceId or profileId is needed.
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
revenueevent 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, like49.99, not cents.currency: an ISO 4217 code, likeEUR. Optional, your site currency by default. See Currency.deviceId: from a server, the visitor's device ID frompieowl.getDeviceId().- Any other properties, like
plan.
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.
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.
pieowl.revenue(amount: number, properties?: Record<string, unknown>): Promise<void>Fetch your current users device id
pieowl.getDeviceId(): string