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.
Endpoints at a glance
Section titled “Endpoints at a glance”| 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 |
Authentication
Section titled “Authentication”Authorization: Bearer cly_live_5f3a9c2e1b7d4a80f6c3e9d2a1b4c7e0A 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).
Rate limit
Section titled “Rate limit”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.
Provisioning
Section titled “Provisioning”POST /v1/projectsThe one unauthenticated route. It is gated by an invite code instead of a key, since no key exists yet.
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.
Project
Section titled “Project”GET /v1/projectReturns ProjectInfo: id, name, keyPrefix, domains, retentionDays, dailyEventCap, createdAt, readKeyPrefix (null if none minted), allowLocalOrigins.
Validate
Section titled “Validate”GET /v1/validate?domain=myapp.com&origin=https://myapp.comDry-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/timeseriesGET /v1/stats/pages GET /v1/stats/sourcesGET /v1/stats/countries GET /v1/stats/regions?country=ES (country is required)GET /v1/stats/events GET /v1/stats/utmGET /v1/stats/devices GET /v1/stats/goalsGET /v1/stats/realtimecurl "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.
Sessions
Section titled “Sessions”GET /v1/sessions?window=7d&limit=50&offset=0&country=ES®ionCode=ES-MD&includeBots=falseGET /v1/sessions/:sessionIdlimit is 1–200 (default 50); the list response includes hasMore. The detail route 404s (not_found) if the session has no recorded events.
Export
Section titled “Export”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).
curl "https://api.analytics.canarycoders.es/v1/export?window=7d&format=ndjson&limit=5000" \ -H "Authorization: Bearer cly_read_a1b2c3d4e5f60718293a4b5c6d7e8f90" \ -D - -o events.ndjsonBoth 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.
Track a custom event
Section titled “Track a custom event”POST /v1/eventsFull key only. Read keys get 403 read_only_key.
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.