Created
March 21, 2026 19:09
-
-
Save jaamo/2fa161338167c0c19a041c68aa845ca8 to your computer and use it in GitHub Desktop.
tinytrail api
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # TinyTrail API Documentation | |
| API reference for TinyTrail — privacy-focused, cookie-less website analytics engine. | |
| **Base URL:** `http://localhost:8000` (default) | |
| --- | |
| ## Table of Contents | |
| - [Authentication](#authentication) | |
| - [Response Format](#response-format) | |
| - [Error Codes](#error-codes) | |
| - [Rate Limiting](#rate-limiting) | |
| - [Endpoints](#endpoints) | |
| - [Health](#health) | |
| - [Event Collection](#event-collection) | |
| - [Accounts](#accounts) | |
| - [Users](#users) | |
| - [Websites](#websites) | |
| - [Admin](#admin) | |
| - [Reports](#reports) | |
| - [Summary](#get-apiv1reportssummary) | |
| - [Pages](#get-apiv1reportspages) | |
| - [Sources](#get-apiv1reportssources) | |
| - [Devices](#get-apiv1reportsdevices) | |
| - [Flow](#get-apiv1reportsflow) | |
| - [Funnel](#get-apiv1reportsfunnel) | |
| - [Realtime](#get-apiv1reportsrealtime) | |
| - [Custom Events](#get-apiv1reportscustom-events) | |
| - [Timeseries](#get-apiv1reportstimeseries) | |
| - [Static Assets](#static-assets) | |
| --- | |
| ## Authentication | |
| Protected endpoints require an API key passed via the `X-API-KEY` header. | |
| ``` | |
| X-API-KEY: your-api-key-here | |
| ``` | |
| Two roles exist: | |
| | Role | Access | | |
| | ------- | ------------------------------------------------------------ | | |
| | `admin` | Full system access (accounts, users, all websites, aggregation) | | |
| | `user` | Scoped to own account (websites and reports within that account) | | |
| Cross-account access by a `user` role returns `404 NOT_FOUND` (not `403`) to prevent resource enumeration. | |
| --- | |
| ## Response Format | |
| All endpoints use a consistent JSON envelope (except `/health`). | |
| **Success:** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { ... } | |
| } | |
| ``` | |
| **Error:** | |
| ```json | |
| { | |
| "status": "error", | |
| "error": { | |
| "code": "ERROR_CODE", | |
| "message": "Human-readable description" | |
| } | |
| } | |
| ``` | |
| --- | |
| ## Error Codes | |
| | HTTP Status | Code | Description | | |
| | ----------- | ----------------- | ---------------------------------------------- | | |
| | 400 | `INVALID_PAYLOAD` | Malformed JSON or missing/invalid fields | | |
| | 401 | `UNAUTHORIZED` | Missing or invalid `X-API-KEY` header | | |
| | 403 | `FORBIDDEN` | Valid API key but insufficient permissions | | |
| | 404 | `NOT_FOUND` | Resource does not exist (or access denied) | | |
| | 429 | `RATE_LIMITED` | Rate limit exceeded | | |
| | 500 | `INTERNAL_ERROR` | Unexpected server error | | |
| --- | |
| ## Rate Limiting | |
| | Scope | Limit | Key | | |
| | --------------------- | ------------------------------ | ------ | | |
| | `POST /api/v1/collect` | 10 requests per 10 seconds | IP | | |
| | All protected routes | 60 requests per 60 seconds | API key | | |
| Rate-limited responses include a `Retry-After` header (seconds). | |
| --- | |
| ## Endpoints | |
| ### Health | |
| #### `GET /health` | |
| Health check for load balancers and uptime monitoring. No authentication required. | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "ok", | |
| "db": "connected", | |
| "version": "0.1.0" | |
| } | |
| ``` | |
| **Response (503):** | |
| ```json | |
| { | |
| "status": "degraded", | |
| "db": "disconnected" | |
| } | |
| ``` | |
| > Note: This endpoint does not use the standard response envelope. | |
| --- | |
| ### Event Collection | |
| #### `OPTIONS /api/v1/collect` | |
| CORS preflight handler. | |
| - **Auth:** None | |
| - **Response:** `204 No Content` | |
| - **Headers:** `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods: POST, OPTIONS`, `Access-Control-Allow-Headers: Content-Type`, `Access-Control-Allow-Credentials: true`, `Vary: Origin` | |
| --- | |
| #### `POST /api/v1/collect` | |
| Public event ingestion endpoint. No authentication required. | |
| - **Auth:** None | |
| - **Rate limit:** 10 req / 10s per IP | |
| - **Content-Type:** `application/json` | |
| **Request Body:** | |
| | Field | Type | Required | Description | | |
| | -------------- | ------ | -------- | ------------------------------------------ | | |
| | `website_id` | string | Yes | UUID of the target website | | |
| | `event_type` | string | Yes | Event type (see below) | | |
| | `url` | string | Yes | Full page URL including query string | | |
| | `path` | string | Yes | URL pathname | | |
| | `referrer` | string | No | Referrer URL | | |
| | `screen_width` | number | No | Viewport width in pixels | | |
| | `screen_height`| number | No | Viewport height in pixels | | |
| | `properties` | object | No | Arbitrary custom event data (JSONB) | | |
| | `utm_source` | string | No | UTM source parameter | | |
| | `utm_medium` | string | No | UTM medium parameter | | |
| | `utm_campaign` | string | No | UTM campaign parameter | | |
| **Default event types:** `page_view`, `close_tab`, `scroll_25`, `scroll_50`, `scroll_75`, `outbound_link`, `custom` | |
| **Example:** | |
| ```json | |
| { | |
| "website_id": "550e8400-e29b-41d4-a716-446655440000", | |
| "event_type": "page_view", | |
| "url": "https://example.com/blog/post-1?utm_source=twitter", | |
| "path": "/blog/post-1", | |
| "referrer": "https://t.co/abc123", | |
| "screen_width": 1920, | |
| "screen_height": 1080 | |
| } | |
| ``` | |
| **Responses:** | |
| | Status | Description | | |
| | ------ | ----------------------------------------------------------------- | | |
| | 202 | `{"status":"accepted"}` — Event stored successfully | | |
| | 204 | Silent drop (invalid `website_id`, bot, or excluded traffic) | | |
| | 400 | `INVALID_PAYLOAD` — Missing required fields | | |
| | 429 | `RATE_LIMITED` — Rate limit exceeded | | |
| **Server-side processing pipeline:** | |
| 1. Validate `website_id` exists (silent 204 drop if invalid) | |
| 2. Extract IP and User-Agent from request headers | |
| 3. Parse User-Agent → `browser_family`, `os_family`, `device_type` | |
| 4. Parse UTM parameters from `url` query string | |
| 5. Classify `referrer` → `referrer_channel` | |
| 6. Generate `ip_hash` = HMAC-SHA256(DailySalt, IP + UA + WebsiteID) | |
| 7. Check IP against `website.excluded_ips` → set `is_excluded` | |
| 8. Run bot detection → set `is_bot` | |
| 9. Match path against `website.content_groups` patterns | |
| 10. Store event row | |
| --- | |
| ### Accounts | |
| All account endpoints require **admin** role. | |
| #### `GET /api/v1/accounts` | |
| List all accounts. | |
| - **Auth:** Admin | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": [ | |
| { | |
| "id": "uuid", | |
| "name": "Acme Corp", | |
| "createdAt": "2026-01-15T10:30:00.000Z" | |
| } | |
| ] | |
| } | |
| ``` | |
| --- | |
| #### `POST /api/v1/accounts` | |
| Create a new account. | |
| - **Auth:** Admin | |
| **Request Body:** | |
| | Field | Type | Required | Description | | |
| | ------ | ------ | -------- | ----------------- | | |
| | `name` | string | Yes | Account name | | |
| **Response (201):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "id": "uuid", | |
| "name": "Acme Corp", | |
| "createdAt": "2026-01-15T10:30:00.000Z" | |
| } | |
| } | |
| ``` | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | -------------------- | | |
| | 400 | `INVALID_PAYLOAD` | Missing `name` field | | |
| --- | |
| #### `DELETE /api/v1/accounts/:id` | |
| Delete an account and all associated data (users, websites, events, aggregated data). | |
| - **Auth:** Admin | |
| **Path Parameters:** | |
| | Param | Type | Description | | |
| | ----- | ---- | ----------- | | |
| | `id` | UUID | Account ID | | |
| **Response (200):** Deleted account object. | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | ------------------------ | | |
| | 400 | `INVALID_PAYLOAD` | Invalid UUID format | | |
| | 404 | `NOT_FOUND` | Account does not exist | | |
| --- | |
| ### Users | |
| All user endpoints require **admin** role. | |
| #### `GET /api/v1/users` | |
| List all users (API key field excluded from response). | |
| - **Auth:** Admin | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": [ | |
| { | |
| "id": "uuid", | |
| "accountId": "uuid", | |
| "email": "user@example.com", | |
| "role": "user", | |
| "createdAt": "2026-01-15T10:30:00.000Z" | |
| } | |
| ] | |
| } | |
| ``` | |
| --- | |
| #### `POST /api/v1/users` | |
| Create a new user. Returns the generated API key (only time it's shown). | |
| - **Auth:** Admin | |
| **Request Body:** | |
| | Field | Type | Required | Description | | |
| | ------------ | ------ | -------- | ------------------------------------ | | |
| | `account_id` | UUID | Yes | Account to assign the user to | | |
| | `role` | string | Yes | `"admin"` or `"user"` | | |
| | `email` | string | No | Optional label (not used for login) | | |
| **Response (201):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "id": "uuid", | |
| "accountId": "uuid", | |
| "email": "user@example.com", | |
| "role": "user", | |
| "apiKey": "tt_abc123...", | |
| "createdAt": "2026-01-15T10:30:00.000Z" | |
| } | |
| } | |
| ``` | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | ------------------------------------------- | | |
| | 400 | `INVALID_PAYLOAD` | Missing `role` or `account_id`, invalid role, or account not found | | |
| --- | |
| #### `GET /api/v1/users/:id/api-key` | |
| Retrieve a user's API key. | |
| - **Auth:** Admin | |
| **Path Parameters:** | |
| | Param | Type | Description | | |
| | ----- | ---- | ----------- | | |
| | `id` | UUID | User ID | | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "id": "uuid", | |
| "apiKey": "tt_abc123..." | |
| } | |
| } | |
| ``` | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | -------------------- | | |
| | 400 | `INVALID_PAYLOAD` | Invalid UUID format | | |
| | 404 | `NOT_FOUND` | User does not exist | | |
| --- | |
| #### `DELETE /api/v1/users/:id` | |
| Delete a user. | |
| - **Auth:** Admin | |
| **Path Parameters:** | |
| | Param | Type | Description | | |
| | ----- | ---- | ----------- | | |
| | `id` | UUID | User ID | | |
| **Response (200):** Deleted user object. | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | -------------------- | | |
| | 400 | `INVALID_PAYLOAD` | Invalid UUID format | | |
| | 404 | `NOT_FOUND` | User does not exist | | |
| --- | |
| ### Websites | |
| Website endpoints require authentication. Non-admin users are scoped to their own account. | |
| #### `GET /api/v1/websites` | |
| List websites. Admins see all; users see only their account's websites. | |
| - **Auth:** User+ | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": [ | |
| { | |
| "id": "uuid", | |
| "accountId": "uuid", | |
| "domain": "example.com", | |
| "contentGroups": [], | |
| "excludedIps": [], | |
| "createdAt": "2026-01-15T10:30:00.000Z" | |
| } | |
| ] | |
| } | |
| ``` | |
| --- | |
| #### `POST /api/v1/websites` | |
| Create a new website. | |
| - **Auth:** User+ | |
| **Request Body:** | |
| | Field | Type | Required | Description | | |
| | ---------------- | -------- | -------- | --------------------------------------------------- | | |
| | `domain` | string | Yes | Domain name (alphanumeric, hyphens, dots; max 255) | | |
| | `content_groups` | array | No | Array of `{name, pattern}` objects (default: `[]`) | | |
| | `excluded_ips` | array | No | IP addresses/ranges to exclude (default: `[]`) | | |
| | `account_id` | UUID | No | Admin only — assign to specific account | | |
| **Response (201):** Created website object. | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | ------------------------------------------------------ | | |
| | 400 | `INVALID_PAYLOAD` | Missing/invalid domain, or duplicate domain in account | | |
| --- | |
| #### `PATCH /api/v1/websites/:id` | |
| Update website settings. At least one field must be provided. | |
| - **Auth:** User+ | |
| **Path Parameters:** | |
| | Param | Type | Description | | |
| | ----- | ---- | ----------- | | |
| | `id` | UUID | Website ID | | |
| **Request Body:** | |
| | Field | Type | Required | Description | | |
| | ---------------- | ------ | -------- | ---------------------------------- | | |
| | `domain` | string | No | New domain name | | |
| | `content_groups` | array | No | Updated content group definitions | | |
| | `excluded_ips` | array | No | Updated IP exclusion list | | |
| **Response (200):** Updated website object. | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | -------------------------------------- | | |
| | 400 | `INVALID_PAYLOAD` | Invalid ID, invalid domain, or no fields provided | | |
| | 404 | `NOT_FOUND` | Website not found or access denied | | |
| --- | |
| #### `DELETE /api/v1/websites/:id` | |
| Delete a website and all associated events and aggregated data. | |
| - **Auth:** User+ | |
| **Path Parameters:** | |
| | Param | Type | Description | | |
| | ----- | ---- | ----------- | | |
| | `id` | UUID | Website ID | | |
| **Response (200):** Deleted website object. | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | ---------------------------------- | | |
| | 400 | `INVALID_PAYLOAD` | Invalid UUID format | | |
| | 404 | `NOT_FOUND` | Website not found or access denied | | |
| --- | |
| ### Admin | |
| #### `POST /api/v1/admin/aggregate` | |
| Manually trigger the aggregation pipeline for a specific date. | |
| - **Auth:** Admin | |
| **Request Body:** | |
| | Field | Type | Required | Description | | |
| | ------ | ------ | -------- | --------------------------------------------------- | | |
| | `date` | string | No | Target date in `YYYY-MM-DD` format (default: yesterday UTC) | | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "message": "Aggregation completed for 2026-03-18" | |
| } | |
| } | |
| ``` | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | -------------------- | | |
| | 400 | `INVALID_PAYLOAD` | Invalid date format | | |
| --- | |
| ### Reports | |
| All report endpoints require authentication and are scoped by account for non-admin users. | |
| #### Common Query Parameters | |
| | Parameter | Type | Default | Description | | |
| | --------------- | ------- | -------- | -------------------------------------------------------- | | |
| | `website_id` | UUID | required | Target website | | |
| | `period` | string | `7d` | Predefined: `yesterday`, `today`, `7d`, `30d`, `90d`, `12m`, `ytd`, `all` | | |
| | `start_date` | string | — | Custom start date (`YYYY-MM-DD`). Overrides `period`. | | |
| | `end_date` | string | — | Custom end date (`YYYY-MM-DD`). Overrides `period`. | | |
| | `compare` | string | — | `previous_period` or `yoy` (year-over-year) | | |
| | `include_bots` | string | `false` | `"true"` to include bot traffic | | |
| | `content_group` | string | — | Filter by content group name | | |
| | `page` | number | `1` | Pagination page (min: 1) | | |
| | `per_page` | number | `50` | Results per page (max: 500) | | |
| | `format` | string | `json` | `json` or `csv` | | |
| When `format=csv`, the response uses `Content-Type: text/csv` with a `Content-Disposition: attachment` header. | |
| #### Common Paginated Response Structure | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "period": { "start": "YYYY-MM-DD", "end": "YYYY-MM-DD" }, | |
| "page": 1, | |
| "per_page": 50, | |
| "rows": [...] | |
| } | |
| } | |
| ``` | |
| **Common Errors (all report endpoints):** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | ----------------------------- | | |
| | 400 | `INVALID_PAYLOAD` | Missing `website_id` or invalid parameters | | |
| | 401 | `UNAUTHORIZED` | Missing or invalid API key | | |
| | 404 | `NOT_FOUND` | Website not found or no access | | |
| --- | |
| #### `GET /api/v1/reports/summary` | |
| High-level metrics overview with optional period comparison. | |
| - **Auth:** User+ | |
| - **Extra params:** `compare` (`previous_period` or `yoy`) | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "period": { "start": "2026-02-23", "end": "2026-03-01" }, | |
| "comparison": "2026-02-16 to 2026-02-22", | |
| "metrics": { | |
| "visits": { "value": 12450, "previous": 11200, "change_pct": 11.16 }, | |
| "unique_visitors": { "value": 9870, "previous": 8930, "change_pct": 10.53 }, | |
| "pageviews": { "value": 34200, "previous": 30100, "change_pct": 13.62 }, | |
| "bounce_rate": { "value": 42.3, "previous": 44.1, "change_pct": -4.08 }, | |
| "avg_session_duration_sec": { "value": 185, "previous": 172, "change_pct": 7.56 } | |
| } | |
| } | |
| } | |
| ``` | |
| > When `compare` is not set, each metric contains only `value` (no `previous` or `change_pct`). | |
| **CSV columns:** `metric`, `value`, `previous`, `change_pct` | |
| --- | |
| #### `GET /api/v1/reports/pages` | |
| Page-level traffic breakdown sorted by pageviews (descending). | |
| - **Auth:** User+ | |
| - **Paginated:** Yes | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "period": { "start": "2026-02-23", "end": "2026-03-01" }, | |
| "page": 1, | |
| "per_page": 50, | |
| "rows": [ | |
| { | |
| "path": "/blog/getting-started", | |
| "contentGroup": "Blog", | |
| "pageviews": 5420, | |
| "uniquePageviews": 3210 | |
| } | |
| ] | |
| } | |
| } | |
| ``` | |
| **CSV columns:** `path`, `contentGroup`, `pageviews`, `uniquePageviews` | |
| --- | |
| #### `GET /api/v1/reports/sources` | |
| Traffic source analysis with configurable grouping. | |
| - **Auth:** User+ | |
| - **Paginated:** Yes | |
| **Extra Query Parameters:** | |
| | Parameter | Type | Default | Description | | |
| | ---------- | ------ | --------- | ------------------------------------ | | |
| | `group_by` | string | `channel` | `channel`, `domain`, or `utm` | | |
| **Response — `group_by=channel` (200):** | |
| ```json | |
| { | |
| "rows": [ | |
| { "channel": "Organic Search", "visits": 4500 }, | |
| { "channel": "Direct", "visits": 3200 }, | |
| { "channel": "Social", "visits": 1800 } | |
| ] | |
| } | |
| ``` | |
| **Response — `group_by=domain` (200):** | |
| ```json | |
| { | |
| "rows": [ | |
| { "referrer": "google.com", "visits": 3200 }, | |
| { "referrer": "twitter.com", "visits": 1100 } | |
| ] | |
| } | |
| ``` | |
| **Response — `group_by=utm` (200):** | |
| ```json | |
| { | |
| "rows": [ | |
| { | |
| "utmSource": "twitter", | |
| "utmMedium": "social", | |
| "utmCampaign": "launch", | |
| "visits": 850 | |
| } | |
| ] | |
| } | |
| ``` | |
| **Referrer channel classifications:** | |
| | Channel | Rule | | |
| | -------------- | -------------------------------------------------------------------- | | |
| | Organic Search | Referrer matches known search engines (google.*, bing.com, etc.) | | |
| | Paid Search | Search engine + `utm_medium` contains `cpc`, `ppc`, or `paid` | | |
| | Social | Referrer matches social platforms (t.co, facebook.com, etc.) | | |
| | Email | `utm_medium=email` or referrer matches email providers | | |
| | Referral | Referrer exists but doesn't match other channels | | |
| | Direct | No referrer | | |
| --- | |
| #### `GET /api/v1/reports/devices` | |
| Device, browser, and OS breakdown. | |
| - **Auth:** User+ | |
| - **Paginated:** No | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "period": { "start": "2026-02-23", "end": "2026-03-01" }, | |
| "device_types": [ | |
| { "deviceType": "desktop", "visits": 8500 }, | |
| { "deviceType": "mobile", "visits": 3200 } | |
| ], | |
| "browsers": [ | |
| { "browserFamily": "Chrome", "visits": 6400 }, | |
| { "browserFamily": "Safari", "visits": 3100 } | |
| ], | |
| "os_families": [ | |
| { "osFamily": "Windows", "visits": 5200 }, | |
| { "osFamily": "macOS", "visits": 3800 } | |
| ], | |
| "screen_resolutions": [ | |
| { "bucket": "1025-1440", "visits": 4200 }, | |
| { "bucket": "376-768", "visits": 2800 } | |
| ] | |
| } | |
| } | |
| ``` | |
| **Screen resolution buckets:** `320-375`, `376-768`, `769-1024`, `1025-1440`, `1441+`, `unknown` | |
| **CSV columns:** `deviceType`, `visits` (only `device_types` exported) | |
| --- | |
| #### `GET /api/v1/reports/flow` | |
| Landing and exit page analysis. | |
| - **Auth:** User+ | |
| - **Paginated:** Yes (applies to both lists) | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "period": { "start": "2026-02-23", "end": "2026-03-01" }, | |
| "landing_pages": [ | |
| { "path": "/", "entries": 5200 }, | |
| { "path": "/blog", "entries": 2100 } | |
| ], | |
| "exit_pages": [ | |
| { "path": "/pricing", "exits": 3400 }, | |
| { "path": "/contact", "exits": 1800 } | |
| ] | |
| } | |
| } | |
| ``` | |
| **CSV columns:** `path`, `entries` (only `landing_pages` exported) | |
| --- | |
| #### `GET /api/v1/reports/funnel` | |
| Multi-step conversion funnel analysis. | |
| - **Auth:** User+ | |
| **Extra Query Parameters:** | |
| | Parameter | Type | Required | Description | | |
| | --------- | -------------- | -------- | -------------------------------------- | | |
| | `steps[]` | string (array) | Yes | Ordered funnel steps (min 2). Each step is a path or event type. | | |
| **Example request:** | |
| ``` | |
| GET /api/v1/reports/funnel?website_id=...&steps[]=/pricing&steps[]=/signup&steps[]=/onboarding | |
| ``` | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "period": { "start": "2026-02-23", "end": "2026-03-01" }, | |
| "funnel": [ | |
| { "step": "/pricing", "sessions": 5000, "conversion_rate": 100 }, | |
| { "step": "/signup", "sessions": 1200, "conversion_rate": 24.0 }, | |
| { "step": "/onboarding", "sessions": 900, "conversion_rate": 75.0 } | |
| ] | |
| } | |
| } | |
| ``` | |
| **Conversion rate:** First step is always 100%. Subsequent steps: `(sessions at step N / sessions at step N-1) * 100`, rounded to 2 decimals. | |
| **Session logic:** Grouped by IP hash with a 30-minute gap threshold. | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | ------------------------ | | |
| | 400 | `INVALID_PAYLOAD` | Fewer than 2 steps provided | | |
| --- | |
| #### `GET /api/v1/reports/realtime` | |
| Real-time traffic for the last 30 minutes. Queries raw events table directly. | |
| - **Auth:** User+ | |
| - **Paginated:** No | |
| **Query Parameters:** | |
| | Parameter | Type | Required | Description | | |
| | ------------ | ---- | -------- | --------------- | | |
| | `website_id` | UUID | Yes | Target website | | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "active_visitors": 42, | |
| "top_pages": [ | |
| { "path": "/", "visitors": 18 }, | |
| { "path": "/blog/new-post", "visitors": 12 } | |
| ], | |
| "events_by_type": [ | |
| { "eventType": "page_view", "count": 87 }, | |
| { "eventType": "scroll_50", "count": 34 } | |
| ] | |
| } | |
| } | |
| ``` | |
| - **Active visitors:** Unique IP hashes in 30-minute window | |
| - **Top pages:** Limited to top 10 | |
| - **Filters:** Excludes bot and excluded traffic | |
| --- | |
| #### `GET /api/v1/reports/custom-events` | |
| Custom event tracking and analysis. | |
| - **Auth:** User+ | |
| - **Paginated:** Yes | |
| **Extra Query Parameters:** | |
| | Parameter | Type | Required | Description | | |
| | ------------ | ------ | -------- | ------------------------------- | | |
| | `event_name` | string | No | Filter by custom event name | | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "period": { "start": "2026-02-23", "end": "2026-03-01" }, | |
| "page": 1, | |
| "per_page": 50, | |
| "rows": [ | |
| { "name": "signup_complete", "count": 342 }, | |
| { "name": "video_play", "count": 218 } | |
| ] | |
| } | |
| } | |
| ``` | |
| **CSV columns:** `name`, `count` | |
| --- | |
| #### `GET /api/v1/reports/timeseries` | |
| Daily time-series data for a single metric over the selected period. | |
| - **Auth:** User+ | |
| - **Paginated:** No | |
| **Extra Query Parameters:** | |
| | Parameter | Type | Default | Description | | |
| | --------- | ------ | ----------- | ---------------------------------------------------------------------------------------------------------------- | | |
| | `metric` | string | `pageviews` | Metric to plot: `visits`, `pageviews`, `unique_pageviews`, `bounce_rate`, `avg_session_duration`, `landing_page_entries`, `exit_page_exits` | | |
| | `path` | string | — | Filter to a specific page path (e.g. `/about`). Omit for site-wide totals. | | |
| Rate metrics (`bounce_rate`, `avg_session_duration`) are averaged per day. Count metrics are summed. | |
| **Response (200):** | |
| ```json | |
| { | |
| "status": "success", | |
| "data": { | |
| "period": { "start": "2026-03-13", "end": "2026-03-19" }, | |
| "metric": "pageviews", | |
| "path": "*", | |
| "series": [ | |
| { "date": "2026-03-13", "value": 1200 }, | |
| { "date": "2026-03-14", "value": 1350 }, | |
| { "date": "2026-03-15", "value": 980 }, | |
| { "date": "2026-03-16", "value": 870 }, | |
| { "date": "2026-03-17", "value": 1100 }, | |
| { "date": "2026-03-18", "value": 1420 }, | |
| { "date": "2026-03-19", "value": 1290 } | |
| ] | |
| } | |
| } | |
| ``` | |
| **CSV columns:** `date`, `value` | |
| **Errors:** | |
| | Status | Code | Cause | | |
| | ------ | ----------------- | ------------------------------- | | |
| | 400 | `INVALID_PAYLOAD` | Invalid `metric` value | | |
| --- | |
| ### Static Assets | |
| #### `GET /js/tracker.js` | |
| Client-side tracking snippet (vanilla JavaScript). | |
| - **Auth:** None | |
| - **Rate limit:** None | |
| - **Content-Type:** `application/javascript` | |
| - **Cache-Control:** `public, max-age=86400` (24 hours) | |
| **Installation:** | |
| ```html | |
| <script defer data-id="YOUR_WEBSITE_UUID" src="https://your-api.com/js/tracker.js"></script> | |
| ``` | |
| **Custom events API:** | |
| ```javascript | |
| window.analytics.track("signup_complete", { plan: "pro", source: "pricing_page" }); | |
| ``` | |
| **Auto-tracked events:** | |
| | Event | Trigger | | |
| | ---------------- | -------------------------------------- | | |
| | `page_view` | `DOMContentLoaded` | | |
| | `close_tab` | `visibilitychange` → hidden | | |
| | `scroll_25` | 25% scroll depth | | |
| | `scroll_50` | 50% scroll depth | | |
| | `scroll_75` | 75% scroll depth | | |
| | `outbound_link` | Click on external link | | |
| --- | |
| ## Endpoint Summary | |
| | Method | Path | Auth | Rate Limited | Description | | |
| | ------- | ------------------------------- | ------- | ------------ | ------------------------ | | |
| | GET | `/health` | No | No | Health check | | |
| | OPTIONS | `/api/v1/collect` | No | No | CORS preflight | | |
| | POST | `/api/v1/collect` | No | Yes (IP) | Event ingestion | | |
| | GET | `/api/v1/accounts` | Admin | Yes | List accounts | | |
| | POST | `/api/v1/accounts` | Admin | Yes | Create account | | |
| | DELETE | `/api/v1/accounts/:id` | Admin | Yes | Delete account | | |
| | GET | `/api/v1/users` | Admin | Yes | List users | | |
| | POST | `/api/v1/users` | Admin | Yes | Create user | | |
| | GET | `/api/v1/users/:id/api-key` | Admin | Yes | Get user API key | | |
| | DELETE | `/api/v1/users/:id` | Admin | Yes | Delete user | | |
| | GET | `/api/v1/websites` | User+ | Yes | List websites | | |
| | POST | `/api/v1/websites` | User+ | Yes | Create website | | |
| | PATCH | `/api/v1/websites/:id` | User+ | Yes | Update website | | |
| | DELETE | `/api/v1/websites/:id` | User+ | Yes | Delete website | | |
| | POST | `/api/v1/admin/aggregate` | Admin | Yes | Trigger aggregation | | |
| | GET | `/api/v1/reports/summary` | User+ | Yes | Summary metrics | | |
| | GET | `/api/v1/reports/pages` | User+ | Yes | Page traffic | | |
| | GET | `/api/v1/reports/sources` | User+ | Yes | Traffic sources | | |
| | GET | `/api/v1/reports/devices` | User+ | Yes | Device analytics | | |
| | GET | `/api/v1/reports/flow` | User+ | Yes | Landing/exit pages | | |
| | GET | `/api/v1/reports/funnel` | User+ | Yes | Funnel analysis | | |
| | GET | `/api/v1/reports/realtime` | User+ | Yes | Real-time activity | | |
| | GET | `/api/v1/reports/custom-events` | User+ | Yes | Custom events | | |
| | GET | `/api/v1/reports/timeseries` | User+ | Yes | Daily metric timeseries | | |
| | GET | `/js/tracker.js` | No | No | Tracker script | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment