matomo-rs
An async Rust client for the Matomo Reporting API, built primarily for data export and migration — pull your visits, actions, and referrers out of Matomo and into whatever you're moving to.
Matomo's API isn't a typical REST surface: it's a single dispatch endpoint (POST /index.php with module=API&method=Module.action plus a shared parameter envelope). This crate wraps that in typed methods for the core reporting modules, with a generic escape hatch for everything else.
Status
Early but working — verified end-to-end against a live Matomo 5.8 instance. It covers the methods that matter for migration:
Live.getLastVisitsDetails— the raw, per-visit clickstream (the migration workhorse), with resumable paging.VisitsSummary.get— aggregate metrics that can't be recomputed from raw data.Actions.*— page URLs, page titles, downloads, outlinks.Referrers.*— referrer types, full referrer list.API.*— version, report metadata, bulk requests.
Anything not yet typed is reachable through call / call_typed / call_raw.
It's also transport-agnostic: the core (Client, Endpoint, Query) carries no HTTP client. A reqwest-backed transport ships behind a feature flag, or you bring your own by implementing Client.
Requirements
- Matomo 5.x.
- An API auth token. Default auth sends it as
token_authin the POST body, which works on all 5.x and keeps the token out of the URL.Authorization: Beareris also supported, but only useful on Matomo 5.4+ and where the web server forwards theAuthorizationheader to PHP — so the body token is the safer default.
Bring your own async runtime — the crate pulls in no runtime directly. It's tested against tokio.
Install
No HTTP client is pulled in by default. For the ready-made reqwest transport, enable a feature with a TLS backend:
[]
= { = "0.1", = ["reqwest", "reqwest-rustls"] }
= { = "1", = ["macros", "rt-multi-thread"] }
= "0.3"
Or cargo add matomo-rs --features reqwest,reqwest-rustls. Use reqwest-native-tls instead if you want the OS-native TLS stack.
The library name is matomo, so you use matomo::.... The reqwest-backed client lives at matomo::reqwest::MatomoClient.
Features
reqwest— the bundledMatomoClienttransport. No TLS backend on its own; pair it with one below.reqwest-rustls—reqwestwithrustls.reqwest-native-tls—reqwestwith the OS-native TLS stack.chrono/time—Fromconversions intoDate.
All features are off by default.
Quick start
use MatomoClient;
use ;
async
Exporting raw visits (streaming)
Live.getLastVisitsDetails is paged. The stream auto-pages until the data runs out (an empty page is the authoritative terminator — a short page is not), and it's 'static + Send, so you can tokio::spawn it.
use StreamExt;
use MatomoClient;
use ;
# async
Resumable paging
For long exports that may crash or restart, page manually with a Cursor (it's Serialize/Deserialize, so you can checkpoint it):
# async
Date ranges
Matomo is picky about range grammar, so the types only let you express what it accepts: a single rolling keyword, or an absolute start with an absolute/today/yesterday end. A keyword start like last7,today simply isn't representable.
use ;
LastN; // last7
PreviousN; // previous30
ymd; // 2024-01-01,2024-06-30
Between ;
The escape hatch
Not every method is typed yet. For anything else, call it directly and deserialize into your own type (or just a serde_json::Value):
# async
call_raw returns the response bytes untouched if you'd rather stream-parse a large payload yourself.
Preflight
On the first call, the client checks the instance version and confirms the reports it depends on are actually exposed, failing early with a clear error instead of a confusing decode failure later. Opt out with MatomoClient::builder().skip_preflight().
Error handling
Matomo signals failures as HTTP 200 with a {"result":"error",...} body, which the client turns into a structured Error::Api — classified into an ApiErrorKind (best-effort, from the message text) and tagged with the method that failed, so a mid-export error is debuggable:
use ;
# async
Bring your own HTTP client
The reqwest transport is optional. If you'd rather not pull it in — to reuse an existing client, or run on a different stack — implement the Client trait over neutral http/bytes types and drive endpoints through Query:
use Bytes;
use ;
use ;
// Then drive any endpoint through the trait:
# async
To reuse a pre-configured reqwest::Client (custom TLS, proxy, timeouts) without writing a transport, pass it in: MatomoClient::with_reqwest_client(base_url, auth, http).
A few caveats
- Matomo's JSON is loose — numbers arrive as strings, empty results flip between
[]and{}, fields come and go between releases. The models handle the known cases and deliberately don't reject unknown fields, but if you hit a shape that doesn't deserialize, the escape hatch is your friend (and a bug report is welcome). - The
Visitmodel keeps the data-bearing fields and drops Matomo's presentation fields (*Pretty, icons, flags). Usecall_rawif you need the raw lot. - Aggregate metrics like unique visitors can't be derived from raw visits — use
VisitsSummary.getfor those.
Testing
Unit tests run against mocked HTTP — no network, no credentials:
The integration test hits a real Matomo instance and is #[ignore]d by default. Point it at an instance with a read-only token:
MATOMO_URL=https://stats.example.com/ \
MATOMO_TOKEN=your-token \
MATOMO_IDSITE=1 \
License
MIT OR Apache-2.0.