Tinylytics API
Use the Tinylytics API to read analytics, send hits, events, and kudos from your backend, and automate reporting.
For AI clients, use the MCP server at https://tinylytics.app/mcp with the same API keys. To start a trial for a human owner, follow the instructions for agents.
1. Quick Start
Base URL
https://tinylytics.app/api/v1
Header format
Authorization: Bearer tly-ro-your-api-key
Accept: application/json
User-Agent: your-agent
Use tly-fa-... (Full Access) for write endpoints.
HTTP API calls need a User-Agent. Cloudflare returns 1010 without one.
Test your key in 30 seconds
curl "https://tinylytics.app/api/v1/me" \
-H "Authorization: Bearer tly-ro-your-api-key" \
-H "Accept: application/json" \
-H "User-Agent: your-agent"
If valid, you get your account payload with HTTP 200.
Discovery and schema
You can bootstrap clients without reading prose docs first:
curl "https://tinylytics.app/api/v1"
curl "https://tinylytics.app/api/v1/openapi.json"
2. Authentication and Access
- Auth scheme:
Bearertoken. - Key location: Account Settings → API Access.
- Read-only keys (
tly-ro-...) can call allGETendpoints. - Full-access keys (
tly-fa-...) are required for:POST /sites/:id/hitsPOST /sites/:id/hits/batchPOST /sites/:id/agent_visitsPOST /sites/:id/eventsPOST /sites/:id/events/batchPOST /sites/:id/kudosDELETE /sites/:id/kudos/:kudo_uidPOST /sites/:id/signalsPOST /groupsPATCH /groups/:idPUT /groups/:idDELETE /groups/:id
Access rules
- Any account with a valid API key can use core API endpoints.
- API keys stay personal. Omit
account_idto act on the key owner’s personal account. - Pass
account_id(the team owner’s user id) to act on an active team account you belong to. List available accounts withGET /accounts. - Team viewers can read the owner’s sites and groups. Team admins can create, update, and delete the owner’s sites. Team owners can also create, update, and delete the owner’s groups.
- Premium endpoints require an active subscription on the selected account owner:
GET /sites/:id/insightsGET /sites/:id/signalsPOST /sites/:id/signalsGET /sites/:id/uptimeGET /sites/:id/content
- Group endpoints require an Ultra subscription:
GET /groupsGET /groups/:idPOST /groupsPATCH /groups/:idPUT /groups/:idDELETE /groups/:id
- Signals endpoints also require insights to be enabled on the site.
- Revoked or invalid keys return
401. - Write endpoint with read-only key returns
403.
3. Request Conventions
- Dates use
YYYY-MM-DD. - Optional
account_idselects a team account. Send it as a query parameter or in the JSON body. Omit it for the personal account. - Date range limit for analytics endpoints: max
730days. - Date boundaries for analytics endpoints default to
UTC. - Optional timezone mode:
time_zone=utc(default) uses UTC day boundaries.time_zone=useruses your account timezone day boundaries.
- Pagination:
pagedefault varies by endpointper_pagemax1000(hits,kudos,leaderboard),50(user_journeys,insights),100(signals,uptime)
- Hits filtering:
countryexact matchpathexact matchreferrerpartial match
- Kudos filtering:
pathexact match
- Grouped hits:
grouped=truegroup_byone ofpath,country,referrer,browser_name,platform_name,source
4. Endpoint Directory
| Method | Endpoint | Purpose |
|---|---|---|
| GET | / |
Public API discovery metadata |
| GET | /openapi.json |
OpenAPI 3.1 schema for API v1 |
| GET | /me |
Validate API key and return account info |
| GET | /accounts |
List personal and accessible team accounts |
| POST | /agent_signups |
Start signup for a human owner |
| GET | /agent_signups/:id |
Poll agent signup status |
| GET | /sites |
List accessible sites |
| GET | /sites/:id |
Get one site |
| POST | /sites |
Create one site Full Access |
| PATCH | /sites/:id |
Update one site Full Access |
| PUT | /sites/:id |
Update one site Full Access |
| DELETE | /sites/:id |
Delete one site Full Access |
| PATCH | /sites/:id/features |
Update site monitoring features Full Access |
| PUT | /sites/:id/features |
Update site monitoring features Full Access |
| GET | /sites/:id/hits |
Raw or grouped analytics hits |
| POST | /sites/:id/hits |
Create one hit Full Access |
| POST | /sites/:id/hits/batch |
Create many hits in one request Full Access |
| GET | /sites/:id/agent_visits |
AI assistant and crawler visits |
| POST | /sites/:id/agent_visits |
Record one AI crawler visit Full Access |
| POST | /sites/:id/events |
Create one event Full Access |
| POST | /sites/:id/events/batch |
Create many events in one request Full Access |
| GET | /sites/:id/kudos |
Read kudos records |
| POST | /sites/:id/kudos |
Create one kudo Full Access |
| DELETE | /sites/:id/kudos/:kudo_uid |
Delete one kudo by UID Full Access |
| GET | /sites/:id/leaderboard |
All-time path leaderboard |
| GET | /sites/:id/user_journeys |
Visitor journey analysis |
| GET | /sites/:id/insights |
AI insights for the site Subscription |
| GET | /sites/:id/signals |
Owner-submitted context signals Subscription |
| POST | /sites/:id/signals |
Submit one context signal Full Access Subscription |
| GET | /sites/:id/uptime |
Uptime + SSL/domain status Subscription |
| GET | /sites/:id/content |
Content monitoring status and issues Subscription |
| GET | /groups |
List site groups Ultra |
| GET | /groups/:id |
Get one site group Ultra |
| POST | /groups |
Create one site group Full Access Ultra |
| PATCH | /groups/:id |
Update one site group Full Access Ultra |
| PUT | /groups/:id |
Update one site group Full Access Ultra |
| DELETE | /groups/:id |
Delete one site group Full Access Ultra |
5. Endpoint Reference
Agent signup
Unauthenticated. Starts signup for a human owner. The 14-day trial starts when they activate. The response never includes API keys or passwords. After the owner activates, connect over MCP. Full walkthrough: instructions for agents.
POST /agent_signups
| Property | Required | Description |
|---|---|---|
| Yes | Owner email. | |
| tos_accepted | Yes | Must be true. |
| agent_name | No | Display name of the agent. |
| agent_client | No | Client identifier, such as cursor. |
| site_url | No | Suggested first site. Not created automatically. |
| intended_plan | No | Hint only: zen, pro, or ultra. |
Returns 201 with pending_owner, or 200 with existing_account if the email already has an account.
GET /agent_signups/:id
Authorize with X-Agent-Signup-Token. token as a query parameter also works. Poll until owner_activated, then connect to /mcp.
Account and Sites
GET /me
Returns current user + current API key metadata.
Accepted properties
| Property | Required | Description |
|---|---|---|
| None | Yes | This endpoint does not accept query or body properties. |
curl "https://tinylytics.app/api/v1/me" \
-H "Authorization: Bearer tly-ro-your-api-key" \
-H "User-Agent: your-agent"
{
"id": 123,
"email": "[email protected]",
"is_subscribed": true,
"created_at": "2025-06-01T12:00:00Z",
"api_key": {
"name": "CLI integration",
"access_type": "read_only",
"last_used_at": "2026-02-12T10:00:00Z"
},
"accounts": [
{
"id": 123,
"name": "Personal",
"kind": "personal",
"role": "owner",
"is_subscribed": true,
"is_plus": false
},
{
"id": 456,
"name": "Tiny Team",
"kind": "team",
"role": "viewer",
"is_subscribed": true,
"is_plus": true
}
]
}
GET /accounts
Lists the personal account and every active team account available to this API key.
Accepted properties
| Property | Required | Description |
|---|---|---|
| None | Yes | This endpoint does not accept query or body properties. |
curl "https://tinylytics.app/api/v1/accounts" \
-H "Authorization: Bearer tly-ro-your-api-key"
{
"accounts": [
{
"id": 123,
"name": "Personal",
"kind": "personal",
"role": "owner",
"is_subscribed": true,
"is_plus": false
},
{
"id": 456,
"name": "Tiny Team",
"kind": "team",
"role": "admin",
"is_subscribed": true,
"is_plus": true
}
]
}
Use a team id as account_id on later requests.
GET /sites
Lists your sites with lifetime counters.
Accepted properties
| Property | Required | Description |
|---|---|---|
account_id |
No | Team account owner id. Omit for the personal account. |
curl "https://tinylytics.app/api/v1/sites" \
-H "Authorization: Bearer tly-ro-your-api-key"
curl "https://tinylytics.app/api/v1/sites?account_id=456" \
-H "Authorization: Bearer tly-ro-your-api-key"
{
"sites": [
{
"id": 456,
"uid": "abc123",
"url": "https://example.com",
"label": "My Blog",
"lifetime_hits": 12340,
"lifetime_unique_hits": 8920,
"lifetime_kudos": 87,
"active": true,
"public": false,
"created_at": "2025-06-01T12:00:00Z",
"updated_at": "2026-02-14T09:30:00Z"
}
]
}
GET /sites/:id
Returns one site by numeric ID.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. Use the id returned from GET /sites. |
curl "https://tinylytics.app/api/v1/sites/456" \
-H "Authorization: Bearer tly-ro-your-api-key"
{
"id": 456,
"uid": "abc123",
"url": "https://example.com",
"label": "My Blog",
"lifetime_hits": 12340,
"lifetime_unique_hits": 8920,
"lifetime_kudos": 87,
"active": true,
"public": false,
"created_at": "2025-06-01T12:00:00Z",
"updated_at": "2026-02-14T09:30:00Z"
}
POST /sites
Creates a site owned by the selected account. Omit account_id to create on the key owner’s personal account. Team admins and owners can pass account_id to create a site owned by the team owner.
Accepted properties
| Property | Required | Description |
|---|---|---|
url |
Yes | Site URL. Must start with http:// or https://. |
label |
No | Display label. |
display_root_path |
No | Optional public path prefix for display URLs. |
public or is_public |
No | Whether the site stats page is public. |
active |
No | Whether the site accepts analytics ingestion. Defaults to true. |
allow_from_any_domain |
No | When true, the embed accepts hits from any hostname (not only the configured site URL). Defaults to false. |
account_id |
No | Team account owner id. The new site belongs to that owner. |
Requires a full-access API key (tly-fa-*) and owner or team-admin access on the selected account. Team owners can also write groups.
curl -X POST "https://tinylytics.app/api/v1/sites" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"url": "https://example.com",
"label": "My Blog",
"public": false
}'
{
"site": {
"id": 456,
"uid": "abc123",
"url": "https://example.com",
"label": "My Blog",
"display_root_path": null,
"lifetime_hits": 0,
"lifetime_unique_hits": 0,
"lifetime_kudos": 0,
"active": true,
"public": false,
"created_at": "2026-02-14T09:30:00Z",
"updated_at": "2026-02-14T09:30:00Z"
}
}
PATCH /sites/:id
Updates one owned site. Accepts the same body fields as POST /sites, except url is optional on update.
Requires a full-access API key (tly-fa-*).
curl -X PATCH "https://tinylytics.app/api/v1/sites/456" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"label": "Renamed Blog",
"display_root_path": "/blog",
"public": true,
"active": true
}'
{
"site": {
"id": 456,
"uid": "abc123",
"url": "https://example.com",
"label": "Renamed Blog",
"display_root_path": "/blog",
"lifetime_hits": 12340,
"lifetime_unique_hits": 8920,
"lifetime_kudos": 87,
"active": true,
"public": true,
"created_at": "2025-06-01T12:00:00Z",
"updated_at": "2026-02-14T10:15:00Z"
}
}
DELETE /sites/:id
Soft-deletes one owned site immediately, hides it from future GET /sites responses, and queues background cleanup.
This action is irreversible from the API client’s perspective.
Requires a full-access API key (tly-fa-*).
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
curl -X DELETE "https://tinylytics.app/api/v1/sites/456" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Accept: application/json"
Returns 204 No Content on success.
Groups
Group endpoints let Ultra subscribers bundle sites for combined analytics dashboards. Mutations require a full-access API key.
GET /groups Ultra
Lists your site groups with member site summaries.
Accepted properties
| Property | Required | Description |
|---|---|---|
| None | Yes | This endpoint does not accept query or body properties. |
curl "https://tinylytics.app/api/v1/groups" \
-H "Authorization: Bearer tly-ro-your-api-key"
{
"groups": [
{
"id": 12,
"name": "Blog Portfolio",
"description": "All personal blogs",
"icon": "rocket",
"accent_color": "#4F46E5",
"pinned": true,
"default_chart_view": "separate",
"position": 1,
"site_ids": [456, 789],
"sites": [
{ "id": 456, "uid": "abc123", "url": "https://example.com", "label": "My Blog" },
{ "id": 789, "uid": "def456", "url": "https://notes.example.com", "label": "Notes" }
],
"created_at": "2025-11-30T18:58:32Z",
"updated_at": "2026-02-14T09:30:00Z"
}
]
}
GET /groups/:id Ultra
Returns one group by numeric ID.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Group numeric ID. |
curl "https://tinylytics.app/api/v1/groups/12" \
-H "Authorization: Bearer tly-ro-your-api-key"
POST /groups Full Access Ultra
Creates a group on the selected account. Omit account_id to create on the key owner’s personal account. Team owners can pass account_id to create a group on the team account.
Accepted properties
| Property | Required | Description |
|---|---|---|
name |
Yes | Group name. |
description |
No | Optional description. |
icon |
No | Icon key used by the dashboard UI. |
accent_color |
No | Hex color such as #4F46E5. |
pinned |
No | Whether the group is pinned in the dashboard. |
default_chart_view |
No | One of separate or combined. Defaults to separate. |
site_ids |
No | Array of site numeric IDs owned by the selected account. Unknown or foreign site IDs are ignored. |
account_id |
No | Team account owner id. The new group belongs to that owner. |
Requires a full-access API key (tly-fa-*) and personal or team-owner access on the selected account.
curl -X POST "https://tinylytics.app/api/v1/groups" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "Blog Portfolio",
"description": "All personal blogs",
"accent_color": "#4F46E5",
"default_chart_view": "separate",
"site_ids": [456, 789]
}'
{
"group": {
"id": 12,
"name": "Blog Portfolio",
"description": "All personal blogs",
"icon": null,
"accent_color": "#4F46E5",
"pinned": false,
"default_chart_view": "separate",
"position": 1,
"site_ids": [456, 789],
"sites": [
{ "id": 456, "uid": "abc123", "url": "https://example.com", "label": "My Blog" },
{ "id": 789, "uid": "def456", "url": "https://notes.example.com", "label": "Notes" }
],
"created_at": "2026-02-14T09:30:00Z",
"updated_at": "2026-02-14T09:30:00Z"
}
}
PATCH /groups/:id Full Access Ultra
Updates one owned group. Send only the fields you want to change. Include site_ids to replace group membership with the provided owned site IDs.
Requires a full-access API key (tly-fa-*).
curl -X PATCH "https://tinylytics.app/api/v1/groups/12" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "Renamed Portfolio",
"pinned": true,
"site_ids": [456]
}'
DELETE /groups/:id Full Access Ultra
Deletes one owned group. Member sites are not deleted.
Requires a full-access API key (tly-fa-*).
curl -X DELETE "https://tinylytics.app/api/v1/groups/12" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Accept: application/json"
Returns 204 No Content on success.
PATCH /sites/:id/features Full Access
Enable or disable monitoring features for one owned site. Send only the features you want to change.
Requires a full-access API key (tly-fa-*).
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
content_monitoring |
No | Enable or disable content monitoring. Requires a paid plan. Enabling also turns on content monitoring emails. |
content_monitoring_emails |
No | Enable or disable email alerts for content monitoring issues. Requires content monitoring to be enabled before turning emails on. |
uptime |
No | Enable or resume uptime monitoring, or pause it when set to false. Pausing does not delete the monitor. |
At least one supported feature property must be present in the request body.
curl -X PATCH "https://tinylytics.app/api/v1/sites/456/features" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"content_monitoring": true,
"uptime": true
}'
{
"site": {
"id": 456,
"uid": "abc123",
"url": "https://example.com",
"label": "Example Blog",
"display_root_path": "/",
"lifetime_hits": 12340,
"lifetime_unique_hits": 8920,
"lifetime_kudos": 87,
"active": true,
"public": true,
"created_at": "2025-06-01T12:00:00Z",
"updated_at": "2026-02-14T10:15:00Z"
},
"features": {
"content_monitoring": true,
"content_monitoring_emails": true,
"uptime": true
}
}
To keep content monitoring on while turning off email alerts:
curl -X PATCH "https://tinylytics.app/api/v1/sites/456/features" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "content_monitoring_emails": false }'
To pause uptime monitoring without deleting the monitor:
curl -X PATCH "https://tinylytics.app/api/v1/sites/456/features" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "uptime": false }'
Analytics Endpoints
GET /sites/:id/hits
Read detailed hits or grouped analytics.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
start_date |
No | Range start (YYYY-MM-DD). Defaults to 30 days ago in the selected timezone mode. |
end_date |
No | Range end (YYYY-MM-DD). Defaults to today in the selected timezone mode. |
time_zone |
No | Date-boundary mode: utc (default) or user (use account timezone). |
country |
No | Filter by exact 2-letter country code. |
path |
No | Filter by exact path (for example /pricing). |
referrer |
No | Case-insensitive partial match on referrer. |
grouped |
No | Set to true to return grouped/aggregated results. |
group_by |
No | One of path, country, referrer, browser_name, platform_name, source. |
page |
No | Page number. |
per_page |
No | Page size, max 1000. |
curl "https://tinylytics.app/api/v1/sites/456/hits?grouped=true&group_by=path" \
-H "Authorization: Bearer tly-ro-your-api-key"
Grouped by path returns views (+ unique_views when enabled). Other groupings return hit_count.
To evaluate start_date and end_date in your account timezone, add time_zone=user:
curl "https://tinylytics.app/api/v1/sites/456/hits?start_date=2026-02-13&end_date=2026-02-13&time_zone=user" \
-H "Authorization: Bearer tly-ro-your-api-key"
Response (ungrouped)
{
"hits": [
{
"id": 789,
"url": "https://example.com/pricing",
"path": "/pricing",
"referrer": "https://google.com",
"country": "US",
"browser_name": "Safari",
"platform_name": "macOS",
"is_mobile": false,
"source": "google",
"created_at": "2026-02-13T14:22:00Z"
}
],
"pagination": {
"current_page": 1,
"per_page": 100,
"total_count": 1,
"total_pages": 1
},
"filters": {
"start_date": "2026-02-13",
"end_date": "2026-02-13",
"time_zone": "user",
"country": null,
"path": null,
"referrer": null,
"grouped": false
}
}
Response (grouped by path)
{
"grouped_hits": [
{
"path": "/pricing",
"views": 142,
"unique_views": 98
}
],
"pagination": {
"current_page": 1,
"per_page": 100,
"total_count": 1,
"total_pages": 1
},
"filters": {
"start_date": "2026-02-13",
"end_date": "2026-02-13",
"time_zone": "user",
"country": null,
"path": null,
"referrer": null,
"grouped": true,
"group_by": "path"
}
}
unique_views is only included when unique hit tracking is enabled for the site. Other group_by values (country, referrer, browser_name, platform_name, source) return hit_count instead of views/unique_views.
POST /sites/:id/hits Full Access
Create one hit.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
path |
Yes | Path to track. Leading slash is auto-added if missing. |
country |
No | 2-letter uppercase country code (for example US, PL, XX). If provided, this value takes precedence. |
ip_address |
No | IPv4/IPv6 address used to resolve country via local lookup first, then IPinfo Lite API as fallback when country is not provided. Raw IP is not stored in hits. |
url |
No | Full page URL. Defaults to site.url + path. |
referrer |
No | Referrer URL. |
user_agent |
No | User agent string. |
visitor_id |
No | Stable visitor identifier used for dedupe/journey grouping. |
source |
No | Source override. If missing, Tinylytics may infer from URL parameters. |
Payload rules
- Body must be a single JSON object
- Required fields:
path countrymust be 2-letter uppercase when provided (example:US,PL,XX)- Country resolution order: provided
country→ local lookup fromip_address→ IPinfocountry_codeAPI fallback →XX pathis normalized to begin with/
curl -X POST "https://tinylytics.app/api/v1/sites/456/hits" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"path": "/pricing",
"ip_address": "8.8.8.8",
"visitor_id": "user-123"
}'
Response (201 created)
{
"status": "created",
"hit": {
"id": 789,
"url": "https://example.com/pricing",
"path": "/pricing",
"referrer": null,
"country": "US",
"browser_name": null,
"platform_name": null,
"is_mobile": false,
"source": null,
"unique_hash": "a1b2c3",
"visitor_hash": "d4e5f6",
"created_at": "2026-02-14T10:00:00Z"
}
}
Response (202 ignored)
{
"status": "ignored",
"reason": "Path matches ignore rule"
}
Response (422 error)
{
"status": "error",
"errors": ["Path can't be blank"]
}
POST /sites/:id/hits/batch Full Access
Create many hits in one request.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
[] |
Yes | Top-level array of hit objects. |
[].path |
Yes | Path to track. Leading slash is auto-added if missing. |
[].country |
No | 2-letter uppercase country code. If provided, this value takes precedence. |
[].ip_address |
No | IPv4/IPv6 address used to resolve country via local lookup first, then IPinfo Lite API as fallback when [].country is not provided. Raw IP is not stored in hits. |
[].url |
No | Full page URL. |
[].referrer |
No | Referrer URL. |
[].user_agent |
No | User agent string. |
[].visitor_id |
No | Stable visitor identifier used for dedupe/journey grouping. |
[].source |
No | Source override. |
Payload rules
- Body must be a top-level JSON array
- Each row follows the same field rules as single hit creation.
- Per row country resolution order: provided
country→ local lookup fromip_address→ IPinfocountry_codeAPI fallback →XX - Batch is partial-success: one bad row does not fail the whole request.
curl -X POST "https://tinylytics.app/api/v1/sites/456/hits/batch" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-d '[
{ "path": "/valid", "country": "PL" },
{ "path": "/from-ip", "ip_address": "8.8.8.8" },
{ "path": "/fallback-xx", "ip_address": "999.999.999.999" }
]'
{
"created_count": 3,
"ignored_count": 0,
"error_count": 0,
"results": [
{ "index": 0, "status": "created" },
{ "index": 1, "status": "created" },
{ "index": 2, "status": "created" }
]
}
POST /sites/:id/events Full Access
Create one event.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
event |
Yes | Event name in category.action format. |
value |
No | Optional event value stored as event_properties["value"]. |
path |
No | Optional path context. Leading slash is auto-added if missing. |
country |
No | 2-letter uppercase country code (for example US, PL, XX). If provided, this value takes precedence. |
ip_address |
No | IPv4/IPv6 address used to resolve country via local lookup first, then IPinfo Lite API as fallback when country is not provided. Raw IP is not stored in events. |
url |
No | Full page URL. Defaults to site.url + path when path is provided. |
referrer |
No | Referrer URL. |
user_agent |
No | User agent string. |
visitor_id |
No | Stable visitor identifier used for event identity/grouping hashes. |
source |
No | Source override. If missing, Tinylytics may infer from URL parameters. |
Payload rules
- Body must be a single JSON object
- Required fields:
event eventmust usecategory.actionformatcountrymust be 2-letter uppercase when provided (example:US,PL,XX)- Country resolution order: provided
country→ local lookup fromip_address→ IPinfocountry_codeAPI fallback →XX pathis optional and normalized to begin with/when present
curl -X POST "https://tinylytics.app/api/v1/sites/456/events" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"event": "signup.started",
"value": "pricing",
"ip_address": "8.8.8.8",
"visitor_id": "user-123"
}'
Response (201 created)
{
"status": "created",
"event": {
"id": 790,
"event": "signup.started",
"value": "pricing",
"url": null,
"path": null,
"referrer": null,
"country": "US",
"source": null,
"unique_hash": "a1b2c3",
"visitor_hash": "d4e5f6",
"created_at": "2026-02-14T10:00:00Z"
}
}
Response (202 ignored)
{
"status": "ignored",
"reason": "Event matched ignore rules"
}
Response (422 error)
{
"status": "error",
"errors": ["Event must be 'category.action' format (2+ dot-separated segments)"]
}
POST /sites/:id/events/batch Full Access
Create many events in one request.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
[] |
Yes | Top-level array of event objects. |
[].event |
Yes | Event name in category.action format. |
[].value |
No | Optional event value stored as event_properties["value"]. |
[].path |
No | Optional path context. Leading slash is auto-added if missing. |
[].country |
No | 2-letter uppercase country code. If provided, this value takes precedence. |
[].ip_address |
No | IPv4/IPv6 address used to resolve country via local lookup first, then IPinfo Lite API as fallback when [].country is not provided. Raw IP is not stored in events. |
[].url |
No | Full page URL. |
[].referrer |
No | Referrer URL. |
[].user_agent |
No | User agent string. |
[].visitor_id |
No | Stable visitor identifier used for event identity/grouping hashes. |
[].source |
No | Source override. |
Payload rules
- Body must be a top-level JSON array
- Each row follows the same field rules as single event creation
- Per row country resolution order: provided
country→ local lookup fromip_address→ IPinfocountry_codeAPI fallback →XX - Batch is partial-success: one bad row does not fail the whole request
curl -X POST "https://tinylytics.app/api/v1/sites/456/events/batch" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-d '[
{ "event": "signup.started", "value": "pricing", "country": "PL" },
{ "event": "signup.completed", "ip_address": "8.8.8.8" },
{ "event": "signup.cancelled", "ip_address": "999.999.999.999" }
]'
{
"created_count": 3,
"ignored_count": 0,
"error_count": 0,
"results": [
{ "index": 0, "status": "created" },
{ "index": 1, "status": "created" },
{ "index": 2, "status": "created" }
]
}
GET /sites/:id/kudos
Read detailed Kudos activity.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
start_date |
No | Range start (YYYY-MM-DD). Defaults to 30 days ago in the selected timezone mode. |
end_date |
No | Range end (YYYY-MM-DD). Defaults to today in the selected timezone mode. |
time_zone |
No | Date-boundary mode: utc (default) or user (use account timezone). |
path |
No | Filter by exact path (for example /pricing). |
uid |
No | Filter by exact kudo UID. |
page |
No | Page number. |
per_page |
No | Page size, max 1000. |
curl "https://tinylytics.app/api/v1/sites/456/kudos?start_date=2026-02-01&end_date=2026-02-14" \
-H "Authorization: Bearer tly-ro-your-api-key"
{
"kudos": [
{
"id": 321,
"uid": "pricing-kudo-1",
"path": "/pricing",
"created_at": "2026-02-10T08:15:00Z"
}
],
"pagination": {
"current_page": 1,
"per_page": 100,
"total_count": 1,
"total_pages": 1
},
"filters": {
"start_date": "2026-02-01",
"end_date": "2026-02-14",
"time_zone": "utc",
"path": null,
"uid": null
}
}
POST /sites/:id/kudos Full Access
Create one kudo.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
path |
Yes | Path to track. Leading slash is auto-added if missing. |
custom_uid |
No | Custom identifier for the kudo. If omitted, Tinylytics generates one. |
Payload rules
- Body must be a single JSON object
- Required fields:
path pathis normalized to begin with/
curl -X POST "https://tinylytics.app/api/v1/sites/456/kudos" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"path": "/pricing",
"custom_uid": "pricing-kudo-1"
}'
Response (201 created)
{
"status": "created",
"kudo": {
"id": 321,
"uid": "pricing-kudo-1",
"path": "/pricing",
"created_at": "2026-02-14T10:00:00Z"
}
}
Response (202 ignored)
{
"status": "ignored",
"reason": "Path matches ignore rule"
}
Response (422 error)
{
"status": "error",
"errors": ["Path can't be blank"]
}
DELETE /sites/:id/kudos/:kudo_uid Full Access
Delete one kudo by UID.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
kudo_uid (URL path) |
Yes | Kudo UID to delete. |
curl -X DELETE "https://tinylytics.app/api/v1/sites/456/kudos/pricing-kudo-1" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Accept: application/json"
Response (200 deleted)
{
"status": "deleted",
"uid": "pricing-kudo-1"
}
Response (404 not found)
{
"error": "Kudo not found"
}
GET /sites/:id/leaderboard
All-time path ranking with caching.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
path |
No | Case-insensitive partial filter for path text. |
page |
No | Page number. |
per_page |
No | Page size, max 1000. |
curl "https://tinylytics.app/api/v1/sites/456/leaderboard?path=blog" \
-H "Authorization: Bearer tly-ro-your-api-key"
{
"leaderboard": [
{
"path": "/blog/hello-world",
"total_hits": 540,
"unique_hits": 320,
"percentage": 12.5
}
],
"site": {
"id": 456,
"uid": "abc123",
"url": "https://example.com",
"label": "My Blog"
},
"pagination": {
"current_page": 1,
"per_page": 100,
"total_count": 1,
"total_pages": 1
},
"cache_info": {
"cached_at": "2026-02-14T09:00:00Z",
"expires_at": "2026-02-14T10:00:00Z"
},
"filters": {
"path": "blog"
}
}
GET /sites/:id/user_journeys
Session-style visitor path analysis with summary metrics.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
start_date |
No | Range start (YYYY-MM-DD). Defaults to 30 days ago in the selected timezone mode. |
end_date |
No | Range end (YYYY-MM-DD). Defaults to today in the selected timezone mode. |
time_zone |
No | Date-boundary mode: utc (default) or user (use account timezone). |
page |
No | Page number. |
per_page |
No | Page size, max 50. |
curl "https://tinylytics.app/api/v1/sites/456/user_journeys?start_date=2026-01-01&end_date=2026-01-31&time_zone=user" \
-H "Authorization: Bearer tly-ro-your-api-key"
{
"user_journeys": [
{
"visitor_hash": "v1a2b3",
"page_count": 4,
"first_hit": "2026-01-15T10:00:00Z",
"last_hit": "2026-01-15T10:12:00Z",
"duration_minutes": 12,
"pages": [
{ "path": "/" },
{ "path": "/blog" },
{ "path": "/blog/hello-world" },
{ "path": "/pricing" }
],
"entry_page": "/",
"exit_page": "/pricing",
"session_duration": 720,
"referrer": "https://google.com",
"country": "DE",
"browser": "Firefox"
}
],
"summary": {
"total_visitors": 230,
"multi_page_visitors": 95,
"single_page_visitors": 135,
"bounce_rate": 58.7
},
"insights": {
"top_entry_pages": [
{ "path": "/", "visitors": 120 },
{ "path": "/blog", "visitors": 45 }
],
"top_exit_pages": [
{ "path": "/pricing", "visitors": 60 },
{ "path": "/blog/hello-world", "visitors": 30 }
]
},
"pagination": {
"current_page": 1,
"per_page": 50,
"total_count": 230,
"total_pages": 5
},
"filters": {
"start_date": "2026-01-01",
"end_date": "2026-01-31",
"time_zone": "user"
}
}
GET /sites/:id/insights Subscription
Returns generated insights, signal snapshots, and site insight settings.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
page |
No | Page number. |
per_page |
No | Page size, max 50. |
curl "https://tinylytics.app/api/v1/sites/456/insights" \
-H "Authorization: Bearer tly-ro-your-api-key"
{
"insights": [
{
"id": 42,
"insights_for_date": "2026-02-13",
"formatted_insights_date": "February 13, 2026",
"generated_at": "2026-02-14T06:00:00Z",
"summary": "Traffic was steadier than usual overall, with one blog post and a new referrer doing most of the lifting.",
"signals": [
{
"type": "traffic_change",
"headline": "Traffic is up 28% this week",
"summary": "The site picked up 378 hits in the last 7 days, up from 296 the week before.",
"importance_score": 64,
"detected_at": "2026-02-14T06:00:00Z",
"window": {
"started_at": "2026-02-07T00:00:00Z",
"ended_at": "2026-02-14T06:00:00Z"
},
"payload_excerpt": {
"direction": "increase",
"current_hits": 378,
"previous_hits": 296,
"absolute_change": 82,
"change_percentage": 27.7
}
}
],
"traffic_patterns": "Wednesday and Thursday were the busiest days, with evenings remaining your strongest hour.",
"best_content": "Your recent Rails post is getting more attention than usual and is now one of the site's top pages.",
"recommendations": "Keep an eye on the post that is breaking out, and consider sharing similar content while the momentum is still fresh.",
"context_signals_count": 1,
"context_signals": [
{
"id": 12,
"occurred_at": "2026-02-12T09:00:00Z",
"title": "Newsletter #42 sent",
"description": "Featured the Rails post that is now breaking out.",
"category": "newsletter",
"metadata": {
"path": "/blog/rails-post"
}
}
]
}
],
"pagination": {
"current_page": 1,
"per_page": 50,
"total_count": 1,
"total_pages": 1
},
"site": {
"id": 456,
"uid": "abc123",
"url": "https://example.com",
"label": "My Blog",
"insights_enabled": true,
"daily_insight_reports_active": true,
"next_insight_job_scheduled_at": "2026-02-15T06:00:00Z"
}
}
Each insight returns:
summary: the short AI overview of what changed most.signals: stored auto-detected signal snapshots for that report, including headline, summary, score, detection time, window, and a small payload excerpt.context_signals_count: how many owner-submitted signals were included when the insight was generated.context_signals: the owner-submitted signals included in that insight.traffic_patterns,best_content, andrecommendations: the fuller AI explanation for the week.
GET /sites/:id/signals Subscription
Returns owner-submitted context signals for a site. Requires insights to be enabled.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
start_date |
No | Start date (YYYY-MM-DD). Defaults to 30 days ago. |
end_date |
No | End date (YYYY-MM-DD). Defaults to today. |
category |
No | Filter by category (newsletter, social, launch, maintenance, content, podcast, other). |
page |
No | Page number. |
per_page |
No | Page size, max 100. |
curl "https://tinylytics.app/api/v1/sites/456/signals?start_date=2026-02-01&end_date=2026-02-14" \
-H "Authorization: Bearer tly-ro-your-api-key"
POST /sites/:id/signals Full Access Subscription
Submit one owner context signal. Use this to tell Tinylytics about newsletters, launches, social posts, maintenance windows, and other events that help explain traffic changes in Insights and traffic spike analysis.
Accepted properties
| Property | Required | Description |
|---|---|---|
signal.occurred_at |
Yes | When the event happened (ISO8601 datetime). |
signal.title |
Yes | Short headline, max 200 characters. |
signal.description |
No | Longer context, max 5000 characters. |
signal.category |
No | One of newsletter, social, launch, maintenance, content, podcast, other. |
signal.metadata |
No | Optional JSON object (for example url, path, campaign, tags). |
signal.external_id |
No | Idempotency key unique per site. Reusing it updates the existing signal. |
curl -X POST "https://tinylytics.app/api/v1/sites/456/signals" \
-H "Authorization: Bearer tly-fa-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"signal": {
"occurred_at": "2026-02-12T09:00:00Z",
"title": "Newsletter #42 sent",
"description": "Featured the Rails post that is now breaking out.",
"category": "newsletter",
"metadata": { "path": "/blog/rails-post" },
"external_id": "newsletter-42"
}
}'
Monitoring Endpoints
GET /sites/:id/uptime Subscription
Returns uptime monitor status, SSL/domain details, and downtime history.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
page |
No | Page number for downtime records. |
per_page |
No | Page size for downtime records, max 100. |
curl "https://tinylytics.app/api/v1/sites/456/uptime" \
-H "Authorization: Bearer tly-ro-your-api-key"
If uptime is not enabled for the site, response is 404.
{
"monitor": {
"id": 101,
"url": "https://example.com",
"enabled": true,
"is_down": false,
"uptime": 99.95,
"last_check_at": "2026-02-14T09:55:00Z",
"next_check_at": "2026-02-14T10:00:00Z",
"last_status_code": 200,
"last_error_message": null,
"status_description": "OK (200)",
"current_check_interval": 300,
"period": "30d",
"ssl": {
"expires_at": "2026-08-01T00:00:00Z",
"valid": true,
"expiring_soon": false,
"expired": false,
"days_until_expiry": 168
},
"domain": {
"tested_at": "2026-02-14T00:00:00Z",
"expires_at": "2027-06-01T00:00:00Z",
"remaining_days": 472,
"source": "whois",
"expired": false,
"expiring_soon": false,
"days_until_expiry": 472
},
"auto_paused": false,
"created_at": "2025-06-01T12:00:00Z",
"updated_at": "2026-02-14T09:55:00Z"
},
"downtimes": [
{
"id": 55,
"error": "Connection timed out",
"started_at": "2026-02-10T03:00:00Z",
"ended_at": "2026-02-10T03:15:00Z",
"duration": 900,
"duration_in_words": "15 minutes",
"partial": false,
"ongoing": false
}
],
"pagination": {
"current_page": 1,
"per_page": 100,
"total_count": 1,
"total_pages": 1
},
"summary": {
"total_downtimes": 3,
"ongoing_downtimes": 0,
"recent_downtimes_30_days": 1
}
}
GET /sites/:id/content Subscription
Returns content monitoring status, issues, ignored issues, and stats.
Accepted properties
| Property | Required | Description |
|---|---|---|
id (URL path) |
Yes | Site numeric ID. |
curl "https://tinylytics.app/api/v1/sites/456/content" \
-H "Authorization: Bearer tly-ro-your-api-key"
{
"site": {
"id": 456,
"uid": "abc123",
"url": "https://example.com",
"label": "My Blog"
},
"monitoring_status": {
"enabled": true,
"root_path": "/blog",
"last_check_at": "2026-02-14T08:00:00Z",
"is_initial_check": false,
"is_rechecking": false,
"has_issues": true,
"emails_enabled": true,
"emails_paused": false,
"emails_paused_until": null
},
"issues": {
"broken_links": [
{
"id": 201,
"url": "https://example.com/old-page",
"status_code": 404,
"error_message": "Not Found",
"issue_type": "broken_link",
"checked_at": "2026-02-14T08:00:00Z",
"ignored": false
}
],
"mixed_content": []
},
"ignored_issues": [],
"ok_links": [],
"stats": {
"total_checked": 48,
"broken_links_count": 1,
"mixed_content_count": 0,
"ignored_count": 0,
"ok_count": 47
}
}
If content monitoring is disabled for the site, response is 403 with:
{
"error": "Content monitoring is not enabled for this site",
"content_monitoring_enabled": false
}
6. Common Flows
Build a dashboard
GET /sitesGET /sites/:id/hits?grouped=true&group_by=pathGET /sites/:id/leaderboard
Add server-side tracking
- Create full-access key
POST /sites/:id/hitsfrom your backendPOST /sites/:id/eventsfor backend interaction trackingPOST /sites/:id/kudoswhen users react- Verify ingestion with
GET /sites/:id/hitsand your site’s Events/Kudos dashboard views
Monitor health in one poll cycle
GET /sites/:id/uptimeGET /sites/:id/content- Alert from
summary/statsfields
7. Errors and Status Codes
| Status | Meaning |
|---|---|
200 |
Success |
201 |
Resource created |
202 |
Accepted but skipped (for ignored hits, events, or kudos) |
400 |
Invalid parameter(s) |
401 |
Missing/invalid/revoked API key |
403 |
Premium endpoint requires subscription, write access required, or feature disabled |
404 |
Resource not found |
422 |
Validation or payload format error |
500 |
Unexpected server error |
Typical error payload:
{
"error": "Invalid API key"
}
8. Rate Limits and Support
Authenticated API requests are rate limited to 1000 requests per hour per API key.
For implementation help: [email protected].