Skip to main content

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:

HelperAnswers
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