--- title: Revenue tracking description: Learn how to easily track your revenue with OpenPanel and how to get it shown directly in your dashboard. --- import { FlowStep } from '@/components/flow-step'; [Revenue tracking](/features/revenue-tracking) is a great way to get a better understanding of what your best revenue source is. On this page we'll break down how to get started. Before we start, we need to know some fundamentals about how OpenPanel and your payment provider work and how we can 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. ### OpenPanel OpenPanel is a cookieless analytics tool that identifies visitors using 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-webhook) - [Revenue tracking from your backend (identified)](#revenue-tracking-from-your-backend-webhook-identified) - [Revenue tracking from your frontend](#revenue-tracking-from-your-frontend) - [Revenue tracking without linking it to a identity or device](#revenue-tracking-without-linking-it-to-an-identity-or-device) ### 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. When you create the checkout, you should first call `op.getDeviceId()`, which will return your visitor's current `deviceId`. Pass this to your checkout endpoint. ```javascript fetch('https://domain.com/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ deviceId: op.getDeviceId(), // ✅ since deviceId is here we can link the payment now // ... other checkout data }), }) .then(response => response.json()) .then(data => { // Handle checkout response, e.g., redirect to payment link window.location.href = data.paymentUrl; }) ``` ```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, // ✅ since deviceId is here we can link the payment now }, success_url: 'https://domain.com/success', cancel_url: 'https://domain.com/cancel', }); return Response.json({ paymentUrl: session.url, }); } ``` ```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; const deviceId = session.metadata.deviceId; const amount = session.amount_total; op.revenue(amount, { deviceId }); // ✅ since deviceId is here we can link the payment now } return Response.json({ received: true }); } ``` --- ### 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 revenue call. When a visitor logs in or is identified, call `op.identify()` with their unique `profileId`. ```javascript op.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. ```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; }) ``` 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, }); } ``` 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; const profileId = session.metadata.profileId; const amount = session.amount_total; op.revenue(amount, { profileId }); // ✅ Use profileId instead of deviceId } return Response.json({ received: true }); } ``` --- ### Revenue tracking from your frontend This flow tracks revenue directly from your frontend. Since the success page doesn't have access to the payment amount (payment happens on Stripe's side), we track revenue when checkout is initiated and then confirm it on the success page. When the visitor clicks the checkout button, track the revenue with the amount. ```javascript async function handleCheckout() { const amount = 2000; // Amount in cents // Create a pending revenue (stored in sessionStorage) op.pendingRevenue(amount, { productId: '123', // ... other properties }); // Redirect to Stripe checkout window.location.href = 'https://checkout.stripe.com/...'; } ``` On your success page, flush all pending revenue events. This will send all pending revenues tracked during checkout and clear them from sessionStorage. ```javascript // Flush all pending revenues await op.flushRevenue(); // Or if you want to clear without sending (e.g., payment was cancelled) op.clearRevenue(); ``` #### Pros: - Quick way to get going - No backend required - Can track revenue immediately when checkout starts #### Cons: - Less accurate (visitor might not complete payment) - Less "secure" meaning anyone could post revenue data --- ### 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 `op.revenue()` 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. Simply call `op.revenue()` with the 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; const amount = session.amount_total; op.revenue(amount); // ✅ Simple revenue tracking without linking to a visitor } 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 ## Available methods ### Revenue The revenue method will create a revenue event. It's important to know that this method will not work if your OpenPanel instance didn't receive a client secret (for security reasons). You can enable frontend revenue tracking within your project settings. ```javascript op.revenue(amount: number, properties: Record): Promise ``` ### Add a pending revenue This method will create a pending revenue item and store it in sessionStorage. It will not be sent to OpenPanel until you call `flushRevenue()`. Pending revenues are automatically restored from sessionStorage when the SDK initializes. ```javascript op.pendingRevenue(amount: number, properties?: Record): void ``` ### Send all pending revenues This method will send all pending revenues to OpenPanel and then clear them from sessionStorage. Returns a Promise that resolves when all revenues have been sent. ```javascript await op.flushRevenue(): Promise ``` ### Clear any pending revenue This method will clear all pending revenues from memory and sessionStorage without sending them to OpenPanel. Useful if a payment was cancelled or you want to discard pending revenues. ```javascript op.clearRevenue(): void ``` ### Fetch your current users device id ```javascript op.getDeviceId(): string ```