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: 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/agent_visits
    • POST /sites/:id/events
    • POST /sites/:id/events/batch
    • POST /sites/:id/kudos
    • DELETE /sites/:id/kudos/:kudo_uid
    • POST /sites/:id/signals
    • POST /groups
    • PATCH /groups/:id
    • PUT /groups/:id
    • DELETE /groups/:id

Access rules

  • Any account with a valid API key can use core API endpoints.
  • API keys stay personal. Omit account_id to 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 with GET /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/insights
    • GET /sites/:id/signals
    • POST /sites/:id/signals
    • GET /sites/:id/uptime
    • GET /sites/:id/content
  • Group endpoints require an Ultra subscription:
    • GET /groups
    • GET /groups/:id
    • POST /groups
    • PATCH /groups/:id
    • PUT /groups/:id
    • DELETE /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_id selects 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 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 (signals, 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, 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
email 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
  • 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" }
  ]
}

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
  • event must use category.action format
  • 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 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 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/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
  • 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.",
      "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, and recommendations: 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

  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/events for backend interaction tracking
  4. POST /sites/:id/kudos when users react
  5. Verify ingestion with GET /sites/:id/hits and your site’s Events/Kudos dashboard views

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

Tinylytics — privacy-first analytics & monitoring for small sites and indie projects.
Incredibly OK™ software by Vincent Ritter.
Terms Privacy Contact
© 2026 Tinylytics