Set up revenue attribution to track which marketing channels drive actual sales. Step-by-step implementation guide.
Prerequisites
Before starting:
- Zenovay tracking script installed
- Access to checkout/purchase code
- Revenue goal enabled
Basic Setup
Simple Revenue Tracking
Track revenue when a purchase completes:
// On order confirmation page or in success callback
zenovay('revenue', 99.99, 'USD');
That's it for basic tracking!
With Goal Name
Associate with a specific goal:
zenovay('goal', 'purchase', {
value: 99.99
});
Complete Implementation
Full Revenue Object
Include all relevant data:
zenovay('revenue', 149.99, 'USD', {
// Recommended
order_id: 'ORD-12345',
// Optional - for detailed analysis
items: [
{
id: 'SKU-001',
name: 'Product Name',
price: 49.99,
quantity: 2,
category: 'Electronics'
},
{
id: 'SKU-002',
name: 'Accessory',
price: 50.00,
quantity: 1,
category: 'Accessories'
}
],
// Optional metadata
coupon: 'SAVE10',
shipping: 9.99,
tax: 12.50
});
Implementation by Platform
Shopify
For Shopify stores:
- Go to Settings → Checkout
- Add to "Order status page":
{% if first_time_accessed %}
<script>
zenovay('revenue', {{ total_price | money_without_currency }}, '{{ currency }}', {
order_id: '{{ order_number }}',
items: [
{% for item in line_items %}
{
id: '{{ item.sku }}',
name: '{{ item.title | escape }}',
price: {{ item.final_price | money_without_currency }},
quantity: {{ item.quantity }}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
});
</script>
{% endif %}
WooCommerce
For WordPress/WooCommerce:
- Add to your theme's
functions.phpor a plugin:
add_action('woocommerce_thankyou', 'zenovay_track_revenue');
function zenovay_track_revenue($order_id) {
$order = wc_get_order($order_id);
?>
<script>
zenovay('revenue', <?php echo $order->get_total(); ?>, '<?php echo $order->get_currency(); ?>', {
order_id: '<?php echo $order_id; ?>'
});
</script>
<?php
}
React/Next.js
In your checkout success component:
import { useEffect } from 'react';
function OrderConfirmation({ order }) {
useEffect(() => {
if (order && window.zenovay) {
window.zenovay('revenue', order.total, order.currency, {
order_id: order.id,
items: order.items.map(item => ({
id: item.sku,
name: item.name,
price: item.price,
quantity: item.quantity
}))
});
}
}, [order]);
return <div>Thank you for your order!</div>;
}
Server-Side Tracking
Enterprise PlanFor secure server-side tracking, use the tracking endpoint with your tracking code:
// Node.js example - use the tracking endpoint directly
app.post('/api/order/complete', async (req, res) => {
const order = await processOrder(req.body);
await fetch('https://api.zenovay.com/e/YOUR_TRACKING_CODE', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Forwarded-For': req.headers['x-forwarded-for'] || req.socket.remoteAddress,
},
body: JSON.stringify({
type: 'revenue',
url: req.headers.referer || 'https://yoursite.com/checkout',
revenue: order.total,
order_id: order.id,
currency: 'USD'
}),
});
res.json({ success: true });
});
Attribution Configuration
Attribution Model
Zenovay doesn't make you commit to a single model up front. Open your website's dashboard, go to the Revenue tab, and switch the model directly on the Attribution card. Five models are available:
- Last Touch (default)
- First Touch
- Linear
- Position-Based
- Time-Decay
You can change the model any time to look at the same data through a different lens — there's no separate "default model" or attribution-window setting to configure first.
Choosing an attribution model
The Revenue tab's Attribution card lets you switch between five models per report. Pick the one that matches the question you're asking:
| Model | When to use it |
|---|---|
| Last-Touch (default) | You want to know what closes deals |
| First-Touch | You want to know what drives discovery |
| Linear | You want a balanced, neutral split across the journey |
| Position-Based | You want to reward both discovery (40%) and closing (40%) |
| Time-Decay | You have shorter sales cycles — recent touches weigh more |
The model you pick is saved in the URL, so a refresh keeps it. If most conversions in a period came from a single session, the multi-touch models will look very similar to Last-Touch — that's expected and resolves as more multi-session journeys accumulate. For a closer look at how first-touch and last-touch differ, see First-touch vs last-touch attribution.
Why do all my attribution models show almost the same numbers?
This is expected and not a bug. When most conversions come from visitors who had a single session — or stayed on one channel — before converting, every model gives that channel 100% of the credit, so Last-Touch, First-Touch, Linear, Position-Based, and Time-Decay all look nearly identical.
The models start to diverge for visitors who genuinely moved across several different channels and sessions before converting; the more multi-session journeys you accumulate, the bigger the differences become.
Switching models will not change the AI channel — AI traffic is always credited to its own channel under every model.
Tip: to see the models differ most, compare First-Touch vs Last-Touch — they diverge whenever a visitor's first and final channels are different.
A conversion without a monetary value still counts toward conversion attribution; revenue figures only appear when revenue is attached to the goal.
AI as an Attribution Channel
Zenovay surfaces AI as a first-class channel in the Attribution panel, next to Direct, Organic, Paid, Social, and Referral. When a converting visitor was identified as AI traffic, that conversion is credited to AI instead of folding into Direct or a UTM channel.
Expand the AI row to break it down by AI product:
- Named products — ChatGPT, Claude, Perplexity, Gemini, Copilot, DeepSeek and others, when the visit carried a recognizable AI referrer or UTM parameter.
- Likely AI (unspecified) — visitors the Dark AI heuristic identified as AI-driven even though they arrived with no referrer. These can't be tied to one product, so they're grouped honestly under this label rather than guessed.
Keep in mind:
- This is a conversions view. An AI product that sends visits but no attributable conversion in the selected period won't appear here — use the AI Influence view for AI traffic by product.
- Seeing only Likely AI (unspecified) is accurate, not missing data: it means the period's AI conversions were heuristic-detected without a named referrer.
- AI attribution appears on the authenticated dashboard; it is intentionally not shown on public or shared dashboards.
Cross-Device Attribution
Enterprise PlanTrack across devices:
- Identify users when they log in
- Journey stitched automatically
- Full path visible
// When user logs in
zenovay('identify', userId);
UTM Parameter Setup
Campaign Tracking
Ensure UTM parameters are used:
https://yoursite.com/?utm_source=google
&utm_medium=cpc
&utm_campaign=spring_sale
&utm_content=banner_1
UTM Parameters
| Parameter | Purpose | Example |
|---|---|---|
| utm_source | Traffic source | google, facebook |
| utm_medium | Marketing medium | cpc, email |
| utm_campaign | Campaign name | spring_sale |
| utm_content | Ad variant | banner_a |
| utm_term | Keywords | blue shoes |
Testing Setup
Debug Mode
Enable debug logging:
zenovay('debug');
Then track a test purchase and check console.
Test Purchase
- Place test order
- Check browser console
- Verify in real-time view
- Confirm in revenue report
Verification Checklist
- Revenue value correct
- Order ID recorded
- Items tracked (if sent)
- Source attributed correctly
- Currency correct
Handling Edge Cases
Recurring Revenue
For subscriptions:
// Track initial purchase
zenovay('revenue', 29.99, 'USD', {
order_id: 'SUB-001',
type: 'subscription',
interval: 'monthly'
});
// Track renewals
zenovay('revenue', 29.99, 'USD', {
order_id: 'SUB-001-RENEWAL',
type: 'subscription_renewal',
original_order: 'SUB-001'
});
Refunds
Handle refunds properly:
// Track refund
zenovay('revenue', -49.99, 'USD', {
order_id: 'REFUND-ORD-12345',
type: 'refund',
original_order: 'ORD-12345'
});
Partial Shipments
If revenue is recognized at shipping:
// Track each shipment
zenovay('revenue', 50.00, 'USD', {
order_id: 'ORD-12345-SHIP-1',
parent_order: 'ORD-12345'
});
Data Quality
Prevent Duplicates
Ensure revenue isn't tracked twice:
// Using localStorage to prevent duplicates
function trackOrderOnce(order) {
const tracked = localStorage.getItem(`tracked_${order.id}`);
if (tracked) return;
zenovay('revenue', order.total, 'USD', {
order_id: order.id
});
localStorage.setItem(`tracked_${order.id}`, 'true');
}
Validate Before Sending
Check data quality:
function trackRevenue(orderData) {
// Validate
if (!orderData.value || orderData.value <= 0) {
console.warn('Invalid order value');
return;
}
if (!orderData.order_id) {
console.warn('Missing order ID');
return;
}
// Track
zenovay('revenue', orderData.value, orderData.currency || 'USD', {
order_id: orderData.order_id
});
}
Troubleshooting
Revenue Not Appearing
If revenue doesn't show:
-
Check tracking fires
- Debug mode enabled?
- Console errors?
-
Verify goal setup
- Revenue goal created?
- Goal active?
-
Check timing
- After page fully loads?
- In correct callback?
-
Validate data
- Value is number?
- Not $0?
Attribution Missing
If source not credited:
- Check UTM parameters present
- Verify referrer not blocked
- Try a different attribution model on the Revenue tab's Attribution card
- Confirm the converting visit was tracked at all (check the real-time view)
Amounts Don't Match
If totals differ from accounting:
- Compare date ranges
- Check for refunds
- Verify currency handling
- Look for missing orders
Security Considerations
Client-Side Limitations
Be aware:
- Values can be manipulated
- Not for billing purposes
- For analytics only
Server-Side for Accuracy
For critical accuracy:
- Implement server-side tracking
- Verify against backend
- Use for reporting
Connect a Payment Processor (Server-to-Server)
If you already process payments through a supported processor, you can connect your account to Zenovay instead of editing your checkout code. Zenovay receives webhooks from the processor, attributes each captured payment to the originating session, and feeds the Revenue dashboard automatically — no client-side zenovay('revenue', …) calls required.
The Revenue settings tab lets you connect Stripe, PayPal, and Paddle. Each has a step-by-step setup guide:
| Processor | Setup guide | What's tracked |
|---|---|---|
| Stripe | Stripe revenue integration | Captures, refunds, subscription invoices |
| PayPal | PayPal revenue integration | Captures and refunds |
| Paddle | Paddle revenue integration | Captures and refunds |
Pick your processor on the Revenue settings tab, paste in your credentials, and Zenovay does the rest.
When to choose webhook ingestion over the JS tracker:
- Server-side accuracy — webhook payloads are signed by the processor and can't be tampered with by browsers or ad-blockers.
- No checkout-code changes — open your website's dashboard, go to the Revenue settings tab, choose your processor, and paste in your credentials.
- Refunds handled automatically — when a refund is issued in your processor's dashboard, Zenovay records it.
When to keep using the JS tracker:
- You need item-level data (
items[]) that the processor doesn't include in webhooks. - Your processor isn't in the list above.
- You want the order to appear in the real-time visitor view the moment the customer hits the success page (webhooks arrive a few seconds later).
You can use both approaches on the same site — Zenovay deduplicates by order_id, so the same order won't double-count.