Tools & Integrations

Plausible & Umami APIs: Pulling Privacy-First Data Programmatically

Alicia Bennett
· · 11 min read
privacy-first analytics api

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.

privacy-first analytics api

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.

privacy-first analytics api

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-After header. Respect it with exponential backoff rather than hammering the endpoint.
  • Validate the response shape before inserting. A null pageviews is fine to store; a missing results key 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 .env file 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