{
  "openapi": "3.1.0",
  "info": {
    "title": "Pure Analytics Public API",
    "version": "1.0.0",
    "summary": "Read aggregated, privacy-friendly web analytics for your websites.",
    "description": "Read-only REST API of Pure Analytics. It returns the same numbers as the dashboard: every figure is computed by the same database functions.\n\nAuthentication: an API key created in the dashboard (Settings > API keys), sent in the `X-API-Key` header. A key covers one or more websites.\n\nRate limit: 60 requests per 60 seconds per API key (per client IP without a key). Every response carries the IETF `RateLimit-Policy` and `RateLimit` headers; a `429` adds `Retry-After`.\n\nErrors: `application/problem+json` (RFC 9457) with `type`, `title`, `status`, `detail`, `code` and `resolution`.\n\nThe same data is available to AI agents through the MCP server at https://pure-analytics.com/mcp.",
    "contact": {
      "name": "Simone Ruggiero",
      "email": "simone.ruggiero97@gmail.com",
      "url": "https://pure-analytics.com/contact"
    },
    "termsOfService": "https://pure-analytics.com/privacy"
  },
  "externalDocs": {
    "description": "Developer documentation",
    "url": "https://pure-analytics.com/docs"
  },
  "servers": [
    {
      "url": "https://pure-analytics.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Analytics",
      "description": "Aggregated traffic, sources, pages and revenue"
    },
    {
      "name": "Status",
      "description": "Service health"
    }
  ],
  "paths": {
    "/api/v1/analytics": {
      "get": {
        "operationId": "getAnalytics",
        "tags": [
          "Analytics"
        ],
        "summary": "Get aggregated analytics",
        "description": "Returns summary metrics for the requested period and, on request, a breakdown by one dimension, a timeseries and revenue per currency. Without `website_id` the figures cover every website on the API key. Filters restrict every figure in the response and combine with `groupBy`: `groupBy=entry_page&referrer=google.com` returns the landing pages of the traffic coming from Google. Attribution is per session, first touch: pair `referrer`, `channel`, `ai_assistant` and the `utm_*` filters with `entry_page`.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "website_id",
            "in": "query",
            "required": false,
            "description": "Website UUID or tracking ID. Default: every website on the key.",
            "schema": {
              "type": "string",
              "maxLength": 128
            }
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "ISO 8601 start of the period. Default: 30 days before `end`.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "ISO 8601 end of the period. Default: now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "metrics",
            "in": "query",
            "required": false,
            "description": "Comma-separated metrics. Summary: visitors, sessions, pageviews, unique_pageviews, bounce_rate, engagement_rate, avg_session_seconds, views_per_session. Arrays: timeseries, revenue. Aliases of avg_session_seconds: avg_session_duration, avg_session_time. Unknown names are ignored.",
            "schema": {
              "type": "string",
              "default": "visitors,pageviews,bounce_rate",
              "examples": [
                "visitors,sessions,bounce_rate,timeseries"
              ]
            }
          },
          {
            "name": "groupBy",
            "in": "query",
            "required": false,
            "description": "Adds a `breakdown` array grouped by this dimension.",
            "schema": {
              "$ref": "#/components/schemas/GroupBy"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows in the breakdown.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000,
              "default": 100
            }
          },
          {
            "name": "granularity",
            "in": "query",
            "required": false,
            "description": "Bucket size of the timeseries, in the website's timezone (UTC when several websites are queried).",
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week",
                "month"
              ],
              "default": "day"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Filter: pageviews of this path.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "entry_page",
            "in": "query",
            "required": false,
            "description": "Filter: sessions that landed on this path.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "exit_page",
            "in": "query",
            "required": false,
            "description": "Filter: sessions that left from this path.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "browser",
            "in": "query",
            "required": false,
            "description": "Filter: browser name.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Filter: country.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "device",
            "in": "query",
            "required": false,
            "description": "Filter: device type (desktop, mobile, tablet).",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "os",
            "in": "query",
            "required": false,
            "description": "Filter: operating system.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "Filter: sessions from this channel (for example `organic_search`, `social`, `ai`, `direct`).",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "referrer",
            "in": "query",
            "required": false,
            "description": "Filter: sessions from this referrer label (a hostname, or `Direct`).",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "ai_assistant",
            "in": "query",
            "required": false,
            "description": "Filter: sessions sent by this AI assistant (for example `ChatGPT`, `Perplexity`, `Claude`).",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "utm_source",
            "in": "query",
            "required": false,
            "description": "Filter: sessions with this utm_source.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "utm_medium",
            "in": "query",
            "required": false,
            "description": "Filter: sessions with this utm_medium.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "utm_campaign",
            "in": "query",
            "required": false,
            "description": "Filter: sessions with this utm_campaign.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "button",
            "in": "query",
            "required": false,
            "description": "Filter: sessions that clicked this button.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregated analytics for the period.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalyticsReport"
                },
                "example": {
                  "website_ids": [
                    "8f3ca1d2-6b1e-4c1a-9b7e-2f0d5c9a1e44"
                  ],
                  "period": {
                    "start": "2026-08-02T00:00:00.000Z",
                    "end": "2026-09-01T00:00:00.000Z",
                    "timezone": "Europe/Rome"
                  },
                  "visitors": 4821,
                  "pageviews": 11204,
                  "bounce_rate": 42.7,
                  "breakdown": [
                    {
                      "country": "IT",
                      "visitors": 1902,
                      "sessions": 2210,
                      "pageviews": 5104
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Status"
        ],
        "summary": "Check service health",
        "description": "Returns `ok` when the API Worker is serving requests. No authentication.",
        "security": [],
        "responses": {
          "200": {
            "description": "The API is up.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "example": {
                  "status": "ok",
                  "runtime": "cloudflare-workers"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key created in the dashboard at https://pure-analytics.com/dashboard (Settings > API keys). The key is scoped to one or more of your websites; it can be deactivated or given an expiry date."
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "Quota policy (IETF draft-ietf-httpapi-ratelimit-headers): 60 requests per 60-second window.",
        "schema": {
          "type": "string",
          "examples": [
            "\"api\";q=60;w=60"
          ]
        }
      },
      "RateLimit": {
        "description": "Remaining requests (`r`) and seconds until the window resets (`t`), as seen by the edge node that served the request.",
        "schema": {
          "type": "string",
          "examples": [
            "\"api\";r=59;t=60"
          ]
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Unsupported parameter value, or no website on the key (`invalid_group_by`, `invalid_granularity`, `no_websites`).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The API key is missing, unknown, deactivated or expired (`missing_api_key`, `invalid_api_key`, `inactive_api_key`, `expired_api_key`).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://pure-analytics.com/docs#errors",
              "title": "Unauthorized",
              "status": 401,
              "detail": "Missing X-API-Key header",
              "code": "missing_api_key",
              "resolution": "Send a valid API key in the X-API-Key header. Create one in the dashboard under Settings > API keys.",
              "instance": "/api/v1/analytics",
              "error": "Missing X-API-Key header"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The requested website is not associated with the key (`website_forbidden`).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "More than 60 requests in 60 seconds (`rate_limited`).",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "InternalError": {
        "description": "The query failed; retry later (`query_failed`, `internal_error`).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "schemas": {
      "GroupBy": {
        "type": "string",
        "description": "Breakdown dimension. `source`, `medium` and `campaign` are the UTM tags.",
        "enum": [
          "page",
          "entry_page",
          "exit_page",
          "country",
          "browser",
          "device",
          "os",
          "referrer",
          "channel",
          "ai_assistant",
          "source",
          "medium",
          "campaign"
        ]
      },
      "Period": {
        "type": "object",
        "required": [
          "start",
          "end",
          "timezone"
        ],
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "timezone": {
            "type": "string",
            "description": "IANA timezone of the buckets: the website's own, or UTC across several websites.",
            "examples": [
              "Europe/Rome"
            ]
          }
        }
      },
      "BreakdownRow": {
        "type": "object",
        "description": "One row of the breakdown. The dimension value is under a key named like the `groupBy` value (for example `country`).",
        "required": [
          "visitors",
          "sessions",
          "pageviews"
        ],
        "properties": {
          "visitors": {
            "type": "integer",
            "minimum": 0
          },
          "sessions": {
            "type": "integer",
            "minimum": 0
          },
          "pageviews": {
            "type": "integer",
            "minimum": 0
          }
        },
        "additionalProperties": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "TimeseriesPoint": {
        "type": "object",
        "required": [
          "date",
          "visitors",
          "sessions",
          "pageviews"
        ],
        "properties": {
          "date": {
            "type": "string",
            "description": "Start of the bucket."
          },
          "visitors": {
            "type": "integer",
            "minimum": 0
          },
          "sessions": {
            "type": "integer",
            "minimum": 0
          },
          "pageviews": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "RevenueRow": {
        "type": "object",
        "description": "Revenue in one currency. Amounts in different currencies are never summed.",
        "required": [
          "currency",
          "gross",
          "refunded",
          "net",
          "transactions",
          "refunds"
        ],
        "properties": {
          "currency": {
            "type": "string",
            "examples": [
              "eur"
            ]
          },
          "gross": {
            "type": "number"
          },
          "refunded": {
            "type": "number"
          },
          "net": {
            "type": "number"
          },
          "transactions": {
            "type": "integer",
            "minimum": 0
          },
          "refunds": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "AnalyticsReport": {
        "type": "object",
        "required": [
          "website_ids",
          "period"
        ],
        "properties": {
          "website_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "period": {
            "$ref": "#/components/schemas/Period"
          },
          "visitors": {
            "type": "integer",
            "minimum": 0
          },
          "sessions": {
            "type": "integer",
            "minimum": 0
          },
          "pageviews": {
            "type": "integer",
            "minimum": 0
          },
          "unique_pageviews": {
            "type": "integer",
            "minimum": 0
          },
          "bounce_rate": {
            "type": "number",
            "description": "Percentage of sessions with one pageview and under 10 seconds."
          },
          "engagement_rate": {
            "type": "number",
            "description": "Percentage of sessions that did not bounce."
          },
          "avg_session_seconds": {
            "type": "number",
            "minimum": 0
          },
          "views_per_session": {
            "type": "number",
            "minimum": 0
          },
          "breakdown": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BreakdownRow"
            }
          },
          "timeseries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TimeseriesPoint"
            }
          },
          "revenue": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RevenueRow"
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "status",
          "runtime"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "runtime": {
            "type": "string",
            "examples": [
              "cloudflare-workers"
            ]
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. `error` repeats `detail` for older clients.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code.",
            "examples": [
              "missing_api_key"
            ]
          },
          "resolution": {
            "type": "string",
            "description": "What to do to fix the request."
          },
          "instance": {
            "type": "string"
          },
          "error": {
            "type": "string"
          }
        }
      }
    }
  }
}