API Documentation
Welcome! This is the documentation for the NeatStats API, which lets you pull your own site’s view statistics programmatically, and send it new views and updoots.
Authentication
All requests require authentication with an API key, which you can find on your account page. You can use the key with an Authorization: Bearer <token> header (where the token is your API key), or as the password in an HTTP Basic authentication request (the username can be anything, as long as the password is your API key).
Every request is scoped to a single site, specified in the URL path. Your API key must belong to the account that owns that site (you can only retrieve stats for, and send data to, sites you own).
The API supports cross-origin requests (CORS), so it's safe to call directly from browser-based JavaScript.
Common parameters
Most endpoints accept a range parameter, and several also accept a limit parameter. There’s also a sort parameter that can be used where it makes sense. Here’s how these work:
rangedefines how far back to look. Acceptable values are24h,7d,30d,6m, and1y. Defaults to24hif omitted.limitdefines how many results to return, for endpoints that return a breakdown of many items. Defaults to0, which means unbounded (you get everything). Pass a positive integer to cap the number of results to your top N by view count.sortdefines the order in which the data is provided. Defaults tocount, but acceptschronological(served in descending order).
Errors
When something goes wrong, you'll get a non-200 http_response_code along with an error field describing what happened:
{
"http_response_code": 400,
"http_response_text": "Bad Request",
"error": "Invalid range. Must be one of: 24h, 7d, 30d, 6m, 1y."
}
Endpoints
The API currently offers endpoints in a single category: Sites.
Sites
Site endpoints support retrieving stats for your sites (and sending them new views and updoots), identified by the site domain in the URL path.
Get site info
GET /api/v1/sites/site/info
Fetches basic info about a site, like its verification status and a lifetime summary of its views.
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": {
"domain": "example.com",
"verified": true,
"verified_at": "2026-01-19 03:12:07 UTC",
"verified_by": "dns",
"summary": {
"view_count": 303908,
"first_view_at": 1768863455,
"last_view_at": 1788524730,
"updoot_count": 42,
"executive_summary": "Your site has a total of 303,908 views since 2026-01-19. The most recent view was 9 minutes ago. It’s received 42 total Updoots."
}
}
}
verified_bywill be one ofdns,file, ormeta, matching whichever verification method you used. If the site hasn't been verified yet,verifiedisfalseandverified_at/verified_byare bothnull. If the site has no views yet,summary.view_countis0andfirst_view_at/last_view_at/executive_summaryare allnull.updoot_countis the total of all of the site’s updoots, all time. When it’s more than zero, the executive summary mentions it, too.
Get page views
GET /api/v1/sites/site/views
Fetches a time-series of view counts, bucketed automatically based on the requested range (15-minute buckets for 24h, hourly for 7d, every six hours for 30d, daily for 6m, and weekly for 1y). Also returns an SVG line chart of the same data. And the total number of views recorded for the requested range.
Accepts: range
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": "example.com",
"range": "24h",
"bucket_seconds": 900,
"start": 1788490800,
"end": 1788577200,
"total_views": 13,
"data": [
{ "ts": 1788490800, "views": 4 },
{ "ts": 1788491700, "views": 7 },
{ "ts": 1788492600, "views": 2 }
],
"chart_svg": ""
}
chart_svgis trimmed above for readability — in a real response it's the full markup for a self-contained line chart you can drop straight into a page.
Get browser breakdown
GET /api/v1/sites/site/browsers
Fetches a breakdown of views by browser, along with an SVG donut chart of the same data.
Accepts: range, limit
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": "example.com",
"range": "7d",
"data": {
"Chrome": 412,
"Safari": 298,
"Firefox": 54,
"Edge": 22,
"Yandex": 9
},
"chart_svg": ""
}
Get platform breakdown
GET /api/v1/sites/site/platforms
Fetches a breakdown of views by platform (operating system), along with an SVG donut chart of the same data.
Accepts: range, limit
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": "example.com",
"range": "7d",
"data": {
"Macintosh": 301,
"Windows": 187,
"Android": 122,
"iPhone": 98,
"Linux": 41
},
"chart_svg": ""
}
Get top pages
GET /api/v1/sites/site/pages
Fetches the most viewed pages on your site, ranked by view count.
Accepts: range, limit
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": "example.com",
"range": "7d",
"total_paths": 47,
"data": {
"/": 1204,
"/about": 389,
"/blog/hello-world": 201
}
}
total_pathsis the total number of distinct pages that had at least one view in the requested range, even iflimitcapped thedataobject to fewer than that.
Get top referrers
GET /api/v1/sites/site/referrers
Fetches the sites and pages that sent you the most traffic, ranked by view count.
Accepts: range, limit
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": "example.com",
"range": "7d",
"total_referrers": 132,
"data": {
"https://google.com/": 1204,
"https://reddit.com/": 897,
"https://neatnik.net/": 213
}
}
total_referrersworks the same way astotal_pathsabove (it's the true total, independent of anylimityou applied).- Referrers that match your filters are left out of both
dataandtotal_referrers. Filters come from your Preferences (all sites) and from the Preferences section of the site's stats page (that site only).filters_appliedin the response says how many patterns were used. Addunfilteredto the query string to get every referrer.
Get updoots
GET /api/v1/sites/site/updoots
Fetches every page on your site whose updoots have been clicked, with each page’s total and its most recent activity.
Accepts: sort (count, the default, for most updoots first; recent for latest activity first; or page for alphabetical by path)
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": "example.com",
"sort": "count",
"total_pages": 2,
"total_updoots": 61,
"data": [
{
"path": "/blog/hello",
"url": "https://example.com/blog/hello",
"count": 55,
"last_activity": 1794690000,
"updoots": [
{ "key": "⭐️", "emoji": "⭐️", "count": 50, "last_activity": 1794680000 },
{ "key": "❤️", "emoji": "❤️", "count": 5, "last_activity": 1794690000 }
]
},
{
"path": "/about",
"url": "https://example.com/about",
"count": 6,
"last_activity": 1794600000,
"updoots": [
{ "key": "⭐️", "emoji": "⭐️", "count": 6, "last_activity": 1794600000 }
]
}
]
}
- Each page's
countis the total of its updoots.last_activityis a Unix timestamp. - An updoot's
keyis its emoji unless the page setdata-key.
Get map data
GET /api/v1/sites/site/map
Fetches per-location view counts, with coordinates suitable for plotting on a map.
Accepts: range, limit
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": "example.com",
"range": "24h",
"total_locations": 62,
"data": [
{
"geonameid": 5128581,
"name": "New York City",
"country_iso": "US",
"latitude": 40.71427,
"longitude": -74.00597,
"views": 214
},
{
"geonameid": 2643743,
"name": "London",
"country_iso": "GB",
"latitude": 51.50853,
"longitude": -0.12574,
"views": 88
}
]
}
- With no
limitspecified, this returns every distinct location that’s had at least one view in the requested range. At a range like1ythat can be a lot of locations. If you’re rendering pins on a map yourself, you may want to either pass alimitor use marker clustering on your end to keep things responsive.
Get country totals
GET /api/v1/sites/site/countries
Fetches view totals grouped by country, using ISO 3166-1 alpha-2 country codes.
Accepts: range, limit
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": "example.com",
"range": "24h",
"total_countries": 18,
"data": {
"US": 512,
"GB": 88,
"DE": 41
}
}
Sending data
You can also send data to NeatStats, so you can count things that aren’t page views in a browser, like downloads, app launches, form signups, or anything else you can make an HTTP request from. These requests need your API key (signing in on the website isn’t enough).
Send the request body as JSON (with a Content-Type: application/json header) or as regular form fields. Paths are always on the site in the URL, and query strings and #fragments are removed. Instead of a path, you can send a full url on your site.
Record views
POST /api/v1/sites/site/views
Records one or more views, just like the view counter would. Views show up in your stats within a few seconds, timestamped when we receive them.
Accepts:
path(required): the page or thing that was viewed, like/downloads/app.zip. It doesn’t need to be a real page.referrer(optional): a full URL that sent the visitor.user_agent(optional): the visitor’s user agent, for the browser and platform breakdowns.ip(optional): the visitor’s IP address. It’s only used to look up an approximate location, and it’s never stored.
To record several views at once (up to 100), send {"views": [ … ]} with one object per view.
Sample request
curl https://neatstats.net/api/v1/sites/example.com/views \
-H "Authorization: Bearer YOUR_API_KEY" \
-d "path=/downloads/app.zip"
Sample response
{
"http_response_code": 202,
"http_response_text": "Accepted",
"site": "example.com",
"recorded": 1,
"message": "Thanks! The view will show up in a few seconds."
}
- If any view in a batch is invalid, nothing is recorded and you’ll get a
400telling you which one. More than 100 views gets a413.
Add updoots
POST /api/v1/sites/site/updoots
Adds to an updoot’s count, creating the updoot if it doesn’t exist yet. Updoots make handy counters for anything you like.
Accepts: path (required), emoji (defaults to ⭐️), key (defaults to the emoji), and count (how many to add, from 1 to 1,000,000; defaults to 1)
Sample request
curl https://neatstats.net/api/v1/sites/example.com/updoots \
-H "Authorization: Bearer YOUR_API_KEY" \
-d "path=/blog/hello" -d "emoji=🎉" -d "count=5"
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": "example.com",
"path": "/blog/hello",
"url": "https://example.com/blog/hello",
"updoot": {
"key": "🎉",
"emoji": "🎉",
"count": 12,
"last_activity": 1794700000
}
}
- An updoot is identified by its page and its
key, the same as on your pages. Use akeywhen you want two updoots with the same emoji on one page. - Updoots added with the API don’t count toward slow mode or any visitor limits.
Set an updoot count
PUT /api/v1/sites/site/updoots
Sets an updoot’s count to an exact number, creating the updoot if it doesn’t exist yet. It’s great for importing counts from somewhere else, or for fixing a count.
Accepts: path (required), count (required, from 0 to 1,000,000,000), emoji, and key
The response is the same as for Add updoots. An updoot with a count of 0 isn’t shown on your page until it’s clicked again.
Delete an updoot
DELETE /api/v1/sites/site/updoots
Deletes an updoot and its count. This can’t be undone.
Accepts: path (required), and emoji or key (at least one is required)
Sample response
{
"http_response_code": 200,
"http_response_text": "OK",
"site": "example.com",
"path": "/blog/hello",
"key": "🎉",
"deleted": true
}
- If there’s no updoot with that key on that page, you’ll get a
404.
You've reached the end of our API documentation.
We hope this was helpful. If you have questions, or there's an endpoint you'd find useful that isn't here yet, please let us know.