How-tos

Guides

How-tos

Practical walkthroughs for the tasks product teams set up most often. Each guide covers the dashboard steps and, where it applies, code with the React SDK. For full reference material, see the documentation.

Create a custom event

Track a product-specific action — like a newsletter signup or demo request — so it shows up in reports, funnels, and experiment goals.

In the dashboard

  1. Open your property and go to Events/Funnel/Audience in the left nav.
  2. Stay on the Custom events tab and scroll to Add event.
  3. Enter a display name and choose Manual (fired from code) or On click (DOM-triggered) if a button click should record the event with no code.
  4. Confirm the generated token starts with custom_ (for example custom_newsletter_signup). Add optional metadata fields if you plan to break down the event in reports.
  5. Click Create event. The token is live on your next page load — unregistered tokens are dropped at ingestion.

In your React app

React SDK — tracker is ready for you

Wrap your app in BadgerlyticsProvider (see Script Installation → React). The provider loads the script and bridges React to the tracker — use useBadgerlyticsTracker() in components and call trackCustomEvent, trackFunnelStep, trackProductView, and the rest on that object. You don't need window.badgerlytics in React code. For shared modules outside components, use callWhenTrackerReady() from the same package (see useful script functions in the docs).

Use useBadgerlyticsTracker() to call trackCustomEvent when the action happens:

import { useCallback } from 'react';
import { useBadgerlyticsTracker } from '@badgerlytics/sdk/react';
export function NewsletterSignup() {
const tracker = useBadgerlyticsTracker();
const onSubmit = useCallback(() => {
// Token must match the dashboard registration exactly
tracker?.trackCustomEvent('custom_newsletter_signup', {
source: 'footer',
});
}, [tracker]);
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); }}>
{/* … */}
<button type="submit">Subscribe</button>
</form>
);
}

Not using React?

The same token works with badgerlytics.trackCustomEvent() on window.badgerlytics after the script tag loads — see Custom Events in the docs.

Verify and explore

  1. Trigger the event on your site (or use test mode to watch the browser console).
  2. Open Reports → Engagement → Custom Events to confirm counts are flowing.

More on registration limits, DOM triggers, and reporting: Custom Events and Tracking script API.

Create a funnel

Map the ordered steps visitors take toward a goal — for example browsing products through checkout — and measure drop-off between each step.

In the dashboard

  1. Open Events/Funnel/Audience and switch to the Funnels tab.
  2. Under Add funnel, give the funnel a name and token (for example purchase).
  3. Add steps in order. A typical ecommerce purchase funnel looks like:
    • 1 — Product listing page (product_listing_page)
    • 2 — Product display page (product_display_page)
    • 3 — Add to cart (add_to_cart)
    • 4 — View cart (view_cart)
    • 5 — Payment and review (payment_and_review)
    • 6 — Conversion (conversion)
  4. Click Create funnel. Use the row's Tracking example button anytime to copy ready-made trackFunnelStep calls.

In your React app

React SDK — tracker is ready for you

Wrap your app in BadgerlyticsProvider (see Script Installation → React). The provider loads the script and bridges React to the tracker — use useBadgerlyticsTracker() in components and call trackCustomEvent, trackFunnelStep, trackProductView, and the rest on that object. You don't need window.badgerlytics in React code. For shared modules outside components, use callWhenTrackerReady() from the same package (see useful script functions in the docs).

Keep step numbers in a shared module. In components, call trackFunnelStep on the tracker from useBadgerlyticsTracker(); use callWhenTrackerReady() in the helper for non-component call sites:

lib/tracking.jsjavascript
import { callWhenTrackerReady } from '@badgerlytics/sdk/react';
export const FUNNEL_NAME = 'purchase';
export const FUNNEL_STEPS = Object.freeze({
PRODUCT_LISTING_PAGE: { number: 1, name: 'product_listing_page' },
PRODUCT_DISPLAY_PAGE: { number: 2, name: 'product_display_page' },
ADD_TO_CART: { number: 3, name: 'add_to_cart' },
VIEW_CART: { number: 4, name: 'view_cart' },
PAYMENT_AND_REVIEW: { number: 5, name: 'payment_and_review' },
CONVERSION: { number: 6, name: 'conversion' },
});
/** Shared helper for call sites outside React components (e.g. cart context). */
export function trackFunnelStep(step) {
if (!step?.number || !step?.name) return;
callWhenTrackerReady((tracker) => {
tracker.trackFunnelStep(FUNNEL_NAME, step.number, step.name);
});
}
import { useEffect, useCallback } from 'react';
import { useBadgerlyticsTracker } from '@badgerlytics/sdk/react';
import { FUNNEL_NAME, FUNNEL_STEPS } from '@/lib/tracking';
export function ProductDetail({ product }) {
const tracker = useBadgerlyticsTracker();
useEffect(() => {
if (!product || !tracker) return;
tracker.trackFunnelStep(
FUNNEL_NAME,
FUNNEL_STEPS.PRODUCT_DISPLAY_PAGE.number,
FUNNEL_STEPS.PRODUCT_DISPLAY_PAGE.name,
);
}, [product, tracker]);
const handleAddToCart = useCallback(() => {
// …add to cart logic
tracker?.trackFunnelStep(
FUNNEL_NAME,
FUNNEL_STEPS.ADD_TO_CART.number,
FUNNEL_STEPS.ADD_TO_CART.name,
);
}, [tracker]);
return (/* PDP UI */);
}

Note

Fire each step once per session at the moment it happens — for example in a click handler for add_to_cart, not on every re-render.

Read the results

  1. After traffic flows, open Reports → Engagement → Conversion Funnel.
  2. Pick your funnel and date range to see step-by-step completion and drop-off.

Full setup notes and considerations: Funnels.

Integrate a product view event

Record what visitors look at — a product detail page in ecommerce or a plan on your pricing page in SaaS — so view-to-purchase rates and top-viewed items show up in reports.

When to fire it

  • Ecommerce: once per product detail page (PDP) visit.
  • SaaS: once per plan card or pricing row the visitor sees (match product_code to your plan catalogue).

In your React app

React SDK — tracker is ready for you

Wrap your app in BadgerlyticsProvider (see Script Installation → React). The provider loads the script and bridges React to the tracker — use useBadgerlyticsTracker() in components and call trackCustomEvent, trackFunnelStep, trackProductView, and the rest on that object. You don't need window.badgerlytics in React code. For shared modules outside components, use callWhenTrackerReady() from the same package (see useful script functions in the docs).

import { useEffect } from 'react';
import { useBadgerlyticsTracker } from '@badgerlytics/sdk/react';
function dollarsToCents(value) {
return Math.round(Number(value) * 100);
}
export function ProductDetail({ product }) {
const tracker = useBadgerlyticsTracker();
// One product_view per PDP visit powers view-to-purchase reporting.
useEffect(() => {
if (!product || !tracker) return;
tracker.trackProductView({
product_code: product.sku,
name: product.name,
category: product.category,
price: dollarsToCents(product.price), // cents
});
}, [product, tracker]);
return (/* PDP UI */);
}

Note

Amounts are always in cents (integer). The tracking script de-dupes product views per page load, but keeping the call in a useEffect tied to the product or plan id keeps your component predictable.

Verify

  1. Load a PDP or pricing page with test mode enabled and confirm a product_view event in the console.
  2. Open Reports after traffic flows — ecommerce teams use product performance reports; SaaS teams use plan-level conversion views (requires SaaS features on the property).

Payload reference: Ecommerce → Product view, SaaS → Product view, and Tracking script API → Commerce & funnels.

Integrate a cart update event

Snapshot the visitor's cart (or selected plan) after every change so abandoned-cart reporting and plan-level drop-off stay accurate.

When to fire it

  • Ecommerce: after add to cart, remove, or quantity change — send the full cart, not just the delta.
  • SaaS: after the visitor picks a plan and reaches checkout or review (treat the selected plan as a one-line cart).

In your React app

React SDK — tracker is ready for you

Wrap your app in BadgerlyticsProvider (see Script Installation → React). The provider loads the script and bridges React to the tracker — use useBadgerlyticsTracker() in components and call trackCustomEvent, trackFunnelStep, trackProductView, and the rest on that object. You don't need window.badgerlytics in React code. For shared modules outside components, use callWhenTrackerReady() from the same package (see useful script functions in the docs).

import { useCallback } from 'react';
import { useBadgerlyticsTracker } from '@badgerlytics/sdk/react';
function buildCartPayload(items) {
const products = items.map((item) => ({
product_code: item.sku,
name: item.name,
category: item.category,
price: item.priceCents,
quantity: item.quantity,
}));
const subtotal = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
if (subtotal <= 0) return null;
return {
subtotal,
currency: 'USD',
cart_id: 'session',
products,
};
}
export function useCartTracking() {
const tracker = useBadgerlyticsTracker();
// Call after add, remove, or quantity change — always send the full cart.
return useCallback(
(items) => {
const payload = buildCartPayload(items);
if (payload && tracker) tracker.trackCartUpdate(payload);
},
[tracker],
);
}

Skip empty carts

If subtotal is zero (cart cleared), skip the call — empty updates are ignored by reports anyway.

Verify

  1. Add or change items in the cart (or confirm a plan) and watch for a cart_update event in test mode.
  2. Check Reports for abandoned-cart or checkout drop-off after the maturity window closes (see docs for the 1-day abandonment window).

More detail: Ecommerce → Cart update, SaaS → Cart update, and abandoned cart considerations.

Integrate a conversion event on your confirmation page

Fire a conversion when an order or subscription actually completes. This is the revenue event that powers conversion rate, A/B test winners, and (for SaaS) new MRR.

On the confirmation page

  1. Only fire after payment succeeds — typically on the order-confirmation or subscription-success page, not when the user clicks "Pay."
  2. Pass a stable order_id (your order or invoice id, not a timestamp) so a page refresh does not double-count.
  3. Include the full line-item breakdown in products.
  4. Guard with a useRef or sessionStorage flag so React strict mode or remounts do not fire twice.

In your React app

React SDK — tracker is ready for you

Wrap your app in BadgerlyticsProvider (see Script Installation → React). The provider loads the script and bridges React to the tracker — use useBadgerlyticsTracker() in components and call trackCustomEvent, trackFunnelStep, trackProductView, and the rest on that object. You don't need window.badgerlytics in React code. For shared modules outside components, use callWhenTrackerReady() from the same package (see useful script functions in the docs).

import { useEffect, useRef } from 'react';
import { useBadgerlyticsTracker } from '@badgerlytics/sdk/react';
function buildConversionPayload(order) {
return {
order_id: order.id,
order_total: order.totalCents,
total_tax: order.taxCents,
total_shipping: order.shippingCents,
total_discount: order.discountCents,
currency: 'USD',
customer_id: order.customerId,
products: order.lineItems.map((item) => ({
product_code: item.sku,
name: item.name,
category: item.category,
price: item.priceCents,
quantity: item.quantity,
})),
};
}
export function OrderConfirmation({ order }) {
const tracker = useBadgerlyticsTracker();
const trackedRef = useRef(false);
useEffect(() => {
if (!order || !tracker || trackedRef.current) return;
trackedRef.current = true;
tracker.trackConversion(buildConversionPayload(order));
}, [order, tracker]);
return (/* thank-you page */);
}

SaaS: mark recurring revenue

Include billing_interval and interval_count on each product line so the conversion counts as new MRR rather than a one-time purchase. Trial starts can use order_total: 0with the plan's eventual MRR on the line — see SaaS → Conversion.

Verify

  1. Complete a test purchase and confirm a conversion event in test mode.
  2. Open Reports — revenue, conversion rate, and A/B experiment results all read from this event.

Full payloads: Ecommerce → Conversion, SaaS → Conversion, and Tracking script API.

Create an A/B test

Compare two or more variants of a page or component, measure lift vs control, and pick a winner with statistical significance.

In the dashboard

  1. Open Feature flags in the left nav and click New flag / experiment.
  2. Set a Display name — the Test token (for example hero_test) is generated from it and is what you reference in code.
  3. Under Type, choose Experiment (A/B/C).
  4. Define variations. Experiments require a control arm plus one or more variants (v1, v2, …) and a traffic split.
  5. Choose a Winning metric (conversion rate, average order value, or a registered custom event) and optionally set Focus pages or an Audience segment.
  6. Click Create flag, then Start on the flag detail page when you are ready to bucket visitors.

In your React app

Wrap your app in BadgerlyticsProvider (see Script Installation → React), then branch on the assigned variation:

import { useVariation } from '@badgerlytics/sdk/react';
export function Hero() {
const variation = useVariation('hero_test', 'control');
return variation === 'v1' ? <HeroVariantB /> : <HeroVariantA />;
}

Next.js (server-rendered)

Pair the React provider with SSR middleware for Next.js so the first HTML already reflects the assigned variant:

// App Router — requires createBadgerlyticsMiddleware (see docs)
import { headers } from 'next/headers';
import { getVariation } from '@badgerlytics/sdk/nextjs';
export default async function Page() {
const h = await headers();
const hero = getVariation(h, 'hero_test', 'control');
return hero === 'v1' ? <HeroVariantB /> : <HeroVariantA />;
}

Read the results

  1. Open Reports → A/B Testing and select your experiment.
  2. Compare lift, significance, and revenue side by side. When you have a winner, stop the test and ship the winning variant.

Bandit mode, iterations, focus pages, and more examples (Remix, Astro, Nuxt): Feature Flags and SSR middleware (SDK).

Create an on/off feature flag

Roll a feature out gradually or gate it behind a switch — without running a full A/B comparison or significance analysis.

In the dashboard

  1. Go to Feature flags and click New flag / experiment.
  2. Enter a Display name and note the generated Test token (for example new_pricing_table).
  3. Under Type, choose Feature flag (on/off). Traffic splits between control (off) and v1 (on).
  4. Raise the v1 weight to ramp the feature up (new flags start at 100% off).
  5. Click Create flag, then Start to begin assigning visitors.

In your React app

Use useIsFlagEnabled() for a simple boolean gate:

import { useIsFlagEnabled } from '@badgerlytics/sdk/react';
export function PricingSection() {
const newPricingEnabled = useIsFlagEnabled('new_pricing_table');
return newPricingEnabled ? <PricingV2 /> : <PricingV1 />;
}

Next.js (server-rendered)

With middleware installed, read the flag on the server before sending HTML:

import { headers } from 'next/headers';
import { isFlagEnabled } from '@badgerlytics/sdk/nextjs';
export default async function Page() {
const h = await headers();
const showNewPricing = isFlagEnabled(h, 'new_pricing_table');
return showNewPricing ? <PricingV2 /> : <PricingV1 />;
}

Install steps and framework-specific APIs: Feature Flags → Code: React, Code: Next.js, and SSR middleware → Next.js.

When the test is done

Stop assigning new visitors, deactivate, and archive flags you no longer need — active flags add to the visitor assignment cookie. See starting and stopping tests.

Create an audience trait for a test

Register a trait so you can target an A/B experiment to a slice of visitors — for example logged-in users on the pro plan.

Register the trait

  1. Open Events/Funnel/Audience and switch to the Audience tab.
  2. Under Add trait, enter a label (for example "Signed in") and pick a type: boolean, string, or number.
  3. Confirm the generated token (for example signed_in) and click Add trait.

Send trait values from your app

Update traits whenever the visitor's state changes — login, logout, plan upgrade:

import { useSetTraits } from '@badgerlytics/sdk/react';
export function useAuthTraits(user) {
const setTraits = useSetTraits();
async function syncTraits() {
if (user) {
await setTraits({ signed_in: true, plan: user.plan });
} else {
await setTraits({ signed_in: false, plan: null });
}
}
return { syncTraits };
}

Note

For SSR apps, also set the _bai_traits cookie on login so segment rules apply on the first render — see Audiences → Code examples and SSR middleware (SDK).

Target an experiment

  1. Open an Experiment (A/B/C) under Feature flags (create a new one or edit a draft).
  2. Scroll to Audience segment and switch from All visitors to a custom rule.
  3. Add a condition using your trait — for example signed_in equals true, or plan equals pro.
  4. Save and Start the experiment. Only visitors who match the segment at assignment time are bucketed into the test.

Built-in traits like visitor_type and device_type work without registration. Full reference: Audiences and Feature Flags → Audiences.

Geolocate visitors to show or hide content

Read a visitor's IP-derived region from the tracker and conditionally render UI — for example a Texas-only promotion banner.

How location works

Badgerlytics resolves location from the visitor's IP when a new session starts. The result is cached for that session (about 30 minutes) and exposed through getLocation(). Wait for onSessionStartReady before reading — calling too early returns null.

In your React app

React SDK — tracker is ready for you

Wrap your app in BadgerlyticsProvider (see Script Installation → React). The provider loads the script and bridges React to the tracker — use useBadgerlyticsTracker() in components and call trackCustomEvent, trackFunnelStep, trackProductView, and the rest on that object. You don't need window.badgerlytics in React code. For shared modules outside components, use callWhenTrackerReady() from the same package (see useful script functions in the docs).

This example shows a custom component only when regionCode is TX. Call getLocation() on the tracker from useBadgerlyticsTracker():

import { useEffect, useState } from 'react';
import { useBadgerlyticsTracker } from '@badgerlytics/sdk/react';
export function TexasPromoBanner() {
const tracker = useBadgerlyticsTracker();
const [showTexasOffer, setShowTexasOffer] = useState(false);
useEffect(() => {
if (!tracker) return;
const apply = () => {
const loc = tracker.getLocation();
setShowTexasOffer(loc?.regionCode === 'TX');
};
if (typeof tracker.onSessionStartReady === 'function') {
tracker.onSessionStartReady(apply);
} else {
apply();
}
}, [tracker]);
if (!showTexasOffer) return null;
return <TexasOnlyPromo />;
}

Other fields

getLocation() also returns country, city, region (full name), and timezone. Use whichever fits your targeting rule.

Alternatives

  • For experiment bucketing by geography, use an Audience segment with a context condition instead of branching in code — see Audiences → Setup.
  • For static sites without React, call badgerlytics.getLocation() on window.badgerlytics inside onSessionStartReady — see Tracking script API → Visitor location.

Integrate a subscription webhook

Forward subscription lifecycle changes from your billing system so MRR, churn, expansion, and cancellation-reason reports stay accurate after signup. SaaS properties only — ecommerce orders do not use this event.

In the dashboard

  1. Enable SaaS features on the property (when creating or editing it under Properties).
  2. Define your plan catalogue under SaaS Setup Manage plans so plan codes in webhook payloads match.
  3. Open Settings → API Keys for your organization and create a secret API key. subscription_change is server-only — browser calls are rejected by design.
  4. Point your billing provider's webhook (Stripe, Paddle, etc.) at a route in your backend that forwards qualifying events to Badgerlytics.

Do not forward new subscriptions

New MRR already comes from trackConversion at checkout. Only forward post-signup changes — cancellations, upgrades, downgrades, reactivations, and trial conversions — or every signup is counted twice.

Node.js helper

Add a small module your webhook route can call. Each request needs a fresh unique_event_id; retries with the same id are deduplicated on our side.

lib/badgerlytics.jsjavascript
import crypto from 'node:crypto';
const INGEST_URL = 'https://events.badgerlytics.com/ingestion-api/events';
export async function reportSubscriptionChange({
propertyId,
secretKey,
subscriptionId,
customerId,
changeType,
mrrDeltaCents,
previousPlanCode = '',
newPlanCode = '',
cancelReason = '',
}) {
const body = {
property_id: propertyId,
event_type: 'subscription_change',
unique_event_id: crypto.randomUUID(),
session_id: 'srv_' + crypto.randomUUID(),
visitor_id: 'srv_' + customerId,
event_time: new Date().toISOString(),
customer_id: customerId,
subscription_data: {
subscription_id: subscriptionId,
customer_id: customerId,
change_type: changeType,
mrr_delta_cents: mrrDeltaCents,
previous_plan_code: previousPlanCode,
new_plan_code: newPlanCode,
currency: 'USD',
cancel_reason: cancelReason,
effective_at: new Date().toISOString(),
},
};
const res = await fetch(INGEST_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + secretKey,
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error('Badgerlytics ingest failed: ' + res.status);
}

Wire it to your billing webhook

Verify your provider's signature, map the event to a change_type, and call the helper. mrr_delta_cents should be the monthly impact in cents (negative for churn, positive for expansion):

routes/billing-webhook.jsjavascript
// Example: call from your billing provider's webhook handler
import { reportSubscriptionChange } from './lib/badgerlytics.js';
export async function handleBillingWebhook(req, res) {
// Verify the provider signature before trusting req.body (Stripe, Paddle, etc.)
const event = req.body;
if (event.type === 'customer.subscription.deleted') {
const sub = event.data.object;
await reportSubscriptionChange({
propertyId: process.env.BADGERLYTICS_PROPERTY_ID,
secretKey: process.env.BADGERLYTICS_SECRET_KEY,
subscriptionId: sub.id,
customerId: String(sub.customer),
changeType: 'cancel',
mrrDeltaCents: -15900, // monthly MRR removed, in cents
previousPlanCode: sub.items?.data?.[0]?.price?.lookup_key || '',
cancelReason: sub.cancellation_details?.reason || '',
});
}
// Handle upgrade / downgrade / reactivate similarly — see docs for change types.
res.sendStatus(200);
}

Change types

Use cancel, upgrade, downgrade, renewal, reactivate, trial_started, or trial_converted. Other values are rejected at ingest.

Verify

  1. Send a test payload with curl (see docs) or trigger a cancel/upgrade in your billing sandbox.
  2. Open Reports → SaaS lifecycle views after events process (allow a few minutes for ingestion).
  3. Optional: use the SaaS Setup wizard in the app — it generates a Stripe forwarder snippet pre-filled for your property.

Endpoint, auth, curl example, and adapters for other languages: SaaS → Lifecycle events webhook and SaaS Reporting Setup.

Integrate a usage charge webhook

Forward usage-based and pack revenue from your billing system so Usage Revenue reports stay accurate. Fire a charge when the customer is actually billed — not on every meter tick. SaaS properties only; keep this separate from subscription MRR.

In the dashboard

  1. Enable SaaS features on the property (when creating or editing it under Properties).
  2. Define each billable meter, overage, or pack under SaaS SetupUsage items (or Manage usage items). The usage_item_code you create is the whitelist token your backend must send — unknown codes are rejected at ingest.
  3. Open Settings → API Keys for your organization and create a secret API key (same keys as lifecycle events). usage_charge is server-only — browser calls are rejected by design.
  4. Point your billing provider's invoice / charge webhook at a route in your backend that forwards qualifying line items to Badgerlytics.

Do not fold usage into MRR

Usage revenue is its own report stream. Do not put overage or pack amounts into mrr_delta_cents on subscription_change — keep committed subscription MRR clean and report recognized charges here instead.

Charge when billed, not when metered

Send usage_charge when an invoice is finalized or paid, or when a one-time pack purchase completes. Wait until the billing system has turned usage into a line item with an amount.

Node.js helper

Add a small module your webhook route can call. Required on usage_data: a whitelisted usage_item_code and amount_cents (integer). Optional: quantity (defaults to 1), plan_code (when set, Usage Revenue breaks down by plan; when omitted, charges appear under Unaffiliated), plus customer_id / subscription_id for attribution.

lib/badgerlytics-usage.jsjavascript
import crypto from 'node:crypto';
const INGEST_URL = 'https://events.badgerlytics.com/ingestion-api/events';
export async function reportUsageCharge({
propertyId,
secretKey,
customerId,
subscriptionId = '',
usageItemCode,
amountCents,
quantity = 1,
planCode = '',
uniqueEventId = null,
}) {
const body = {
property_id: propertyId,
event_type: 'usage_charge',
unique_event_id: uniqueEventId || crypto.randomUUID(),
session_id: 'srv_' + crypto.randomUUID(),
visitor_id: 'srv_' + customerId,
event_time: new Date().toISOString(),
customer_id: customerId,
subscription_id: subscriptionId,
usage_data: {
usage_item_code: usageItemCode,
amount_cents: amountCents,
quantity,
plan_code: planCode,
customer_id: customerId,
subscription_id: subscriptionId,
},
};
const res = await fetch(INGEST_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + secretKey,
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error('Badgerlytics ingest failed: ' + res.status);
}

Wire it to your invoice webhook

Verify your provider's signature, map metered / overage / pack price keys to your usage item codes, and call the helper. Use a stable unique_event_id per charge (for example the invoice line id) so retries do not double-count:

routes/invoice-webhook.jsjavascript
// Example: forward metered / overage / pack lines from invoice.paid
import { reportUsageCharge } from './lib/badgerlytics-usage.js';
// Map Stripe price lookup_key → usage_item_code from SaaS Setup → Usage items
const USAGE_ITEM_BY_PRICE_LOOKUP = {
// api_overage_monthly: 'api_overage',
// token_pack_1000: 'token_pack',
};
export async function handleInvoiceWebhook(req, res) {
// Verify the provider signature before trusting req.body
const event = req.body;
if (event.type !== 'invoice.paid' && event.type !== 'invoice.finalized') {
return res.sendStatus(200);
}
const invoice = event.data.object;
const lines = invoice.lines?.data || [];
for (const line of lines) {
const lookup = line.price?.lookup_key || '';
const usageItemCode = USAGE_ITEM_BY_PRICE_LOOKUP[lookup];
if (!usageItemCode) continue; // skip base subscription lines / unknown SKUs
if (!line.amount) continue;
await reportUsageCharge({
propertyId: process.env.BADGERLYTICS_PROPERTY_ID,
secretKey: process.env.BADGERLYTICS_SECRET_KEY,
customerId: String(invoice.customer || ''),
subscriptionId: String(invoice.subscription || ''),
usageItemCode,
amountCents: line.amount,
quantity: line.quantity || 1,
// Optional: set plan_code so Usage Revenue breaks down by plan
planCode: '',
uniqueEventId: String(line.id), // stable id so retries do not double-count
});
}
res.sendStatus(200);
}

Verify

  1. Send a test payload with curl (see docs) or trigger a paid overage / pack invoice in your billing sandbox.
  2. Open Reports → Usage Revenue after events process (allow a few minutes for ingestion).
  3. Optional: use the SaaS Setup wizard in the app — it generates a Stripe invoice forwarder snippet pre-filled for your property and usage items.

Endpoint, auth, curl example, and Stripe adapter: SaaS → Usage charge webhook, Usage items, and SaaS Reporting Setup.