You run a few websites, maybe a WordPress multisite network, you self-host Matomo, and the data won't behave. Every blog reports into the same Matomo site no matter what you change. You go looking for "a separate token_auth for each website" and there isn't one. You paste a second tracking snippet onto a page to split traffic, and the first site goes dark. Or you want the same custom dimension on thirty sites and the thought of clicking through each one by hand is enough to close the tab.
These look like four different problems. They're one. Each comes from tangling three things Matomo treats as completely separate: a measurable, a domain, and a token_auth. Straighten those out and every one of these has a clean, boring fix.
In Matomo, JavaScript tracking is routed by Site ID (idSite) and nothing else, so merged data or a site that suddenly goes dark almost always means a shared or reassigned Site ID, not a missing per-site token. Give each measurable its own Site ID. When you want one page to report into two sites, add a second tracker with addTracker queued before trackPageView rather than pasting a second snippet, and to push the same custom dimension everywhere, create the slot once from the CLI then loop the Reporting API over your Site IDs. The token_auth only authenticates API and plugin calls; it never picks the destination.
The three things Matomo keeps separate
Start with the measurable. It's the bucket your data lands in, usually a Website, and each one carries a unique Site ID: the idSite number you pass to setSiteId. That number is what separates your data. Not the domain, not the snippet, not a token. If two sources send the same Site ID, their hits pile into the same bucket and merge, and nothing you change elsewhere will pull them back apart.
A domain is just a URL, and it doesn't pair off with a measurable one-to-one in either direction. One measurable can own several domains; you add them as extra URLs or aliases on the site. One domain can feed several measurables. What a domain doesn't do is pick the destination bucket; that's the Site ID's job. It still matters for plenty else: Matomo uses your configured URLs to tell internal links from outlinks, to wire up cross-domain tracking, and to scope cookies. So set your domains up correctly. Just don't expect them to route a hit into the right site.
Then there's token_auth. It's an API token tied to a user account, used for the Reporting API, admin actions, and a few advanced tracking jobs like server-side log import. Plain JavaScript pageview tracking needs no token at all. There's no per-site token that routes data, so if you're hunting for one, stop: that's the misconception sitting underneath half of these problems. Sites are routed by Site ID. A token only proves who's asking the API; it says nothing about where a hit lands.
Keep those three apart and the rest is mechanical. Here are the three setups people actually hit.
Scenario 1: many sites, each into its own measurable (WordPress multisite)
Goal: every blog in the network reports into its own Matomo site.
The classic symptom is everything piling up under site #1, and the instinct is to go hunting for per-site credentials. Wrong layer. Every blog is emitting the same Site ID, and that's the whole bug. The fix is per-site Site IDs, and the auth token has nothing to do with it.
- Create one measurable per blog in Matomo, each with its own Site ID (or let the WordPress plugin auto-create them).
- Connect WordPress to your self-hosted Matomo with the Connect Matomo plugin (formerly WP-Matomo / WP-Piwik). Set the Matomo Mode dropdown to Self hosted (HTTP API, default), enter your Matomo URL, and paste one auth token. You create that token in Matomo under Administration → Personal → Security → Auth tokens → Create new token. For a network, use a token from a user that can see and create every site; a super-user token is simplest, with the security trade-offs that come with one.
- Map each blog to its own Site ID. On a network install, enable Connect Matomo as a network plugin and confirm each blog's tracking code carries the correct
idSite.
Don't confuse the two. Matomo for WordPress embeds a whole Matomo inside WordPress, a different product for a different goal. For one central, self-hosted Matomo serving many blogs, Connect Matomo is the right tool. Network mode in the plugin is still marked experimental, so check the exact menu labels and the permission scope your token needs against the version you install.
One token, many Site IDs. The token is just the plugin's API credential; the Site IDs do the separating.
Scenario 2: one site, into multiple measurables
Goal: send the same page to two Matomo sites (say, to split a slice of traffic off), so you add a second snippet. And the first site goes silent.
Here's the trap. Both snippets share one global _paq queue and one default tracker. The second snippet's setSiteId just reconfigures that same default tracker instead of creating a new one, so you don't end up with two destinations. You end up with one that's now pointed at site #2. You didn't add a tracker; you reassigned the one you had.
So don't duplicate the snippet. Add a second tracker to the one you already have, with addTracker. The catch is where you put it. A tracker only receives a hit if it already exists when that hit is pushed, and the standard snippet queues trackPageView right at the top. Tack addTracker on at the end and the first pageview has already gone to site #1 alone. So addTracker has to be queued before the pageview you want duplicated. The clean way is to fold everything into one snippet:
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
(function() {
var u = "//your-matomo.example/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '1']);
_paq.push(['addTracker', u + 'matomo.php', '2']); // second Site ID
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
var d = document, g = d.createElement('script'),
s = d.getElementsByTagName('script')[0];
g.async = true; g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
<!-- End Matomo Code -->With addTracker queued ahead of trackPageView, both sites get the first pageview and everything after it, because every _paq.push from here on applies to all trackers. Each tracker added through _paq inherits the default tracker's config (cookies, user ID, custom dimensions), so you rarely need to set them up twice. One caveat: only the last addTracker in the _paq queue takes effect, so this pattern gives you a second destination, not a third or fourth. And each tracker is a real extra request per hit. If you genuinely need many destinations, that's a server-side tracking job, not a pile of browser trackers.
But step back, because "I need two measurables" is usually the wrong answer to the real question. The reason is almost always separating internal traffic from external: staff visits versus customers. A second site is a clumsy way to do that. You split your data and lose any apples-to-apples comparison between the two halves. Better to keep one measurable, set a custom dimension like Internal Traffic = true/false, and use segments to view either slice or compare them side by side in one dataset. A custom dimension also keeps working once you anonymize IPs, which can make the IP-based segments people reach for first unreliable.
Scenario 3: the same custom dimension on every measurable
Goal: add one custom dimension, say Internal Traffic, to all your sites at once without clicking through each.
There are two layers here, and the confusion is almost always mixing them up.
The dimension slots are installation-wide. Adding a slot changes the database schema for every site at once, so you do it once, from the CLI:
# add 10 new visit-scope slots (default is 5 per scope)
./console customdimensions:add-custom-dimension --scope=visit --count=10Use --scope=action for action scope. The --count flag is real and saves you running the command ten times. It rewrites tables, so on a large database it's a real migration. Run it in a window where a schema change is acceptable, the same way you'd treat any archiving or DB-heavy job at scale.
The configuration of each slot is per-site. The name, whether it's active, the extraction rules: those live per measurable. To apply identical config everywhere, loop the Reporting API over your Site IDs. POST it, and put the token in the request body, not the URL:
curl "https://your-matomo.example/index.php" \
--data-urlencode "module=API" \
--data-urlencode "method=CustomDimensions.configureNewCustomDimension" \
--data-urlencode "idSite=1" \
--data-urlencode "name=Internal Traffic" \
--data-urlencode "scope=visit" \
--data-urlencode "active=1" \
--data-urlencode "format=json" \
--data-urlencode "token_auth=YOUR_TOKEN"The token belongs in the body for a reason. A token in the query string lands in server logs and browser history, and a Matomo 5 token created with Only allow secure requests won't authenticate as a URL parameter at all. --data-urlencode also handles the space in Internal Traffic for you. The required parameters are idSite, name, and scope; active, description, extractions, and caseSensitive are optional.
Two cautions before you run this across a live network. First, configureNewCustomDimension isn't idempotent. Call it twice and you get two dimensions, or an error on a site whose slots are already full. So have the loop look before it writes: call CustomDimensions.getConfiguredCustomDimensions for each site and only create the dimension where a matching name isn't already there. Second, don't assume the new dimension lands on the same idDimension on every site. Matomo assigns the next free index per site, so if your sites started from different custom-dimension states, the same name can end up under different IDs. Capture the returned idDimension for each site and keep it; don't hard-code one number and expect it to match everywhere. To edit an existing dimension, use configureExistingCustomDimension, which takes idDimension and idSite first, then the same fields. It overwrites, so any field you leave out gets reset to default. Pass the full config, not just the part you're changing.
Notice what's doing the work in that last call: token_auth. Authenticating an admin API request is exactly what the token is for, and exactly what it had nothing to do with back in Scenario 1's tracking. Same word, two different jobs.
Quick check that it actually worked
- Open Visitors → Visits Log (or Real-Time) on each site and confirm hits arrive where you expect them.
- Look at the All Websites dashboard. Every measurable should show its own traffic, not one site hoarding everything. That's the fastest way to catch a duplicated Site ID.
- For the bulk dimension, confirm it appears and is active in each site's settings, not just created in the schema.
What we'd actually do
For a WordPress network reporting into one self-hosted Matomo: one measurable per blog, the Connect Matomo plugin in self-hosted mode, one super-user token, and a hard check that each blog emits a distinct idSite. The token is plumbing; the Site IDs are the work.
For "two measurables to split internal traffic": don't. Use one measurable, a custom dimension, and segments. You keep one clean dataset you can actually compare against itself, and it doesn't fall over when you turn on IP anonymization.
For the same dimension across dozens of sites: add the slot once from the CLI, then loop configureNewCustomDimension over your Site IDs. Treat the schema change like the migration it is.
Martez connects Matomo with Meta Ads and Google Ads so ROAS, CLV, and attribution sit next to your web analytics instead of in a separate spreadsheet. It's in private beta. Join the waitlist if that's relevant.
Almost every "Matomo won't separate my sites" problem is a Site ID problem wearing a token costume. Fix the ID, ignore the token, and the data sorts itself.