Going further
API reference
The tracking script is a thin wrapper over one endpoint. If you need to send events from a server, a mobile app, or an environment where JavaScript isn't an option, post to it directly.
POST /api/collect
Accepts JSON, returns 202 on success. Requests are fire-and-forget: the script uses navigator.sendBeacon where available so a pageview survives the page unloading.
curl -X POST https://numberhill.com/api/collect \
-H "Content-Type: application/json" \
-d '{
"site": "YOUR_SITE_ID",
"visitor": "9f2c4a1b8e7d6c5f4a3b2c1d0e9f8a7b",
"type": "pageview",
"url": "/pricing",
"hostname": "example.com",
"referrer": "https://github.com/some/repo"
}'Request body
| Field | Type | Description |
|---|---|---|
| site | string | Required. Your site ID. |
| visitor | string | Required. Stable identifier for this browser. |
| type | string | pageview (default), goal, or identify. |
| session | string? | Session identifier; groups a single visit. |
| url | string? | Path and query string, e.g. /pricing?ref=x. |
| hostname | string? | Hostname the event happened on. |
| referrer | string? | Full referrer URL; resolved to a named channel server-side. |
| utm_source | string? | Overrides referrer-based attribution when present. |
| utm_medium | string? | Used to detect email/newsletter traffic. |
| utm_campaign | string? | Campaign name, surfaced in the Campaign tab. |
| goal | string? | Goal name. Required when type is goal. |
| meta | object? | Arbitrary JSON attached to a goal. |
| traits | object? | Arbitrary JSON attached by identify. |
| screen | string? | e.g. 1920x1080. |
| lang | string? | Browser language tag. |
| cookieless | boolean? | Marks the event as coming from the cookieless script. |
Responses
| Status | Body | Meaning |
|---|---|---|
| 202 | { "ok": true } | Event accepted. |
| 202 | { "ok": true, "filtered": "bot" } | Recognised as a bot — tallied in the bot report, excluded from analytics. |
| 400 | { "ok": false, "error": … } | Malformed JSON, or missing site/visitor. |
| 503 | { "ok": false, "error": … } | Backend not configured. Nothing is stored — check /api/health. |
Country resolution
Country comes from CDN edge headers (x-vercel-ip-country, cf-ipcountry), not from the request body. Server-to-server calls that skip the edge will have no country attached.GET /api/live
Public aggregate feed for the site's own live counter: unique visitors per minute for the last 30 minutes, plus a country split. Counts only — no visitor IDs or URLs.
{
"total": 88,
"buckets": [3, 2, 4, 3, …], // 30 entries, oldest first
"countries": [{ "code": "US", "visitors": 27 }],
"generated_at": "2026-07-21T09:02:55.807Z"
}Environment variables
| Variable | Required | Purpose |
|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | yes | Supabase project URL. |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | yes | Public key used by the ingest route. Insert-only under RLS. |
| SUPABASE_SERVICE_ROLE_KEY | yes | Server-only key for dashboard reads and payment writes, which are RLS-closed to the anon key. Never expose it to the browser. |
| NEXT_PUBLIC_NUMBERHILL_SITE_ID | optional | Site ID this deployment tracks itself under. |
| — | n/a | Payment webhook signing secrets are not environment variables. They are stored per site and pasted in through Dashboard → Settings → Payment webhooks. |
Limits
- Events are accepted asynchronously; expect a second or two before they appear.
- The live endpoint is uncached (
no-store) — poll it no more than a few times a minute. - Duplicate payments are collapsed on
(processor, external_id).
GET /api/export
Downloads a dataset as CSV for the window you ask for. Authenticated with your dashboard session, so it works from the browser you are signed in to — the buttons on the Overview, Revenue, Goals, Bots and AI pages are links to this endpoint with the range you have selected already applied.
# every payment in March, with its attributed channel
/api/export?siteId=SITE_UUID&dataset=payments&from=2026-03-01&to=2026-03-31
# channel performance for the last 90 days
/api/export?siteId=SITE_UUID&dataset=sources&days=90Parameters
| Field | Type | Description |
|---|---|---|
| siteId | string | Required. The site's UUID, as shown on the Websites page. |
| dataset | string | payments, sources, pages, countries, devices, goals, bots, ai-crawlers or daily. Defaults to payments. |
| days | number | Trailing window, 1–731 days. Defaults to 30. Mutually exclusive with from/to. |
| from / to | string | An exact window as YYYY-MM-DD. Both days are included, read in the site's timezone. Must be given together. |
reported_amount in the site's reporting currency, which is what every dashboard figure sums, and charged_amount in the currency the processor actually took — so the file reconciles against both the dashboard and the processor's own statement. Refunds carry a negative amount.