{"openapi":"3.1.0","info":{"title":"twelvebit API","version":"1.0.0","summary":"Web and revenue analytics for people and their coding agents.","description":"Read analytics, push the tracking plan (analytics.yaml), and send server-side events.\n\n**Authentication.** Send `Authorization: Bearer <key>`. A site key (`tbk_…`) reads and writes one site, and ignores `?site=`. An account token (`tbt_…`) reaches every site of its user; name the site with `?site=` (site id, public id `tb_…`, or domain). A Better Auth session (cookie, or the session token as a Bearer) works like an account token. Create keys in the site settings or on /account, or run `npx twelvebit init`.\n\n**Responses.** Success: `{ \"data\": …, \"meta\": { … } }`. Errors: `{ \"error\": { \"code\": \"…\", \"message\": \"…\" } }`; the message says what to do next.\n\n**Rate limit.** 120 requests per minute per key (1,200 for POST /api/v1/bots). Over the limit: HTTP 429 with `Retry-After`.\n\n**Time ranges.** `period` = today, yesterday, 24h, 7d, 30d, 90d, 6m, 12m, wtd, mtd, ytd, all, or custom with `from` and `to` (YYYY-MM-DD, in the site's time zone, both inclusive). Default: 30d. `meta.range` echoes the exact range.\n\n**Filters.** `f_<dim>=value` narrows to visits that match, for example `f_channel=Organic Search` or `f_country=DE`. Filters stack (AND). Use `(none)` for empty values and `Direct` for f_referrer.\n\n**Revenue attribution.** Revenue in breakdowns goes to the visit that gets the credit: last touch: the visit before the payment. Amounts are in the site currency, in major units."},"servers":[{"url":"https://twelvebit.app"}],"security":[{"bearer":[]}],"tags":[{"name":"Sites","description":"Sites this key can read."},{"name":"Analytics","description":"Traffic, revenue, goals, funnels and people."},{"name":"Tracking plan","description":"analytics.yaml and the install check (drift)."},{"name":"Server-side","description":"Events, identify, payments and crawler hits from your server."},{"name":"Meta","description":"This description."}],"paths":{"/api/v1/sites":{"get":{"tags":["Sites"],"operationId":"listSites","summary":"List sites","description":"Every site the key can read. A site key returns only its own site.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Site"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sites/{siteId}":{"get":{"tags":["Sites"],"operationId":"getSite","summary":"Get a site","description":"One site by id, public id or domain, with its script tag.","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"},"description":"Site id, public id (tb_…) or domain."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/Site"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/overview":{"get":{"tags":["Analytics"],"operationId":"getOverview","summary":"KPIs for a range","description":"Visitors, visits, page views, bounce rate, time on site, revenue, customers and conversion, for the range and the previous range of the same length, plus the number of people online now.","parameters":[{"$ref":"#/components/parameters/site"},{"$ref":"#/components/parameters/period"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/f_channel"},{"$ref":"#/components/parameters/f_referrer"},{"$ref":"#/components/parameters/f_campaign"},{"$ref":"#/components/parameters/f_keyword"},{"$ref":"#/components/parameters/f_source"},{"$ref":"#/components/parameters/f_hostname"},{"$ref":"#/components/parameters/f_entry"},{"$ref":"#/components/parameters/f_exit"},{"$ref":"#/components/parameters/f_country"},{"$ref":"#/components/parameters/f_region"},{"$ref":"#/components/parameters/f_city"},{"$ref":"#/components/parameters/f_browser"},{"$ref":"#/components/parameters/f_os"},{"$ref":"#/components/parameters/f_device"},{"$ref":"#/components/parameters/f_page"},{"$ref":"#/components/parameters/f_goal"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"current":{"$ref":"#/components/schemas/Kpis"},"previous":{"$ref":"#/components/schemas/Kpis"},"change":{"type":"object","additionalProperties":{"type":["number","null"]},"description":"Change as a fraction (0.12 = +12%). Null when the previous value is 0."},"online_now":{"type":"integer"},"currency":{"type":"string"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/timeseries":{"get":{"tags":["Analytics"],"operationId":"getTimeseries","summary":"Visitors and revenue over time","description":"One point per hour, day, week or month in the site's time zone.","parameters":[{"$ref":"#/components/parameters/site"},{"$ref":"#/components/parameters/period"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/f_channel"},{"$ref":"#/components/parameters/f_referrer"},{"$ref":"#/components/parameters/f_campaign"},{"$ref":"#/components/parameters/f_keyword"},{"$ref":"#/components/parameters/f_source"},{"$ref":"#/components/parameters/f_hostname"},{"$ref":"#/components/parameters/f_entry"},{"$ref":"#/components/parameters/f_exit"},{"$ref":"#/components/parameters/f_country"},{"$ref":"#/components/parameters/f_region"},{"$ref":"#/components/parameters/f_city"},{"$ref":"#/components/parameters/f_browser"},{"$ref":"#/components/parameters/f_os"},{"$ref":"#/components/parameters/f_device"},{"$ref":"#/components/parameters/f_page"},{"$ref":"#/components/parameters/f_goal"},{"$ref":"#/components/parameters/g"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"granularity":{"type":"string","enum":["hour","day","week","month"]},"points":{"type":"array","items":{"type":"object","properties":{"t":{"type":"string","description":"Bucket start, local time (YYYY-MM-DDTHH:MM)"},"visitors":{"type":"integer"},"pageviews":{"type":"integer"},"revenue":{"type":"number"}}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/breakdown":{"get":{"tags":["Analytics"],"operationId":"getBreakdown","summary":"Top values of one dimension","description":"Visitors and attributed revenue (last touch: the visit before the payment) per value, sorted by visitors. dim=goal adds completions; dim=referrer adds the channel; dim=region and dim=city add the country.","parameters":[{"$ref":"#/components/parameters/site"},{"$ref":"#/components/parameters/period"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/f_channel"},{"$ref":"#/components/parameters/f_referrer"},{"$ref":"#/components/parameters/f_campaign"},{"$ref":"#/components/parameters/f_keyword"},{"$ref":"#/components/parameters/f_source"},{"$ref":"#/components/parameters/f_hostname"},{"$ref":"#/components/parameters/f_entry"},{"$ref":"#/components/parameters/f_exit"},{"$ref":"#/components/parameters/f_country"},{"$ref":"#/components/parameters/f_region"},{"$ref":"#/components/parameters/f_city"},{"$ref":"#/components/parameters/f_browser"},{"$ref":"#/components/parameters/f_os"},{"$ref":"#/components/parameters/f_device"},{"$ref":"#/components/parameters/f_page"},{"$ref":"#/components/parameters/f_goal"},{"$ref":"#/components/parameters/dim"},{"$ref":"#/components/parameters/limit"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"dim":{"type":"string"},"label":{"type":"string"},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"visitors":{"type":"integer"},"revenue":{"type":"number"}},"additionalProperties":true}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime":{"get":{"tags":["Analytics"],"operationId":"getRealtime","summary":"Who is on the site now","description":"People active in the last 5 minutes (with page, source, place and goals), visitors per minute for the last 30 minutes (oldest first), and the last 30 minutes of events and payments.","parameters":[{"$ref":"#/components/parameters/site"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"online_now":{"type":"integer"},"visitors_per_minute":{"type":"array","items":{"type":"integer"}},"visitors":{"type":"array","items":{"type":"object"}},"recent_events":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"recent_payments":{"type":"array","items":{"type":"object"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/goals":{"get":{"tags":["Analytics"],"operationId":"listGoals","summary":"Goals and their completions","description":"Visitors, completions and revenue of those visitors per goal. `declared` says whether the goal is in the tracking plan; `undeclared` and `not_seen` show drift.","parameters":[{"$ref":"#/components/parameters/site"},{"$ref":"#/components/parameters/period"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/f_channel"},{"$ref":"#/components/parameters/f_referrer"},{"$ref":"#/components/parameters/f_campaign"},{"$ref":"#/components/parameters/f_keyword"},{"$ref":"#/components/parameters/f_source"},{"$ref":"#/components/parameters/f_hostname"},{"$ref":"#/components/parameters/f_entry"},{"$ref":"#/components/parameters/f_exit"},{"$ref":"#/components/parameters/f_country"},{"$ref":"#/components/parameters/f_region"},{"$ref":"#/components/parameters/f_city"},{"$ref":"#/components/parameters/f_browser"},{"$ref":"#/components/parameters/f_os"},{"$ref":"#/components/parameters/f_device"},{"$ref":"#/components/parameters/f_page"},{"$ref":"#/components/parameters/f_goal"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"goals":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"visitors":{"type":"integer"},"completions":{"type":"integer"},"revenue":{"type":"number"},"declared":{"type":"boolean"}}}},"undeclared":{"type":"array","items":{"type":"string"}},"not_seen":{"type":"array","items":{"type":"string"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/funnels":{"get":{"tags":["Analytics"],"operationId":"getFunnels","summary":"Compute a funnel","description":"With `steps`, computes that funnel: each step counts visitors who reached it after the step before. Without `steps`, computes every funnel saved in the dashboard and every funnel in the tracking plan (`name` picks one).","parameters":[{"$ref":"#/components/parameters/site"},{"$ref":"#/components/parameters/period"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/f_channel"},{"$ref":"#/components/parameters/f_referrer"},{"$ref":"#/components/parameters/f_campaign"},{"$ref":"#/components/parameters/f_keyword"},{"$ref":"#/components/parameters/f_source"},{"$ref":"#/components/parameters/f_hostname"},{"$ref":"#/components/parameters/f_entry"},{"$ref":"#/components/parameters/f_exit"},{"$ref":"#/components/parameters/f_country"},{"$ref":"#/components/parameters/f_region"},{"$ref":"#/components/parameters/f_city"},{"$ref":"#/components/parameters/f_browser"},{"$ref":"#/components/parameters/f_os"},{"$ref":"#/components/parameters/f_device"},{"$ref":"#/components/parameters/f_page"},{"$ref":"#/components/parameters/f_goal"},{"name":"steps","in":"query","schema":{"type":"string"},"example":"page:/pricing,goal:signup,goal:purchase","description":"2 to 8 comma-separated steps: `page:/path` (use * as a wildcard) or `goal:name` (a bare name is a goal)."},{"name":"name","in":"query","schema":{"type":"string"},"description":"Name of a saved or planned funnel."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"oneOf":[{"$ref":"#/components/schemas/Funnel"},{"type":"array","items":{"$ref":"#/components/schemas/Funnel"}}]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/people":{"get":{"tags":["Analytics"],"operationId":"listPeople","summary":"List visitors","description":"Visitors seen in the range, newest first or by revenue. Anonymous visitors get a stable friendly name like amber-fox-4821.","parameters":[{"$ref":"#/components/parameters/site"},{"$ref":"#/components/parameters/period"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"name":"filter","in":"query","schema":{"type":"string","enum":["all","customers","identified"],"default":"all"}},{"name":"q","in":"query","schema":{"type":"string"},"description":"Search email or name (contains), or an exact visitor id or user id."},{"name":"sort","in":"query","schema":{"type":"string","enum":["recent","revenue"],"default":"recent"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/offset"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Person"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/people/{visitorId}":{"get":{"tags":["Analytics"],"operationId":"getJourney","summary":"One visitor's journey","description":"Profile, attribution (first touch and the credited visit: last touch: the visit before the payment), every visit with its events, and payments.","parameters":[{"name":"visitorId","in":"path","required":true,"schema":{"type":"string"},"description":"Visitor id (_tb_vid) or the user_id from identify."},{"$ref":"#/components/parameters/site"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"person":{"$ref":"#/components/schemas/Person"},"attribution":{"type":"object"},"visits":{"type":"array","items":{"type":"object"}},"payments":{"type":"array","items":{"type":"object"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/tracking-plan":{"get":{"tags":["Tracking plan"],"operationId":"getTrackingPlan","summary":"Get the tracking plan","description":"The stored plan as JSON and YAML. Send `Accept: text/yaml` (or ?format=yaml) to get only the YAML text. The YAML is rebuilt from the stored plan, so comments are not kept.","parameters":[{"$ref":"#/components/parameters/site"},{"name":"format","in":"query","schema":{"type":"string","enum":["json","yaml"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"plan":{"oneOf":[{"$ref":"#/components/schemas/TrackingPlan"},{"type":"null"}]},"yaml":{"type":["string","null"]},"updated_at":{"type":["string","null"],"format":"date-time"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}},"text/yaml":{"schema":{"type":"string"}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Tracking plan"],"operationId":"putTrackingPlan","summary":"Replace the tracking plan","description":"Send analytics.yaml as the body (`Content-Type: text/yaml`), or the same plan as JSON. The schema is strict: unknown keys are errors. The response lists added, removed and changed events. `?dry_run=1` validates without saving.","parameters":[{"$ref":"#/components/parameters/site"},{"name":"dry_run","in":"query","schema":{"type":"boolean"}}],"requestBody":{"required":true,"content":{"text/yaml":{"schema":{"type":"string"},"example":"version: 1\nproject: example.com\nevents:\n  signup:\n    description: A visitor creates an account\n    props:\n      plan: { type: string, enum: [free, pro] }\n"},"application/json":{"schema":{"$ref":"#/components/schemas/TrackingPlan"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"saved":{"type":"boolean"},"summary":{"type":"string","example":"Plan saved: added signup, purchase."},"events":{"type":"integer"},"diff":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"changed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"integer"},"funnelsChanged":{"type":"boolean"}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid tracking plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/install-check":{"get":{"tags":["Tracking plan"],"operationId":"getInstallCheck","summary":"Install check and drift","description":"What the site has sent: first and last event, the last 10 events, counts by type in the last 24 hours, host names seen, and for each event in the tracking plan whether it has been seen in the last 30 days. `undeclared` lists goals that were sent but are not in the plan. `next_steps` says what to fix.","parameters":[{"$ref":"#/components/parameters/site"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"installed":{"type":"boolean"},"first_event_at":{"type":["string","null"],"format":"date-time"},"last_event_at":{"type":["string","null"],"format":"date-time"},"recent":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"counts_24h":{"type":"object","additionalProperties":{"type":"integer"}},"hosts":{"type":"array","items":{"type":"string"}},"tracking_plan":{"type":"object","properties":{"exists":{"type":"boolean"},"updated_at":{"type":["string","null"]},"events":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"seen":{"type":"boolean"},"last_seen_at":{"type":["string","null"]},"count_24h":{"type":"integer"},"count_30d":{"type":"integer"}}}}}},"undeclared":{"type":"array","items":{"type":"string"}},"next_steps":{"type":"array","items":{"type":"string"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/annotations":{"get":{"tags":["Analytics"],"operationId":"listAnnotations","summary":"List annotations","description":"Notes shown on the time charts in the range.","parameters":[{"$ref":"#/components/parameters/site"},{"$ref":"#/components/parameters/period"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Annotation"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Analytics"],"operationId":"addAnnotation","summary":"Add an annotation","description":"Mark a launch, a deploy or a price change on the charts. `ts` defaults to now.","parameters":[{"$ref":"#/components/parameters/site"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label"],"additionalProperties":false,"properties":{"label":{"type":"string","maxLength":200},"ts":{"type":"string","format":"date-time"},"url":{"type":"string","format":"uri"}}},"example":{"label":"Launched on Product Hunt","ts":"2026-10-01T09:00:00Z","url":"https://www.producthunt.com/posts/example"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/Annotation"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/events":{"post":{"tags":["Server-side"],"operationId":"trackEvent","summary":"Send a goal from your server","description":"Records a goal for a visitor that the browser script has already seen. Pass `visitor_id` (the `_tb_vid` cookie) or `user_id` (sent earlier with identify). The goal joins the visitor's latest visit. Names are lowercased; use snake_case.","parameters":[{"$ref":"#/components/parameters/site"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":64},"visitor_id":{"type":"string"},"user_id":{"type":"string"},"props":{"type":"object","additionalProperties":{"type":["string","number","boolean"]},"maxProperties":30},"url":{"type":"string","format":"uri","description":"Page the goal belongs to. Defaults to the visit's last page."}}},"example":{"name":"subscription_started","user_id":"user_123","props":{"plan":"pro"}}}}},"responses":{"202":{"description":"Recorded","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"recorded":{"type":"boolean"},"name":{"type":"string"},"visitor_id":{"type":"string"},"visit_id":{"type":"string"},"ts":{"type":"string","format":"date-time"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/identify":{"post":{"tags":["Server-side"],"operationId":"identify","summary":"Identify a visitor from your server","description":"Attaches your user id, email, name, avatar and traits to a visitor. Later server-side events can then use `user_id`.","parameters":[{"$ref":"#/components/parameters/site"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["visitor_id"],"properties":{"visitor_id":{"type":"string"},"user_id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"image":{"type":"string","format":"uri"},"traits":{"type":"object","additionalProperties":{"type":["string","number","boolean"]}}}},"example":{"visitor_id":"k3j5h2g7f9d1s8a4q6w0e2r5","user_id":"user_123","email":"ada@example.com","name":"Ada","traits":{"plan":"pro"}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"identified":{"type":"boolean"},"visitor_id":{"type":"string"},"user_id":{"type":["string","null"]}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/payments":{"post":{"tags":["Server-side"],"operationId":"recordPayment","summary":"Record a payment","description":"For payment providers without a built-in connection. Safe to repeat: `transaction_id` makes it idempotent. The payment is credited to a visit (last touch: the visit before the payment) through `visitor_id`/`session_id`, or through the email or customer id of an identified visitor. An amount of 0 is a free trial and is not counted. Account tokens pass `site_id` in the body.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount","currency","transaction_id"],"properties":{"amount":{"type":"number","minimum":0,"description":"Major units, like 49.99."},"currency":{"type":"string","pattern":"^[A-Z]{3}$"},"transaction_id":{"type":"string","maxLength":255},"visitor_id":{"type":"string","description":"The _tb_vid cookie at checkout."},"session_id":{"type":"string","description":"The _tb_sid cookie at checkout."},"email":{"type":"string"},"customer_id":{"type":"string"},"plan":{"type":"string"},"renewal":{"type":"boolean"},"refunded":{"type":"boolean"},"timestamp":{"type":["string","number"],"description":"ISO 8601 or Unix time. Defaults to now."},"site_id":{"type":"string","description":"Only for account tokens."}}},"example":{"amount":49,"currency":"USD","transaction_id":"inv_1042","visitor_id":"k3j5h2g7f9d1s8a4q6w0e2r5","email":"ada@example.com","plan":"Pro"}}}},"responses":{"200":{"description":"Already recorded (duplicate transaction_id), or a free trial"},"201":{"description":"Recorded","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"transaction_id":{"type":"string"},"duplicate":{"type":"boolean"},"attributed":{"type":"boolean"},"visitor_id":{"type":["string","null"]}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/bots":{"get":{"tags":["Server-side"],"operationId":"getBotTraffic","summary":"Crawler traffic","description":"Hits from AI and search crawlers in the range, by kind (ai_answer, indexing, training) and by bot.","parameters":[{"$ref":"#/components/parameters/site"},{"$ref":"#/components/parameters/period"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"by_kind":{"type":"object","additionalProperties":{"type":"integer"}},"bots":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string"},"bot":{"type":"string"},"n":{"type":"integer"}}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Server-side"],"operationId":"trackBotHit","summary":"Record a crawler hit","description":"Crawlers do not run JavaScript, so your server reports them. Send the user agent and path of a request; known crawlers are stored as ai_answer (an assistant fetches the page for a person now), indexing (search engines) or training (model training). Unknown user agents are ignored. Fire and forget: do not make the page wait.","parameters":[{"$ref":"#/components/parameters/site"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_agent"],"properties":{"user_agent":{"type":"string"},"path":{"type":"string"}}},"example":{"user_agent":"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot","path":"/pricing"}}}},"responses":{"202":{"description":"Accepted","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","properties":{"tracked":{"type":"boolean"},"bot":{"type":"string"},"kind":{"type":"string","enum":["ai_answer","indexing","training"]},"company":{"type":"string"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cli/bootstrap":{"post":{"tags":["Sites"],"operationId":"cliBootstrap","summary":"Find or create a site and mint a key (CLI)","description":"Used by `twelvebit init` after the device sign-in. Needs a session (the device flow's access token as a Bearer). Finds the user's site for the domain, or creates it, and returns a new site key named after the machine.","security":[{"session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"type":"string"},"timezone":{"type":"string"},"hostname":{"type":"string"}}},"example":{"domain":"example.com","timezone":"Europe/Berlin","hostname":"ada-mbp"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/openapi.json":{"get":{"tags":["Meta"],"operationId":"getOpenApi","summary":"This document","security":[],"responses":{"200":{"description":"OpenAPI 3.1 document","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"Site key (tbk_…), account token (tbt_…) or a session token."},"session":{"type":"http","scheme":"bearer","description":"Better Auth session token (from the device sign-in) or the session cookie."}},"parameters":{"site":{"name":"site","in":"query","schema":{"type":"string"},"description":"Site id, public id (tb_…) or domain. Needed for account tokens with more than one site; site keys ignore it."},"period":{"name":"period","in":"query","schema":{"type":"string","enum":["today","yesterday","24h","7d","30d","90d","6m","12m","wtd","mtd","ytd","all","custom"],"default":"30d"}},"from":{"name":"from","in":"query","schema":{"type":"string","format":"date"},"description":"First day (YYYY-MM-DD, site time zone). Use with `to`."},"to":{"name":"to","in":"query","schema":{"type":"string","format":"date"},"description":"Last day, inclusive."},"g":{"name":"g","in":"query","schema":{"type":"string","enum":["hour","day","week","month"]},"description":"Bucket size. Default depends on the range length."},"dim":{"name":"dim","in":"query","schema":{"type":"string","enum":["channel","referrer","campaign","keyword","source","hostname","entry","exit","country","region","city","browser","os","device","page","goal","outbound"],"default":"channel"}},"limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200}},"offset":{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}},"f_channel":{"name":"f_channel","in":"query","schema":{"type":"string"},"description":"Only visits where channel is this value."},"f_referrer":{"name":"f_referrer","in":"query","schema":{"type":"string"},"description":"Only visits where referrer is this value."},"f_campaign":{"name":"f_campaign","in":"query","schema":{"type":"string"},"description":"Only visits where campaign is this value."},"f_keyword":{"name":"f_keyword","in":"query","schema":{"type":"string"},"description":"Only visits where keyword is this value."},"f_source":{"name":"f_source","in":"query","schema":{"type":"string"},"description":"Only visits where source is this value."},"f_hostname":{"name":"f_hostname","in":"query","schema":{"type":"string"},"description":"Only visits where hostname is this value."},"f_entry":{"name":"f_entry","in":"query","schema":{"type":"string"},"description":"Only visits where entry page is this value."},"f_exit":{"name":"f_exit","in":"query","schema":{"type":"string"},"description":"Only visits where exit page is this value."},"f_country":{"name":"f_country","in":"query","schema":{"type":"string"},"description":"Only visits where country is this value."},"f_region":{"name":"f_region","in":"query","schema":{"type":"string"},"description":"Only visits where region is this value."},"f_city":{"name":"f_city","in":"query","schema":{"type":"string"},"description":"Only visits where city is this value."},"f_browser":{"name":"f_browser","in":"query","schema":{"type":"string"},"description":"Only visits where browser is this value."},"f_os":{"name":"f_os","in":"query","schema":{"type":"string"},"description":"Only visits where os is this value."},"f_device":{"name":"f_device","in":"query","schema":{"type":"string"},"description":"Only visits where device is this value."},"f_page":{"name":"f_page","in":"query","schema":{"type":"string"},"description":"Only visits where page is this value."},"f_goal":{"name":"f_goal","in":"query","schema":{"type":"string"},"description":"Only visits where goal is this value."}},"schemas":{"Error":{"type":"object","required":["error","hint","docs"],"description":"Every error has a stable code, a message, a hint with the next step and a docs link. Unknown query parameters are rejected with unknown_parameter (400).","properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"unknown_parameter"},"message":{"type":"string","example":"Unknown query parameter: \"dimension\". This endpoint takes: dim, f_channel, from, g, limit, period, site, to."},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}},"parameters":{"type":"array","items":{"type":"string"},"description":"unknown_parameter: the names that were not accepted."},"allowed":{"type":"array","items":{"type":"string"},"description":"unknown_parameter: the names this endpoint takes."},"retry_after":{"type":"integer","description":"rate_limited: seconds to wait."}}},"hint":{"type":"string","description":"What to do next, written for a model or a person.","example":"Remove the parameter or use one of the names in `allowed`."},"docs":{"type":"string","format":"uri","description":"The page that explains this error."}}},"Meta":{"type":"object","description":"Context for the data: the site, the exact range, the filters and the attribution model.","properties":{"site":{"type":"object","properties":{"id":{"type":"string"},"public_id":{"type":"string"},"domain":{"type":"string"},"timezone":{"type":"string"},"currency":{"type":"string"}}},"range":{"type":"object","properties":{"period":{"type":"string"},"label":{"type":"string"},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"timezone":{"type":"string"},"granularity":{"type":"string"}}},"filters":{"type":"object","additionalProperties":{"type":"string"}},"attribution":{"type":"string","example":"last touch: the visit before the payment"}},"additionalProperties":true},"Site":{"type":"object","properties":{"id":{"type":"string"},"public_id":{"type":"string","example":"tb_8Kd2…"},"domain":{"type":"string"},"name":{"type":["string","null"]},"role":{"type":"string","enum":["owner","member","viewer"]},"timezone":{"type":"string"},"currency":{"type":"string"},"cookieless":{"type":"boolean"},"first_event_at":{"type":["string","null"],"format":"date-time"},"last_event_at":{"type":["string","null"],"format":"date-time"},"tracking_plan":{"type":"boolean"},"dashboard_url":{"type":"string"},"script_tag":{"type":"string"}}},"Kpis":{"type":"object","properties":{"visitors":{"type":"integer"},"visits":{"type":"integer"},"pageviews":{"type":"integer"},"bounce_rate":{"type":"number","description":"Fraction of visits with one page view and no goal."},"avg_visit_seconds":{"type":"integer"},"revenue":{"type":"number","description":"Net of refunds, site currency."},"refunds":{"type":"number"},"customers":{"type":"integer"},"paying_visitors":{"type":"integer"},"conversion_rate":{"type":"number","description":"Paying visitors / visitors."},"revenue_per_visitor":{"type":"number"}}},"Event":{"type":"object","properties":{"ts":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["pageview","goal","identify","outbound"]},"name":{"type":["string","null"]},"path":{"type":["string","null"]},"hostname":{"type":["string","null"]},"props":{"type":["object","null"]}}},"Funnel":{"type":"object","properties":{"name":{"type":["string","null"]},"steps":{"type":"array","items":{"type":"object","properties":{"step":{"type":"string"},"label":{"type":"string"},"visitors":{"type":"integer"},"from_previous":{"type":"number"},"from_start":{"type":"number"}}}},"conversion":{"type":"number"},"weakest_step":{"type":["string","null"]},"by_channel":{"type":"array","items":{"type":"object"}},"days_to_convert":{"type":"array","items":{"type":"object"}}}},"Person":{"type":"object","properties":{"visitor_id":{"type":"string"},"name":{"type":"string","description":"Identified name, or a friendly name like amber-fox-4821."},"identified":{"type":"boolean"},"user_id":{"type":["string","null"]},"email":{"type":["string","null"]},"first_seen_at":{"type":"string","format":"date-time"},"last_seen_at":{"type":"string","format":"date-time"},"visits":{"type":"integer"},"first_channel":{"type":["string","null"]},"country":{"type":["string","null"]},"city":{"type":["string","null"]},"revenue":{"type":"number"}}},"Annotation":{"type":"object","properties":{"id":{"type":"string"},"ts":{"type":"string","format":"date-time"},"label":{"type":"string"},"source":{"type":"string"},"url":{"type":["string","null"]}}},"TrackingPlan":{"type":"object","description":"analytics.yaml. Unknown keys are errors.","required":["version","events"],"additionalProperties":false,"properties":{"version":{"const":1},"project":{"type":"string","description":"The site's domain."},"integrations":{"type":"array","items":{"type":"string","enum":["stripe","polar","lemonsqueezy","paddle","dodo","shopify","other"]}},"events":{"type":"object","propertyNames":{"pattern":"^[a-z][a-z0-9_]*$","maxLength":64},"additionalProperties":{"type":"object","required":["description"],"additionalProperties":false,"properties":{"description":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"revenue":{"type":"boolean"},"props":{"type":"object","maxProperties":30,"additionalProperties":{"type":"object","required":["type"],"additionalProperties":false,"properties":{"type":{"enum":["string","number","boolean"]},"enum":{"type":"array","items":{"type":"string"}},"required":{"type":"boolean"},"description":{"type":"string"}}}}}}},"dashboards":{"type":"object","additionalProperties":false,"properties":{"funnels":{"type":"array","items":{"type":"object","required":["name","steps"],"additionalProperties":false,"properties":{"name":{"type":"string"},"steps":{"type":"array","minItems":2,"maxItems":8,"items":{"type":"string","description":"An event name, or page:/path."}}}}}}}}}}},"externalDocs":{"description":"Docs and agent guide","url":"https://twelvebit.app/docs/api"},"x-mcp":{"url":"https://twelvebit.app/api/mcp","transport":"streamable-http","auth":"Bearer tbk_… or tbt_…"}}