Reporting overview
The read side is a thin, cached client over Matomo's Reporting API. It reuses the
host, site_id and token you already configured — a token with at least view
access is required — and posts the token_auth in the request body, never in the
query string, so it does not end up in an access log.
use MatomoAnalytics\Facades\MatomoReports;
$summary = MatomoReports::visitsSummary(['period' => 'day', 'date' => 'today']);
$pages = MatomoReports::topPageUrls(['period' => 'month', 'date' => '2026-01']);
// Anything not covered by a helper:
$goals = MatomoReports::get('Goals.get', ['period' => 'week', 'date' => 'today']);
reporting.default_period and reporting.default_date fill in what you omit, so a
call with no parameters is a valid call.
Failure never throws
Every read returns null on failure and surfaces Matomo's own error envelope through
lastError():
if ($summary === null) {
logger()->warning('Matomo reporting failed: '.MatomoReports::lastError());
}
That is the whole error convention, and it exists because a reporting widget should degrade, not take the page down. A failed call is never cached, so the next request retries rather than serving a cached failure for an hour. Failures also go through the same throttled alerting as tracking.
Curated helpers
The curated helpers cover the reports most applications actually want, each wrapping a Reporting API method with sensible parameters:
| Helper | Answers |
|---|---|
visitsSummary() | Visits, unique visitors, actions, bounce rate, time on site |
liveCounters($lastMinutes) | Who is on the site right now |
lastVisits($count) | The most recent visits, in detail |
topPageUrls() · topPageTitles() | The most-visited pages, by URL and by title |
siteSearchKeywords() | What visitors searched for |
topReferrers() · referrerTypes() | Where visitors came from, and through which channel |
countries() · deviceTypes() · browsers() | Who they are |
goals() · eventCategories() | Conversions and custom events |
customDimension($idDimension) | One custom dimension's values |
contentNames() · contentPieces() | Content-tracking impressions |
Every helper takes the same optional $params array as get(), so you can override
period, date, segment or any report filter on any of them. On the four that need a value
of their own it is the second argument, after that value — liveCounters(),
lastVisits(), customDimension() and funnelFlow():
MatomoReports::topPageUrls(['period' => 'week']);
MatomoReports::liveCounters(30, ['period' => 'week']); // not liveCounters(['period' => 'week'])
Anything not in the list is one get() call away — the helpers are a convenience, not
a boundary.
One round-trip for several reports
A dashboard usually needs five reports, not one. bulk() sends them as a single
API.getBulkRequest:
[$visits, $actions] = MatomoReports::bulk([
'VisitsSummary.get',
['method' => 'Actions.get', 'period' => 'week'],
]);
Each entry is either a method name or an array with a method key plus that request's
own parameters, and results come back aligned by index.
A slot is null when the whole bulk call failed, not when one sub-request did. The
{"result":"error"} envelope is detected on the OUTER response — if that fires, every slot
is null and lastError() says why. A sub-request that Matomo reports as failed comes back
as whatever Matomo put in that slot, and an error object is still an object, so it survives
into your data. Check the shape before you use it:
[$visits, $actions] = MatomoReports::bulk([...]);
if (($actions['result'] ?? null) === 'error') {
// this one request failed; $visits may still be good
}
On Matomo Cloud this matters for a second reason: the Reporting API is the endpoint most likely to be throttled, and one bulk request counts once.
Caching
'cache' => [
'enabled' => true,
'store' => env('MATOMO_REPORTING_CACHE_STORE'), // null = default store
'prefix' => 'matomo-analytics:report',
'ttl' => [
'live' => 60, // Live.* realtime counters
'today' => 300, // periods covering today (not yet archived)
'recent' => 900, // yesterday / lastN / previous ranges
'historical' => 3600, // fully archived past periods
],
],
The TTL is date-aware, which is the point: last March's numbers will never change again, so caching them for an hour costs nothing, while a realtime counter cached for an hour would be a bug. The client picks the bucket from the report and the date range you asked for.
Invalidate everything at once:
MatomoReports::flushCache();
That is a versioned-prefix bump rather than a key sweep, so it works on every cache store — including the ones that cannot enumerate keys — and it is cheap regardless of how much is cached.
Set store to keep report caching off your application's main cache store. Set
enabled to false while debugging a report, so you see Matomo's live answer.
Timeouts
reporting.timeout is separate from the tracking timeout and defaults higher — a
report over a long date range legitimately takes longer than a tracking hit. If a
dashboard query times out, raise this rather than the tracking timeout.
Next
- Queries and segments — the fluent builder, filters, and segments.
- Premium plugin reports — thin adapters for the licensed plugins.
matomo:report— run any method from the command line.