Skip to content
Documentation menu: Revenue attribution

Revenue attribution

Connect your revenue data to marketing campaigns and see which channels bring in paying customers.

Setup time

10 minutes

Difficulty

Medium

You get

Verified revenue by channel

How revenue attribution works

How a sale is linked to the visit that came before it

Each sale keeps the source of the visitor's most recent tracked visit before the purchase that has referrer or UTM data, so you can see which channels produce revenue, not just traffic. If that visit has no source data, the sale uses the source of the same checkout or, failing that, the customer's earliest attributed purchase. Renewals have no visit of their own, so they use the customer's earliest attributed purchase. A return from Stripe Checkout (and its invoice and payment pages), PayPal, Lemon Squeezy, Paddle or Google sign-in does not replace the original referrer, so a buyer who comes back from checkout keeps the channel that brought them in.

Which source wins. A sale is credited to the source the visitor most recently arrived with in the 30 days before the sale. The value direct, which the script sends for a visit that has no source of its own, never overrides a real source. So with data-cookie-domain, a visitor who lands from Google on www.example.com and buys on app.example.com is credited to Google, even though the page that reports the sale has no source of its own. The script stops carrying a source forward after 30 days, so a visitor whose last real source is older than that is normally shown as Direct.

1. Visit

Visitor arrives from a source

User clicks Facebook ad with UTM parameters
2. Engagement

Pageviews are recorded for the same visitor

Views product pages, reads blog posts
3. Conversion

Customer makes purchase

Completes checkout with Stripe
4. Attribution

Sale is linked to the visit's source

The $99 sale is recorded with the Facebook source and campaign
Key benefits
  • • See which sources and campaigns bring in paying customers
  • • Compare revenue by source, medium and campaign
  • • See each customer's total spent and order count
  • • Follow the visits that led to each purchase

Verified and client-reported revenue

Every revenue record says who vouches for it, and the dashboard keeps the two apart

Verified
Stripe or server

Sales that Stripe reports through your connection, and sales you send from your server with your ingest secret. When Stripe is connected, the revenue headline in your dashboard is verified revenue.

Client-reported
Unverified

Sales and conversion values reported from a visitor's browser, which can be blocked, repeated or faked. They are listed separately as "Client-reported". When Stripe is not connected they are the only revenue you have, and the dashboard shows them with an "Unverified" badge.

  • No double counting. A sale is unique per website and order ID. If your page or your server reports a sale under a Stripe Checkout Session ID (cs_...) or payment intent ID (pi_...) that Stripe has already recorded, the report is skipped. If Stripe reports it later, the client- or server-reported record is replaced by Stripe's: it is marked as superseded and left out of totals, and the tracked event it used is given back. This works whichever of the two arrives first. A different order ID, such as your own order number, cannot be matched to Stripe's record and still counts twice.
  • Server reports upgrade browser reports. If your page reported a sale and your server then reports the same order ID with the ingest secret, the sale becomes verified in place and takes the server's amount, currency, email and product. It is not counted a second time.
  • Test mode is excluded. Payments made with Stripe test keys are stored but never reach your analytics. The Configure page shows when the last test event arrived, so you can check your setup without affecting your numbers. This also covers a purchase or conversion that your page or server reports with a Stripe test Checkout Session ID (cs_test_...) as the order ID, which is what the thank-you page snippet below does: the sale is stored as test data, left out of your analytics and not counted toward your plan usage, and the answer includes testMode: true. The purchase event itself is still stored and counted. A payment intent ID (pi_...) looks the same in test and live mode, so a sale reported with one cannot be recognized as a test.
  • $0 sales are not revenue. Free checkouts and $0 invoices are never recorded as orders.
  • Currencies. Each website has a reporting currency: your Stripe account's default currency, else the currency of most of your revenue, else USD. You can change it under Configure, Website details. Totals are kept per currency. The headline uses the reporting currency, other currencies are listed beside it (for example "+ €120 in EUR"), and every amount is shown in its own currency. Nothing is converted.

Integration methods

Connect Stripe (recommended)

theBizness.ai reads your sales from Stripe, so every payment is recorded as verified revenue even if the visitor closes the page

Step 1: Create a restricted key

On the Configure page, open the link to create a restricted API key. Stripe opens with these permissions already selected (open it now (opens in a new tab)). Create the key, paste it into the Stripe connection form and choose Connect Stripe. Use a restricted key, never your secret key.

  • Checkout sessions: read. The sale itself and the product that was bought.
  • Charges and refunds: read. Refunds and subscription renewals.
  • Customers: read. The customer's email on a cancellation.
  • Invoices: read. The payment behind a subscription, and what a customer paid before cancelling.
  • Products: read. Plan names on cancellations.
  • Connect: read. Your Stripe account ID and default currency.
  • Webhook endpoints: write. Creates the one endpoint that sends us your sales, and removes it when you disconnect.

Every other permission stays at None. The key is encrypted before it is stored, and it is never shown in full again.

Step 2: Let us create the webhook

When you save the key, theBizness.ai reads your Stripe account ID and default currency and creates one webhook endpoint in your account. It listens for these events:

  • checkout.session.completed and checkout.session.async_payment_succeeded: new sales, including delayed payment methods (ACH, SEPA, Boleto) once the money arrives
  • charge.succeeded: subscription renewals, and the first payment after a free trial, which counts as a new order
  • charge.refunded: refunds
  • customer.subscription.updated and customer.subscription.deleted: cancellations and resumed subscriptions

If your endpoint was created a while ago and you accept delayed payment methods, check in Stripe that it also receives checkout.session.async_payment_succeeded.

Step 3: Pass the website and visitor IDs into Checkout

Create each Checkout Session with your website ID and the visitor and session IDs that the tracker stores on your domain. They link the sale to the visit that came before it.

// lib/checkout.ts (Next.js 15 App Router)
import { cookies } from 'next/headers';
import Stripe from 'stripe';

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

export async function createCheckoutSession(priceId: string) {
  // The tracker stores these cookies on your domain. Read them here, on the server that calls Stripe
  const cookieStore = await cookies();

  return stripe.checkout.sessions.create({
    mode: 'payment',
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: 'https://yourdomain.com/checkout/success',
    cancel_url: 'https://yourdomain.com/cart',
    metadata: {
      websiteId: process.env.THEBIZNESS_WEBSITE_ID ?? '', // names the website this sale belongs to
      visitorId: cookieStore.get('thebizness_visitor_id')?.value ?? '',
      sessionId: cookieStore.get('thebizness_session_id')?.value ?? ''
    }
  });
}

Express: install cookie-parser and read req.cookies.thebizness_visitor_id and req.cookies.thebizness_session_id the same way. Without the visitor and session IDs the sale is still recorded, but it is not linked to a traffic source.

Subscriptions and free trials: also set subscription_data.metadata to the same values. The metadata of a Checkout Session never reaches the renewal charges that follow, so without it a renewal has to be matched to its website another way (see the box below).

// Subscriptions and free trials: also set subscription_data.metadata (Next.js 15 App Router)
import { cookies } from 'next/headers';

const cookieStore = await cookies();
const tracking = {
  websiteId: process.env.THEBIZNESS_WEBSITE_ID ?? '',
  visitorId: cookieStore.get('thebizness_visitor_id')?.value ?? '',
  sessionId: cookieStore.get('thebizness_session_id')?.value ?? ''
};

const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: priceId, quantity: 1 }],
  success_url: 'https://yourdomain.com/checkout/success',
  cancel_url: 'https://yourdomain.com/pricing',
  // Read from the Checkout Session itself (also when a free trial collects nothing)
  metadata: tracking,
  // Copied onto the subscription and its invoices. The metadata of a Checkout Session never reaches renewal charges
  subscription_data: { metadata: tracking }
});
Which website does a sale belong to?

Each connected website gets its own webhook, so when several websites share one Stripe account every sale reaches all of them. A sale is recorded for a website only when its websiteId metadata names that website, when your page or server already reported that checkout to this website, when the visitor or session ID in its metadata belongs to a visit recorded for that website, when the Stripe customer is known to that website, or when it is the only website of your account connected to that Stripe account. Otherwise it is skipped for that website, and the Configure page tells you to pass the metadata above. A sale whose metadata names another website is never recorded here. Each theBizness account records its own copy of a sale, so separate theBizness accounts that use one Stripe account never block each other.

How a customer becomes known to a website. The customer is known to a website through an earlier sale there, or through the customer link we save when a Checkout Session completes there, including a free-trial session that collected nothing. Renewals, the first payment after a trial and repeat purchases carry no metadata of their own, so this link is how they find their website.

  • • Subscriptions and free trials: also set subscription_data.metadata with websiteId. The metadata of a Checkout Session never reaches renewal charges.
  • • One customer on several websites of the same account: a renewal or cancellation is tied to the website that holds its subscription (by subscription ID). If that cannot be matched, it goes to the only website that holds a subscription for the customer. Otherwise it is skipped, never recorded on every website.
  • • Repeat purchases: a repeat purchase by a customer that two websites know is skipped unless it carries websiteId.
  • • Customers who started before the link existed: on a shared account we also ask Stripe whether the subscription's own metadata names this website. The answer is saved as a link.

Disconnecting

Disconnect Stripe on the Configure page removes the webhook endpoint from your Stripe account and deletes the key, the signing secret and the account ID from theBizness.ai. The revenue already recorded stays in your reports. Deleting a website, or your account, removes the webhook the same way.

Server-side API integration

Send revenue from your own backend: for payment processors other than Stripe, or when you want to include the customer's email

API implementation

Send revenue data from your backend after processing payments. Sales sent with your ingest secret are recorded as verified revenue.

// Server-side revenue reporting (Node.js 18+, using the built-in fetch)
// Stripe amounts are in the currency's smallest unit (cents for USD, fils for KWD)
const ZERO_DECIMAL_CURRENCIES = new Set(['BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'VND', 'VUV', 'XAF', 'XOF', 'XPF']);
const THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'JOD', 'KWD', 'OMR', 'TND']);

// Converts a Stripe amount to currency units, e.g. toMajorUnits(5000, 'JPY') === 5000, toMajorUnits(999, 'usd') === 9.99
// and toMajorUnits(1234, 'KWD') === 1.234
function toMajorUnits(amount, currency) {
  const code = String(currency).toUpperCase();
  const decimals = ZERO_DECIMAL_CURRENCIES.has(code) ? 0 : THREE_DECIMAL_CURRENCIES.has(code) ? 3 : 2;
  return amount / 10 ** decimals;
}

async function sendConversion(conversion) {
  const response = await fetch('https://thebizness.ai/api/conversions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      // The ingest secret from your Configure page. Keep it in an environment variable on the server.
      Authorization: `Bearer ${process.env.THEBIZNESS_INGEST_SECRET}`
    },
    body: JSON.stringify({
      websiteId: process.env.THEBIZNESS_WEBSITE_ID,
      name: conversion.name,                    // e.g. 'Purchase'
      conversionUrl: conversion.conversionUrl,  // the page where the conversion happened
      amount: conversion.amount,                // in currency units, e.g. 99.99
      currency: conversion.currency,            // ISO code, e.g. 'USD'
      visitorId: conversion.visitorId,          // from the thebizness_visitor_id cookie
      sessionId: conversion.sessionId,          // from the thebizness_session_id cookie
      orderId: conversion.orderId,              // optional: a repeated orderId is not counted twice
      metadata: conversion.metadata             // optional, up to 4 KB (a larger object keeps only email)
    })
  });

  // 429 and 5xx mean the sale was not stored: throw so the caller retries. The same orderId is safe to resend.
  if (response.status === 429 || response.status >= 500) {
    throw new Error(`theBizness.ai responded with ${response.status}`);
  }
  return response.json(); // { success: true, accepted: true, id: '...' }, or { accepted: false, reason: '...' }
}

// Usage: report the sale when your own payment flow confirms it
await sendConversion({
  name: 'Purchase',
  conversionUrl: 'https://yourdomain.com/checkout/success',
  amount: toMajorUnits(order.totalInCents, order.currency),
  currency: order.currency.toUpperCase(),
  visitorId: req.cookies.thebizness_visitor_id, // with cookie-parser in Express
  sessionId: req.cookies.thebizness_session_id,
  orderId: order.id,
  metadata: { email: order.customerEmail } // the customer on the revenue record: only ever send it from your server
});

Endpoint and headers

POST https://thebizness.ai/api/conversions
Content-Type: application/json
Authorization: Bearer <your ingest secret>

Find or rotate the ingest secret on the Configure page. A server request that sends neither the secret nor a browser Origin from your domain is refused with 403, and nothing is stored. Never put the secret in browser code. See the API reference for every field, limit and response.

Use one method per payment processor

If Stripe is connected, do not also forward the same Stripe payments from your own webhook. A sale your server reports with the Checkout Session ID (cs_...) or PaymentIntent ID (pi_...) as the order ID is replaced by Stripe's record, whichever arrives first, and the usage it counted is given back. A different order ID, such as your own order number, cannot be matched, and the sale would still be counted twice. Use the server-side API for sales that Stripe does not report.

Forwarding from your own Stripe webhook

Only if you chose not to connect Stripe, you can forward Stripe payments from a webhook endpoint you run. It needs the checkout metadata from step 3 above, and a webhook for checkout.session.completed and checkout.session.async_payment_succeeded:

// Your own Stripe webhook endpoint (Next.js API route). Use it only when Stripe is NOT connected in your dashboard.
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';

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

// Stripe amounts are in the currency's smallest unit (cents for USD, fils for KWD)
const ZERO_DECIMAL_CURRENCIES = new Set(['BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'VND', 'VUV', 'XAF', 'XOF', 'XPF']);
const THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'JOD', 'KWD', 'OMR', 'TND']);

// Converts a Stripe amount to currency units, e.g. toMajorUnits(5000, 'JPY') === 5000, toMajorUnits(999, 'usd') === 9.99
// and toMajorUnits(1234, 'KWD') === 1.234
function toMajorUnits(amount, currency) {
  const code = String(currency).toUpperCase();
  const decimals = ZERO_DECIMAL_CURRENCIES.has(code) ? 0 : THREE_DECIMAL_CURRENCIES.has(code) ? 3 : 2;
  return amount / 10 ** decimals;
}

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    return NextResponse.json({ error: 'Webhook error' }, { status: 400 });
  }

  // Handle successful payment. Delayed payment methods (ACH, SEPA, Boleto) send completed as 'unpaid' first,
  // so the sale is recorded when async_payment_succeeded arrives instead.
  if (event.type === 'checkout.session.completed' || event.type === 'checkout.session.async_payment_succeeded') {
    const session = event.data.object as Stripe.Checkout.Session;
    if (session.payment_status === 'unpaid') return NextResponse.json({ received: true });

    // Forward the sale to theBizness.ai with the website's ingest secret. It stays on your server.
    const res = await fetch('https://thebizness.ai/api/conversions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${process.env.THEBIZNESS_INGEST_SECRET}`
      },
      body: JSON.stringify({
        websiteId: process.env.THEBIZNESS_WEBSITE_ID,
        name: 'Purchase',
        conversionUrl: 'https://yourdomain.com/checkout/success', // the page where the purchase completes
        amount: toMajorUnits(session.amount_total ?? 0, session.currency ?? 'usd'), // in currency units, e.g. 99.99
        currency: session.currency?.toUpperCase(),
        visitorId: session.metadata?.visitorId, // saved to checkout metadata at checkout creation
        sessionId: session.metadata?.sessionId,
        orderId: session.id, // Stripe checkout session ID, sent on every delivery so retries are not counted twice
        metadata: { email: session.customer_details?.email, stripeSessionId: session.id } // metadata.email is the customer on the revenue record
      })
    });

    // Return a 500 on a transient failure so Stripe retries the webhook. A 202 (accepted: false) is final.
    if (res.status === 429 || res.status >= 500) {
      console.error('theBizness.ai conversion failed:', res.status);
      return NextResponse.json({ error: 'Conversion forward failed' }, { status: 500 });
    }
  }

  return NextResponse.json({ received: true });
}

The handler sends the buyer's email in metadata.email, which is stored with each revenue record. Without it the sale shows as Anonymous. Remove that field if you do not want customer emails stored.

Client-side revenue tracking

Track revenue from your frontend after a successful payment. It is recorded as client-reported, unverified revenue.

Implementation

Add this code to your payment success page or thank you page:

Browser tracking is not a source of truth
  • • The sale is missed if the customer closes the page before the script runs. Connect Stripe or report from your server for revenue you rely on.
  • • The browser can send any value, so client-side totals can be inflated. They are labeled client-reported, and the dashboard keeps them apart from verified revenue.
  • • The amount must be a plain number in currency units, such as 99.99. A formatted string such as "1,500.00" or "$49" stores the event but records no sale, and the response says revenueReason: "invalid_amount".
  • • Never send the customer's email, or other personal data you do not need, from the browser. Everything you send is stored with the revenue record, and the page is public.
  • • Browser reports count toward your plan's tracked events and are dropped beyond 120% of your limit. Revenue from Stripe and from your server is not. A purchase event that includes an amount is recorded as a custom event and as a sale, so it counts as two tracked events. An event without an amount counts as one.

1. Add to the page head (HTML)

<!-- Put this inline script in the page <head>. Calls made before the deferred tracking script loads are queued and replayed. -->
<script>window.thebizness=window.thebizness||function(){(window.thebizness.q=window.thebizness.q||[]).push(arguments)}</script>

2. Call it from your payment success page (JavaScript)

// Call from your payment success page (JavaScript, for a .js file or a <script> block in the body)
// window.thebizness(eventName, props) is defined by the stub in the page head above

function trackPurchase(orderData) {
  window.thebizness('purchase', {
    amount: orderData.amount, // in currency units (e.g., 99.99)
    currency: orderData.currency || 'USD',
    orderId: orderData.orderId
    // Do not send the customer's email from the browser. Report the sale from your server to include it.
  });
}

// Usage after successful payment
trackPurchase({
  amount: 99.99,
  currency: 'USD',
  orderId: 'order_123456' // your own order number. With Stripe connected, use the Checkout Session id instead (see below)
});

// For subscription events (amount is read the same way as for purchases)
function trackSubscription(subscriptionData) {
  window.thebizness('subscription', {
    amount: subscriptionData.amount,
    currency: subscriptionData.currency,
    orderId: subscriptionData.orderId,
    plan: subscriptionData.planName,
    interval: subscriptionData.interval // 'month' or 'year'
  });
}
Stripe connected? Use the Checkout Session ID as the order ID

On a site with Stripe connected, the orderId must be the Checkout Session ID (cs_...) or the PaymentIntent ID (pi_...). Only then does the sale Stripe records replace the browser's report. Any other order ID, such as your own order number, cannot be matched, and the sale is counted twice. Set the Checkout Session's success_url to https://yoursite.com/thank-you?session_id={CHECKOUT_SESSION_ID} and read session_id from the address on the thank-you page:

// Thank-you page of a site with Stripe connected.
// Create the Checkout Session with success_url: 'https://yoursite.com/thank-you?session_id={CHECKOUT_SESSION_ID}'
// Stripe replaces {CHECKOUT_SESSION_ID} with the real id (cs_...) when it sends the buyer back.
const sessionId = new URLSearchParams(location.search).get('session_id');

if (sessionId) {
  window.thebizness('purchase', {
    amount: 99.99, // the real amount of the order, in currency units (not cents)
    currency: 'USD',
    orderId: sessionId // the Checkout Session id, so Stripe's own record of the sale can replace this report (a cs_test_ id is kept out of your analytics as test data)
  });
}

Pros

  • • Quick to implement
  • • No server-side changes needed
  • • Works with any payment processor
  • • Events are sent the moment they happen

Limitations

  • • Can be blocked by ad blockers
  • • Missed if user closes browser
  • • Unverified, and easy to fake
  • • Less reliable than Stripe or server-side reporting

Fields, limits and responses

Everything you can send, and how the API answers

A conversion needs a websiteId, a name and a conversionUrl. An amount (a plain number in currency units, not cents and not a formatted string such as "1,500.00"; at most 1,000,000 from a browser, more for currencies with a small unit such as JPY, KRW or IDR, and up to 1e12 with the ingest secret), a currency, an orderId (a repeated one is recorded once), the visitorId and sessionId cookies and metadata are optional. The API reference lists each field, the request and rate limits, how plan limits are enforced and every response.

API reference

Revenue analytics and reports

What you'll see in your theBizness.ai dashboard

Customer revenue

Total spent and order count for each customer, with first visit and last purchase

Sale sources

Each sale shows the source, referrer and UTM campaign it is linked to

Path to purchase

The visits a customer made before buying, from the first source to the purchase

Revenue by source

Visitors, orders, conversion rate and revenue by source, medium and campaign

Cancellations

Total paid by canceled customers, average subscription length, and average lifetime value

Revenue totals

Verified revenue on the main dashboard, with client-reported conversion value shown separately. Day boundaries follow your website's time zone, which you can change under Configure, Website details.

Start tracking revenue attribution

Ready to see which marketing channels bring in paying customers? Set up revenue tracking and compare sources by the sales they produce.

Stripe connection

Connect your Stripe account for automatic, verified tracking. Do not also forward the same payments from your own webhook.

Revenue dashboard

See total spent, order count and revenue by source

Sale sources

Each sale keeps the source from the visitor's latest tracked visit