Skip to content

REST API

The /v1 REST API at https://api.analytics.canarycoders.es exposes every stat the dashboard shows, over Bearer-authed JSON endpoints. Every route is scoped to the project that owns the key. There is no cross-project read path.

Method & path Key required Returns
POST /v1/projects invite code (no key) New project + plaintext key, shown once
GET /v1/project live or read Project info (ProjectInfo)
GET /v1/validate live or read Dry-run of the ingest acceptance rules
GET /v1/stats/* live or read Overview, timeseries, pages, sources, countries, regions, events, UTM, devices, goals, realtime
GET /v1/sessions · /:sessionId live or read Paginated session list, session detail
GET /v1/export live or read Cursor-paginated raw event dump (CSV/NDJSON)
POST /v1/events live only 202, records a server-side custom event
Authorization: Bearer cly_live_5f3a9c2e1b7d4a80f6c3e9d2a1b4c7e0

A cly_live_… key authorizes every endpoint. A cly_read_… key authorizes everything except POST /v1/events, which answers 403 read_only_key. POST /v1/projects is the only unauthenticated route. See provisioning.

Errors are always { "error": { "code": "...", "message": "..." } }, with the HTTP status carrying the usual meaning (400, 401, 403, 404, 429).

120 requests/minute per key, fixed 60-second window. Over the limit you get 429 rate_limited with a Retry-After header (seconds until the window resets).

/v1 responses reflect Access-Control-Allow-Origin only when the request’s Origin matches the authenticated project’s trusted domains, the dashboard origin, or a localhost-ish origin while the project’s “Allow local origins” toggle is on. Server-to-server calls (no Origin header) skip this check entirely. It only matters for browser callers such as the widget or an SDK client running in a browser.

An untrusted browser Origin is not a 403: the request succeeds, but the response omits Access-Control-Allow-Origin, so the browser refuses to hand your script the body. This is why a call can work in curl (and any server-side caller) yet fail in the browser. Add the page’s domain to the project’s trusted domains to fix it.

POST /v1/projects

The one unauthenticated route. It is gated by an invite code instead of a key, since no key exists yet.

Terminal window
curl https://api.analytics.canarycoders.es/v1/projects \
-H "Content-Type: application/json" \
-d '{"inviteCode":"...","name":"My App","domains":["myapp.com"]}'

Returns 201 with { apiKey, project }, where apiKey is the plaintext key, shown exactly once. 403 invalid_invite_code on a bad invite; 400 bad_request on a malformed body or invalid domain list; 409 conflict if a domain is already claimed by another project.

GET /v1/project

Returns ProjectInfo: id, name, keyPrefix, domains, retentionDays, dailyEventCap, createdAt, readKeyPrefix (null if none minted), allowLocalOrigins.

GET /v1/validate?domain=myapp.com&origin=https://myapp.com

Dry-runs the /ingest acceptance rules for the authenticated project. See the troubleshooting guide for a field-by-field walkthrough.

All GET /v1/stats/* routes take ?window=24h|7d|30d|90d (default 7d) and, except realtime, ?includeBots=true|1 (default false):

GET /v1/stats/overview GET /v1/stats/timeseries
GET /v1/stats/pages GET /v1/stats/sources
GET /v1/stats/countries GET /v1/stats/regions?country=ES (country is required)
GET /v1/stats/events GET /v1/stats/utm
GET /v1/stats/devices GET /v1/stats/goals
GET /v1/stats/realtime
Terminal window
curl "https://api.analytics.canarycoders.es/v1/stats/overview?window=30d" \
-H "Authorization: Bearer cly_read_a1b2c3d4e5f60718293a4b5c6d7e8f90"

/v1/stats/realtime is the odd one out: it returns { activeSessions }, meaning distinct non-bot sessions with at least one event in the last 5 minutes, and it ignores window entirely. Bots are always excluded (a crawler is never a “current visitor”), which is why it has no includeBots toggle.

These are thin wrappers over the same query layer the dashboard uses, so numbers can never drift between the widget/SDK and the dashboard UI.

GET /v1/sessions?window=7d&limit=50&offset=0&country=ES&regionCode=ES-MD&includeBots=false
GET /v1/sessions/:sessionId

limit is 1–200 (default 50); the list response includes hasMore. The detail route 404s (not_found) if the session has no recorded events.

GET /v1/export?window=7d&format=csv&limit=1000&cursor=...

A cursor-paginated raw event dump. format is csv (default) or ndjson; limit is 1–10000 (default 1000). The response body is the page of data. A next-page cursor arrives in the X-Next-Cursor response header, present only when the page filled up completely (an exactly-full last page costs one extra empty request to confirm there is nothing more).

Terminal window
curl "https://api.analytics.canarycoders.es/v1/export?window=7d&format=ndjson&limit=5000" \
-H "Authorization: Bearer cly_read_a1b2c3d4e5f60718293a4b5c6d7e8f90" \
-D - -o events.ndjson

Both formats emit the same columns, in a fixed order: id, createdAt, sessionId, eventType, eventName, props, path, referrerPath, targetPath, country, region, regionCode, city, asn, asOrganization, browser, os, deviceType, isBot, botName, isVpn, locale, trafficSource, utmSource, utmMedium, utmCampaign, userAgent.

POST /v1/events

Full key only. Read keys get 403 read_only_key.

Terminal window
curl https://api.analytics.canarycoders.es/v1/events \
-H "Authorization: Bearer cly_live_5f3a9c2e1b7d4a80f6c3e9d2a1b4c7e0" \
-H "Content-Type: application/json" \
-d '{"name":"signup","props":{"plan":"pro"},"path":"/onboarding"}'

202 { "accepted": true }, or 429 daily_cap_exceeded if the project’s daily event cap is reached. See Custom events, goals & UTM tracking for the field constraints.