OpenPanel
Get started

Revenue tracking

Learn how to easily track your revenue with OpenPanel and how to get it shown directly in your dashboard.

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 (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.

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, you should first call op.fetchDeviceId(), which will return your visitor's current deviceId. Pass this to your checkout endpoint.

fetch('https://domain.com/api/checkout', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    deviceId: await op.fetchDeviceId(), // ✅ 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;
  })
4
Your backend: Will generate and return the checkout URL
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,
  });
} 
5
Visitor: Gets redirected to payment link
6
Visitor: Pays on your payment provider
7
Your backend: Receives a webhook for a successful payment
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 });
}
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 revenue call.

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

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

op.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.

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.

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.

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 });
}
9
Visitor: Redirected to your website with payment confirmation

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.

1
Visitor: Visits your website
2
Visitor: Clicks to purchase
3
Your website: Track revenue when checkout is initiated

When the visitor clicks the checkout button, track the revenue with the amount.

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/...';
}
4
Visitor: Gets redirected to payment link
5
Visitor: Pays on your payment provider
6
Visitor: Redirected back to your success page
7
Your website: Confirm/flush the revenue on success page

On your success page, flush all pending revenue events. This will send all pending revenues tracked during checkout and clear them from sessionStorage.

// 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.

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

Simply call op.revenue() with the 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;
    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.

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

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.

op.pendingRevenue(amount: number, properties?: Record<string, unknown>): 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.

await op.flushRevenue(): Promise<void>

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.

op.clearRevenue(): void

Fetch your current users device id

op.fetchDeviceId(): Promise<string>

On this page