Documentation
Everything you need to measure your site the privacy-friendly way.
Overview
frapi.io is privacy-friendly web analytics: pageviews, visitors, referrers, browsers, operating systems, screen sizes and languages — with no cookies, no fingerprinting and no personal data. There is no consent banner to install and nothing to configure except your site code.
Everything is per-site, keyed by a public site code. You can track dozens of sites from one account and see them all on a single overview page.
Quick start
Sign up to create your account — a site code is generated for you from the site name. Then add this
snippet to every page you want to track, just before </body>:
<script data-frapi="https://frapi.io/frapi"
data-site="YOUR-SITE-CODE"
data-frapi-fallback="https://frapi.io/count"
async src="https://frapi.io/frapi.js"></script>
Replace YOUR-SITE-CODE with the code shown in your dashboard header. Open the dashboard
and pageviews start appearing within seconds.
/frapi and falls back to /count if the
first is blocked. If you see no pageviews, check your blocker and allow frapi.io.How data gets in
frapi accepts hits through two independent paths. Both write to the same store, use the same rollups, and show up in the same dashboard — use whichever fits your product.
| Browser snippet | Server-side API | |
|---|---|---|
| Purpose | Pageviews & events from visitors | Hits from your own code |
| Auth | None — public site code | Bearer API token (secret) |
| How | <script> tag in pages | POST /api/v0/count |
| Best for | Public websites & frontends | Logs, cron, SSR, backfills, events |
1. Browser snippet
Your visitors' browsers call the tracker directly. No authentication is involved and no personal data is transmitted.
<script data-frapi="https://frapi.io/frapi"
data-site="your-code"
data-frapi-fallback="https://frapi.io/count"
async src="https://frapi.io/frapi.js"></script>
The snippet reports automatically on page load, prefers fetch with
keepalive (falling back to sendBeacon and then a 1×1 image), and
retries the secondary endpoint if the first request is blocked.
2. Server-side API
Send hits from your backend — great for server-side events, log importers, cron jobs and backfills. Authenticate with an API token created in Dashboard → Settings → API tokens.
curl https://frapi.io/api/v0/count \
-X POST \
-H 'Authorization: Bearer frapi_…' \
-H 'Content-Type: application/json' \
-d '{"hits":[{"path":"/checkout","title":"Checkout","event":false}]}'
→ {"counted": 1}
Snippet options
Configure the snippet with attributes on the script tag, or at runtime via the window.frapi
object.
| Attribute | Purpose |
|---|---|
data-frapi | Tracker endpoint (defaults to https://frapi.io/frapi) |
data-frapi-fallback | Second endpoint tried if the first is blocked (recommended: https://frapi.io/count) |
data-site | Your site code (required) |
data-frapi-settings | JSON object of options, applied at load |
{
"no_onload": false, // don't count on page load
"no_events": true, // don't auto-bind click handlers
"allow_local": false, // count hits from localhost / private IPs
"allow_frame": false, // count hits rendered in frames
"path": "/override", // string or function
"title": "My title", // string or function (defaults to document.title)
"referrer": "…", // string or function (defaults to document.referrer)
"event": false // mark every count as an event
}
Set options via the attribute:
<script data-frapi="https://frapi.io/frapi"
data-site="your-code"
data-frapi-fallback="https://frapi.io/count"
data-frapi-settings='{"no_onload":true}'
async src="https://frapi.io/frapi.js"></script>
Or in your own code before the page fires the onload hit:
<script>window.frapi = { no_onload: true }</script>
Events
Track a click or any custom action with a data attribute — no JS required:
<a href="/signup" data-frapi-click="signup-click">Sign up</a>
Or call the JavaScript API directly:
frapi.count({ path: 'log:button', event: true })
Events are counted separately and appear with an event flag in your stats and exports.
Custom paths
Override the reported path before a hit is sent — useful for staging URLs or renaming pages:
<script>
window.frapi = {
path: function () { return '/custom-' + location.pathname }
}
</script>
Opt-out & filters
Let your own team opt a browser out by visiting any page with #toggle-frapi in the
URL. A confirmation is shown and the choice is stored in that browser only.
The snippet also skips hits that would be useless: prerendered pages, pages inside frames
(unless allow_frame is on), localhost and file:// pages (unless
allow_local is on), and browser automation flags like navigator.webdriver.
Server-side, a bot heuristic filters known crawler user-agents from your counts.
HTTP API
All API calls are JSON, versioned under /api/v0, and authenticated with a bearer
token. Create one in Dashboard → Settings → API tokens — the raw token
is shown only once, so store it somewhere safe.
curl https://frapi.io/api/v0/me \ -H 'Authorization: Bearer frapi_…'
Endpoints
| Endpoint | Description |
|---|---|
POST /count | Ingest a batch of hits |
GET /me | Token info + the site it belongs to |
GET /sites | List of all your sites |
GET /stats/total | Total pageviews & visitors for a range |
GET /stats/hits | Top pages for a range |
GET /stats/{page} | Top refs, browsers, systems, sizes, locations, languages |
GET /paths | Catalog of every tracked path |
GET /export | Raw hits as CSV |
POST /count — ingest hits
Accepts a batch. Each hit becomes a pageview (or event).
{
"hits": [
{ "path": "/welcome", "title": "Welcome", "event": false, "referrer": "" }
],
"no_sessions": true
}
Returns {"counted": 1} with the number of hits accepted.
(unknown) unless you pass a visitor's user_agent per hit (useful when
proxying real requests server-side). API hits don't count as visitors.GET /stats/total
Query params: start, end (ISO dates or relative dates like
2006-01-02). Returns pageviews and visitors for the range.
GET /stats/{page}
page is one of refs (aliases referrers),
browsers, systems, sizes, locations,
languages. Optional limit (max 100).
GET /export — CSV
Dumps raw hits for a range as CSV with columns
id, path, title, referrer, first_visit, width, language, location, created_at.
Requires raw hit storage, which is on by default.
Visitor counter
Show a live "views" badge for a page. It's a public, cache-friendly image:
<img src="https://frapi.io/counter/read-me.svg?site=your-code" alt="Reads">
The path in the URL (here /read-me) is the page path to count. Use
.html instead of .svg for a text-only badge. Optional
start and end query params limit the date range.
Data & privacy
We never store: IP addresses, full user-agent strings, cookies, device IDs, fingerprint data, or any identifier that can be traced back to a person.
What we store per hit: page path, title, referrer, screen width, language and the browser/OS classifier. Visitor counts are derived from a salted hash of short-lived inputs that rotates daily — the raw inputs are discarded, so the hash can't be reversed.
Aggregation: hits roll up hourly and daily. The dashboard reads those rollups, so day-to-day usage stays fast even at scale.
Retention & deletion: raw hits and rollups are retained for the life of your account. Deleting a site (or your account) removes its data. Export via the API or CSV first if you want a copy.
FAQ
What's the difference between a site code and an API token?
A site code is public and identifies your site in tracking URLs and dashboard paths. An API token is secret and authenticates your backend to the JSON API. They authorize different things — see How data gets in.
Can I change my site code?
Your site code is baked into your installed snippets, so changing it would break tracking. Create a new site if you need a new code, and export the old data first.
Do you offer self-hosting?
No — frapi.io is a hosted service. That's how we keep the tracker one line and the pricing simple.