Fits your existing stack
We pull ad spend via the platforms' APIs, and conversions from your site, shop, and CRM. So next to every source you see not just the count, but the cost and the revenue too.
Conversion types
Everything that drives revenue counts
Not just the checkout. Also the phone call on Tuesday morning and the deal that lands in your CRM three weeks later.
Forms
Quote, contact, download
Bookings
Restaurant, appointment, table
Phone calls
Phone click or confirmed call via call tracking
Emails
Click on an email link, not a received email
Online store orders
With recorded order value
Offline deals
CRM import of closed revenue
Integrations
Connects to your stack, not the other way around
Cost data from the ad platforms, conversions from your site and shop. Combined in one report with real ROAS per source.
Advertising sources and analytics
- Google Ads
- Meta Ads
- LinkedIn Ads
- TikTok Ads
- Microsoft Ads
- GA4
Online store and website platforms
- WordPress
- Shopify
- WooCommerce
- Google Tag Manager
- Magento
- Webflow
- Wix
- Squarespace
- Custom code (JS + API)
Installation per platform
Place the base pixel first. Then configure conversions and values and check receipt. The steps depend on your platform, access and checkout process; a developer's help may be needed.
WordPress
Depends on platform and accessScript in your theme's <head>, using the theme editor or your theme's 'header scripts' field.
- 1Log in to your admin area at jouwdomein.nl/wp-admin.
- 2In the black menu on the left, click Appearance → Theme File Editor.
- 3In the 'Theme Files' list on the right, select Theme Header (header.php). If you cannot see this list, use step 6.
- 4Find the </head> line in the code, usually around lines 20–40. Tip: use Ctrl+F (Cmd+F on Mac) in the editor.
- 5Place your cursor on the blank line immediately above </head>, paste the script and click Update File at the bottom.
- 6Using Elementor, Divi, Astra or a child theme? Go to Appearance → Customize → Additional CSS/Head code, or to the theme's 'Header scripts' / 'Custom code' setting, and paste the script into the HEAD field.
- 7No access to code? Install the 'WPCode' or 'Insert Headers and Footers' plugin, go to Code Snippets → Header & Footer, paste the script into the Header box and click Save.
Check that it works
- • Open your website in a private window and visit two pages.
- • In Roots, go to Websites & pixel: check that a real visit has been received and the pixel is marked as verified.
Please note
- • Do not paste the script into footer.php: ad click IDs would be read too late.
- • Caching plugins (WP Rocket, LiteSpeed, W3 Total Cache) serve the old page: clear the cache after saving.
A theme update may overwrite header.php. Prefer a child theme or the WPCode plugin.
Shopify
Depends on platform and accessBase pixel in theme.liquid; purchases via a Custom Pixel in Customer events.
- 1Open Shopify → Online Store → Themes → active theme → Edit code → Layout → theme.liquid. Place the base pixel before </head> and save.
- 2For purchases, go to Settings → Customer events → Add custom pixel. Name the pixel, paste the Shopify code below and replace sr_xxx with your pixel key.
- 3Set analytics consent according to your Shopify privacy settings and connect the Custom Pixel. The adapter uses checkout_completed and Shopify browser.cookie, not your storefront's window.
- 4The adapter sends order ID, amount and currency. Without recorded Roots advertising consent, it sends neither email nor phone number and does not enable forwarding to platforms.
- 5Also use the order webhook for server confirmation. Configure the Roots webhook key and Shopify HMAC signature through a secure server integration.
const ROOTS_KEY = "sr_xxx";
const ROOTS_ENDPOINT = "https://rootsmetrics.com/api/public/collect";
let privacyStatus = init.customerPrivacy;
const privacyApi = typeof api !== "undefined" ? api.customerPrivacy : customerPrivacy;
privacyApi.subscribe("visitorConsentCollected", (event) => {
privacyStatus = event.customerPrivacy;
});
// Self-contained adapter inside Shopify's sandbox, not the storefront window.
const srAttribution = {
async track(name, { value, currency, order_id, email, phone }, event) {
if (!privacyStatus.analyticsProcessingAllowed) return;
const visitor = await browser.cookie.get("sr_vid");
const url = new URL(event.context.document.location.href);
const response = await fetch(ROOTS_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
key: ROOTS_KEY,
visitor_id: visitor || event.clientId,
event_type: "conversion",
event_name: name,
value_cents: Math.round(Number(value) * 100),
currency,
external_id: "order:" + order_id,
dedup_id: "order:" + order_id,
page_url: url.origin + url.pathname,
consent_state: "unknown",
advertising_consent: false
// Shopify permission is not a recorded Roots advertising consent.
// Email and phone are intentionally not transmitted by this adapter.
})
});
if (!response.ok) throw new Error("Roots purchase was not accepted: " + response.status);
}
};
analytics.subscribe("checkout_completed", async (event) => {
const checkout = event.data.checkout;
if (!checkout.order?.id) return;
await srAttribution.track("purchase", {
value: checkout.totalPrice.amount,
currency: checkout.currencyCode,
order_id: checkout.order.id.split("/").pop(),
email: checkout.email,
phone: checkout.phone
}, event);
});Check that it works
- • Check a visit on your published store domain in Roots. This alone confirms the base pixel, not purchase tracking yet.
- • Place a Shopify test order and compare order ID, amount and currency in Roots. Check that the pixel and webhook do not count the same order twice.
Please note
- • Shopify Custom Pixels run in a sandbox. The storefront's srAttribution is unavailable there; use the full adapter below.
- • Additional scripts has been phased out. Only use it for older stores where Shopify still offers it, not for new installations.
- • Checkout on another domain requires an allowed origin on your Roots website. Without visitor linking, the source may remain unknown.
checkout_completed may be missing if the thank-you page does not load. An authenticated order webhook provides additional server confirmation, not a substitute for consent.
Magento / Adobe Commerce
Depends on platform and accessScript via Content → Design → Configuration in your store view's HTML Head.
- 1Log in to your Magento admin panel (usually jouwdomein.nl/admin).
- 2In the left menu, click Content → Configuration under 'Design'.
- 3You will see a table with Default, Website and Store View. Click Edit on the right of the store view you want to track.
- 4Expand the HTML Head section (click its title).
- 5Paste the script into 'Scripts and Style Sheets', below existing lines, not between them.
- 6Click Save Configuration at the top right.
- 7Go to System → Tools → Cache Management, select Page Cache and Configuration, choose Refresh and click Submit.
Check that it works
- • Open your storefront in a private window and navigate to a category page.
- • In Roots, check Test your tracking to see whether pageviews are coming in.
Please note
- • If you edit Default instead of the store view, you will also track test environments.
- • Do not forget to refresh the cache: otherwise the old head HTML will still be served.
Webflow
Depends on platform and accessScript in your project's site-wide custom code head.
- 1Open your project in the Webflow Designer.
- 2Click the Webflow logo at the top left → Site settings (or go from your dashboard to the project → Settings).
- 3Click the Custom code tab at the top.
- 4Paste the script into the top box: 'Head code' (above 'Footer code').
- 5Click Save changes at the top right.
- 6Then click Publish at the top right → select your domain(s) → Publish to selected domains.
Check that it works
- • Open your published site (not the .webflow.io preview in the Designer) and navigate to another page.
- • Submit a test form: Roots automatically records form submissions as conversions.
Please note
- • Custom code does not work in the Designer preview: you must publish.
- • Site-wide head code requires a paid Site plan.
Wix
Depends on platform and accessScript as custom code on all pages, loaded in the head.
- 1Go to manage.wix.com and select your site.
- 2Click Settings at the bottom left.
- 3Scroll to the 'Advanced' section and click Custom code.
- 4At the top right, next to 'Head', click + Add Custom Code.
- 5Paste the script into the large code box and name it 'Roots pixel'.
- 6Under 'Add Code to Pages', select All pages and enable 'Load code on each new page'.
- 7Under 'Place Code in', select Head and click Apply.
- 8Click Publish at the top right of the editor if your site still has pending changes.
Check that it works
- • Open your live site (jouwdomein.nl, not the editor view) in a private window.
- • Check in Roots that 'Script installed' turns green.
Please note
- • Custom code requires a premium Wix plan and a connected domain.
- • Using Wix Studio? Custom code is under Settings → Custom code; everything else is identical.
Squarespace
Depends on platform and accessScript via code injection in your site's header.
- 1Log in and open your site in the Squarespace admin area.
- 2In the left menu, click Settings → Developer tools (in version 7.1: Website → Website tools).
- 3Click Code injection.
- 4Paste the script into the top field labelled Header.
- 5Click Save at the top left.
- 6Running an online store? Also paste the script into 'Order confirmation page' with srAttribution.track("purchase", { value: {orderGrandTotal} }).
Check that it works
- • Open your site in a private window and view two pages.
- • Fill in a form block and check in Roots that the conversion appears.
Please note
- • Code injection is available from the Business plan; it does not work on Personal.
- • Code injection does not load in the Squarespace editor view.
WooCommerce
Depends on platform and accessBase pixel via WPCode or functions.php; purchase on the thank-you page via woocommerce_thankyou.
- 1Place the base pixel via WPCode → Header & Footer → Header, or use the wp_head hook from the PHP example below. Use one or the other, not both.
- 2Add the PHP code for woocommerce_thankyou using a child theme or a PHP snippet in WPCode. Use your own pixel key.
- 3The purchase event is only sent for a paid order, with order value, currency, order ID and billing email. Roots only sends contact details when advertising consent has been recorded.
- 4For server confirmation, create a secure order integration to /api/public/order with x-roots-platform: woocommerce and x-roots-secret. Store sr_vid in order metadata so visits and orders can be linked.
- 5WooCommerce → Settings → Advanced → Webhooks offers order.created/order.updated. A server relay can add x-roots-secret and filter payment status; the native WooCommerce signature does not replace the Roots key.
// PHP: child theme functions.php or a PHP snippet in WPCode.
add_action('wp_head', function () {
echo '<script async src="https://rootsmetrics.com/sr.js" data-key="sr_xxx"></script>';
});
add_action('woocommerce_thankyou', function ($order_id) {
$order = wc_get_order($order_id);
if (!$order || !$order->is_paid()) return;
$payload = wp_json_encode([
'value' => (float) $order->get_total(),
'currency' => $order->get_currency(),
'orderId' => (string) $order->get_id(),
'email' => $order->get_billing_email(),
'phone' => $order->get_billing_phone()
], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
echo '<script>window.addEventListener("load", function () {
if (window.srAttribution) srAttribution.track("purchase", ' . $payload . ');
});</script>';
});Check that it works
- • Place a paid test order and check purchase, order ID, amount and currency in Roots.
- • Reload the thank-you page and check that the same order is counted once. Also test the server integration separately.
Please note
- • For delayed payments, the order may not yet be paid on the thank-you page. In that case, use the payment-status webhook through your server integration.
- • Never put webhook keys in browser code. Coordinate caching and payment status with your developer.
Google Tag Manager
Depends on platform and accessBase pixel as a Custom HTML tag on All Pages; custom conversions via a dataLayer event.
- 1In GTM, create a Custom HTML tag with the base pixel and the All Pages trigger. Replace sr_xxx with your pixel key.
- 2Do not enable an extra consent trigger or additional consent requirement for this Roots tag: sr.js handles consent itself. This does not apply to other advertising tags. Your cookie banner must still load before GTM.
- 3Create Data Layer Variables roots_name, roots_value and roots_currency, and a second Custom HTML tag with the conversion code below. Trigger: Custom Event roots_conversion.
- 4Have your website push roots_conversion after the booking or conversion is actually confirmed. Do not use just a button click. Publish the container after checking.
<!-- Custom HTML: All Pages, no additional consent requirement.
sr.js handles consent. Keep your CMP before GTM. -->
<script async src="https://rootsmetrics.com/sr.js" data-key="sr_xxx"></script>
<!-- Push ONLY after a confirmed conversion, not a button click. -->
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({event: "roots_conversion", roots_name: "reservering", roots_value: 84.5, roots_currency: "EUR"});
</script>
<!-- Second Custom HTML tag: Custom Event trigger roots_conversion.
Create Data Layer Variables roots_name, roots_value, roots_currency.
Tag sequencing: load the Roots base tag first. -->
<script>
(function () {
var name = {{roots_name}}, value = {{roots_value}}, currency = {{roots_currency}};
var attempts = 0;
function send() {
if (window.srAttribution) srAttribution.track(name, {value: value, currency: currency});
else if (++attempts < 100) setTimeout(send, 100);
}
send();
})();
</script>Check that it works
- • In GTM Preview, check that the base tag loads once and the conversion tag only fires after roots_conversion.
- • Check the event and value in Roots; also test refusing consent.
Please note
- • Do not use a second purchase tag for a standard ecommerce purchase event: sr.js already reads it from the dataLayer and deduplicates by order ID.
- • A fired GTM tag does not prove that Roots has received the event.
Custom website or app
Depends on platform and accessScript immediately before the closing </head> tag in your shared template or layout.
- 1Open the file loaded on every page: index.html, base.html.twig, layout.blade.php, _document.tsx, app.html or your root layout component.
- 2Paste the script as the last line before </head> so click IDs are captured on the first render.
- 3Using React, Vue or Svelte with SSR? Put the script in the HTML template, not in a component, so it also loads on the first pageview.
- 4Commit and deploy your site.
- 5Send custom conversions with srAttribution.track("naam", { value: 89.5, currency: "EUR" }) when the conversion succeeds.
- 6Send server-side conversions (refunds, offline orders, phone bookings) via the conversion API using the same pixel key.
Check that it works
- • Open your site and check in your browser's Network tab that sr.js loads with status 200.
- • Use the 'Test your tracking' page in Roots to send a test event and watch the event stream live.
Please note
- • Do not load the script through a tag manager with a consent delay if you want to preserve click IDs; place it directly in the head.
- • Do not put the script in a component that mounts only after hydration: you would miss the first pageview.
Order webhook and conversion API
Shopify, WooCommerce and Magento can confirm orders from their servers via POST /api/public/order?key=sr_xxx. Use x-roots-secret and x-roots-platform. Find your keys under your website after logging in; keep them only on your server.
Shopify also requires X-Shopify-Hmac-SHA256 over the unchanged request body. Store the Shopify secret in Roots and use a server integration that also sends the Roots key. For WooCommerce and Magento, the server integration must enforce authentication and payment status.
curl -X POST "https://rootsmetrics.com/api/public/order?key=sr_xxx" \
-H "Content-Type: application/json" \
-H "x-roots-secret: YOUR_WEBHOOK_SECRET" \
-H "x-roots-platform: woocommerce" \
-d '{"id":1234,"total":"84.50","currency":"EUR","billing":{"email":"customer@example.com"},"meta_data":[{"key":"sr_vid","value":"VISITOR_ID"}]}'Closed deals and qualified leads
The conversion API accepts POST /api/public/v1/offline-conversions with an account-specific API key as a Bearer header, site_id and up to 500 conversions per request. Use a unique external_id, the actual value and the original occurred_at. visitor_id links the conversion to a measured visit where possible.
curl -X POST "https://rootsmetrics.com/api/public/v1/offline-conversions" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"site_id":"YOUR_SITE_UUID","conversions":[{"external_id":"crm-deal-1234","visitor_id":"VISITOR_ID","event_name":"qualified_lead","value":84.5,"currency":"EUR","occurred_at":"2026-10-29T12:00:00Z"}]}'Receipt does not prove visitor linking. Unknown consent is not treated as granted; forwarding requires recorded consent. GET /api/public/v1/conversions is an export, not a receiving endpoint.
Rootboard partner API
With an account-specific partner key, Rootboard can retrieve attribution totals by website, period, channel and model via GET /api/public/partners/rootboard/attribution. The exchange includes conversion credit, value in cents and connected costs. This is not a built-in Analytics Hub or an exchange of customer profiles.
What is measured automatically?
Once the script is on your site, Roots measures the key conversions without any setup. Anything that needs a value gets one extra attribute on the button, link or form.
| Event | When | Value |
|---|---|---|
Page view pageview | On every page load. Click IDs (gclid, fbclid, ttclid, li_fat_id) and utm parameters are recorded here too. | No value |
Form submitted form_submit | When a visitor submits a <form> on your site. Works for every form, including those from your theme or page builder. | No value, unless you add data-sr-value to the form. |
Phone link click phone_call | A click on a link starting with tel:, for example <a href="tel:+31612345678">. | No value. Want an average value per phone click? Add data-sr-value to the link. |
Email click (mailto) email_click | A click on a link starting with mailto:. | No value, unless you pass one yourself. |
Any button or link with data-sr-event eigen naam | A click on an element with the data-sr-event attribute. Its value becomes the event name, for example reservation, whatsapp or download. | Optional via data-sr-value and data-sr-currency. |
Pass a value with data attributes
No JavaScript needed: put the attributes directly on the element. Roots reads them on click or submit.
data-sr-eventThe conversion name. Put it on a button, link or form. Without this attribute a form is called form_submit.
data-sr-event="reservering"data-sr-valueThe conversion value in euros. Use a dot as the decimal separator, no euro sign.
data-sr-value="89.95"data-sr-currencyCurrency of the value. If omitted, Roots uses EUR.
data-sr-currency="EUR"data-sr-ignorePut this on a form you do not want to measure, such as a search or filter form.
<form data-sr-ignore> … </form>A value only known at checkout
In a web shop the order value is not fixed in advance. Send it from the thank-you page so Roots counts the actual revenue per source.
<!-- Your shop's thank-you page: send the actual order value -->
<script>
srAttribution.track("purchase", {
value: ORDER_TOTAL, // replace with your shop's variable
currency: "EUR"
});
</script>No amount, as with a reservation or enquiry? Set a fixed value per conversion type under Conversions, for example the average spend per cover.