“Can we do Matomo server-side?” is a question I get on almost every Matomo project. The short answer is yes. The useful answer is: it depends what you mean by server-side, because there are at least five different setups behind that word, and they don’t solve the same problem. Some remove the JavaScript from the browser entirely. Others keep it and only change where the hits go.
Pick the wrong one and you end up with the classic symptoms: every server hit geolocated in your data centre, thousands of “unique visitors” that are really one machine, purchases tracked as events instead of orders, and a consent problem you thought had gone away. This guide compares the options, then walks through the one I use most, sending hits from your backend with the Matomo Tracking HTTP API, with a working request, a test plan and the traps I see over and over.
The five ways to run Matomo server-side
All of them end up in the same place: an HTTP request to your Matomo tracking endpoint (matomo.php). What changes is who sends that request, and what still runs in the browser.
| Setup | Who sends the hit | Browser JS still needed? | What it solves | What it does not solve |
|---|---|---|---|---|
| Tracking HTTP API, direct | Your backend (any language, a plain GET or POST) | No, for backend events. Yes, if you also want browsing behaviour | Events that only exist on the server: confirmed payments, refunds, CRON jobs, webhooks | Screen resolution, page titles and client-side interactions, unless you pass them yourself. Consent |
| Official SDKs (PHP, Java, C#) | Your backend, through a client library | Same as above | Same as above, with helpers for ecommerce, bulk tracking and authentication | Same as above |
| Server-side GTM + Matomo client | The browser sends to your sGTM container, which forwards to Matomo | Yes, the Matomo tracking code, pointed at your container domain | One collection point on your own domain, more control over what is forwarded | Removing the browser script. You also operate an extra server: the container |
| Tracker proxy | The browser sends to a matomo.php proxy on your site, which forwards to Matomo |
Yes | Hides your Matomo server URL from the tracked sites | Removing the browser script |
| Log Analytics | A Python script that imports your web server access logs | No | Traffic analysis with no tag at all, history import | No tracking cookies, no screen resolution or browser plugins, page titles mostly empty |
Two things are missing from that table on purpose.
Matomo Tag Manager is not a server-side option. It is a client-side container that runs in the browser. Matomo’s own FAQ says that server-side tagging through Matomo Tag Manager “is not currently supported and may be added in a future release”. If someone sells you “Matomo Tag Manager server-side”, ask what they actually mean.
The sGTM route is officially documented. Matomo lists exactly two supported server-side methods: the Tracking API from your server, and its tag in a Google Tag Manager server-side container. The sGTM guide has you import the “Matomo Server-side Client” template into your container, then replace the Matomo domain in your tracking code with your container URL. The browser script stays. Only its destination changes.
If you want the same comparison across vendors (GA4 Measurement Protocol, Adobe Analytics direct HTTP calls, Matomo), I covered it in Native server-side tracking: GA4 vs Adobe vs Matomo. Here I stay on Matomo and go deep.
Which one should you pick?
- Your problem is events the browser never sees (payment confirmed by the PSP, subscription renewed, order shipped): use the Tracking HTTP API or an SDK. This is real server-side tracking.
- Your problem is collection on your own domain, with one pipeline for several tools: sGTM, if you already run one. Otherwise the tracker proxy is lighter.
- You want no tag at all and can live with less detail: Log Analytics.
- In practice, most setups I build are hybrid: the JavaScript tracker for browsing, the HTTP API for the conversions that matter, both stitched to the same visitor. That is what the rest of this guide shows.
Step by step: sending a server-side hit with the Tracking HTTP API
1. Create a dedicated tracking user and token
Some parameters require authentication with a token_auth: overriding the visitor IP (cip), sending a date older than 24 hours (cdt), overriding the country or city. Matomo recommends creating a user specifically for the Tracking API, with write permission only on the websites concerned. Do that, and keep the token on the server. It must never appear in the page source or in a browser request.
2. Decide how the server hit joins the browser visit
This is the step most implementations skip, and it is why server hits end up “floating” with no source, no campaign and no path. You have two keys:
- Visitor ID (
cid): exactly 16 hexadecimal characters. On the browser side, the JavaScript tracker gives it to you withgetVisitorId(). Store it with the order or the session on your backend. When you setcid, Matomo only matches visits with that exact visitor ID. - User ID (
uid): your own identifier for a logged-in user. If a visit with this User ID exists in the last 30 minutes, the action is added to it. Otherwise a new visit is created.
On the browser side, reading the visitor ID looks like this:
var _paq = window._paq = window._paq || [];
_paq.push([function () {
var visitorId = this.getVisitorId(); // 16 hex characters
// send it to your backend with the checkout form or an API call
}]);
Note the privacy side: Matomo’s privacy guide states that User IDs are personal data, even when you enable the option to replace them with a pseudonym. Use uid only if your consent setup covers it.
3. Build the request
Here is an ecommerce order sent from the backend after the payment provider confirms it. It is a POST to matomo.php, with every value URL-encoded by curl:
curl -s -o /dev/null -w "%{http_code}\n" -X POST "https://matomo.example.com/matomo.php" \
--data-urlencode "idsite=1" \
--data-urlencode "rec=1" \
--data-urlencode "apiv=1" \
--data-urlencode "send_image=0" \
--data-urlencode "rand=8f3a91" \
--data-urlencode "url=https://shop.example.com/checkout/confirmed" \
--data-urlencode "action_name=Checkout / Order confirmed" \
--data-urlencode "cid=5b1d8c3f0a9e7d21" \
--data-urlencode "uid=customer-48213" \
--data-urlencode "ua=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15" \
--data-urlencode "lang=fr-FR,fr;q=0.9" \
--data-urlencode "cip=203.0.113.42" \
--data-urlencode "token_auth=YOUR_TRACKING_USER_TOKEN" \
--data-urlencode "idgoal=0" \
--data-urlencode "ec_id=ORD-2026-10457" \
--data-urlencode 'ec_items=[["SKU-118","Running shoes","Shoes",89.90,1],["SKU-502","Socks","Accessories",9.90,2]]' \
--data-urlencode "revenue=109.70" \
--data-urlencode "ec_st=109.70" \
--data-urlencode "ec_sh=0" \
--data-urlencode "ec_tx=18.28"
What each block does:
idsiteandrec=1are the only required parameters.apiv=1andrandare recommended;randavoids caching by a proxy.send_image=0makes Matomo answer with HTTP 204 instead of a GIF. Cleaner for a backend.cidanduidjoin this hit to the browser visitor and to the customer (step 2).uaandlangoverride the User-Agent and Accept-Language headers. Without them, Matomo sees your server’s own headers.cipoverrides the visitor IP and requirestoken_auth. Without it, Matomo sees your server’s IP.idgoal=0is mandatory for an ecommerce order or cart update.ec_idandrevenueare required for an order.ec_itemsis a JSON array of [SKU, name, category, price, quantity].
The same order with the official PHP client (matomo-php-tracker) uses methods documented in the PHP tracker reference:
$tracker = new MatomoTracker(1, 'https://matomo.example.com/');
$tracker->setTokenAuth(getenv('MATOMO_TRACKING_TOKEN'));
$tracker->setVisitorId('5b1d8c3f0a9e7d21');
$tracker->setUserId('customer-48213');
$tracker->setIp($order->clientIp); // requires setTokenAuth()
$tracker->setUserAgent($order->clientUserAgent);
$tracker->setUrl('https://shop.example.com/checkout/confirmed');
$tracker->addEcommerceItem('SKU-118', 'Running shoes', 'Shoes', 89.90, 1);
$tracker->addEcommerceItem('SKU-502', 'Socks', 'Accessories', 9.90, 2);
$tracker->doTrackEcommerceOrder('ORD-2026-10457', 109.70, 109.70, 18.28, 0);
4. Send many hits at once with bulk tracking
For batch jobs (a nightly export of refunds, a CRM sync), don’t fire one HTTP call per hit. The Tracking API accepts a POST with a JSON body containing a requests array, one query string per hit, plus an optional token_auth. Matomo asks for the requests in chronological order, oldest first.
{
"requests": [
"?idsite=1&rec=1&cid=5b1d8c3f0a9e7d21&e_c=Order&e_a=Shipped&e_n=ORD-2026-10457&ca=1",
"?idsite=1&rec=1&cid=9a7c0e51b2d4f388&e_c=Order&e_a=Shipped&e_n=ORD-2026-10461&ca=1"
],
"token_auth": "YOUR_TRACKING_USER_TOKEN"
}
The ca=1 flag marks a request as a custom action, not a page view. Matomo documents it for events: if the Events plugin is ever disabled, those requests are ignored instead of being counted as page views.
How to verify it works
- Check the HTTP status. With
send_image=0, a successful request answers 204. Without that parameter you get a 200 and a 1×1 GIF. A 4xx or 5xx, or a timeout, points upstream: wrong URL, firewall, server down. - Turn on tracker debugging, briefly. In
config/config.ini.php, setdebug_on_demand = 1under[Tracker], then add&debug=1to your test request. Matomo then outputs its tracking log messages for that request only. Matomo warns not to leave debugging on in production. If you use Queued Tracking, addqueuedtracking=0so the request is processed immediately. - Open the Visits Log for today. Find the visit by User ID or visitor ID. You should see the browser pages first, then the order, in the same visit, with the right country and browser. If the order sits alone in a visit from your hosting provider’s city,
ciporuais missing. - Check the Ecommerce reports, not the Events report. An order sent with
idgoal=0lands in the Ecommerce reports. If you find it under Events, it was sent as an event. - Compare with the backend. Over a full day, count orders in Matomo against orders in your database. The gap should be close to zero for server-sent orders; that is the whole point of sending them from the server.
Pitfalls I keep finding
Every visit comes from your server
Without ua and cip, Matomo uses your server’s User-Agent and IP. Geolocation points to your data centre. Worse, when no visitor ID is provided, Matomo matches visitors with a heuristic based on IP and other device attributes, so unrelated customers can be merged into one visitor. Always forward the real browser User-Agent and IP, and set cid or uid.
A made-up User-Agent
Some tutorials send ua=Python-Server-Agent/1.0. Two problems: you lose the device and browser reports, and Matomo ignores requests it detects as bots unless you set bots=1. Forward the customer’s real User-Agent instead.
Purchases tracked as events
An event with e_v=79.99 is not revenue. It will never appear in Ecommerce reports or in goal attribution. Use idgoal=0 with ec_id and revenue for orders, and idgoal=<id> with revenue for a regular goal conversion.
Back-dated hits that don’t show up
You can set the hit time with cdt (UTC). If it is older than 24 hours, token_auth is required. And reports already processed for that day won’t change on their own: Matomo documents that you have to invalidate and re-process them.
A visitor ID in the wrong format
cid and _id must be exactly 16 hexadecimal characters. A UUID, an email or your internal customer number goes in uid, not in cid.
Unencoded parameters
All string values (url, action_name, ec_items…) must be URL-encoded. An unencoded & in a product name breaks the request into wrong parameters.
“Server-side means no consent”
It does not. Matomo’s own FAQ on server-side tagging is explicit: it does not automatically make you compliant, and you remain responsible for obtaining valid consent, limiting personal data before forwarding it, and following the applicable rules. On the browser, the JavaScript tracker has requireConsent and setConsentGiven. I found no equivalent consent parameter in the Tracking HTTP API reference, so on the server, the consent check is your code’s job: store the visitor’s choice with the session, and don’t send the hit (or don’t send identifiers) when it says no. What your legal basis is, is a question for your DPO, not for your tracking plan.
“Server-side beats ad blockers”
Partly. A hit sent from your backend never goes through the browser, so no browser extension can block it. But the sGTM and proxy setups still load a script in the page and send a request from the browser. Matomo’s sGTM guide talks about reducing ad blockers’ impact, not removing it. And bypassing a blocker is not the same as having consent.
Conclusion
Matomo server-side is not one feature, it is a choice of who sends the hit. For the events that matter most, the ones your backend knows for sure, send them from the server with the Tracking HTTP API or an official SDK. Pass the real IP and User-Agent, join the hit to the browser visitor with cid or uid, send orders as orders, and check the Visits Log before calling it done. Keep the JavaScript tracker for browsing, and keep consent where it belongs: before the hit, on both sides.
If your Matomo numbers don’t match your backend and you can’t tell which side is wrong, that is exactly the kind of audit I do.
Related reading
- Custom Domain ≠ Server-Side Tagging: What Really Changes for GA4 and Adobe
- Server-Side Tracking Demystified: How It Really Works (And Why Ad Blockers Still Matter)
- Server-Side Tracking Architecture: Reverse Proxy, DNS Cloaking, and First-Party Resilience Explained
- Comparing Google Analytics, Adobe Analytics, & Matomo
Sources
- Matomo Developer: Tracking HTTP API reference
- Matomo Developer: Tracking API clients (PHP, Java, C#, mobile SDKs)
- Matomo Developer: PHP Tracking Client reference
- Matomo Developer: JavaScript Tracking Client reference (getVisitorId)
- Matomo Developer: Tracking and cookie consent
- Matomo FAQ: Does Matomo Tag Manager support server-side tagging?
- Matomo FAQ: What is server-side tagging?
- Matomo FAQ: How to integrate Matomo tracking in server-side Google Tag Manager
- Matomo FAQ: Tracking without revealing the Matomo server URL (tracker proxy)
- Matomo FAQ: How does Matomo detect unique and returning visitors?
- Matomo FAQ: Configure privacy settings in Matomo
- Matomo FAQ: How do I force the reports to be re-processed?
- Matomo Guide: Import server logs (Log Analytics)
- Matomo FAQ: Differences between Log Analytics and the JavaScript client
- Matomo FAQ: Requirements for Log Analytics
- Matomo Guide: Real-time reports and Visits Log