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: Bearer token.
  • Key location: Account Settings → API Access.
  • Read-only keys (tly-ro-...) can call all GET endpoints.
  • Full-access keys (tly-fa-...) are required for:
    • POST /sites/:id/hits
    • POST /sites/:id/hits/batch
    • POST /sites/:id/kudos
    • DELETE /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/insights
    • GET /sites/:id/uptime
    • GET /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 730 days.
  • Date boundaries for analytics endpoints default to UTC.
  • Optional timezone mode:
    • time_zone=utc (default) uses UTC day boundaries.
    • time_zone=user uses your account timezone day boundaries.
  • Pagination:
    • page default varies by endpoint
    • per_page max 1000 (hits, kudos, leaderboard), 50 (user_journeys, insights), 100 (uptime)
  • Hits filtering:
    • country exact match
    • path exact match
    • referrer partial match
  • Kudos filtering:
    • path exact match
  • Grouped hits:
    • grouped=true
    • group_by one of path, 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
  • country must be 2-letter uppercase when provided (example: US, PL, XX)
  • Country resolution order: provided country → local lookup from ip_address → IPinfo country_code API fallback → XX
  • path is 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 from ip_address → IPinfo country_code API 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
  • path is 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, and recommendations: 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

  1. GET /sites
  2. GET /sites/:id/hits?grouped=true&group_by=path
  3. GET /sites/:id/leaderboard

Add server-side tracking

  1. Create full-access key
  2. POST /sites/:id/hits from your backend
  3. POST /sites/:id/kudos when users react
  4. Verify ingestion with GET /sites/:id/hits and GET /sites/:id/kudos

Monitor health in one poll cycle

  1. GET /sites/:id/uptime
  2. GET /sites/:id/content
  3. Alert from summary/stats fields

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].