Docs
How to put metrics in your app, send hits from anywhere, and read the numbers through the REST API or an AI agent. The full contract — every endpoint, object and error — lives in the repository.
Read the full contract (CONTRACT.md) →Tracker script
A zero-dependency script under 3 KB served from this host. Add it once in your layout; it sends one hit per page view and per single-page-app route change, with no cookies and nothing stored in the browser.
<script defer src="https://metrics.3m4.net/m.js" data-key="metrics_pub_…"></script>Attributes (defaults shown)
<script defer src="https://metrics.3m4.net/m.js" data-key="metrics_pub_…"
data-spa="true" data-hash="false" data-dnt="false" data-local="false"></script>data-key— the project's public key (required).data-spa— hook the History API and send a page view on every route change. Set to false to call metrics.pageview() yourself.data-hash— hash routing: keep the #fragment in the URL and listen to hashchange.data-dnt— honour the browser's Do Not Track signal.data-local— also track on localhost, raw IPs, file: and *.local (ignored by default).data-api— override the collect endpoint (self-hosted instances behind another host).
JavaScript API
window.metrics exposes track(name, props) for custom events and pageview() for manual page views. Calls queued before the script loads are replayed.
// custom events (≤ 2 KB of props)
metrics.track("signup", { plan: "pro" });
// manual page view (when data-spa="false")
metrics.pageview();
// queue calls before the script has loaded
window.metrics = { q: [["track", "cta_click", { where: "hero" }]] };Clicks on elements with data-metrics-event are tracked automatically, with optional JSON props:
<button data-metrics-event="cta_click" data-metrics-props='{"where":"hero"}'>Start</button>What is sent: the page URL without its query string (UTM tags are read and then dropped), the referrer, the title, the screen size and the browser language. Device, browser, OS and location are derived on the server from the request and never stored raw.
Tracking pixel
For places where no JavaScript runs — newsletters, README files, documentation on another host. The image is a 43-byte transparent GIF, never cached. Pass the page as u and, optionally, an event name as n.
<img src="https://metrics.3m4.net/api/collect/pixel.gif?k=metrics_pub_…&u=/newsletter/october&n=open"
alt="" width="1" height="1" style="position:absolute;left:-9999px">Collect API
POST /api/collect with the project's public key in the X-Project-Key header (or a k field). JSON, form-encoded and text/plain bodies are accepted, CORS is open, and the answer is always 202 so a tracked site never sees an error.
curl -X POST https://metrics.3m4.net/api/collect \
-H "X-Project-Key: metrics_pub_…" -H "Content-Type: application/json" \
-H "User-Agent: my-backend/1.0" \
-d '{
"n": "purchase",
"u": "https://myapp.example.com/checkout",
"r": "https://news.ycombinator.com/",
"l": "es-MX",
"p": { "amount": 42, "currency": "USD" },
"s": "api"
}'- n: event name (defaults to pageview). 1-60 letters, digits, spaces, _ . : / or -.
- u: the page URL or path (required for anything meaningful). Query strings are only read for UTM tags.
- r: referrer URL; t: page title (≤ 200); l: language (≤ 16); w / h: screen size.
- p: a JSON object of properties for custom events (≤ 2 KB).
- s: source — script (default), pixel or api. Device, browser, OS and location come from the request's own headers and address.
The body says what happened: { ok: true } when stored, or { ok: false, reason } with reason = paused (tracking off for the project), origin (hostname not in the allowed list), bot (crawler or headless browser) or limit (over the monthly budget — counted, not stored).
Base URL
https://metrics.3m4.net/apiAuthentication
Three kinds of credentials, each for one job:
- Dashboard sessions: JWT bearer tokens issued by /auth/login. Hold every scope.
- Management API: API keys with a role (viewer / editor / admin). Header X-API-Key: metrics_live_… (or Authorization: Bearer).
- Collect API, tracker and pixel: project public keys. Header X-Project-Key: metrics_pub_… (or the k field). They identify one project and can only send hits.
Projects
A project is one site or app. It owns a public key, a time zone (used to bucket days), an optional list of allowed hostnames, and the collect / bot switches.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /projects?q= | projects:read | with 30-day counts |
| POST | /projects | projects:write | {name, key?, url?, timezone?, allowed_origins?} → 201 · 402 limit_reached |
| GET | /projects/:id | projects:read | |
| PATCH | /projects/:id | projects:write | name, url, timezone, allowed_origins, collect_enabled, exclude_bots |
| DELETE | /projects/:id | projects:delete | cascades events |
| POST | /projects/:id/rotate-key | projects:write | old public key stops immediately |
| GET | /projects/:id/summary | stats:read | today / yesterday / 7d / 30d, live |
| DELETE | /projects/:id/events?before= | events:delete | purge stored events |
curl -X POST https://metrics.3m4.net/api/projects \
-H "X-API-Key: metrics_live_…" -H "Content-Type: application/json" \
-d '{"name": "My app", "url": "https://myapp.example.com", "timezone": "Europe/Madrid"}'Stats
Ranges are inclusive local dates in the tz parameter (default: the project's zone), as period=today|yesterday|7d|30d|90d|12mo or from/to. Any dimension can be passed as a filter (e.g. country=ES&device=mobile) and everything in the answer is scoped to it. Visitors are counted with a daily hash, so across several days they are daily uniques, summed.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /projects/:id/stats?period=&from=&to=&tz=&interval=&limit=&<dimension>= | stats:read | totals, previous period, series, top-8 breakdowns, live |
| GET | /projects/:id/stats/breakdown?by=&page=&per_page= | stats:read | one dimension, paginated |
| GET | /projects/:id/stats/series | stats:read | the time series alone |
| GET | /projects/:id/events?kind=&q=&page= | stats:read | raw events, newest first |
| GET | /projects/:id/live | stats:read | visitors in the last 5 minutes |
curl "https://metrics.3m4.net/api/projects/$PROJECT/stats?period=7d&tz=Europe/Madrid" \
-H "X-API-Key: metrics_live_…"Dimensions
Usable as breakdown (by=) and as filters on every stats and events endpoint. referrer=(direct) selects hits with no referrer.
path · hostname · referrer · country · region · city · device · browser · os · lang · utm_source · utm_medium · utm_campaign · nameErrors & limits
Errors are JSON: { "error": { "code", "message" } }. Creating past a plan limit answers 402 limit_reached with error.which = projects | api_keys. Hits over the monthly event budget are accepted (202) but not stored.
Privacy: by default metrics stores no raw IP addresses or user agents — only a salted daily hash and the derived device / browser / OS / location fields. A per-project switch (store_ip) keeps the raw address on each hit. Page URLs lose their query string before storage.
MCP (AI agents)
The MCP server exposes the management API as tools, filtered by the role of the key you connect with. Destructive tools ask for confirmation. A weekly_report prompt walks a model through a project's numbers.
claude mcp add metrics --scope user --env METRICS_API_KEY=metrics_live_… -- npx -y metrics-mcpclaude mcp add --transport http metrics https://metrics.3m4.net/mcp --header "Authorization: Bearer metrics_live_…"Tools: list_projects, get_project, create_project, update_project, rotate_project_key, delete_project, project_summary, get_stats, get_breakdown, list_events, get_live, purge_events, get_usage, get_audit_log.