Revenue tracking
Learn how to easily track your revenue with Helion and how to get it shown directly in your dashboard.
Revenue tracking gives you a clear view of your top revenue sources. This page covers the fundamentals and the available integration patterns.
Before we start, we need to understand how Helion and your payment provider work together to link a payment to a specific visitor.
Payment providers
Checkout sessions are typically created server-side, which returns a payment link that the visitor is redirected to. When creating the checkout link, you can include additional fields such as metadata, customer information, or order details. We use this metadata field to carry the visitor's device_id so the payment can be linked back to them.
Helion
Helion is a cookieless analytics tool that identifies visitors using a device_id. To link a payment to a visitor, capture their device_id before they complete checkout. Store it in your payment provider's metadata, and when the payment webhook arrives, use it to attribute the revenue to 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
Revenue tracking from your backend (webhook)
This is the most common and most secure flow. Your backend receives webhooks from your payment provider — this is the ideal place to record revenue.
When creating the checkout, call hl.getDeviceId() to retrieve 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: hl.getDeviceId(), // ✅ links the payment to this visitor
// ... other checkout data
}),
})
.then(response => response.json())
.then(data => {
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, // ✅ stored for webhook attribution
},
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();
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
const deviceId = session.metadata.deviceId;
const amount = session.amount_total;
hl.revenue(amount, { deviceId }); // ✅ attributes revenue to the correct visitor
}
return Response.json({ received: true });
}Revenue tracking from your backend (webhook) - Identified users
If your visitors are identified (i.e., you have called identify with a profileId), the flow is simpler. You do not need to pass the deviceId when creating the checkout; provide the profileId in the webhook handler instead.
When a visitor logs in, call hl.identify() with their unique profileId.
hl.identify({
profileId: 'user-123',
email: 'user@example.com',
firstName: 'John',
lastName: 'Doe',
});Since the visitor is already identified, no deviceId is required.
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 => {
window.location.href = data.paymentUrl;
})Retrieve the profileId from the authenticated session and store it in the checkout metadata.
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();
const profileId = req.session.userId;
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,
},
quantity: 1,
},
],
mode: 'payment',
metadata: {
profileId: profileId, // ✅ stored for webhook attribution
},
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();
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
const profileId = session.metadata.profileId;
const amount = session.amount_total;
hl.revenue(amount, { profileId }); // ✅ attributes revenue to the identified user
}
return Response.json({ received: true });
}Revenue tracking from your frontend
This flow tracks revenue directly from the browser. Because the success page does not have access to the payment amount, revenue is recorded when checkout is initiated and confirmed on the success page.
When the visitor clicks the checkout button, record a pending revenue event.
async function handleCheckout() {
const amount = 2000; // Amount in cents
// Store in sessionStorage — not sent until flushRevenue() is called
hl.pendingRevenue(amount, {
productId: '123',
// ... other properties
});
window.location.href = 'https://checkout.stripe.com/...';
}On your success page, flush all pending revenue events. This sends every pending revenue recorded during checkout and clears them from sessionStorage.
// Send all pending revenues
await hl.flushRevenue();
// Or discard without sending (e.g., payment was cancelled)
hl.clearRevenue();Pros:
- Quick to implement
- No backend required
- Revenue is recorded when checkout starts
Cons:
- Less accurate — the visitor may not complete payment
- Less secure — anyone can post revenue data
Revenue tracking without linking it to an identity or device
If you only need aggregate revenue totals and do not need to link payments to specific visitors, call hl.revenue() from your backend without a deviceId or profileId. This is the simplest approach.
export async function POST(req: Request) {
const event = await req.json();
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
const amount = session.amount_total;
hl.revenue(amount); // ✅ simple aggregate revenue tracking
}
return Response.json({ received: true });
}Pros:
- Simplest implementation
- No device ID or profile ID required
- Suitable for aggregate revenue reporting
Cons:
- No source attribution. You cannot determine which campaigns, channels, or visitors generated the revenue.
- Revenue events are not linked to specific user journeys or sessions.
Available methods
Revenue
Records a revenue event immediately. Requires a client secret — frontend revenue tracking must be explicitly enabled in your project settings for security reasons.
hl.revenue(amount: number, properties: Record<string, unknown>): Promise<void>Add a pending revenue
Creates a pending revenue entry stored in sessionStorage. Nothing is sent until flushRevenue() is called. Pending entries are automatically restored from sessionStorage when the SDK initialises.
hl.pendingRevenue(amount: number, properties?: Record<string, unknown>): voidSend all pending revenues
Sends all pending revenue entries to Helion, then clears them from sessionStorage. Returns a Promise that resolves when all revenues have been sent.
await hl.flushRevenue(): Promise<void>Clear any pending revenue
Discards all pending revenue entries from memory and sessionStorage without sending them. Use this when a payment is cancelled or you need to reset pending state.
hl.clearRevenue(): voidFetch the current visitor's device ID
hl.getDeviceId(): string