API

Read your analytics from your own code — pull pageviews for any page into your CMS, admin, or dashboard.

The read API lives at https://moonship.com/api/v1. It returns JSON, and every number it reports matches what the dashboard shows for the same range and site.

Authentication

Generate a key on your Account page, then send it as a bearer token:

curl -H "Authorization: Bearer mship_YOUR_KEY" \
  "https://moonship.com/api/v1/sites"

The key can read every site on your account, so treat it like a password: keep it on your server and never ship it to a browser. If your admin page needs the data client-side, call the API from your own backend and pass the result down.

Your site's tracking key will not work here. That one is published in your page source, so it can't protect anything.

Requests are limited to 120 per minute.

Find your site ID

GET /api/v1/sites
{
  "sites": [
    { "id": "3f0c…", "name": "HeyTaco", "owner": true }
  ]
}

Pageviews for every page

One request for a whole section of the site — the call to use when listing posts with their view counts, rather than one request per post.

GET /api/v1/sites/:siteId/pages?range=30d&path_prefix=/blog/
{
  "range": { "since": "…", "until": "…", "timezone": "America/Chicago" },
  "pages": [
    { "path": "/blog/taco-tuesday", "pageviews": 1204, "visitors": 880 },
    { "path": "/blog/remote-recognition", "pageviews": 642, "visitors": 501 }
  ]
}

Add limit (up to 500) and offset to page through.

Detail for one page

Totals, the change against the previous period, and the time series to chart.

GET /api/v1/sites/:siteId/pages/stats?path=/blog/taco-tuesday&range=30d
{
  "range":   { "since": "…", "until": "…", "timezone": "…", "interval": "day" },
  "matched": ["/blog/taco-tuesday"],
  "totals":  { "pageviews": 1204, "visitors": 880, "sessions": 910 },
  "previous":{ "pageviews": 980,  "visitors": 700, "sessions": 733 },
  "change":  { "pageviews": 22.9, "visitors": 25.7, "sessions": 24.1 },
  "series": [
    { "date": "2026-08-24T00:00:00.000Z", "pageviews": 41, "visitors": 33 },
    { "date": "2026-08-25T00:00:00.000Z", "pageviews": 38, "visitors": 30 }
  ]
}

change is a percentage, or null when the previous period had nothing to compare against.

Parameters

  • range — 1d, 7d, 30d, 90d, 6mo, 12mo, 24mo, ytd, or all. Defaults to 30d.
  • start & end — YYYY-MM-DD for a custom window; the end date is included.
  • path — one path, repeated, or comma-separated. A leading slash is optional and a trailing slash is matched either way, so /blog/post and /blog/post/ return the same thing.
  • path_prefix — every path starting with this, e.g. /blog/.
  • interval — hour, day, week, or month. Chosen for you based on the range if you leave it out.
  • timezone — an IANA name. Defaults to your account timezone, so buckets line up with the dashboard.
  • compare — previous (default), yoy, or none.
  • include — comma-separated extras on the stats endpoint: sources, devices, countries. Each returns a top-20 breakdown for the matched page(s).

Only the path is matched — query strings, hashes, and the domain are ignored. Bot traffic follows your site's own setting.

Charting it

Fetch on your server, then hand the series to any chart library:

// server side — the key never reaches the browser
const res = await fetch(
  `https://moonship.com/api/v1/sites/${SITE_ID}/pages/stats?`
  + new URLSearchParams({ path: '/blog/taco-tuesday', range: '30d' }),
  { headers: { Authorization: `Bearer ${process.env.MOONSHIP_API_KEY}` } }
);
const { totals, change, series } = await res.json();

// client side — plot what your server passed down
new Chart(canvas, {
  type: 'line',
  data: {
    labels: series.map(p => p.date.slice(0, 10)),
    datasets: [{ label: 'Pageviews', data: series.map(p => p.pageviews) }],
  },
});

Errors

  • 401 — the key is missing, malformed, or unknown.
  • 404 — no such site, or your key can't read it.
  • 400 — a parameter is malformed.
  • 429 — over the rate limit.

Every error is JSON: { "error": "…" }.

On this page