Plausible & Umami APIs: Pulling Privacy-First Data Programmatically
Most privacy-first analytics setups stop at the dashboard. You log in, check your numbers, and move on. But if you want to automate reporting, build custom dashboards, or feed analytics data into a pipeline — you need to talk to the API. The good news: both Plausible and Umami expose clean, well-documented REST APIs that make this straightforward.
In this guide I’ll walk you through authenticating with each tool, making your first requests, and understanding when you’d reach for one over the other. I’ll keep the examples concrete — real curl commands you can run today, adapted for a small pipeline at the end.
If you’re newer to the “why” behind privacy-first analytics, I’ve covered that in depth in why privacy-first analytics captures better data than Google Analytics. This article picks up where that one leaves off — at the API level.
What Makes a Privacy-First Analytics API Different
Before jumping into code, it’s worth naming what you’re getting by choosing a privacy-first analytics API over a proprietary one.
With GA4’s Data API, you’re querying sampled, cookie-based data behind OAuth2 flows that require service accounts and a Google Cloud project. Consent gaps mean missing data. Sampling kicks in on larger date ranges. And your data lives on Google’s infrastructure — full stop.
Plausible and Umami take a different position. Neither sets cookies. Neither fingerprints visitors. Both are open-source, which means you can audit exactly what data is collected — and, if you need it, self-host so the data never leaves your own servers. The APIs reflect this: simpler auth, straightforward JSON responses, and no sampling caveats to navigate.
That simplicity translates into faster integrations. In my experience helping teams migrate away from GA, the Plausible and Umami APIs are the part that surprises people most — you’re up and running in minutes, not hours.

Plausible Stats API: Authentication and Your First Query
Plausible uses a straightforward Bearer token for authentication. You generate the key from your account settings at plausible.io, then pass it in the `Authorization` header on every request. That’s it — no OAuth dance, no refresh tokens.
Getting Your API Key
Go to plausible.io/settings, navigate to the API Keys section, and create a new key. Treat this like a password: store it in an environment variable, never in client-side code or version control.
Making a Query
The current endpoint is POST /api/v2/query (the older v1 GET endpoints are legacy — use v2 for any new work). You POST a JSON body describing what you want: which site, which metrics, what date range, and optionally filters or dimensions to break the data down.
Here’s a minimal example — total pageviews and unique visitors for the last 30 days:
curl -X POST https://plausible.io/api/v2/query \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"site_id": "yourdomain.com",
"metrics": ["pageviews", "visitors"],
"date_range": "30d"
}'
The response is JSON with a results array. For simple aggregate queries like this one, you get a single object with your metric values.
Breaking Down by Dimension
The real power comes from adding dimensions. Want to see traffic broken down by page, source, or country? Add a dimensions array:
curl -X POST https://plausible.io/api/v2/query \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"site_id": "yourdomain.com",
"metrics": ["pageviews", "visitors", "bounce_rate"],
"date_range": "30d",
"dimensions": ["visit:source"]
}'
You can also filter — for example, to get metrics only for traffic originating from a specific UTM campaign, or only for a particular page path. The Plausible Stats API documentation covers the full list of available metrics, dimensions, and filter operators.
Rate Limits
Plausible allows 600 requests per hour by default. For most reporting pipelines that’s generous — but if you’re running high-frequency polling across many sites, track your usage and add a small delay between requests to stay comfortable under the threshold.

Umami API: Two Auth Flows for Two Deployment Models
Umami has two distinct authentication patterns depending on whether you’re using Umami Cloud or a self-hosted instance. The data you get back is the same; the way you prove who you are differs.
Umami Cloud Authentication
On Umami Cloud, generate an API key from your account settings. Then send it as a custom header — x-umami-api-key — on each request. The base URL for Cloud is https://api.umami.is.
curl https://api.umami.is/v1/websites/YOUR_WEBSITE_ID/stats \
-H "x-umami-api-key: YOUR_API_KEY" \
-G \
--data-urlencode "startAt=1717200000000" \
--data-urlencode "endAt=1719791999000"
The startAt and endAt parameters are Unix timestamps in milliseconds. A quick way to generate them: date -d "30 days ago" +%s%3N on Linux, or Date.now() minus the appropriate offset in JavaScript.
Self-Hosted Authentication
Self-hosted Umami uses a login-then-token flow. First you POST your credentials to get a session token, then use that token as a Bearer on subsequent requests:
# Step 1: Authenticate and get a token
TOKEN=$(curl -s -X POST https://your-umami.example.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "YOUR_PASSWORD"}' \
| jq -r '.token')
# Step 2: Query website stats
curl "https://your-umami.example.com/api/websites/YOUR_WEBSITE_ID/stats" \
-H "Authorization: Bearer $TOKEN" \
-G \
--data-urlencode "startAt=1717200000000" \
--data-urlencode "endAt=1719791999000"
The token expires with your session, so for automated scripts you’ll want to re-authenticate at the start of each run, or cache the token and refresh when you get a 401.
What the Stats Endpoint Returns
The /api/websites/{websiteId}/stats endpoint gives you aggregated metrics for a time window: pageviews, sessions, visitors, bounce rate, and average visit duration. For page-level breakdowns, the metrics, and pageviews endpoints let you slice further. The full reference is at docs.umami.is/docs/api.
Plausible vs Umami API: Key Differences at a Glance
Both tools expose solid REST APIs, but they make different design choices. Here’s how they compare on the dimensions that matter most for integrations:
| Aspect | Plausible | Umami |
|---|---|---|
| Auth method | Bearer token (API key from settings) | Cloud: custom header (x-umami-api-key); Self-hosted: login → Bearer token |
| Query style | POST JSON body with metrics/dimensions/filters | GET with query params (startAt, endAt, etc.) |
| Filtering/dimensions | Rich — filter by page, source, country, device, UTM and more via filters array |
Good — segment by URL, referrer, browser, OS, country, device type |
| Rate limit | 600 req/hr (default) | Not published; self-hosted = your own server limits |
| Self-hosted support | Yes — same API on self-hosted instances | Yes — same API, different auth flow |
| Real-time data | Current visitors endpoint available | Active visitors endpoint available |
| Multi-site via one key | Yes — specify site_id per request |
Scoped per website ID in the URL path |
| Open source | Yes (AGPL-3.0) | Yes (MIT) |
The biggest practical difference is query style: Plausible’s POST-body approach makes complex queries with multiple filters and dimensions easier to express, while Umami’s GET-based design is simpler for basic stats pulls and works cleanly from a shell one-liner.
When to Use Which
After integrating both across a range of client setups, here’s how I’d frame the decision:
Reach for Plausible when:
- You need rich dimensional breakdowns — traffic by source + page + country in one query
- You’re building a reporting layer that aggregates across multiple sites (the multi-site API support is genuinely well thought-out)
- UTM campaign tracking is important and you want API access to UTM dimensions
- You want the Cloud option with the fewest operational headaches
Reach for Umami when:
- You’re self-hosting and want MIT-licensed software with no usage restrictions
- You need event tracking with custom properties, and want API access to those events
- Your team already runs a Node.js or similar stack and wants something that feels at home
- Operational simplicity of the self-hosted instance is a priority — Umami is lightweight by design
If you’re building on top of first-party data tracking without cookies, either tool works well — Umami’s event API gives you more direct access to custom event data, while Plausible’s aggregation API is cleaner for traffic summaries.
Building a Small Pipeline: Nightly Stats to Postgres
Let’s put this together in a concrete example. The goal: pull yesterday’s stats from Plausible (or Umami) each night and store them in a local Postgres table for long-term trend analysis.
The Schema
CREATE TABLE daily_stats (
id SERIAL PRIMARY KEY,
site_id TEXT NOT NULL,
stat_date DATE NOT NULL,
pageviews INTEGER,
visitors INTEGER,
source TEXT DEFAULT 'plausible',
fetched_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (site_id, stat_date, source)
);
The Fetch Script (Node.js)
// fetch-stats.mjs
import { Client } from 'pg';
const PLAUSIBLE_KEY = process.env.PLAUSIBLE_API_KEY;
const SITE_ID = process.env.SITE_ID; // e.g. "yourdomain.com"
async function fetchYesterday() {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const dateStr = yesterday.toISOString().split('T')[0]; // "YYYY-MM-DD"
const res = await fetch('https://plausible.io/api/v2/query', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PLAUSIBLE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
site_id: SITE_ID,
metrics: ['pageviews', 'visitors'],
date_range: [dateStr, dateStr],
}),
});
if (!res.ok) throw new Error(`Plausible API error: ${res.status}`);
const data = await res.json();
return { date: dateStr, ...data.results[0] };
}
async function store(stats) {
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
await client.query(
`INSERT INTO daily_stats (site_id, stat_date, pageviews, visitors)
VALUES ($1, $2, $3, $4)
ON CONFLICT (site_id, stat_date, source) DO UPDATE
SET pageviews = EXCLUDED.pageviews,
visitors = EXCLUDED.visitors,
fetched_at = now()`,
[SITE_ID, stats.date, stats.pageviews, stats.visitors]
);
await client.end();
}
fetchYesterday().then(store).catch(console.error);
Drop this in a cron job (0 5 * * * node /opt/scripts/fetch-stats.mjs), and you have a rolling history that no dashboard retention limit can touch. The same pattern works for Umami — swap the fetch call for the Umami Cloud or self-hosted endpoint, adjust the response parsing, and you’re done.
Handling Errors Gracefully
A few things to build in from the start:
- Retry on 429: If you hit Plausible’s rate limit, the response includes a
Retry-Afterheader. Respect it with exponential backoff rather than hammering the endpoint. - Validate the response shape before inserting. A null
pageviewsis fine to store; a missingresultskey means something went wrong upstream. - Log fetched_at so you can tell if yesterday’s run silently failed. A row with data from three days ago is worse than a visible gap.
For teams running more sophisticated setups — server-side tag management, custom event pipelines — the considerations in our server-side tracking setup guide apply here too. The API integration layer and the tracking layer share the same security principle: keep credentials server-side, validate everything, and never expose tokens to the client.
Security Considerations
A few rules that apply regardless of which tool you use:
- Never embed API keys in client-side JavaScript. Your Plausible key or Umami token in a browser bundle means anyone can extract it and query your data — or exhaust your rate limit. Proxy requests through your own backend.
- Use environment variables, not config files in version control. A
.envfile accidentally committed is a common credential leak vector. - For self-hosted Umami, the login-then-token flow means your admin password is transmitted on each script run. Use a dedicated API user with minimal permissions rather than your main admin account.
- Rotate keys periodically — especially if a team member who had access leaves. Both Plausible and Umami let you revoke and regenerate keys from the settings panel.
Because both tools are privacy-first by architecture — no cross-site tracking, no PII in the data model — you’re not creating new compliance risk by querying the API. But treat the API credentials with the same care you’d give any database credential.
Final Thoughts
The thing I appreciate most about both the Plausible and Umami APIs, after working with a lot of analytics integrations over the years, is what’s absent: no OAuth flows to babysit, no sampling disclaimers in the response, no dependency on a third-party identity provider. You generate a key, make a request, get clean data.
That simplicity isn’t accidental — it reflects the same philosophy that makes these tools worth using in the first place. When your analytics don’t collect what they shouldn’t, your API doesn’t expose what it can’t. The privacy-first analytics API story is ultimately just a continuation of the privacy-first tracking story: less surface area, cleaner data, easier to reason about.
Start with one endpoint. Get data into a table. Build from there.
Written by Alicia Bennett
Lead Web Analyst based in Toronto with 12+ years in digital analytics. Specializing in privacy-first tracking, open-source tools, and making data meaningful.
More about Alicia →Related Articles
Top Google Analytics Alternatives for 2025
Why You Should Ditch Google Analytics in 2025 Google Analytics, once a staple for webmasters, now leaves many frustrated since…
Privacy-First Analytics: Why It Captures Better Data Than Google Analytics
Every time a visitor lands on your site, you face a choice: collect everything possible and hope for insights, or…
The Best Web Analytics APIs Compared
If you need to pull analytics data into your own apps, dashboards, or automated reports, you need an API. The…