This script allows to track websites with Matomo without revealing the Matomo server URL.
This is useful for users who track multiple websites on the same Matomo server, but don't want to show the Matomo server URL in the source code of all tracked websites.
To run this properly you will need:
- latest version of Matomo installed on a server (or Matomo Cloud)
- one or several website(s) to track with this Matomo, for example
http://{site_to_be_tracked} - the website to track must run on a server with PHP 7.2 or higher
- PHP must have either the CURL extension enabled or
allow_url_fopen=On
In your Matomo server:
- login as Super user
- create a user, set the login for example: "UserTrackingAPI"
- assign this user write or admin permission on all websites you wish to track (otherwise the visitor IP address and other things won't be tracked correctly)
- log in as this newly created user and create an auth token
You need to install the proxy on the server where your websites are hosted. You can do it both ways:
- download the source archive (recommended)
- or download the individual files manually
git clone |
|---|
A clone of this repository contains more than the proxy: it also contains the test suite, the CI configuration and other development files, none of which belong in a public web root. The source archive contains only the files the proxy needs, so use it to install. If your existing installation contains a tests/ directory — because it was cloned, or extracted from an older archive — remove tests/, .github/, .git/, vendor/ and composer.lock from it, or reinstall from the archive. |
| ⚡ Important note about where to install the proxy |
|---|
To ensure the highest data accuracy possible, and that your Matomo cookies are set correctly, please install the proxy in your main website domain name and web server. This proxy should be ideally installed on your webserver directly under {site_to_be_tracked}. If you installed the proxy in a sub-domain under analytics.{site_to_be_tracked} then this would cause data to be less accurate. (Why? because if the sub-domain analytics.{site_to_be_tracked} was to resolve to a CNAME that does not match {site_to_be_tracked} OR if it was to resolve to A/AAAA addresses that do not match the first half of the A/AAAA addresses running {site_to_be_tracked}, then the cookies set by the Matomo Tracker Proxy in the response would only have a lifetime of maximum 7 days on Safari >= 16.4.) |
Download the archive into the directory you want to serve the proxy from, for example matomo/ in your website root directory, so that the proxy ends up at http://{site_to_be_tracked}/matomo/matomo.php:
cd /path/to/your/website-root
mkdir matomo
cd matomo
curl -L https://github.com/matomo-org/tracker-proxy/archive/refs/heads/master.tar.gz | tar xz --strip-components=1
cp config.php.example config.phpThen change the configuration in the newly created config.php:
$MATOMO_URLshould contain the URL to your Matomo server$PROXY_URLshould contain the URL to the tracker-proxy server$TOKEN_AUTHshould contain thetoken_auth
To update later, run the same curl … | tar xz command again in the same directory. Your config.php is not part of the archive and is therefore left untouched. Note that extracting over an existing installation only adds and overwrites files, it never deletes them: to be sure you end up with exactly the current set of files, extract into an empty directory instead and move your config.php over.
Be aware that with this method, matomo.php and other files are in a matomo/ subdirectory. Keep that in mind when applying the instructions for the next step. Extract the archive directly into your website root directory instead if you prefer the proxy at http://{site_to_be_tracked}/matomo.php.
- download
matomo.php, downloadpiwik.php,proxy.php,matomo-proxy.php& if you are using the Heatmaps and Session recordings plugin also downloadplugins/HeatmapSessionRecording/configs.phpto your website root directory, for example athttp://{site_to_be_tracked}/matomo.php,http://{site_to_be_tracked}/piwik.php,http://{site_to_be_tracked}/proxy.php,http://{site_to_be_tracked}/matomo-proxy.php&http://{site_to_be_tracked}/plugins/HeatmapSessionRecording/configs.php - edit the file to set the configuration variables:
$MATOMO_URLshould contain the URL to your Matomo server$PROXY_URLshould contain the URL to the tracker-proxy server$TOKEN_AUTHshould contain thetoken_auth
The proxy file (http://{site_to_be_tracked}/matomo.php) will be called by the Matomo Javascript tracker instead of calling directly the (secret) Matomo server (http://your-matomo-domain.example.org/matomo/).
To achieve this, change the Matomo Javascript Code that is in the footer of your pages:
-
go to Matomo > Settings > Websites > Show Javascript Tracking Code.
-
copy the Javascript snippet and change the last lines to the following:
[...] (function() { var u="//{site_to_be_tracked}/"; _paq.push(["setTrackerUrl", u+"matomo.php"]); _paq.push(["setSiteId", "tracked-site-id-here"]); var d=document, g=d.createElement("script"), s=d.getElementsByTagName("script")[0]; g.type="text/javascript"; g.async=true; g.defer=true; g.src=u+"matomo.php"; s.parentNode.insertBefore(g,s); })(); </script> <!-- End Matomo Code -->
What has changed in this code snippet compared to the normal Matomo code?
- any reference(s) to the secret Matomo URL are now replaced by your website URL (the proxy).
matomo.jsbecomesmatomo.php(ormatomo/matomo.phpif you installed the proxy into amatomo/subdirectory): matomo.php is the proxy script- make sure to replace
tracked-site-id-herewith your idsite - if the
<noscript>is present in your tracking code, you can remove it (it contains the secret Matomo URL which you want to hide)
-
paste the modified Matomo Javascript code in the pages you wish to track.
This modified Javascript code will then track visits/pages/conversions by calling {site_to_be_tracked}/matomo.php, which will then automatically call your (hidden) Matomo Server URL.
At this stage, example.com should be tracked by your Matomo without showing the Matomo server URL. Repeat the step 3. for each website you wish to track in Matomo.
The tracker-proxy also supports proxying the matomo opt out form. To use this, change the URL used in the opt out iframe to use
the tracker proxy's matomo-proxy.php file.
Note: you can get the opt out iframe from inside the Administration > Privacy > Users opt-out page in your Matomo instance.
By default, the matomo.php proxy will wait 5 seconds for the Matomo server to return the response.
You may change this timeout by editing the $timeout value in config.php.
By default, the matomo.php proxy will contact your Matomo server with the User-Agent of the client requesting matomo.php.
You may force the proxy script to use a particular User-Agent by editing the $user_agent value in config.php.
Because the proxy sits between your visitors and Matomo, it has to tell Matomo the real visitor IP — otherwise Matomo would record the proxy's IP. There are three ways this works:
- Default — via
cip+token_auth: the proxy sends the visitor IP to Matomo as theciptracking parameter, authorized by the$TOKEN_AUTHyou configured (this is why the proxy user needs write or admin permission). Works out of the box with no Matomo-side configuration, for both single requests and bulk requests (the Matomo JavaScript tracker batches several actions into a single bulk request by default). - Header-only — via
$http_ip_forward_header: set$http_ip_forward_headerinconfig.php(for example toX-Forwarded-For) to forward the visitor IP in that header instead. In this mode the proxy injects nocip/token_authat all and relies solely on the header for the visitor IP — so it doesn't even need a write/admin token. This only works if Matomo is configured to trust the header: both the web server in front of Matomo (Apache mod_remoteip, nginx realip) and Matomo's trusted-proxy settings (proxy_client_headers[]/proxy_ips[]in itsconfig.ini.php). If it isn't, Matomo records the proxy's IP for every visitor. - Not at all — via
$REMOVE_VISITOR_IP: the visitor IP is never sent to Matomo. See Removing the visitor IP below.
⚠️ Breaking change: previously$http_ip_forward_headerwas sent in addition tocip+token_auth; the proxy now treats it as the sole IP mechanism and injects nothing else. If you already set it, make sure Matomo's trusted-proxy configuration above is in place — otherwise leave it empty to keep usingcip.
⚠️ Behavior change: two cases that previously ended up with nocipat all — so Matomo recorded the proxy's IP instead of the visitor's — now send the visitor IP as normal. This applies whether or not$REMOVE_VISITOR_IPis set:
- a request whose only
cipis empty or array-valued, which Matomo ignores in favour of the connection IP, so the proxy now treats it as absent;- a
POSTbody in which the bulk marker"requests"appears at the very start, which Matomo does not treat as a bulk request — the proxy now agrees with it instead of forwarding the body unprocessed.
Set $REMOVE_VISITOR_IP = true; in config.php to stop sending the visitor IP to Matomo altogether. The proxy forwards cip=0.0.0.0 in its place — for single and bulk tracking requests alike — and never reads the visitor IP at all, so it cannot leak into a header either.
Use this where the visitor IP must not reach Matomo even to be anonymised there, since Matomo's own IP anonymisation runs inside Matomo and the full IP would otherwise arrive first.
Two things to check before you enable it:
⚠️ You still need the write/admin$TOKEN_AUTH— including if you previously ran header-only mode without one. Matomo honorsciponly on an authenticated request and otherwise records nothing at all, so a missing or read-only token discards your traffic instead of degrading it. This is easy to miss: a single request is rejected with HTTP 400, but a bulk request — what the JavaScript tracker sends by default — comes back as HTTP 200 with{"status":"success","tracked":0,"invalid":N}.
⚠️ Check TrackingSpamPrevention if you use its limits. Every visit now reports0.0.0.0, so the first visitor to reach its maximum actions per visit limit gets0.0.0.0/32added to its blocked ranges — after which every visit is excluded and tracking stops entirely, silently. Its included countries setting behaves the same way, because the country now comes from Matomo'sAccept-Languageguess and isxxwhen it cannot be determined. Both are unset by default. If you use them, add0.0.0.0toiprange_allowlist[]under[TrackingSpamPrevention]in Matomo'sconfig.ini.php, addxxto the country list, or leave the limits unset.
| Area | Effect |
|---|---|
| Visits, pageviews, events, goals, ecommerce, campaigns, referrers, search engines, channels, downloads, outlinks, site search, content | Not directly affected |
| Location reports and maps, location-based segments, dashboards and scheduled reports | "Unknown" — though Matomo may still guess a country from the visitor's Accept-Language header |
| Visitor IP column and IP-based segments | 0.0.0.0 for every visit |
| IP exclusions, IP-based spam/bot blocking | Can no longer identify individual visitors |
| Provider / ISP reports | "Unknown" (the reverse DNS lookup is skipped) |
| Visits, Unique Visitors, Returning Visitors, bounce rate, visit duration | Less accurate when cookies are unavailable: Matomo uses the IP as part of its cookieless visitor fingerprint, so visitors sharing an OS, browser and language may be merged into one visit. With tracking cookies enabled the visitor ID takes precedence and the effect is limited. |
| Goals, funnels, ecommerce attribution | Affected only where the above merges separate visitors |
| QueuedTracking | Requests without a visitor ID are sharded by IP, so they all land in one queue instead of being spread across the configured number |
- The option covers the IP the proxy contributes, not one a caller sends deliberately. A request supplying its own non-empty
cipkeeps it — the proxy adds neither acipnor a token of its own — because Matomo honorsciponly for a valid token holder, making such a request a deliberate decision to track a specific IP. The JavaScript tracker never sendscip, so ordinary visitor traffic is unaffected. To rule that out too, change those integrations or which tokens you issue; the proxy will not overrule them. - An empty or array-valued
cipis not such a decision — Matomo ignores those in favour of the connection IP — so the proxy drops it and sends the placeholder instead. Matomo resolvescipfrom the query string and the POST body together, with the query winning, and the proxy judges that same effective value. - Explicit location parameters (
lat,long,city,region,country) are not removed either, since Matomo already requires authentication for them. - Cookies are forwarded unchanged unless you set
$COOKIE_ALLOWLIST— see Cookie forwarding below. $http_ip_forward_headeris ignored while this is on, since it would send the IP straight back. Clear it to remove the conflict; with$DEBUG_PROXYenabled the proxy also notes the conflict in the PHP error log.- The guarantee covers what the proxy sends. Anything your own infrastructure adds to the outbound request between the proxy and Matomo — a reverse proxy, WAF or egress proxy inserting
X-Forwarded-For, for example — is outside the proxy's control and needs checking separately. - This assumes Matomo's
tracking_requests_require_authenticationis at its default of1. Set to0, Matomo honors an unauthenticatedcip, so anything could submit an IP. - The placeholder is
0.0.0.0rather than nocipat all because Matomo falls back to the connection IP whenevercipis empty — sending nothing would record the proxy's own IP and report its location as real visitor data.
By default, the proxy forwards the visitor's entire Cookie header to Matomo unchanged. If your site also sets other cookies (session, consent-management, A/B testing, etc.) alongside Matomo's, those are forwarded too.
To restrict this, set $COOKIE_ALLOWLIST in config.php to an array of cookie names. Each entry is matched either as an exact name (e.g. mtm_consent) or, if it ends with *, as a prefix (e.g. _pk_id* matches both _pk_id and _pk_id.1.1fff). Prefix entries are needed for Matomo's default id/session/referrer/custom-variable cookies, since the JavaScript tracker appends a per-site/per-domain suffix to their base name. Cookies matching no entry are stripped before the request reaches Matomo. Matching is case-sensitive; a prefix match is a plain string prefix, not delimiter-aware (so _pk_id* could in theory also match an unrelated cookie sharing that prefix, though unlikely in practice).
Note: leaving
$COOKIE_ALLOWLISTunset keeps today's behavior of forwarding all cookies unchanged. Setting it — even to an empty array — switches the proxy into allowlist mode; an empty array forwards no cookies at all. A bare*or empty-string entry is a no-op (matches nothing), not "allow everything", so a strayarray('*')can't defeat the allowlist.
⚠️ Always keepmatomo_ignore(or whatever cookie your Matomo's opt-out/consent setup relies on) in the allowlist. Dropping it silently re-enables tracking for visitors who opted out, with no visible error.
⚠️ Check your Matomo tracker config before copying the example list below. A customsetCookieNamePrefix()replaces_pk_*with your own prefix, and enabled plugins (e.g. HeatmapSessionRecording) or third-party tracking code may set additional cookies not covered by the example.A dropped tracking cookie doesn't error — Matomo just stops recognizing returning visitors, silently inflating visit counts. After configuring this, load a tracked page twice and confirm Matomo logs one visit, not two.
Some tracking parameters (cip, cdt, cdo, country, region, city, lat, long) are only honored by Matomo for an authenticated request. The proxy never lends its $TOKEN_AUTH to a request — or to an individual entry of a bulk request — that carries one of these override parameters or its own token_auth:
- Carries an override parameter, no token: forwarded without the proxy's token, so Matomo rejects/skips it exactly as if it had been sent directly without authentication — rather than being silently tracked with the client-supplied override. To set these parameters legitimately, send your own valid
token_auth.- For
cipthis applies to a non-empty value only. Matomo ignores an empty or array-valuedcipand falls back to the IP of the connection, so the proxy treats such a value as nocipat all: it is dropped and replaced with the IP the proxy would otherwise have sent.
- For
- Carries its own
token_auth: the proxy adds no token of its own and lets the client's token govern. It still forwards the visitor IP ascip, so that token must have write access to authorize it (otherwise the request/entry is rejected).
⚠️ Behavior change: if you add any of these parameters viaappendToTrackingUrl(or otherwise) without your owntoken_auth, those requests are now rejected by Matomo. Previously the proxy stripped the parameter and tracked the rest of the hit; it no longer does. Send a validtoken_authif you need these parameters.
Note: if your Matomo server sets
bulk_requests_require_authentication = 1, it requires a single batch-leveltoken_authfor the whole bulk request. The proxy supplies that batch-level token only when every entry in the batch is clean; if any entry carries an override parameter or its owntoken_auth, the proxy withholds it (a batch-level token would wrongly authorize that entry) and Matomo then rejects the entire bulk request — clean entries included. Under that configuration, send your own batch-leveltoken_author avoid mixing override entries into proxied bulk requests.
Note: bulk tracking requests must be sent with an
application/x-www-form-urlencodedcontent type (as the Matomo JavaScript tracker does). Bulk request bodies sent asapplication/jsonare not forwarded by the proxy.
Note: the proxy rebuilds the forwarded request from the parameters PHP parsed (
$_GET/$_POST, or the decoded JSON for bulk), so it only ever sends Matomo what it inspected. Make sure the proxy host's PHPpost_max_sizeis large enough for your biggest (bulk) tracking requests — a body exceeding it is dropped by PHP and not forwarded.
If you have found a bug, you are welcome to submit a pull request.
To work on the proxy, clone the repository:
git clone https://github.com/matomo-org/tracker-proxy.git
⚠️ A clone is a development checkout, not an installation. Alongside the proxy it contains the test suite and other development files, so only serve a clone from a local development web server, never from a publicly reachable web root. To install the proxy on a live site, use the source archive.
Before running the tests, create a config.php file w/ the following contents in the root repository directory:
<?php
$MATOMO_URL = 'http://localhost/tests/server/';
$PROXY_URL = 'http://localhost/';
$TOKEN_AUTH = 'xyz';
$timeout = 5;
// Test-only request headers (below) let the suite exercise config variations per-request.
// Gated on the local test-server URL so a stray copy to production is inert.
$isTestServer = strpos($MATOMO_URL, '/tests/server/') !== false;
// Exercise IP-forward-header handling.
if ($isTestServer && !empty($_SERVER['HTTP_X_TEST_IP_FORWARD_HEADER'])) {
$http_ip_forward_header = $_SERVER['HTTP_X_TEST_IP_FORWARD_HEADER'];
}
// Exercise removal of the visitor IP (any truthy value enables it).
if ($isTestServer && isset($_SERVER['HTTP_X_TEST_REMOVE_VISITOR_IP'])) {
$REMOVE_VISITOR_IP = $_SERVER['HTTP_X_TEST_REMOVE_VISITOR_IP'];
}
// Exercise cookie-allowlist filtering (comma-separated entries; empty value = explicit empty allowlist).
if ($isTestServer && isset($_SERVER['HTTP_X_TEST_COOKIE_ALLOWLIST'])) {
$COOKIE_ALLOWLIST = $_SERVER['HTTP_X_TEST_COOKIE_ALLOWLIST'] === ''
? array()
: explode(',', $_SERVER['HTTP_X_TEST_COOKIE_ALLOWLIST']);
}
// Exercise misconfiguration handling (a non-array $COOKIE_ALLOWLIST).
if ($isTestServer && isset($_SERVER['HTTP_X_TEST_COOKIE_ALLOWLIST_INVALID'])) {
$COOKIE_ALLOWLIST = $_SERVER['HTTP_X_TEST_COOKIE_ALLOWLIST_INVALID'];
}
// Exercise the test server's refusal to run for anything but a test configuration.
if ($isTestServer && !empty($_SERVER['HTTP_X_TEST_NON_TEST_CONFIG'])) {
$MATOMO_URL = 'https://matomo.example.org/matomo/';
}
The tests need a webserver to be pointed to the root of this repository. The simplest way is to just use Vagrant:
$ vagrant up
$ vagrant ssh
$ cd /vagrant/tests
$ composer install
- Set
allow_url_fopen = Offin your webserver php.ini - Check in phpinfo() that
allow_url_fopen = Off - Run:
vendor/bin/phpunit
- Set
allow_url_fopen = Onin your webserver php.ini - Check in phpinfo() that
allow_url_fopen = On - Run:
vendor/bin/phpunit
Be advised that the proxy and its tests require PHP 7.2 or higher.