Revenue Attribution
Revenue attribution connects the payments your customers make to the marketing that brought them to your site. Once your payments reach Spreeflo, every campaign, journey, email, web push, form and traffic source shows what it earned, not just how many people opened or clicked it.
What is revenue attribution?
A customer rarely buys on their first visit. They might find you through a Google search, come back a week later from your newsletter, and finally pay after clicking a web push. Revenue attribution looks at the visits a buyer made before paying and decides how much of the payment each of those visits earned. The credit then rolls up to whatever sent that visit: a channel, a source, a UTM campaign, or a Spreeflo campaign, journey, email or notification.
Spreeflo needs two things to do this:
Why it matters
Opens, clicks and visitors tell you what got attention. Revenue tells you what paid off. With attribution in place you can:
How it works
Attribution happens in three steps:
sf_t) that names the exact campaign and template. When someone clicks through, the SDK records the visit with its channel, source and campaign. UTM parameters you already put on a link are kept as they are. Only visits that arrived through a channel count as touches. Direct visits (typed URLs, bookmarks) don't earn credit, so a payment with no earlier channel visit in the window shows up as Direct. Returning from a checkout page on Stripe, PayPal, Paddle, Lemon Squeezy, Polar or Dodo Payments also counts as Direct, so the payment page never takes credit for the sale.
Only clicks earn credit. An email that was opened but never clicked, or a web push that was delivered but not tapped, earns no revenue. The recipient has to land on your site from the message.
Attribution models
Choose a model in Web Analytics > Settings > Attribution. The model decides how a payment is split between the visits before it.
| Model | How credit is split | Best for |
|---|---|---|
| Last-touch (default) | The whole payment goes to the last channel visit before the purchase. | Seeing what closes the sale. Simple and easy to explain. |
| Linear (Professional plan) | The payment is split evenly across every channel visit in the window. | Longer buying cycles, where awareness and nurture campaigns deserve a share. |
Under Linear, a visit's share of a payment also counts as a share of an order, so a campaign can show fractional orders such as 2.5. The model in use is shown as a chip next to revenue in Web Analytics.
Lookback window
The lookback window, also in Web Analytics > Settings > Attribution, sets how far back a visit can be and still earn credit: 7, 30 (the default) or 90 days. With a 30-day window, a purchase on January 30 can be credited to visits from January 1 to 30. A longer window suits considered purchases and B2B, while a shorter one keeps credit on the campaigns that drove the decision.
Credit is calculated when you open a report, so a change to the model or window shows up in your past revenue as well as new revenue.
Attributed and influenced revenue
Spreeflo reports two kinds of revenue, depending on whether the touchpoint brings people to your site or lives on it.
| Attributed revenue | Influenced revenue | |
|---|---|---|
| Applies to | Campaign and journey emails, web pushes, transactional emails, and traffic sources. | Forms (inline, hosted and popup) and the popups a campaign or journey shows. |
| What counts | Payments after a click that landed on your site, split using your model and window. | Any purchase by the same person within the lookback window after they submitted the form or popup. |
| Adds up? | Yes. Each payment is shared out, so totals across rows never exceed your revenue. | No. A purchase counts in full for every form and popup before it. |
| Refunds | Subtracted | Not subtracted |
Connect a payment provider
If you take payments with Stripe, Paddle, Lemon Squeezy or Polar, connect it once and payments flow in automatically. Revenue settings are per audience, so connect each audience that has its own website and payments.
Spreeflo creates the webhook in your provider account for you and checks the signature of every event it receives. You don't need to copy a webhook URL anywhere. Disconnecting stops tracking right away and removes the webhook.
| Provider | What to paste | What's recorded |
|---|---|---|
| Stripe | A restricted API key (rk_live_…). Use the API key link in the form: it opens Stripe with the right permissions already selected. Don't change them. | Paid Checkout sessions, PaymentIntents, paid invoices (subscriptions and renewals) and refunds. |
| Paddle | An API key (pdl_live_…) from Developer tools > Authentication, set to never expire, with Transaction (Read) and Notification settings (Read and Write). | Completed transactions and approved refunds. |
| Lemon Squeezy | Your Store ID and an API key, both from your Lemon Squeezy settings. | Orders, subscription payments and their refunds. |
| Polar | An organization access token (polar_oat_…) from Settings > General > Developer, with no expiry and the scopes checkouts:read, orders:read, organizations:read, products:read, subscriptions:read and webhooks:write. | Paid orders and refunds. |
Each payment is recorded as a purchase event, before tax, from the moment you connect. Webhook retries and duplicate events for the same payment are recorded once. In the same tab, the Currency card sets the currency your revenue is reported in (see Currency).
Link payments to visits
Connecting a provider brings your revenue in. To credit each payment to the visits before it, pass the visitor's Spreeflo IDs into the payment's metadata under the keys spreelytics_visitor_id and spreelytics_session_id. Read them in the browser when the visitor clicks to pay, and send them to your server with the checkout request:
// In the browser, when the visitor clicks to pay
const ids = {};
const visitorId = Spreelytics.getVisitorId();
const sessionId = Spreelytics.getSessionId();
if (visitorId) ids.spreelytics_visitor_id = visitorId;
if (sessionId) ids.spreelytics_session_id = sessionId;
// Send `ids` to your server along with the checkout requestgetVisitorId() returns null until the SDK has loaded, so read it at checkout rather than on page load, and leave the key out when it's null. Where the IDs go depends on how you take the payment:
| Checkout | Where to put the IDs |
|---|---|
| Stripe Checkout, one-time payment | Both the session's metadata and payment_intent_data.metadata. |
| Stripe Checkout, subscription | subscription_data.metadata. Every paid invoice, renewals included, then carries them. |
| Stripe PaymentIntent without Checkout | The PaymentIntent's metadata. |
| Stripe Payment Link | Not possible: the link is the same for everyone. Payments are still recorded and matched by email. |
| Paddle | customData when you open the checkout with Paddle.js. |
| Lemon Squeezy | Custom checkout data: checkout[custom][spreelytics_visitor_id]=… on a checkout link, or checkout_data.custom when you create the checkout through the API. |
| Polar | metadata on a checkout you create through Polar's API. Checkout links can't carry it. |
| Anything else | visitorId and sessionId in the Revenue API request (see Other payment providers). |
A Stripe Checkout session for a one-time payment, created on your server with the IDs from the browser:
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: 'price_123', quantity: 1 }],
success_url: 'https://example.com/thanks',
metadata: ids,
payment_intent_data: { metadata: ids },
});A Paddle checkout, opened in the browser:
Paddle.Checkout.open({
items: [{ priceId: 'pri_123', quantity: 1 }],
customData: ids,
});Other payment providers (Revenue API)
Use the Revenue API if you bill through a provider that isn't listed (PayPal, Chargebee, Recurly, Shopify, a bank transfer, your own billing system) or record deals closed by hand. It's the Other option on the Revenue tab. You send each payment from your server, usually from the webhook handler your billing system already calls, and Spreeflo attributes it exactly like a connected provider's payments.
Set it up
GET /lists with your key. The id of the audience your website tracks into is the audienceId you'll send. Spreelytics.getVisitorId() when the visitor clicks to pay, as in Link payments to visits, and store it with the order: in your provider's custom metadata, a hidden form field, or your own order record. Your server can also read it from the spreelytics_vid cookie on a first-party request. POST https://api.spreeflo.com/1.0/revenue once it succeeds.curl -X POST https://api.spreeflo.com/1.0/revenue \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"audienceId": "123e4567-e89b-12d3-a456-426614174000",
"eventName": "purchase",
"revenue": { "amount": 79.99, "currency": "USD" },
"transactionId": "order_1042",
"email": "jane@example.com",
"visitorId": "vid_9f8c1c2a3d4e"
}'In practice you'll call it from the handler that already receives your billing system's "payment succeeded" webhook:
// Your billing system's "payment succeeded" webhook
app.post('/webhooks/billing', async (req, res) => {
const order = req.body;
await fetch('https://api.spreeflo.com/1.0/revenue', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SPREEFLO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
audienceId: process.env.SPREEFLO_AUDIENCE_ID,
eventName: 'purchase',
revenue: { amount: order.total, currency: order.currency }, // major units, before tax
transactionId: order.id,
email: order.customerEmail,
visitorId: order.metadata.spreelytics_visitor_id, // saved at checkout
sessionId: order.metadata.spreelytics_session_id,
timestamp: order.paidAt,
}),
});
res.sendStatus(200);
});Request fields
| Field | Required | Notes |
|---|---|---|
audienceId | Yes | The audience to credit the revenue to. |
eventName | Yes | The name the payment is recorded under, such as purchase or subscription_payment. |
revenue.amount | Yes | A positive amount in major units (79.99, not 7999 cents), before tax. |
revenue.currency | Yes | A three-letter ISO 4217 code such as USD or EUR. |
transactionId | Yes | Your ID for the payment. Sending the same ID again records it once, so retries are safe. |
visitorId | No | The visitor ID from Spreelytics.getVisitorId(). The most reliable way to credit the right visits. |
sessionId | No | The session ID from Spreelytics.getSessionId(), to tie the payment to the exact visit. |
email | No | Matches the payment to an existing contact when there's no visitor ID. It never creates a contact. |
name, customerId | No | Kept with the payment. |
renewal, isFreeTrial | No | Mark a subscription renewal, or a payment that starts a free trial. |
refunded, refundId | No | Record a refund (see below). |
timestamp | No | ISO 8601 time of the payment. Defaults to now and can't be in the future. Set it when a payment reaches you late, so revenue lands on the right day. |
Unknown fields are rejected, so send only the fields above. The full reference is under Revenue in the API Reference.
Record refunds
Send the original transactionId again with refunded: true and a positive amount, never a negative one. With a refundId, the amount is that one refund, which lets you record several partial refunds. Without one, the amount is the total refunded so far. A refund can never exceed the original payment.
{
"audienceId": "123e4567-e89b-12d3-a456-426614174000",
"eventName": "purchase",
"revenue": { "amount": 20.00, "currency": "USD" },
"transactionId": "order_1042",
"refunded": true,
"refundId": "refund_77"
}Responses
A successful call returns the transactionId and a message saying what happened:
| message | Meaning |
|---|---|
attributed | Recorded and tied to a known visitor. |
recorded | Recorded and counted in your revenue totals, but not tied to a visitor. Check that you're sending a visitorId. |
skipped (duplicate) | This transactionId was already recorded. Nothing changed. |
refunded | The refund was applied. |
skipped (already refunded) | This refund was already recorded, or the payment is fully refunded. |
transaction not found | A refund arrived before its payment. Send the payment first, then retry the refund. |
How payments are matched
Whichever way a payment arrives, Spreeflo ties it to a person in this order:
A payment is never rejected for missing IDs. Passing the visitor ID gives the most accurate results, especially for buyers who never gave you their email before paying.
Currency
Payments keep their original amount and currency, and are also converted into your audience's reporting currency at that day's exchange rate. Set the reporting currency in the Currency card on the Revenue tab (USD by default). You can change it once a day. All existing revenue is then re-converted, and Web Analytics shows a notice until that finishes.
Where to see revenue in Spreeflo
Revenue shows up next to the numbers you already use to make decisions. Until an audience has recorded a payment, revenue columns show — and a tooltip on how to turn tracking on.
Web Analytics
purchase, or the eventName you send to the Revenue API) reports the revenue of its conversions. Campaigns and journeys
The Campaigns & Journeys list has Revenue and Orders columns for each channel. On the Email and Web push tabs they show attributed revenue from clicks. On the Forms tab they show influenced revenue from the campaign's popups. A campaign's report shows an Attributed revenue tile with its orders, and a row's Analytics action opens Web Analytics on that campaign's UTM breakdown in revenue mode.
Emails and web push
The Marketing emails, Transactional emails and Web push lists show a Revenue figure for each template. Its tooltip adds orders and revenue per recipient. Because revenue is counted per template, you can compare subject lines, designs and offers by what they earned.
Transactional emails (receipts, onboarding, shipping updates) are credited only for links that point at your tracked website, so links to other sites, like password resets and file downloads, are never changed. Turn this off for a template with Credit revenue to this email in the template settings.
Forms and popups
The Forms list has a Revenue column showing influenced revenue: purchases by people within your lookback window after they submitted the form. Its tooltip also covers people who only viewed it. Use it to see which lead magnets and signup forms bring in buyers, not just leads.
Contacts
Each contact's profile has a Revenue card with their lifetime revenue after refunds, purchases, average order value, amount refunded, and first and last purchase dates. In their activity timeline, each purchase shows its amount and a Credited to line naming the campaign or channel whose visit earned it.
Home
The recent activity card on Home has a Revenue column for your latest campaigns and journeys.
AI assistants
Through the Spreeflo MCP server, Claude, ChatGPT and other AI clients can read revenue as well. Campaign and web push performance include revenue and orders, and traffic breakdowns include revenue per channel or source. You can ask something like "which of last month's campaigns earned the most per recipient?"
Decisions you can make with revenue
| Question | Where to look |
|---|---|
| Which channels deserve more budget? | Web Analytics, Acquisition breakdown in Revenue mode, compared with Visitors mode. |
| Is this campaign worth running again? | Campaigns & Journeys list, Revenue and Orders columns, then the campaign report. |
| Which email version should I keep? | Marketing emails list, revenue per recipient in the Revenue tooltip. |
| Are my receipts and onboarding emails selling? | Transactional emails list, Revenue per template. |
| Which form brings in buyers, not just leads? | Forms list, Revenue column. |
| Which landing pages are worth improving first? | Web Analytics, Pages and Entry pages in Revenue mode, then Revenue / visitor. |
| Who are my best customers? | Contact profiles, Revenue card, and the Users tab in Web Analytics. |
Troubleshooting
recorded or attributed with the right audienceId. spreelytics_visitor_id in your checkout metadata, or visitorId in the Revenue API call. Make sure the SDK runs on the pages where checkout starts. Need Immediate Help? Contact our support team at support@spreeflo.com or check our other guides for more information.