Tinylytics API
Use the Tinylytics API to read analytics, send hits and kudos from your backend, and automate reporting.
1. Quick Start
Base URL
https://tinylytics.app/api/v1
Header format
Authorization: Bearer tly-ro-your-api-key
Accept: application/json
Use tly-fa-... (Full Access) for write endpoints.
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"
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/kudosDELETE /sites/:id/kudos/:kudo_uid
Access rules
- Any account with a valid API key can use core API endpoints.
- Premium endpoints require an active subscription:
GET /sites/:id/insightsGET /sites/:id/uptimeGET /sites/:id/content
- Revoked or invalid keys return
401. - Write endpoint with read-only key returns
403.
3. Request Conventions
- Dates use
YYYY-MM-DD. - 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(uptime)
- Hits filtering:
countryexact matchpathexact matchreferrerpartial match
- Kudos filtering:
pathexact match
- Grouped hits:
grouped=truegroup_byone ofpath,country,referrer,browser_name,platform_name
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 | /sites |
List accessible sites |
| GET | /sites/:id |
Get one site |
| 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/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/uptime |
Uptime + SSL/domain status Subscription |
| GET | /sites/:id/content |
Content monitoring status and issues Subscription |
5. Endpoint Reference
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"
{
"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"
}
}
GET /sites
Lists your sites with lifetime counters.
Accepted properties
| Property | Required | Description |
|---|---|---|
| None | Yes | This endpoint does not accept query or body properties. |
curl "https://tinylytics.app/api/v1/sites" \
-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"
}
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. |
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) 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" }
]
}
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."
}
],
"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 signal snapshots for that report, including headline, summary, score, detection time, window, and a small payload excerpt.traffic_patterns,best_content, andrecommendations: the fuller AI explanation for the week.
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": "Up",
"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_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/kudoswhen users react- Verify ingestion with
GET /sites/:id/hitsandGET /sites/:id/kudos
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 or ignored 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].