Simplytics

Docs › API

API

Read your stats as JSON: totals, a daily series and your dashboard’s top lists. With a key, also manage sites, settings and funnels.

Using an AI assistant? Connect it with the MCP server.

Authentication

Public sites need no key. For a private site, send an API key in the Authorization header.

Authorization: Bearer sim_key_YOUR_KEY

Make keys on your account page. Header only, from your server. Never put a key in a URL or a web page.

Endpoint

GET https://simplytics.dev/api/v1/stats/{domain}
ParameterDefaultAccepts
start, endThe last 30 daysDates as YYYY-MM-DD, in your site’s timezone. Up to 400 days.
includeNoneAny of the lists below, comma-separated.
limit10Rows per list, 1 to 1000.
metricvisitorsvisitors or views: what the lists count and rank by.

With metric=views, lists count views instead, as in {page, views}. The lists table notes the exceptions.

Lists

Any include also adds bounce_rate, a percent, and avg_duration_seconds to the totals.

includeEach row
daily{date, visitors, views, sessions} One per day, oldest first.
pages{page, visitors} Top pages.
sources{source, visitors} Referring sites.
channels{channel, visitors} Direct, Search, Social, AI, Email and Referral. Always visitors.
countries{code, name, visitors} Two-letter country code and its English name.
devices{device, visitors} Desktop, Mobile and Tablet.
browsers{browser, visitors}
os{os, visitors}
campaigns{campaign, visitors} UTM source and campaign.
events{name, count, visitors} Custom events, most fired first, with either metric. Up to 50.
clicks{outbound: [...], downloads: [...]} Rows are {target, visitors}, or {target, clicks} with metric=views. Up to 10 of each.

Examples

Totals for a public site

curl https://simplytics.dev/api/v1/stats/example.com
{
  "domain": "example.com",
  "period": {"start": "2026-10-02", "end": "2026-10-31"},
  "visitors": 1204,
  "views": 3310,
  "sessions": 1587
}

Top pages and countries

curl -H "Authorization: Bearer sim_key_YOUR_KEY" \
  "https://simplytics.dev/api/v1/stats/example.com?include=pages,countries&limit=3"
{
  "domain": "example.com",
  "period": {"start": "2026-10-02", "end": "2026-10-31"},
  "visitors": 1204,
  "views": 3310,
  "sessions": 1587,
  "bounce_rate": 46,
  "avg_duration_seconds": 71,
  "pages": [
    {"page": "/", "visitors": 702},
    {"page": "/pricing", "visitors": 311},
    {"page": "/blog/hello", "visitors": 148}
  ],
  "countries": [
    {"code": "US", "name": "United States", "visitors": 380},
    {"code": "IN", "name": "India", "visitors": 233},
    {"code": "DE", "name": "Germany", "visitors": 121}
  ]
}

Three days, day by day

curl -H "Authorization: Bearer sim_key_YOUR_KEY" \
  "https://simplytics.dev/api/v1/stats/example.com?start=2026-10-01&end=2026-10-03&include=daily"
{
  "domain": "example.com",
  "period": {"start": "2026-10-01", "end": "2026-10-03"},
  "visitors": 124,
  "views": 329,
  "sessions": 157,
  "bounce_rate": 44,
  "avg_duration_seconds": 68,
  "daily": [
    {"date": "2026-10-01", "visitors": 41, "views": 112, "sessions": 52},
    {"date": "2026-10-02", "visitors": 38, "views": 97, "sessions": 47},
    {"date": "2026-10-03", "visitors": 45, "views": 120, "sessions": 58}
  ]
}

Public sites

Without a key, a public site’s API shows what its public dashboard shows.

Keys

Make as many keys as you need, of two kinds:

An access key has a name and a scope:

ScopeChoices
SitesAll, including sites you add later, or a list of your domains.
DataEverything, or totals plus the lists you pick.
Permissionsanalytics:read reads stats. The others are under Manage sites and funnels.

A key outside its scope gets a 403 naming what it can’t do, like {"error": "This key can't read example.com"}.

To rotate a key, make a new one, switch your code to it, then delete the old one. To rotate the master key, roll it.

An access key can expire, if you pick a time as you make it. Then it gets a 401. A master key never expires.

Make a key with the master key

POST https://simplytics.dev/api/v1/keys

Send JSON with a name and permissions. Sites and data default to all. ["keys:create"] alone makes a master key, if the account has none.

To make a key expire, add "expires_at": "2026-12-31T23:59:00Z". Leave it out for never.

curl -X POST -H "Authorization: Bearer sim_key_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Weekly report", "permissions": ["analytics:read"], "sites": ["example.com"], "data": ["pages"]}' \
  https://simplytics.dev/api/v1/keys
{
  "id": 12,
  "name": "Weekly report",
  "last4": "Qx3w",
  "kind": "access",
  "permissions": [
    "analytics:read"
  ],
  "sites": [
    "example.com"
  ],
  "data": [
    "pages"
  ],
  "created_at": "2026-10-31T09:30:00.000Z",
  "created_by": "key:3",
  "last_used_at": null,
  "expires_at": null,
  "key": "sim_key_4dQj…Qx3w"
}

The new key is in this answer only, so store it then.

List, roll and delete keys

GET https://simplytics.dev/api/v1/keys
POST https://simplytics.dev/api/v1/keys/{id}/roll
DELETE https://simplytics.dev/api/v1/keys/{id}

The master key lists every key on the account, never a secret.

Roll gives a key a new secret, in its answer only. The old one stops at once.

Manage sites and funnels

An access key with the right permissions can add, change and delete your sites and funnels, and set each site’s logo and custom domain.

Nothing in the API adds views or visitors or changes a count.

PermissionLets a key
analytics:readRead stats.
sites:readList sites.
sites:editAdd sites.
sites:deleteDelete sites.
funnels:readRead funnels and their counts.
funnels:editMake and rename funnels.
funnels:deleteDelete funnels.
settings:readRead a site’s settings and custom domain.
settings:editChange a site’s settings and logo and custom domain.
history:exportDownload the export ZIP.
keys:createThe master key: make, list, roll and delete keys. Nothing else.

Delete is its own permission: edit never includes it. history:import isn’t available yet.

Paths are under https://simplytics.dev/api/v1.

EndpointNeedsDoes
GET /sitessites:readYour sites, in dashboard order.
POST /sitessites:editAdds a site, as the dashboard does, and returns its tracking tag. Body: {"domain": …, "timezone": …}.
GET /sites/{domain}settings:readIts visibility, badge, timezone, when it was added, and its custom_domain and logo_url.
PATCH /sites/{domain}settings:editAny of visibility (private, public, badge_only), badge (metric, period) and timezone.
DELETE /sites/{domain}sites:deleteDeletes it and its stats for good.
GET /sites/{domain}/custom_domainsettings:readIts custom domain: hostname, status, the CNAME record to add, and the snippet once it is active. Null if it has none.
POST /sites/{domain}/custom_domainsettings:editAdds one: {"hostname": "stats"} for stats.{domain}, or the full name.
DELETE /sites/{domain}/custom_domainsettings:editRemoves it.
PUT /sites/{domain}/logosettings:editSets its logo: the image itself as the body. PNG, JPEG or WebP, up to 100 KB.
DELETE /sites/{domain}/logosettings:editRemoves it.
GET /sites/{domain}/funnelsfunnels:readEach funnel’s steps and counts for start to end (UTC days), 25 at a time. For the next 25, pass after: the response’s next_after (null on the last page).
POST /sites/{domain}/funnelsfunnels:editMakes a funnel: name (up to 100 characters) and 2 to 10 steps (up to 512 each). Unlimited funnels on every paid plan; up to 100 during the free trial.
PATCH /sites/{domain}/funnels/{id}funnels:editRenames it: {"name": …}.
DELETE /sites/{domain}/funnels/{id}funnels:deleteDeletes it and its counts for good.
GET /sites/{domain}/exporthistory:exportThe dashboard’s Everything (ZIP) export, for start to end.

Add a site

curl -X POST -H "Authorization: Bearer sim_key_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "shop.example.com", "timezone": "Europe/Berlin"}' \
  https://simplytics.dev/api/v1/sites
{
  "domain": "shop.example.com",
  "verified": false,
  "visibility": "private",
  "badge": {"metric": "visitors", "period": "month"},
  "timezone": "Europe/Berlin",
  "created_at": "2026-11-02T09:30:00.000Z",
  "custom_domain": null,
  "logo_url": null,
  "snippet": "<script async src=\"https://simplytics.dev/track.js\" data-key=\"a1b2c3d4\"></script>"
}

Make a dashboard public

curl -X PATCH -H "Authorization: Bearer sim_key_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"visibility": "public"}' \
  https://simplytics.dev/api/v1/sites/example.com
{
  "domain": "example.com",
  "verified": true,
  "visibility": "public",
  "badge": {"metric": "visitors", "period": "month"},
  "timezone": "Europe/Berlin",
  "created_at": "2026-06-14T08:12:44.000Z",
  "custom_domain": null,
  "logo_url": null
}

Make a funnel

curl -X POST -H "Authorization: Bearer sim_key_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Signup", "steps": ["/", "/pricing", "/signup"]}' \
  https://simplytics.dev/api/v1/sites/example.com/funnels
{
  "id": 41,
  "name": "Signup",
  "steps": [
    "/",
    "/pricing",
    "/signup"
  ]
}

Delete a site

curl -X DELETE -H "Authorization: Bearer sim_key_YOUR_KEY" \
  https://simplytics.dev/api/v1/sites/example.com
{
  "domain": "example.com",
  "deleted": true
}

Export everything

curl -H "Authorization: Bearer sim_key_YOUR_KEY" -o example.com.zip \
  "https://simplytics.dev/api/v1/sites/example.com/export?start=2026-10-01&end=2026-10-31"

The same ZIP as the dashboard’s export. It holds every list, so the key’s data must be Everything.

Logo and custom domain

Each needs the same check as Public: our tag on your homepage, or a DNS record. It runs on its own.

Removing an active custom domain emails you first, naming the key. A snippet loading from it stops counting.

If the email can’t go, nothing is removed. Logo changes and other custom domain changes don’t email you.

Send the logo as the request body. We read its type from the file itself, so an SVG is refused even when named .png.

A custom domain is pending until its CNAME to proxy.simplytics.dev is live, then active, with its own snippet.

The rules are on Custom domains.

Add a custom domain

curl -X POST -H "Authorization: Bearer sim_key_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hostname": "stats"}' \
  https://simplytics.dev/api/v1/sites/example.com/custom_domain
{
  "domain": "example.com",
  "custom_domain": {"hostname": "stats.example.com", "status": "pending", "detail": null, "record": {"type": "CNAME", "name": "stats.example.com", "target": "proxy.simplytics.dev"}, "snippet": null}
}

Upload a logo

curl -X PUT -H "Authorization: Bearer sim_key_YOUR_KEY" \
  -H "Content-Type: image/png" \
  --data-binary @logo.png \
  https://simplytics.dev/api/v1/sites/example.com/logo
{
  "domain": "example.com",
  "logo_url": "https://simplytics.dev/logo/3f9c2e7a51b84d06c1e9a8f27d4b6035e81c9f4a72d6b0e3958a1c4f7e2d6b90.png"
}

Public and badge-only

Turning either on checks that you control the domain, as the dashboard does, then emails you. If the email can’t go, nothing changes.

Deletes

A delete is permanent, as on the dashboard. We email you first, naming the key. If the email can’t go, nothing is deleted.

Good to know

Errors

Errors come as JSON with a message, like {"error": "Subscription required"}.

StatusWhen
400A parameter or field is wrong. The message names it.
401The key is wrong, was deleted or expired.
403No key and the site is not public, the subscription has ended, the key’s scope doesn’t cover the request, or we couldn’t confirm you control the domain. The message says which.
404No verified site with that name on the key’s account, or no custom domain or logo to remove.
409The site is already added, has had no visits yet, changed while saving or deleting, already has a custom domain, the hostname is already in use, waits for Check now beside Public on its dashboard, or a free trial already has 100 funnels.
413, 415The logo is over 100 KB, or isn’t a PNG, JPEG or WebP.
502, 503We couldn’t email you about a delete, a public change or removing an active custom domain, so nothing changed. Or Cloudflare refused a custom domain.
429Too many requests from your address, with your key or across your account’s keys, 100 new keys or 10 emailed changes in a day, or a second check that you control the domain within a minute. Wait the seconds in Retry-After.

Limits