{
  "openapi": "3.1.0",
  "info": {
    "title": "Stack0 Analytics Website API",
    "version": "1.0.0",
    "summary": "Read revenue-attributed analytics and manage goals and funnels for one website.",
    "description": "Stack0 Analytics ties every pageview to the signups and payments it produces. This API exposes one site's aggregate analytics plus goal and funnel management.\n\nAuthentication is a site-scoped key (`s0a_…`) created in the dashboard under Sites → the key icon. A key is bound to a single site, so no call takes a site id. Each key carries an explicit scope list and can expire or be revoked independently of the site's tracking key.\n\nEvery operation is `POST` with a JSON body to `/api/fn/<operationId>`. Responses always carry `ok` and `version`; a rejected call returns HTTP 200 with `ok: false` and an `error` string, so branch on `ok` rather than on status.\n\nAggregates only — visitor-level rows are never returned by this API.",
    "contact": {
      "name": "Stack0 Analytics support",
      "email": "support@stack0.dev",
      "url": "https://userevtrail.com/resources/api"
    },
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://userevtrail.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "API reference and quickstart",
    "url": "https://userevtrail.com/resources/api"
  },
  "tags": [
    {
      "name": "Website API v1",
      "description": "Site-scoped operations authenticated with an `s0a_` key."
    }
  ],
  "paths": {
    "/api/fn/apiV1Overview": {
      "post": {
        "operationId": "apiV1Overview",
        "summary": "Get the site's analytics overview",
        "description": "Return the full aggregate stats payload for the key's site: per-day traffic and revenue for the retained window, top sources, top pages, dimension breakdowns, goal completions, funnel step counts, AI-bot visibility, and revenue-per-visitor economics. Use this to answer questions about which channels produce paying customers.",
        "tags": [
          "Website API v1"
        ],
        "security": [
          {
            "websiteApiKey": [
              "analytics:read"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The site's aggregate analytics.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OverviewResult"
                }
              }
            }
          },
          "400": {
            "description": "The request body was not valid JSON (`INVALID_JSON`) or did not match the declared argument types (`INVALID_ARGS`). The handler never ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransportError"
                }
              }
            }
          },
          "404": {
            "description": "No such function, or the request used a method other than POST (`FN_NOT_FOUND` / `NOT_FOUND`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransportError"
                }
              }
            }
          },
          "default": {
            "description": "The call was understood but rejected — bad key, missing scope, expired key, or invalid arguments. Transport status is still 200; branch on the `ok` field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/fn/apiV1CreateGoal": {
      "post": {
        "operationId": "apiV1CreateGoal",
        "summary": "Create a conversion goal",
        "description": "Register a goal so completions of one custom event are counted and revenue-attributed. Idempotent by event name: calling it again for an existing event returns the existing goal id rather than creating a duplicate.",
        "tags": [
          "Website API v1"
        ],
        "security": [
          {
            "websiteApiKey": [
              "goals:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateGoalRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The goal id, whether created now or pre-existing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResult"
                }
              }
            }
          },
          "400": {
            "description": "The request body was not valid JSON (`INVALID_JSON`) or did not match the declared argument types (`INVALID_ARGS`). The handler never ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransportError"
                }
              }
            }
          },
          "404": {
            "description": "No such function, or the request used a method other than POST (`FN_NOT_FOUND` / `NOT_FOUND`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransportError"
                }
              }
            }
          },
          "default": {
            "description": "The call was understood but rejected — bad key, missing scope, expired key, or invalid arguments. Transport status is still 200; branch on the `ok` field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/fn/apiV1CreateFunnel": {
      "post": {
        "operationId": "apiV1CreateFunnel",
        "summary": "Create a conversion funnel",
        "description": "Define an ordered path of 2–5 distinct event names (for example pageview → signup → payment) so drop-off between steps is measured. A visitor counts at a step once they have completed every earlier step in order within the window.",
        "tags": [
          "Website API v1"
        ],
        "security": [
          {
            "websiteApiKey": [
              "funnels:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateFunnelRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new funnel id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResult"
                }
              }
            }
          },
          "400": {
            "description": "The request body was not valid JSON (`INVALID_JSON`) or did not match the declared argument types (`INVALID_ARGS`). The handler never ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransportError"
                }
              }
            }
          },
          "404": {
            "description": "No such function, or the request used a method other than POST (`FN_NOT_FOUND` / `NOT_FOUND`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransportError"
                }
              }
            }
          },
          "default": {
            "description": "The call was understood but rejected — bad key, missing scope, expired key, or invalid arguments. Transport status is still 200; branch on the `ok` field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "websiteApiKey": {
        "type": "apiKey",
        "in": "query",
        "name": "key",
        "description": "A site-scoped Stack0 Analytics API key (`s0a_` + 48 hex characters), passed as the `key` property of the JSON request body. Create one in the dashboard under Sites → the key icon; the raw value is shown once. Keys carry explicit scopes, may expire, and can be revoked without rotating the site's tracking key.\n\nScopes:\n- `analytics:read` — Read aggregate analytics for the key's site: daily traffic, sources, pages, goals, funnels, and revenue totals. Never exposes visitor-level rows.\n- `goals:write` — Create conversion goals on the key's site.\n- `funnels:write` — Create ordered conversion funnels on the key's site."
      }
    },
    "schemas": {
      "ApiKey": {
        "type": "string",
        "pattern": "^(?:s0a|rtk)_[0-9a-f]{48}$",
        "description": "Site-scoped API key. New keys use s0a_. Legacy rtk_ keys remain valid during migration. Never log or share this value.",
        "examples": [
          "s0a_000000000000000000000000000000000000000000000000"
        ]
      },
      "EventName": {
        "type": "string",
        "minLength": 1,
        "maxLength": 60,
        "pattern": "^[A-Za-z0-9_:-]{1,60}$",
        "description": "A custom event name as sent by the tracking snippet, e.g. `signup` or `payment`."
      },
      "KeyRequest": {
        "type": "object",
        "required": [
          "key"
        ],
        "additionalProperties": false,
        "properties": {
          "key": {
            "$ref": "#/components/schemas/ApiKey"
          }
        }
      },
      "CreateGoalRequest": {
        "type": "object",
        "required": [
          "key",
          "name",
          "eventName"
        ],
        "additionalProperties": false,
        "properties": {
          "key": {
            "$ref": "#/components/schemas/ApiKey"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "Human-readable goal label shown in the dashboard.",
            "examples": [
              "Signups"
            ]
          },
          "eventName": {
            "$ref": "#/components/schemas/EventName"
          }
        }
      },
      "CreateFunnelRequest": {
        "type": "object",
        "required": [
          "key",
          "name",
          "steps"
        ],
        "additionalProperties": false,
        "properties": {
          "key": {
            "$ref": "#/components/schemas/ApiKey"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "Human-readable funnel label.",
            "examples": [
              "Signup to paid"
            ]
          },
          "steps": {
            "type": "array",
            "minItems": 2,
            "maxItems": 5,
            "uniqueItems": true,
            "description": "Ordered, distinct event names describing the conversion path.",
            "items": {
              "$ref": "#/components/schemas/EventName"
            },
            "examples": [
              [
                "pageview",
                "signup",
                "payment"
              ]
            ]
          },
          "windowHours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "default": 24,
            "description": "How long a visitor has to complete every step and still be counted. Clamped to 1–720 hours."
          }
        }
      },
      "TransportError": {
        "type": "object",
        "description": "A runtime-level error envelope, returned before the handler runs.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "INVALID_JSON",
                  "INVALID_ARGS",
                  "FN_NOT_FOUND",
                  "NOT_FOUND"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "ErrorResult": {
        "type": "object",
        "required": [
          "ok",
          "version",
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false
          },
          "version": {
            "type": "string",
            "const": "v1"
          },
          "error": {
            "type": "string",
            "description": "Human-readable reason the call was rejected."
          }
        }
      },
      "CreatedResult": {
        "type": "object",
        "required": [
          "ok",
          "version"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "version": {
            "type": "string",
            "const": "v1"
          },
          "id": {
            "type": "string",
            "description": "Identifier of the created row."
          }
        }
      },
      "DailyStat": {
        "type": "object",
        "description": "One UTC day of traffic and revenue for the site.",
        "properties": {
          "id": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "format": "date",
            "examples": [
              "2026-08-20"
            ]
          },
          "pageviews": {
            "type": "integer",
            "minimum": 0
          },
          "visitors": {
            "type": "integer",
            "minimum": 0
          },
          "revenueCents": {
            "type": "integer",
            "minimum": 0,
            "description": "Attributed revenue in minor units (US cents)."
          },
          "sessions": {
            "type": "integer",
            "minimum": 0
          },
          "bouncePct": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "avgDurationSecs": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "SourceStat": {
        "type": "object",
        "description": "A traffic source (utm_source, referrer host, or `direct`) with the revenue it produced.",
        "properties": {
          "id": {
            "type": "string"
          },
          "rank": {
            "type": "integer",
            "minimum": 0
          },
          "source": {
            "type": "string"
          },
          "visitors": {
            "type": "integer",
            "minimum": 0
          },
          "revenueCents": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "PathStat": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "rank": {
            "type": "integer",
            "minimum": 0
          },
          "path": {
            "type": "string"
          },
          "pageviews": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "DimStat": {
        "type": "object",
        "description": "A breakdown row. `dim` is one of client, country, device, browser, os, utm_campaign, ai_page. Client labels are web, mobile_app, desktop_app, cli, server, or unknown.",
        "properties": {
          "id": {
            "type": "string"
          },
          "dim": {
            "type": "string"
          },
          "rank": {
            "type": "integer",
            "minimum": 0
          },
          "label": {
            "type": "string"
          },
          "visitors": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "GoalStat": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "eventName": {
            "$ref": "#/components/schemas/EventName"
          },
          "visitors": {
            "type": "integer",
            "minimum": 0
          },
          "completions": {
            "type": "integer",
            "minimum": 0
          },
          "revenueCents": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "Funnel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "steps": {
            "type": "string",
            "description": "JSON-encoded array of the ordered event names."
          },
          "windowHours": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "FunnelStat": {
        "type": "object",
        "description": "Visitors who reached step `rank` having completed every earlier step in order.",
        "properties": {
          "id": {
            "type": "string"
          },
          "funnelId": {
            "type": "string"
          },
          "rank": {
            "type": "integer",
            "minimum": 1
          },
          "label": {
            "type": "string"
          },
          "visitors": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "AiBotStat": {
        "type": "object",
        "description": "AI crawler and agent traffic. `kind` is `ai_agent` (a person asked an assistant about the site now) or `ai_crawler` (training/index crawl).",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "ai_agent",
              "ai_crawler"
            ]
          },
          "botName": {
            "type": "string"
          },
          "requests": {
            "type": "integer",
            "minimum": 0
          },
          "uniquePaths": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "RevenueStat": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "revenuePerVisitorCents": {
            "type": "integer",
            "minimum": 0
          },
          "avgLtvCents": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "OverviewData": {
        "type": "object",
        "required": [
          "found",
          "site"
        ],
        "properties": {
          "found": {
            "type": "boolean",
            "const": true
          },
          "site": {
            "type": "object",
            "required": [
              "name",
              "domain"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "domain": {
                "type": "string",
                "examples": [
                  "example.com"
                ]
              }
            }
          },
          "daily": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DailyStat"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SourceStat"
            }
          },
          "paths": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PathStat"
            }
          },
          "dims": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DimStat"
            }
          },
          "goals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GoalStat"
            }
          },
          "funnels": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Funnel"
            }
          },
          "funnelStats": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FunnelStat"
            }
          },
          "aiBots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiBotStat"
            }
          },
          "revenue": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/RevenueStat"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "OverviewResult": {
        "type": "object",
        "required": [
          "ok",
          "version"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "version": {
            "type": "string",
            "const": "v1"
          },
          "data": {
            "$ref": "#/components/schemas/OverviewData"
          }
        }
      }
    }
  }
}