@tailglow/cli 0.8.1 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/package.json +2 -2
- package/src/commands.generated.js +21 -4
- package/src/config.js +8 -1
- package/src/docs.generated.js +9 -9
- package/src/import-command.js +2 -13
- package/src/import-options.js +0 -1
- package/src/import-worker.generated.js +23 -41
- package/src/import.generated.d.ts +0 -5
- package/src/import.generated.js +60 -86
- package/src/index.js +14 -0
- package/src/request.js +12 -4
- package/src/update-check.js +137 -0
package/src/docs.generated.js
CHANGED
|
@@ -26,30 +26,30 @@ export const DOCS_PAGES = {
|
|
|
26
26
|
"api/alerts": "# Alerts\n\n## Alert Model\n\n### Fields\n\n- **`object`** `\"alert\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `alrt_`.\n\n- **`slug`** `string`\n\n URL-safe identifier for the alert.\n\n- **`team_id`** `string`\n\n- **`project_id`** `string`\n\n- **`metric_id`** `string`\n\n The metric the monitor was watching.\n\n- **`monitor_id`** `string`\n\n The monitor that raised the alert.\n\n- **`monitor_name`** `string | null`\n\n- **`metric_name`** `string | null`\n\n- **`triggered_at`** [`ISODateString`](/api/alerts#iso-date-string)\n\n The check that opened the alert. The start of the period, used for ordering and duration.\n\n- **`ended_at`** [`ISODateString | null`](/api/alerts#iso-date-string)\n\n When the period ended. Null while the alert is still open.\n\n- **`end_reason`** [`AlertEndReason | null`](/api/alerts#alert-end-reason)\n\n Why the period ended. Null while the alert is still open.\n\n- **`created_at`** [`ISODateString`](/api/alerts#iso-date-string)\n\n When the alert was recorded. Later than `triggered_at` when checks were catching up.\n\n- **`series_hash`** `string`\n\n The target that opened the alert: one series for `each_series`, a fixed value for `total` and\n `any_series`.\n\n- **`series_label`** `string | null`\n\n The series name for `each_series` alerts. Null for `total` and `any_series`; an `any_series`\n alert names its series in `trigger_evidence.opened_by`.\n\n- **`trigger_value`** `number`\n\n The statistic value that matched when the alert opened.\n\n- **`condition_summary`** `string`\n\n The rule in words, built from `rule_snapshot`, for example `Average over 1 minute above 1020`.\n\n- **`rule_snapshot`** `MonitorRuleSnapshot`\n\n The monitor's rule as it stood when the alert opened. Later edits do not change it.\n\n- **`trigger_evidence`** `AlertTriggerEvidence`\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### AlertEndReason\n\n`\"recovered\" | \"instant\" | \"monitor_changed\" | \"monitor_paused\" | \"monitor_expired\" | \"metric_rebuilt\" | \"resource_deleted\" | \"target_removed\"`\n\nWhy an alert ended. `recovered`: the condition cleared. `instant`: an instant alert, which ends\nas it opens. `monitor_changed`: the monitor's rule was edited. `monitor_paused` /\n`monitor_expired`: the monitor stopped. `metric_rebuilt`: the metric was rebuilt from its data.\n`resource_deleted`: the metric or something it reads from is being deleted. `target_removed`: the\nseries no longer exists.\n\n#### MonitorStatistic\n\n`\"count\" | \"sum\" | \"average\" | \"min\" | \"max\" | \"last\" | \"p50\" | \"p95\" | \"p99\" | \"count_unique\"`\n\nWhat a monitor computes over its window. `count`, `sum` and `count_unique` measure how much\nhappened; the others read a value from the observations in the window.\n\n#### MonitorOperator\n\n`\"gt\" | \"gte\" | \"lt\" | \"lte\" | \"eq\" | \"neq\"`\n\n#### MonitorEmptyWindowHandling\n\n`\"no_value\" | \"zero\"`\n\nWhat a statistic reads when its window holds no data. `zero` reads 0; `no_value` reads nothing,\nso the check neither opens nor closes an alert. `count`, `sum` and `count_unique` always use\n`zero`.\n\n#### MonitorWindowType\n\n`\"rolling\" | \"all_time\"`\n\n`rolling` reads the `window_minutes` before each check; `all_time` reads everything up to it.\n\n#### MonitorScope\n\n`\"total\" | \"any_series\" | \"each_series\"`\n\n`total` checks the whole feed as one value. `any_series` opens one alert while any series\nmatches. `each_series` checks every series separately.\n\n#### MonitorTriggerType\n\n`\"immediate\" | \"sustained\" | \"repeated\"`\n\nWhen a match opens an alert. `immediate` on the first matching check; `sustained` once matches\nhave held for `sustained_minutes`; `repeated` after `repeat_count` matches within\n`repeat_window_minutes`.\n\n#### MonitorAlertMode\n\n`\"spanning\" | \"instant\"`\n\n`spanning` keeps an alert open until the condition clears; `instant` opens and ends it at once.\n\n## List Alerts\n\n### Endpoint\n\nList the alerts a metric's monitors have raised.\n\n```http\nGET /v1/projects/:project_id/metrics/:metric_id/alerts\n```\n\n**Scope:** `alerts:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`metric_id`** `string` -- **Required**\n Unique identifier of the metric.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field the results are sorted by. Alerts sort by when their period opened. Optional. Defaults to `\"triggered_at\"`. Allowed values: `\"triggered_at\"`.\n\n- **`metric_id`** `string`\n Return only alerts raised on this metric. Optional.\n\n- **`monitor_id`** `string`\n Return only alerts raised by this monitor. Optional.\n\n- **`is_open`** `string`\n Set true for periods still firing, false for periods that have ended. Optional. Allowed values: `\"true\"`, `\"false\"`, `\"1\"`, `\"0\"`.\n\n- **`time_range`** [`AnalyticsTimeRange`](/api/metrics#analytics-time-range)\n Window to search, relative to now. Ignored when start_at and end_at are sent. Optional.\n\n- **`timezone`** `string`\n IANA timezone the time range is anchored to. Defaults to UTC. Optional.\n\n- **`page_id`** `string`\n Resolve the time range from this page's setting instead of the metric's. Optional.\n\n- **`start_at`** [`ISODateString`](/api/alerts#iso-date-string)\n Start of the window to search, as an ISO 8601 value. Optional.\n\n- **`end_at`** [`ISODateString`](/api/alerts#iso-date-string)\n End of the window to search, as an ISO 8601 value. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Alert[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- An alert is a firing PERIOD, not a single notification. `triggered_at` is the check that opened it and `ended_at` is when it ended, with the reason in `end_reason`, so one incident is one row for its whole duration. `created_at` is only when the row was written.\n- Results cover a time window. Send `start_at` and `end_at`, or a `time_range`; with neither, the metric's own configured chart range is used.\n- An alert is returned when its period OVERLAPS the window, so a period that opened before the window and is still firing is included.\n- Use `is_open=true` to see only what is currently firing.\n- `before` and `after` are mutually exclusive. Send one or neither.\n- `timezone` must be a valid IANA timezone name, or `UTC`.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Alert\n\n### Endpoint\n\nRetrieve a single alert.\n\n```http\nGET /v1/projects/:project_id/alerts/:alert_id\n```\n\n**Scope:** `alerts:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`alert_id`** `string` -- **Required**\n Unique identifier of the alert.\n\n### Response\n\nAlert retrieved\n\n```ts\n{\n message: string;\n data: Alert;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A null `ended_at` means the condition still holds and the period is still open.\n\n",
|
|
27
27
|
"api/pages": "# Pages\n\n## Page Model\n\n### Fields\n\n- **`object`** `\"page\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `pag_`.\n\n- **`team_id`** `string`\n\n- **`project_id`** `string`\n\n- **`name`** `string`\n\n- **`created_at`** [`ISODateString`](/api/pages#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/pages#iso-date-string)\n\n- **`is_public`** `boolean`\n\n Whether the page is reachable by anyone holding its URL.\n\n- **`public_url`** `string`\n\n Where the page is served once published. The address resolves only while `is_public` is true.\n\n- **`slug`** `string`\n\n The random segment of the public URL. Changes whenever the URL is refreshed.\n\n- **`access_code`** `string | null`\n\n Code a visitor must enter to view the public page; null when none is set.\n\n- **`components`** [`Component[]`](/api/components#model)\n\n The components displayed on the page, in the order they appear.\n\n- **`time_range`** [`AnalyticsTimeRange`](/api/pages#analytics-time-range)\n\n Window every chart on the page covers.\n\n- **`custom_range_start_at`** [`ISODateString | null`](/api/pages#iso-date-string)\n\n Start of the window, when the time range is custom.\n\n- **`custom_range_end_at`** [`ISODateString | null`](/api/pages#iso-date-string)\n\n End of the window, when the time range is custom.\n\n- **`interval`** [`ChartInterval`](/api/pages#chart-interval)\n\n Bucket size the charts use.\n\n- **`public_ui_show_alerts`** `boolean`\n\n Whether alert markers are drawn on the charts of the public page.\n\n- **`public_ui_show_trends`** `boolean`\n\n Whether trend lines enabled on each metric are shown publicly. Defaults to true.\n\n- **`public_ui_show_forecast`** `boolean`\n\n Whether forecasts enabled on each metric are shown publicly. Defaults to true.\n\n- **`domain_routes`** [`PageDomainRoute[]`](/api/pages#domain-route-model)\n\n The custom domains this page is published on.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### ComponentCardSize\n\n`\"1/1\" | \"1/2\"`\n\n#### MetricStatus\n\n`\"initializing\" | \"waiting_for_transforms\" | \"backfilling\" | \"active\" | \"error\" | \"cancelled\"`\n\n#### NullHandling\n\n`\"skip\" | \"count_as_zero\"`\n\n#### EmptyBucketHandling\n\n`\"zero\" | \"gaps\"`\n\n#### BackfillStrategy\n\n`\"newest_first\" | \"oldest_first\"`\n\n#### ChartFamily\n\n`\"cartesian\" | \"radial\" | \"geographic\" | \"temporal\" | \"hierarchical\"`\n\n#### ChartType\n\n`\"line\" | \"area\" | \"bar\" | \"pie\" | \"scatter\" | \"radar\" | \"stat\" | \"gauge\" | \"calendar\" | \"uptime\" | \"breakdown\" | \"heatmap\" | \"table\"`\n\n#### ChartColorMode\n\n`\"by_series\" | \"by_intensity\" | \"by_value\"`\n\n#### ChartColor\n\n`\"blue\" | \"red\" | \"amber\" | \"green\" | \"teal\" | \"purple\" | \"pink\"`\n\n#### ChartCurve\n\n`\"linear\" | \"step\" | \"smooth\"`\n\n#### ForecastHorizon\n\n`\"next_7_days\" | \"next_30_days\" | \"next_90_days\" | \"end_of_quarter\" | \"end_of_year\" | \"next_year\"`\n\nHow far past now a chart projects its forecast. Values are relative to the moment the chart is\nviewed, so a saved horizon keeps projecting the same distance ahead as time passes rather than\nexpiring on a fixed date. `end_of_quarter` and `end_of_year` run to the end of the calendar\nperiod that contains today, in UTC.\n\n#### ForecastModel\n\n`\"auto\" | \"linear\" | \"exponential\" | \"logarithmic\" | \"logistic\" | \"sinusoidal\"`\n\nThe curve a forecast is fitted with. `auto` follows the best-fitting model for the series, which\nis recalculated as data arrives and can therefore change between views; naming a model pins the\nprojection to that curve.\n\n#### MetricChartValue\n\n`\"count\" | \"average\" | \"sum\" | \"min\" | \"max\" | \"last\" | \"cumulative_sum\" | \"cumulative_count\" | \"p50\" | \"p95\" | \"p99\" | \"count_unique\"`\n\nAggregation applied to a metric's values. Use `last` when records are snapshots of a persistent\nthing such as a deal, user, or inventory item: each time bucket contains the latest reading for\nthat series in the bucket. Within the requested range, the last observed value carries forward\nacross complete empty intervals instead of reading them as zero.\n\n#### AnalyticsTimeRange\n\n`\"last_hour\" | \"last_6_hours\" | \"today\" | \"last_24_hours\" | \"yesterday\" | \"this_week\" | \"last_week\" | \"this_month\" | \"last_month\" | \"this_quarter\" | \"last_quarter\" | \"this_year\" | \"last_year\" | \"last_7_days\" | \"last_30_days\" | \"last_90_days\" | \"last_365_days\" | \"all_time\" | \"custom\" | \"next_7_days\" | \"next_30_days\" | \"next_90_days\"`\n\nThe window a chart reads. Stored as a plain string rather than a database enum: the set is\npresentation, not something any query filters on, and the forward ranges in particular are\nexpected to change as we learn what people forecast over.\n\nA `next_*` range ends after now, which is what turns a fitted trend into a visible forecast. The\nmeasured half of such a range is still measured; only the part past now is projected.\n\n#### ChartInterval\n\n`\"minute\" | \"hour\" | \"day\" | \"month\" | \"auto\"`\n\nThe Data Interval _setting_ (`Metric.ui_chart_interval` and the aggregation `interval` query\nparam). `\"auto\"` means the server picks the finest-safe tier for the current view, so brush-zoom\nnaturally drills into a finer bucket. The RESOLVED tier returned by aggregation is always a plain\n`DataInterval`.\n\n#### TimeDimension\n\n`\"hour_of_day\" | \"weekday\" | \"hour_of_week\"`\n\nThe calendar position a metric's hourly data folds into. `hour_of_day` folds into hours 0 to 23,\n`weekday` into ISO weekdays 1 (Monday) to 7 (Sunday), and `hour_of_week` into the 168 hours of a\nweek, numbered `(weekday - 1) * 24 + hour`.\n\n#### ChartOrder\n\n`\"natural\" | \"value\"`\n\nHow a breakdown chart orders its groups. `natural` keeps their own order (hours, weekdays),\n`value` ranks them largest first.\n\n## Page Domain Route Model\n\n### Fields\n\n- **`object`** `\"page_domain_route\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `pdr_`.\n\n- **`domain_id`** `string`\n\n- **`page_id`** `string`\n\n- **`domain`** `string`\n\n The domain name itself.\n\n- **`pathname`** `string`\n\n Path the page is served at on that domain, leading slash included. `/` is the domain root.\n\n- **`public_url`** `string`\n\n The full address the page is reachable at.\n\n- **`created_at`** [`ISODateString`](/api/pages#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/pages#iso-date-string)\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n## List Pages\n\n### Endpoint\n\nList a project's pages.\n\n```http\nGET /v1/projects/:project_id/pages\n```\n\n**Scope:** `pages:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field the results are sorted by. Optional. Defaults to `\"created_at\"`. Allowed values: `\"created_at\"`.\n\n- **`team_id`** `string`\n Return only pages in this team. Optional.\n\n- **`project_id`** `string`\n Return only pages in this project. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Page[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Page\n\n### Endpoint\n\nRetrieve a single page.\n\n```http\nGET /v1/projects/:project_id/pages/:page_id\n```\n\n**Scope:** `pages:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`page_id`** `string` -- **Required**\n Unique identifier of the page.\n\n### Response\n\nPage retrieved\n\n```ts\n{\n message: string;\n data: Page;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Includes every component on the page and the custom domains it is published to.\n\n## Create Page\n\n### Endpoint\n\nCreate a page that displays a set of metrics.\n\n```http\nPOST /v1/projects/:project_id/pages\n```\n\n**Scope:** `pages:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Request Body\n\n- **`name`** `string` -- **Required**\n Display name for the page. Minimum length: `1`. Maximum length: `64`.\n\n- **`is_public`** `boolean`\n Whether the page is reachable by anyone holding its URL. Optional.\n\n- **`public_ui_show_alerts`** `boolean`\n Whether alert markers are drawn on the charts of the public page. Optional.\n\n- **`public_ui_show_trends`** `boolean`\n Whether trend lines enabled on each metric are shown on the public page. Defaults to true. Optional.\n\n- **`public_ui_show_forecast`** `boolean`\n Whether forecasts enabled on each metric are shown on the public page. Defaults to true. Optional.\n\n- **`access_code`** `string | null`\n Code a visitor must enter to view the public page. Send null for no code. Optional.\n\n- **`time_range`** [`AnalyticsTimeRange`](/api/pages#analytics-time-range)\n Window every chart on the page covers. Optional.\n\n- **`interval`** [`ChartInterval`](/api/pages#chart-interval)\n Bucket size the charts use. Set to auto to pick one from the time range. Optional.\n\n- **`custom_range_start_at`** [`ISODateString`](/api/pages#iso-date-string)\n Start of the window, when the time range is custom. Optional.\n\n- **`custom_range_end_at`** [`ISODateString`](/api/pages#iso-date-string)\n End of the window, when the time range is custom. Optional.\n\n### Response\n\nYour page has been created\n\n```ts\n{\n message: string;\n data: Page;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A page is private until `is_public` is set. Publishing one generates the slug that forms its public URL.\n- A `custom` time range requires both `custom_range_start_at` and `custom_range_end_at`, at least 30 minutes apart, with the start before the end.\n- An `access_code` is trimmed, and an empty one is stored as no code at all.\n\n## Update Page\n\n### Endpoint\n\nChange a page's name, time range, public access, or its layout of cards.\n\n```http\nPOST /v1/projects/:project_id/pages/:page_id\n```\n\n**Scope:** `pages:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`page_id`** `string` -- **Required**\n Unique identifier of the page.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the page. Optional. Minimum length: `1`. Maximum length: `64`.\n\n- **`is_public`** `boolean`\n Whether the page is reachable by anyone holding its URL. Optional.\n\n- **`public_ui_show_alerts`** `boolean`\n Whether alert markers are drawn on the charts of the public page. Optional.\n\n- **`public_ui_show_trends`** `boolean`\n Whether trend lines enabled on each metric are shown on the public page. Defaults to true. Optional.\n\n- **`public_ui_show_forecast`** `boolean`\n Whether forecasts enabled on each metric are shown on the public page. Defaults to true. Optional.\n\n- **`access_code`** `string | null`\n Code a visitor must enter to view the public page. Send null for no code. Optional.\n\n- **`time_range`** [`AnalyticsTimeRange`](/api/pages#analytics-time-range)\n Window every chart on the page covers. Optional.\n\n- **`interval`** [`ChartInterval`](/api/pages#chart-interval)\n Bucket size the charts use. Set to auto to pick one from the time range. Optional.\n\n- **`custom_range_start_at`** [`ISODateString`](/api/pages#iso-date-string)\n Start of the window, when the time range is custom. Optional.\n\n- **`custom_range_end_at`** [`ISODateString`](/api/pages#iso-date-string)\n End of the window, when the time range is custom. Optional.\n\n- **`components`** `object[]`\n The page's whole layout, in page order. Cards left out are removed. Optional.\n\n - **`id`** `string`\n Card to keep. Leave out to add a new card. Optional.\n\n - **`name`** `string | null`\n Title of a card that shows several metrics. Leave out or send null to title it with their names. Optional.\n\n - **`ui_card_size`** `string`\n How much of the page width the card occupies. Defaults to the full width. Optional. Allowed values: `\"1/1\"`, `\"1/2\"`.\n\n - **`metric_ids`** `string[]` -- **Required**\n Metrics the card shows together, in order.\n\n### Response\n\nYour page has been updated\n\n```ts\n{\n message: string;\n data: Page;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Send only the fields you are changing. Anything omitted keeps its current value.\n- Making a page private clears its public URL. Making it public again issues a new slug, so any previously shared link stops working.\n- `components` is the whole layout, in page order: each entry is a card's full state, an entry without an `id` adds a card, and a card you leave out is removed. Leave it out to change settings only. Settings and layout are saved together, or not at all.\n- Every listed card `id` must already be on this page, and every metric must belong to the page's project and not be scheduled for deletion.\n- A `custom` time range requires both `custom_range_start_at` and `custom_range_end_at`, at least 30 minutes apart, with the start before the end. The rule is checked against the page as it will be after the update, not against the fields you send.\n- An `access_code` is trimmed, and an empty one clears the code.\n- A card can be listed only once in `components`, and a metric can be on only one card of the page.\n\n## Add Page Domain\n\n### Endpoint\n\nPublish a page on one of your verified domains.\n\n```http\nPOST /v1/projects/:project_id/pages/:page_id/domains/:domain_id\n```\n\n**Scopes:** `pages:write` + `domains:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`page_id`** `string` -- **Required**\n Unique identifier of the page.\n\n- **`domain_id`** `string` -- **Required**\n Unique identifier of the domain.\n\n### Request Body\n\n- **`pathname`** `string` -- **Required**\n Path this page is served from on one custom Page domain. `/` serves the root.\n\n### Response 201\n\nCustom Page URL saved\n\n```ts\n{\n message: string;\n data: PageDomainRoute;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Response 200\n\nCustom Page URL saved\n\n```ts\n{\n message: string;\n data: PageDomainRoute;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The domain has to be verified before a page can be served on it.\n- Sending this again for the same domain moves the page to the new pathname rather than creating a second route.\n- Send `/` to serve the page at the domain root.\n- `pathname` is trimmed, lowercased, given a leading slash and stripped of any trailing slash before it is stored, so `Status`, `/status` and `status/` all resolve to the same path rather than becoming separate routes.\n- `pathname` is either `/`, which serves the domain root, or a single segment of lowercase letters, numbers and single hyphens. Nested paths such as `/team/status` are rejected.\n- A small set of paths is reserved by Tailglow Pages and cannot be used.\n- Only one page can hold a given path on a domain, including `/`.\n- A stored `pathname` is at most 64 characters. The limit is applied after normalization, so the leading slash counts toward it even when you leave it off, while surrounding whitespace and a trailing slash do not.\n- A `pathname` longer than 256 characters is rejected before any of that, counting whatever you send including whitespace.\n- An empty or blank `pathname` is rejected. Delete the route to stop serving the page on a domain.\n\n## Remove Page Domain\n\n### Endpoint\n\nStop serving a page on one of your domains.\n\n```http\nDELETE /v1/projects/:project_id/pages/:page_id/domains/:domain_id\n```\n\n**Scopes:** `pages:write` + `domains:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`page_id`** `string` -- **Required**\n Unique identifier of the page.\n\n- **`domain_id`** `string` -- **Required**\n Unique identifier of the domain.\n\n### Response\n\nCustom Page URL removed\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The page stays reachable on its Tailglow URL if it is still public.\n\n## Refresh Page URL\n\n### Endpoint\n\nIssue a new public URL for a page.\n\n```http\nPOST /v1/projects/:project_id/pages/:page_id/refresh\n```\n\n**Scope:** `pages:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`page_id`** `string` -- **Required**\n Unique identifier of the page.\n\n### Response\n\nYour page slug has been updated\n\n```ts\n{\n message: string;\n data: Page;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Use this when a shared link should stop working. The old URL returns a 404 immediately.\n\n## Delete Page\n\n### Endpoint\n\nDelete a page.\n\n```http\nDELETE /v1/projects/:project_id/pages/:page_id\n```\n\n**Scope:** `pages:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`page_id`** `string` -- **Required**\n Unique identifier of the page.\n\n### Response\n\nYour page has been deleted.\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The metrics the page displayed are not affected.\n\n",
|
|
28
28
|
"api/components": "# Components\n\n## Component Model\n\n### Fields\n\n- **`object`** `\"component\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `comp_`.\n\n- **`page_id`** `string`\n\n- **`name`** `string | null`\n\n Title of a card that shows several metrics. Null titles it with their names.\n\n- **`created_at`** [`ISODateString`](/api/components#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/components#iso-date-string)\n\n- **`ui_card_size`** [`ComponentCardSize`](/api/components#component-card-size)\n\n How much of the page width the component occupies.\n\n- **`ui_sort_index`** `number`\n\n Position of the component on the page. Lower appears first.\n\n- **`metrics`** [`Metric[]`](/api/metrics#model)\n\n The metrics the card shows together, in order: one to six. Metrics scheduled for deletion are\n left out until they are restored.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### ComponentCardSize\n\n`\"1/1\" | \"1/2\"`\n\n#### MetricStatus\n\n`\"initializing\" | \"waiting_for_transforms\" | \"backfilling\" | \"active\" | \"error\" | \"cancelled\"`\n\n#### NullHandling\n\n`\"skip\" | \"count_as_zero\"`\n\n#### EmptyBucketHandling\n\n`\"zero\" | \"gaps\"`\n\n#### BackfillStrategy\n\n`\"newest_first\" | \"oldest_first\"`\n\n#### ChartFamily\n\n`\"cartesian\" | \"radial\" | \"geographic\" | \"temporal\" | \"hierarchical\"`\n\n#### ChartType\n\n`\"line\" | \"area\" | \"bar\" | \"pie\" | \"scatter\" | \"radar\" | \"stat\" | \"gauge\" | \"calendar\" | \"uptime\" | \"breakdown\" | \"heatmap\" | \"table\"`\n\n#### ChartColorMode\n\n`\"by_series\" | \"by_intensity\" | \"by_value\"`\n\n#### ChartColor\n\n`\"blue\" | \"red\" | \"amber\" | \"green\" | \"teal\" | \"purple\" | \"pink\"`\n\n#### ChartCurve\n\n`\"linear\" | \"step\" | \"smooth\"`\n\n#### ForecastHorizon\n\n`\"next_7_days\" | \"next_30_days\" | \"next_90_days\" | \"end_of_quarter\" | \"end_of_year\" | \"next_year\"`\n\nHow far past now a chart projects its forecast. Values are relative to the moment the chart is\nviewed, so a saved horizon keeps projecting the same distance ahead as time passes rather than\nexpiring on a fixed date. `end_of_quarter` and `end_of_year` run to the end of the calendar\nperiod that contains today, in UTC.\n\n#### ForecastModel\n\n`\"auto\" | \"linear\" | \"exponential\" | \"logarithmic\" | \"logistic\" | \"sinusoidal\"`\n\nThe curve a forecast is fitted with. `auto` follows the best-fitting model for the series, which\nis recalculated as data arrives and can therefore change between views; naming a model pins the\nprojection to that curve.\n\n#### MetricChartValue\n\n`\"count\" | \"average\" | \"sum\" | \"min\" | \"max\" | \"last\" | \"cumulative_sum\" | \"cumulative_count\" | \"p50\" | \"p95\" | \"p99\" | \"count_unique\"`\n\nAggregation applied to a metric's values. Use `last` when records are snapshots of a persistent\nthing such as a deal, user, or inventory item: each time bucket contains the latest reading for\nthat series in the bucket. Within the requested range, the last observed value carries forward\nacross complete empty intervals instead of reading them as zero.\n\n#### AnalyticsTimeRange\n\n`\"last_hour\" | \"last_6_hours\" | \"today\" | \"last_24_hours\" | \"yesterday\" | \"this_week\" | \"last_week\" | \"this_month\" | \"last_month\" | \"this_quarter\" | \"last_quarter\" | \"this_year\" | \"last_year\" | \"last_7_days\" | \"last_30_days\" | \"last_90_days\" | \"last_365_days\" | \"all_time\" | \"custom\" | \"next_7_days\" | \"next_30_days\" | \"next_90_days\"`\n\nThe window a chart reads. Stored as a plain string rather than a database enum: the set is\npresentation, not something any query filters on, and the forward ranges in particular are\nexpected to change as we learn what people forecast over.\n\nA `next_*` range ends after now, which is what turns a fitted trend into a visible forecast. The\nmeasured half of such a range is still measured; only the part past now is projected.\n\n#### ChartInterval\n\n`\"minute\" | \"hour\" | \"day\" | \"month\" | \"auto\"`\n\nThe Data Interval _setting_ (`Metric.ui_chart_interval` and the aggregation `interval` query\nparam). `\"auto\"` means the server picks the finest-safe tier for the current view, so brush-zoom\nnaturally drills into a finer bucket. The RESOLVED tier returned by aggregation is always a plain\n`DataInterval`.\n\n#### TimeDimension\n\n`\"hour_of_day\" | \"weekday\" | \"hour_of_week\"`\n\nThe calendar position a metric's hourly data folds into. `hour_of_day` folds into hours 0 to 23,\n`weekday` into ISO weekdays 1 (Monday) to 7 (Sunday), and `hour_of_week` into the 168 hours of a\nweek, numbered `(weekday - 1) * 24 + hour`.\n\n#### ChartOrder\n\n`\"natural\" | \"value\"`\n\nHow a breakdown chart orders its groups. `natural` keeps their own order (hours, weekdays),\n`value` ranks them largest first.\n\n## Add Component\n\n### Endpoint\n\nAdd a card of one or more metrics to a page.\n\n```http\nPOST /v1/projects/:project_id/pages/:page_id/components\n```\n\n**Scope:** `pages:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`page_id`** `string` -- **Required**\n Unique identifier of the page.\n\n### Request Body\n\n- **`name`** `string | null`\n Title of a card that shows several metrics. Send null to title it with their names. Optional.\n\n- **`ui_sort_index`** `integer`\n Position of the component on the page. Lower appears first. Optional.\n\n- **`ui_card_size`** [`ComponentCardSize`](/api/components#component-card-size)\n How much of the page width the component occupies. Optional.\n\n- **`metric_ids`** `string[]` -- **Required**\n Metrics the card shows together, in order.\n\n- **`page_id`** `string`\n Page the component is added to. Optional.\n\n### Response\n\nYour component has been created\n\n```ts\n{\n message: string;\n data: Component;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A component is a card on one page showing one to six metrics together.\n- To change a whole page's layout in one write, send `components` to Update Page instead.\n- Every metric must belong to the same project as the page, and none can already be on another card of the page.\n- A metric can be listed only once in `metric_ids`.\n- A page holds a limited number of components. Adding one past the limit is rejected.\n\n## Update Component\n\n### Endpoint\n\nChange which metrics a component shows, its name, its size, or its position.\n\n```http\nPOST /v1/projects/:project_id/pages/:page_id/components/:component_id\n```\n\n**Scope:** `pages:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`page_id`** `string` -- **Required**\n Unique identifier of the page.\n\n- **`component_id`** `string` -- **Required**\n Unique identifier of the component.\n\n### Request Body\n\n- **`name`** `string | null`\n Title of a card that shows several metrics. Send null to title it with their names. Optional.\n\n- **`ui_sort_index`** `integer`\n Position of the component on the page. Lower appears first. Optional.\n\n- **`ui_card_size`** [`ComponentCardSize`](/api/components#component-card-size)\n How much of the page width the component occupies. Optional.\n\n- **`metric_ids`** `string[]`\n Metrics the card shows together, in order. Optional.\n\n### Response\n\nYour component has been updated\n\n```ts\n{\n message: string;\n data: Component;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Send only the fields you are changing. Anything omitted keeps its current value.\n- Reorder a page by sending a new `ui_sort_index` for each component you are moving.\n- `metric_ids` replaces the card's metrics. Every metric must belong to the same project as the page, and none can already be on another card of the page.\n- A metric can be listed only once in `metric_ids`.\n\n## Delete Component\n\n### Endpoint\n\nRemove a component from a page.\n\n```http\nDELETE /v1/projects/:project_id/pages/:page_id/components/:component_id\n```\n\n**Scope:** `pages:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`page_id`** `string` -- **Required**\n Unique identifier of the page.\n\n- **`component_id`** `string` -- **Required**\n Unique identifier of the component.\n\n### Response\n\nYour component has been deleted.\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The metrics themselves are not affected, only the card on this page.\n\n",
|
|
29
|
-
"api/teams": "# Teams\n\n## Team Model\n\n### Fields\n\n- **`object`** `\"team\"`\n\n- **`id`** `string`\n\n- **`name`** `string`\n\n- **`created_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n- **`deleted_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the team is scheduled to be deleted; null when it is not.\n\n- **`logo_url`** `string | null`\n\n- **`status`** [`TeamStatus`](/api/teams#team-status)\n\n Account standing. Any status other than `active` stops ingest: `delinquent` marks a missed\n payment, `restricted` also makes the API read-only apart from the billing fields needed to\n recover the account, and `blocked` is applied manually by Tailglow.\n\n- **`billing_email`** `string`\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n\n- **`billing_address`** [`Address`](/api/teams#address-model)\n\n Postal address that appears on the team's invoices.\n\n- **`storage_gb`** `number`\n\n Stored data across the team's projects, in gigabytes of uncompressed data: raw ingested files\n and artifacts, plus view output and minute-level rollups. Recalculated hourly;\n `billing_updated_at` is the last refresh.\n\n- **`billing_period_server_months`** `number`\n\n Server-months the team's servers have accrued in the current calendar-month billing period,\n counted up to the last refresh.\n\n- **`billing_period_input_tokens`** `number`\n\n AI input tokens consumed through Tailglow-provided model access this billing period, counting\n only tokens charged at the full input rate. Anything served from or written to the prompt cache\n is counted separately below. Chats running on your own provider keys are not counted at all.\n\n- **`billing_period_output_tokens`** `number`\n\n AI output tokens consumed through Tailglow-provided model access this billing period.\n\n- **`billing_period_cache_read_tokens`** `number`\n\n Input tokens served from the prompt cache this billing period, charged at the cache rate.\n\n- **`billing_period_cache_write_tokens`** `number`\n\n Input tokens written to the prompt cache this billing period, charged at the cache rate.\n\n- **`billing_updated_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the cached billing quantities were last recalculated; null before the first pass.\n\n- **`max_servers_per_project`** `number`\n\n How many servers each project may run. Scale requests beyond it are rejected.\n\n- **`is_mfa_required`** `boolean`\n\n Whether every member must verify with two-factor authentication before their session can act.\n\n- **`is_tailglow_ai_enabled`** `boolean`\n\n Whether members may run the assistant on Tailglow's model, billed to this team. When false,\n each member must add their own provider key before they can use the assistant at all.\n\n- **`has_payment_method`** `boolean`\n\n Whether the team has a payment method on file.\n\n- **`promo_expires_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the trial's server-hours run out (with a card, at the current rate). A time in the past\n means the trial has ended and its servers are being settled; null when the team has no trial in\n progress.\n\n- **`server_trial_used_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the team used its once-per-account server trial; retained after the trial is settled.\n\n- **`promo_server_hours`** `number`\n\n Shared server-hour credits remaining after the last invoice; unbilled usage is not deducted.\n\n- **`promo_ai_tokens`** `number`\n\n Shared Tailglow AI token credits remaining after the last invoice.\n\n- **`tgl_generations_count`** `number`\n\n Automatic transform-script generations counted against the hourly budget. The counter rolls\n over lazily: after the window elapses it keeps its last value until the next generation.\n\n- **`tgl_generations_reset_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the counted window ends or ended. A past timestamp means no generation has happened since.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### TeamStatus\n\n`\"active\" | \"delinquent\" | \"restricted\" | \"blocked\"`\n\n#### BillingPlan\n\n`\"pro_v1\" | \"enterprise_v1\"`\n\n## Team Billing Model\n\n### Fields\n\n- **`is_estimate_complete`** `boolean`\n\n False when a saved AI rate needs support; monetary estimates are incomplete until resolved.\n\n- **`object`** `\"billing\"`\n\n- **`team_id`** `string`\n\n- **`calculated_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n\n- **`has_payment_method`** `boolean`\n\n- **`promo_expires_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the trial's server-hours run out (with a card, at the current rate). Without a card the\n trial server pauses then; unused promotional units do not expire at this time.\n\n- **`server_count`** `number`\n\n Non-surge capacity across the team, including provisioning and excluding idle/terminating.\n\n- **`uninvoiced_server_hours`** `number`\n\n Billable server-hours since the last saved invoice, including preceding-month usage.\n\n- **`promo_server_hours`** `number`\n\n Stored server-hour balance after the last invoice.\n\n- **`promo_ai_tokens`** `number`\n\n Stored Tailglow-funded token balance after the last invoice.\n\n- **`available_promo_server_hours`** `number`\n\n Server-hour credits remaining after accrued, uninvoiced usage.\n\n- **`available_promo_ai_tokens`** `number`\n\n Tailglow-funded token credits remaining after accrued, uninvoiced usage.\n\n- **`included_storage_gb_months`** `number`\n\n Storage, in GB-months, the plan credits on every monthly invoice.\n\n- **`available_included_storage_gb_months`** `number`\n\n Included storage this period has not used yet, in GB-months.\n\n- **`period_start_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n First moment of the billing period being estimated, in UTC.\n\n- **`period_end_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n First moment of the following period, so the period is `[start, end)`.\n\n- **`billed_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n When the invoice for this period is calculated, which is after the period has closed.\n\n- **`line_items`** [`InvoiceEstimateLineItem[]`](/api/teams#billing-line-item-model)\n\n One entry per billable line. Lines that accrued nothing are present with a zero quantity.\n\n- **`subtotal_cents`** `number`\n\n- **`discount_cents`** `number`\n\n- **`total_cents`** `number`\n\n What the period has accrued so far, after discounts. Excludes tax, which Stripe calculates when\n the invoice is finalized, and any credit note balance, which is applied at payment.\n\n- **`projected_line_items`** [`InvoiceEstimateLineItem[]`](/api/teams#billing-line-item-model)\n\n The same period carried to `billed_at`: storage held and grown at its recent rate, servers that\n are up staying up, tokens continuing at the month-to-date rate. A subscription appears\n unchanged because it is committed for the whole period. A forecast rather than a measurement,\n so it assumes today's usage continues and moves the moment a server is added or removed.\n Discounts here are read as of `billed_at`, so one that lapses mid-period counts toward\n `total_cents` but not toward this.\n\n- **`projected_subtotal_cents`** `number`\n\n- **`projected_discount_cents`** `number`\n\n- **`projected_total_cents`** `number`\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### BillingPlan\n\n`\"pro_v1\" | \"enterprise_v1\"`\n\n#### BillingLineItemType\n\n`\"compute\" | \"storage\" | \"subscription\" | \"ai_input\" | \"ai_output\" | \"ai_cache_read\" | \"ai_cache_write\"`\n\n## Billing Line Item Model\n\n### Fields\n\n- **`line_item_type`** [`BillingLineItemType`](/api/teams#billing-line-item-type)\n\n- **`description`** `string`\n\n The label this line carries on the issued invoice, so an estimate reads like the bill.\n\n- **`quantity`** `number`\n\n Units billed, in the line item's own unit: GB-months, server-months, or token millions.\n\n- **`unit_price_cents`** `number`\n\n- **`subtotal_cents`** `number`\n\n- **`discount_cents`** `number`\n\n Zero when no discount applies to this line item.\n\n- **`total_cents`** `number`\n\n- **`discount_id`** `string | null`\n\n The negotiated discount applied in addition to promo credits, or null.\n\n- **`discount_percent`** `number | null`\n\n Set only for percent discounts; a fixed-amount discount reports its value in cents.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### BillingLineItemType\n\n`\"compute\" | \"storage\" | \"subscription\" | \"ai_input\" | \"ai_output\" | \"ai_cache_read\" | \"ai_cache_write\"`\n\n## Billing Estimate Model\n\n### Fields\n\n- **`line_items`** [`InvoiceEstimateLineItem[]`](/api/teams#billing-line-item-model)\n\n One entry per billable line, in the order the invoice lists them. Lines that accrued nothing\n are present with a zero quantity.\n\n- **`subtotal_cents`** `number`\n\n- **`discount_cents`** `number`\n\n- **`total_cents`** `number`\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### BillingLineItemType\n\n`\"compute\" | \"storage\" | \"subscription\" | \"ai_input\" | \"ai_output\" | \"ai_cache_read\" | \"ai_cache_write\"`\n\n## Address Model\n\n### Fields\n\n- **`thoroughfare`** `string | null`\n\n Street line: street number and street name.\n\n- **`premise`** `string | null`\n\n Unit, suite, or building within the street address.\n\n- **`sublocality`** `string | null`\n\n District or neighborhood within the city, where addresses use one.\n\n- **`locality`** `string | null`\n\n City or town.\n\n- **`administrative_area`** `string | null`\n\n State, province, or region.\n\n- **`postal_code`** `string | null`\n\n ZIP or postal code.\n\n- **`country`** `string | null`\n\n Two-letter ISO country code.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n## List Teams\n\n### Endpoint\n\nRetrieve a list of teams the caller belongs to.\n\n```http\nGET /v1/teams\n```\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the teams. Optional. Defaults to `\"name\"`. Allowed values: `\"created_at\"`, `\"name\"`.\n\n- **`name`** [`TextFilter`](/api/teams#text-filter)\n Filter by team name, written as `operator:value`. For example `contains:acme` or `equals:Acme Inc`. Optional.\n\n- **`status`** [`TeamStatus`](/api/teams#team-status)\n Filter by team status, written as `operator:value`. For example `equals:active` or `in:active,delinquent`. Optional.\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n Filter by billing plan, written as `operator:value`. For example `in:pro_v1,enterprise_v1`. Optional.\n\n- **`created_at`** [`DateFilter`](/api/teams#date-filter)\n Filter by creation date, written as `operator:value`. For example `gt:2026-01-01` or `between:2026-01-01,2026-02-01`. Optional.\n\n- **`deleted_at`** [`NullableDateFilter`](/api/teams#nullable-date-filter)\n Filter by scheduled deletion date. Use `null` for teams that are not scheduled for deletion, `not:null` for teams that are. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Team[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- API key callers only ever receive the single team the key belongs to.\n- Teams scheduled for deletion are only returned to their Owners.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Team\n\n### Endpoint\n\nRetrieve a single team.\n\n```http\nGET /v1/teams/:team_id\n```\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Response\n\n```ts\n{\n message: string;\n data: Team;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can only retrieve the team your credentials are scoped to.\n\n## Retrieve Team Billing\n\n### Endpoint\n\nRetrieve the team's current billing summary.\n\n```http\nGET /v1/teams/:team_id/billing\n```\n\n**Scope:** `billing:read`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Response\n\nTeam billing retrieved\n\n```ts\n{\n message: string;\n data: TeamBilling;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Requires `billing:read` and credentials scoped to this team. Every response includes accrued and projected charges, stored and available promotional credits, and server usage.\n- Available credits account for uninvoiced usage, including the preceding month before its invoice is saved. Reading this summary does not consume credits or create an invoice.\n- Amounts exclude tax and credit notes applied at payment. Projections assume current usage continues; the final invoice can differ.\n\n## Create Team\n\n### Endpoint\n\nCreate a new team owned by the authenticated user.\n\n```http\nPOST /v1/teams\n```\n\n### Request Body\n\n- **`name`** `string`\n Display name for the team. Optional. Minimum length: `2`. Maximum length: `60`.\n\n- **`billing_email`** `string (email)`\n Email address that receives invoices and billing notifications. Optional. Maximum length: `64`.\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n Billing plan for the team. Optional.\n\n- **`status`** [`TeamStatus`](/api/teams#team-status)\n Account status for the team. Optional.\n\n- **`is_mfa_required`** `boolean`\n Whether every member must sign in with multi-factor authentication. Optional.\n\n- **`max_servers_per_project`** `integer`\n Maximum number of servers each project in the team can run. Optional. Minimum: `1`. Maximum: `1000`.\n\n- **`billing_address`** `object`\n Billing address printed on invoices. Optional.\n\n - **`thoroughfare`** `string`\n Optional. Maximum length: `128`.\n\n - **`premise`** `string`\n Optional. Maximum length: `128`.\n\n - **`sublocality`** `string`\n Optional. Maximum length: `128`.\n\n - **`locality`** `string`\n Optional. Maximum length: `128`.\n\n - **`administrative_area`** `string`\n Optional. Maximum length: `128`.\n\n - **`postal_code`** `string`\n Optional. Maximum length: `16`.\n\n - **`country`** `string`\n Optional. Minimum length: `2`. Maximum length: `2`.\n\n- **`logo_url`** `string`\n Team logo, sent as a JPEG or PNG base64 data URI. Optional.\n\n### Response\n\n```ts\n{\n message: string;\n data: Team;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Teams cannot be created with an API key. Use a signed-in user session.\n- `name` defaults to `Team` and `billing_email` defaults to the authenticated user's email address when they are omitted.\n- The team starts on the `pro_v1` plan with a generated logo. `billing_plan`, `status`, `logo_url`, `is_mfa_required`, `billing_address`, and `max_servers_per_project` are ignored on create. Send them to the update endpoint instead.\n- There is a maximum number of teams a single user can create.\n- The authenticated user is added to the new team as its Owner.\n- `billing_email` cannot use the `@tailglow.io` domain.\n- `logo_url` must be a JPEG or PNG base64 data URI of at most 5 MB.\n\n## Update Team\n\n### Endpoint\n\nUpdate an existing team.\n\n```http\nPOST /v1/teams/:team_id\n```\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the team. Optional. Minimum length: `2`. Maximum length: `60`.\n\n- **`billing_email`** `string (email)`\n Email address that receives invoices and billing notifications. Optional. Maximum length: `64`.\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n Billing plan for the team. Optional.\n\n- **`status`** [`TeamStatus`](/api/teams#team-status)\n Account status for the team. Optional.\n\n- **`expected_status`** [`TeamStatus`](/api/teams#team-status)\n The status the team had when you loaded it. Required with `status`; the change is refused if the status has changed since. Optional.\n\n- **`is_mfa_required`** `boolean`\n Whether every member must sign in with multi-factor authentication. Optional.\n\n- **`is_tailglow_ai_enabled`** `boolean`\n Whether members may run the AI assistant on Tailglow's model, billed to this team. When false, each member must add their own provider key before they can use the assistant. Optional.\n\n- **`max_servers_per_project`** `integer`\n Maximum number of servers each project in the team can run. Optional. Minimum: `1`. Maximum: `1000`.\n\n- **`billing_address`** `object`\n Billing address printed on invoices. Optional.\n\n - **`thoroughfare`** `string`\n Optional. Maximum length: `128`.\n\n - **`premise`** `string`\n Optional. Maximum length: `128`.\n\n - **`sublocality`** `string`\n Optional. Maximum length: `128`.\n\n - **`locality`** `string`\n Optional. Maximum length: `128`.\n\n - **`administrative_area`** `string`\n Optional. Maximum length: `128`.\n\n - **`postal_code`** `string`\n Optional. Maximum length: `16`.\n\n - **`country`** `string`\n Optional. Minimum length: `2`. Maximum length: `2`.\n\n- **`logo_url`** `string`\n Team logo, sent as a JPEG or PNG base64 data URI. Optional.\n\n- **`deleted_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n Date and time when the team is scheduled to be permanently deleted. Optional.\n\n### Response\n\n```ts\n{\n message: string;\n data: Team;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Teams cannot be updated with an API key. Use a signed-in user session.\n- At least one field must be provided.\n- Only team Owners can change billing, security, or account settings. Other members can update `name` and `logo_url` only.\n- `status`, `max_servers_per_project`, and `deleted_at` are managed by Tailglow and cannot be set by team members.\n- `status` requires `expected_status`, the status you loaded; the change is refused with a conflict if the team's status has changed since.\n- Changing `is_mfa_required` requires a recently verified multi-factor sign-in.\n- Moving to a plan that bills a payment method requires a valid payment method on the team.\n- `billing_email` cannot use the `@tailglow.io` domain.\n- `logo_url` must be a JPEG or PNG base64 data URI of at most 5 MB.\n\n## Delete Team\n\n### Endpoint\n\nSchedule a team for deletion.\n\n```http\nDELETE /v1/teams/:team_id\n```\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Response\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Teams cannot be deleted with an API key. Use a signed-in user session.\n- Only the team Owner can delete a team.\n- The team must be active. Contact support to delete a suspended or blocked team.\n- Deletion is scheduled, not immediate. Permanent removal happens 40 days later, the team's projects and API keys are cancelled, and every signed-in member is signed out. An owner who signs back in before then cancels the deletion.\n\n## List Team Members\n\n### Endpoint\n\nRetrieve a list of members in a team.\n\n```http\nGET /v1/teams/:team_id/users\n```\n\n**Scope:** `users:read`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the team members. Optional. Defaults to `\"name\"`. Allowed values: `\"created_at\"`, `\"name\"`.\n\n- **`name`** [`TextFilter`](/api/teams#text-filter)\n Filter by member name, written as `operator:value`. For example `contains:riley` or `starts_with:Ri`. Optional.\n\n- **`status`** [`UserStatus`](/api/users#user-status)\n Filter by member account status, written as `operator:value`. For example `in:active,blocked`. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: User[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can only list members of the team your credentials are scoped to.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Team Member\n\n### Endpoint\n\nRetrieve a single member of a team.\n\n```http\nGET /v1/teams/:team_id/users/:user_id\n```\n\n**Scope:** `users:read`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n- **`user_id`** `string` -- **Required**\n Unique identifier of the user.\n\n### Response\n\nUser retrieved\n\n```ts\n{\n message: string;\n data: User;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can only retrieve members of the team your credentials are scoped to.\n\n## Add Team Member\n\n### Endpoint\n\nAdd a person to a team. This does not create a standalone user account: an existing Tailglow account is matched by email address, and an account is created for the address only when none exists yet.\n\n```http\nPOST /v1/teams/:team_id/users\n```\n\n**Scope:** `users:write`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the member. Optional. Minimum length: `2`. Maximum length: `60`.\n\n- **`role_id`** `string` -- **Required**\n Role the member holds in this team.\n\n- **`email`** `string (email)` -- **Required**\n Email address of the person to add. They receive a sign-in link at this address, and an account is created for them if they do not already have one.\n\n### Response\n\nNew user was added to the team\n\n```ts\n{\n message: string;\n data: User;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The person receives an email with a sign-in link for the team.\n- Someone who is already a member of the team cannot be added again.\n- Only Owners can assign the Owner role.\n- There is a maximum number of members per team.\n- Blocked and locked accounts cannot be added to a team.\n- You can assign only scopes that your own authorization has.\n\n## Update Team Member\n\n### Endpoint\n\nUpdate a member of a team.\n\n```http\nPOST /v1/teams/:team_id/users/:user_id\n```\n\n**Scope:** `users:write`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n- **`user_id`** `string` -- **Required**\n Unique identifier of the user.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the member. Optional. Minimum length: `2`. Maximum length: `60`.\n\n- **`role_id`** `string`\n Role the member holds in this team. Optional.\n\n### Response\n\nUser updated\n\n```ts\n{\n message: string;\n data: User;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You cannot change your own role.\n- You can only change your own name. Another member's name is theirs to change.\n- You cannot assign a role that holds scopes you do not hold yourself, and only Owners can assign or replace the Owner role.\n- Changing a member's role signs them out and emails them the new role.\n- A member's email address cannot be changed. Remove them and add them again with the new address.\n\n## Remove Team Member\n\n### Endpoint\n\nRemove a member from a team.\n\n```http\nDELETE /v1/teams/:team_id/users/:user_id\n```\n\n**Scope:** `users:delete`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n- **`user_id`** `string` -- **Required**\n Unique identifier of the user.\n\n### Response\n\nUser removed from team\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You cannot remove yourself from a team. Delete the team instead if you are its last member.\n- You cannot remove a member whose role holds scopes you do not hold yourself, and only Owners can remove another Owner.\n- The last Owner of a team cannot be removed.\n- The member is emailed to let them know they were removed. Their Tailglow account is not deleted, only their membership in this team.\n\n",
|
|
29
|
+
"api/teams": "# Teams\n\n## Team Model\n\n### Fields\n\n- **`object`** `\"team\"`\n\n- **`id`** `string`\n\n- **`name`** `string`\n\n- **`created_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n- **`deleted_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the team is scheduled to be deleted; null when it is not.\n\n- **`logo_url`** `string | null`\n\n- **`status`** [`TeamStatus`](/api/teams#team-status)\n\n Account standing. Any status other than `active` stops ingest: `delinquent` marks a missed\n payment, `restricted` also makes the API read-only apart from the billing fields needed to\n recover the account, and `blocked` is applied manually by Tailglow.\n\n- **`billing_email`** `string`\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n\n- **`billing_address`** [`Address`](/api/teams#address-model)\n\n Postal address that appears on the team's invoices.\n\n- **`storage_gb`** `number`\n\n Stored data across the team's projects, in gigabytes of uncompressed data: raw ingested files\n and artifacts, plus view output and minute-level rollups. Recalculated hourly;\n `billing_updated_at` is the last refresh.\n\n- **`billing_period_server_months`** `number`\n\n Server-months the team's servers have accrued in the current calendar-month billing period,\n counted up to the last refresh.\n\n- **`billing_period_input_tokens`** `number`\n\n AI input tokens consumed through Tailglow-provided model access this billing period, counting\n only tokens charged at the full input rate. Anything served from or written to the prompt cache\n is counted separately below. Chats running on your own provider keys are not counted at all.\n\n- **`billing_period_output_tokens`** `number`\n\n AI output tokens consumed through Tailglow-provided model access this billing period.\n\n- **`billing_period_cache_read_tokens`** `number`\n\n Input tokens served from the prompt cache this billing period, charged at the cache rate.\n\n- **`billing_period_cache_write_tokens`** `number`\n\n Input tokens written to the prompt cache this billing period, charged at the cache rate.\n\n- **`billing_updated_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the cached billing quantities were last recalculated; null before the first pass.\n\n- **`max_servers_per_project`** `number`\n\n How many servers each project may run. Scale requests beyond it are rejected.\n\n- **`is_mfa_required`** `boolean`\n\n Whether every member must verify with two-factor authentication before their session can act.\n\n- **`is_tailglow_ai_enabled`** `boolean`\n\n Whether members may run the assistant on Tailglow's model, billed to this team. When false,\n each member must add their own provider key before they can use the assistant at all.\n\n- **`has_payment_method`** `boolean`\n\n Whether the team has a payment method on file.\n\n- **`promo_expires_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the trial's server-hours run out (with a card, at the current rate). A time in the past\n means the trial has ended and its servers are being settled; null when the team has no trial in\n progress.\n\n- **`server_trial_used_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the team used its once-per-account server trial; retained after the trial is settled.\n\n- **`promo_server_hours`** `number`\n\n Shared server-hour credits remaining after the last invoice; unbilled usage is not deducted.\n\n- **`promo_ai_tokens`** `number`\n\n Shared Tailglow AI token credits remaining after the last invoice.\n\n- **`tgl_generations_count`** `number`\n\n Automatic transform-script generations counted against the hourly budget. The counter rolls\n over lazily: after the window elapses it keeps its last value until the next generation.\n\n- **`tgl_generations_reset_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the counted window ends or ended. A past timestamp means no generation has happened since.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### TeamStatus\n\n`\"active\" | \"delinquent\" | \"restricted\" | \"blocked\"`\n\n#### BillingPlan\n\n`\"pro_v1\" | \"enterprise_v1\"`\n\n## Team Billing Model\n\n### Fields\n\n- **`is_estimate_complete`** `boolean`\n\n False when a saved AI rate needs support; monetary estimates are incomplete until resolved.\n\n- **`object`** `\"billing\"`\n\n- **`team_id`** `string`\n\n- **`calculated_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n\n- **`has_payment_method`** `boolean`\n\n- **`promo_expires_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the trial's server-hours run out (with a card, at the current rate). Without a card the\n trial server pauses then; unused promotional units do not expire at this time.\n\n- **`server_count`** `number`\n\n Non-surge capacity across the team, including provisioning and excluding idle/terminating.\n\n- **`uninvoiced_server_hours`** `number`\n\n Billable server-hours since the last saved invoice, including preceding-month usage.\n\n- **`promo_server_hours`** `number`\n\n Stored server-hour balance after the last invoice.\n\n- **`promo_ai_tokens`** `number`\n\n Stored Tailglow-funded token balance after the last invoice.\n\n- **`available_promo_server_hours`** `number`\n\n Server-hour credits remaining after accrued, uninvoiced usage.\n\n- **`available_promo_ai_tokens`** `number`\n\n Tailglow-funded token credits remaining after accrued, uninvoiced usage.\n\n- **`included_storage_gb_months`** `number`\n\n Storage, in GB-months, the plan credits on every monthly invoice.\n\n- **`available_included_storage_gb_months`** `number`\n\n Included storage this period has not used yet, in GB-months.\n\n- **`period_start_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n First moment of the billing period being estimated, in UTC.\n\n- **`period_end_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n First moment of the following period, so the period is `[start, end)`.\n\n- **`billed_at`** [`ISODateString`](/api/teams#iso-date-string)\n\n When the invoice for this period is calculated, which is after the period has closed.\n\n- **`line_items`** [`InvoiceEstimateLineItem[]`](/api/teams#billing-line-item-model)\n\n One entry per billable line. Lines that accrued nothing are present with a zero quantity.\n\n- **`subtotal_cents`** `number`\n\n- **`discount_cents`** `number`\n\n- **`total_cents`** `number`\n\n What the period has accrued so far, after discounts. Excludes tax, which Stripe calculates when\n the invoice is finalized, and any credit note balance, which is applied at payment.\n\n- **`projected_line_items`** [`InvoiceEstimateLineItem[]`](/api/teams#billing-line-item-model)\n\n The same period carried to `billed_at`: storage held and grown at its recent rate, servers that\n are up staying up, tokens continuing at the month-to-date rate. A subscription appears\n unchanged because it is committed for the whole period. A forecast rather than a measurement,\n so it assumes today's usage continues and moves the moment a server is added or removed.\n Discounts here are read as of `billed_at`, so one that lapses mid-period counts toward\n `total_cents` but not toward this.\n\n- **`projected_subtotal_cents`** `number`\n\n- **`projected_discount_cents`** `number`\n\n- **`projected_total_cents`** `number`\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### BillingPlan\n\n`\"pro_v1\" | \"enterprise_v1\"`\n\n#### BillingLineItemType\n\n`\"compute\" | \"storage\" | \"subscription\" | \"ai_input\" | \"ai_output\" | \"ai_cache_read\" | \"ai_cache_write\"`\n\n## Billing Line Item Model\n\n### Fields\n\n- **`line_item_type`** [`BillingLineItemType`](/api/teams#billing-line-item-type)\n\n- **`description`** `string`\n\n The label this line carries on the issued invoice, so an estimate reads like the bill.\n\n- **`quantity`** `number`\n\n Units billed, in the line item's own unit: GB-months, server-months, or token millions.\n\n- **`unit_price_cents`** `number`\n\n- **`subtotal_cents`** `number`\n\n- **`discount_cents`** `number`\n\n Zero when no discount applies to this line item.\n\n- **`total_cents`** `number`\n\n- **`discount_id`** `string | null`\n\n The negotiated discount applied in addition to promo credits, or null.\n\n- **`discount_percent`** `number | null`\n\n Set only for percent discounts; a fixed-amount discount reports its value in cents.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### BillingLineItemType\n\n`\"compute\" | \"storage\" | \"subscription\" | \"ai_input\" | \"ai_output\" | \"ai_cache_read\" | \"ai_cache_write\"`\n\n## Billing Estimate Model\n\n### Fields\n\n- **`line_items`** [`InvoiceEstimateLineItem[]`](/api/teams#billing-line-item-model)\n\n One entry per billable line, in the order the invoice lists them. Lines that accrued nothing\n are present with a zero quantity.\n\n- **`subtotal_cents`** `number`\n\n- **`discount_cents`** `number`\n\n- **`total_cents`** `number`\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### BillingLineItemType\n\n`\"compute\" | \"storage\" | \"subscription\" | \"ai_input\" | \"ai_output\" | \"ai_cache_read\" | \"ai_cache_write\"`\n\n## Address Model\n\n### Fields\n\n- **`thoroughfare`** `string | null`\n\n Street line: street number and street name.\n\n- **`premise`** `string | null`\n\n Unit, suite, or building within the street address.\n\n- **`sublocality`** `string | null`\n\n District or neighborhood within the city, where addresses use one.\n\n- **`locality`** `string | null`\n\n City or town.\n\n- **`administrative_area`** `string | null`\n\n State, province, or region.\n\n- **`postal_code`** `string | null`\n\n ZIP or postal code.\n\n- **`country`** `string | null`\n\n Two-letter ISO country code.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n## List Teams\n\n### Endpoint\n\nRetrieve a list of teams the caller belongs to.\n\n```http\nGET /v1/teams\n```\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the teams. Optional. Defaults to `\"name\"`. Allowed values: `\"created_at\"`, `\"name\"`.\n\n- **`name`** [`TextFilter`](/api/teams#text-filter)\n Filter by team name, written as `operator:value`. For example `contains:acme` or `equals:Acme Inc`. Optional.\n\n- **`status`** [`TeamStatus`](/api/teams#team-status)\n Filter by team status, written as `operator:value`. For example `equals:active` or `in:active,delinquent`. Optional.\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n Filter by billing plan, written as `operator:value`. For example `in:pro_v1,enterprise_v1`. Optional.\n\n- **`created_at`** [`DateFilter`](/api/teams#date-filter)\n Filter by creation date, written as `operator:value`. For example `gt:2026-01-01` or `between:2026-01-01,2026-02-01`. Optional.\n\n- **`deleted_at`** [`NullableDateFilter`](/api/teams#nullable-date-filter)\n Filter by scheduled deletion date. Use `null` for teams that are not scheduled for deletion, `not:null` for teams that are. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Team[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- API key callers only ever receive the single team the key belongs to.\n- Teams scheduled for deletion are only returned to their Owners.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Team\n\n### Endpoint\n\nRetrieve a single team.\n\n```http\nGET /v1/teams/:team_id\n```\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Response\n\n```ts\n{\n message: string;\n data: Team;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can only retrieve the team your credentials are scoped to.\n\n## Retrieve Team Billing\n\n### Endpoint\n\nRetrieve the team's current billing summary.\n\n```http\nGET /v1/teams/:team_id/billing\n```\n\n**Scope:** `billing:read`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Response\n\nTeam billing retrieved\n\n```ts\n{\n message: string;\n data: TeamBilling;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Requires `billing:read` and credentials scoped to this team. Every response includes accrued and projected charges, stored and available promotional credits, and server usage.\n- Available credits account for uninvoiced usage, including the preceding month before its invoice is saved. Reading this summary does not consume credits or create an invoice.\n- Amounts exclude tax and credit notes applied at payment. Projections assume current usage continues; the final invoice can differ.\n\n## Create Team\n\n### Endpoint\n\nCreate a new team owned by the authenticated user.\n\n```http\nPOST /v1/teams\n```\n\n### Request Body\n\n- **`name`** `string`\n Display name for the team. Optional. Minimum length: `2`. Maximum length: `60`.\n\n- **`billing_email`** `string (email)`\n Email address that receives invoices and billing notifications. Optional. Maximum length: `64`.\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n Billing plan for the team. Optional.\n\n- **`status`** [`TeamStatus`](/api/teams#team-status)\n Account status for the team. Optional.\n\n- **`is_mfa_required`** `boolean`\n Whether every member must sign in with multi-factor authentication. Optional.\n\n- **`max_servers_per_project`** `integer`\n Maximum number of servers each project in the team can run. Optional. Minimum: `1`. Maximum: `1000`.\n\n- **`billing_address`** `object`\n Billing address printed on invoices. Optional.\n\n - **`thoroughfare`** `string`\n Optional. Maximum length: `128`.\n\n - **`premise`** `string`\n Optional. Maximum length: `128`.\n\n - **`sublocality`** `string`\n Optional. Maximum length: `128`.\n\n - **`locality`** `string`\n Optional. Maximum length: `128`.\n\n - **`administrative_area`** `string`\n Optional. Maximum length: `128`.\n\n - **`postal_code`** `string`\n Optional. Maximum length: `16`.\n\n - **`country`** `string`\n Optional. Minimum length: `2`. Maximum length: `2`.\n\n- **`logo_url`** `string`\n Team logo, sent as a JPEG or PNG base64 data URI. Optional.\n\n### Response\n\n```ts\n{\n message: string;\n data: Team;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Teams cannot be created with an API key. Use a signed-in user session.\n- `name` defaults to `Team` and `billing_email` defaults to the authenticated user's email address when they are omitted.\n- `billing_email` cannot use the `@tailglow.io` domain.\n- The team starts on the `pro_v1` plan with a generated logo. `billing_plan`, `status`, `logo_url`, `is_mfa_required`, `billing_address`, and `max_servers_per_project` are ignored on create. Send them to the update endpoint instead.\n- There is a maximum number of teams a single user can create.\n- The authenticated user is added to the new team as its Owner.\n- `logo_url` must be a JPEG or PNG base64 data URI of at most 5 MB.\n\n## Update Team\n\n### Endpoint\n\nUpdate an existing team.\n\n```http\nPOST /v1/teams/:team_id\n```\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the team. Optional. Minimum length: `2`. Maximum length: `60`.\n\n- **`billing_email`** `string (email)`\n Email address that receives invoices and billing notifications. Optional. Maximum length: `64`.\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n Billing plan for the team. Optional.\n\n- **`status`** [`TeamStatus`](/api/teams#team-status)\n Account status for the team. Optional.\n\n- **`expected_status`** [`TeamStatus`](/api/teams#team-status)\n The status the team had when you loaded it. Required with `status`; the change is refused if the status has changed since. Optional.\n\n- **`is_mfa_required`** `boolean`\n Whether every member must sign in with multi-factor authentication. Optional.\n\n- **`is_tailglow_ai_enabled`** `boolean`\n Whether members may run the AI assistant on Tailglow's model, billed to this team. When false, each member must add their own provider key before they can use the assistant. Optional.\n\n- **`max_servers_per_project`** `integer`\n Maximum number of servers each project in the team can run. Optional. Minimum: `1`. Maximum: `1000`.\n\n- **`billing_address`** `object`\n Billing address printed on invoices. Optional.\n\n - **`thoroughfare`** `string`\n Optional. Maximum length: `128`.\n\n - **`premise`** `string`\n Optional. Maximum length: `128`.\n\n - **`sublocality`** `string`\n Optional. Maximum length: `128`.\n\n - **`locality`** `string`\n Optional. Maximum length: `128`.\n\n - **`administrative_area`** `string`\n Optional. Maximum length: `128`.\n\n - **`postal_code`** `string`\n Optional. Maximum length: `16`.\n\n - **`country`** `string`\n Optional. Minimum length: `2`. Maximum length: `2`.\n\n- **`logo_url`** `string`\n Team logo, sent as a JPEG or PNG base64 data URI. Optional.\n\n- **`deleted_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n Set to null, as the only field, to restore a team scheduled for deletion. Only Tailglow can set a date. Optional.\n\n### Response\n\n```ts\n{\n message: string;\n data: Team;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Teams cannot be updated with an API key. Use a signed-in user session.\n- At least one field must be provided.\n- Only team Owners can change billing, security, or account settings. Other members can update `name` and `logo_url` only.\n- `status`, `max_servers_per_project`, and `deleted_at` are managed by Tailglow and cannot be set by team members, with one exception: an Owner restores a team scheduled for deletion by sending only `deleted_at` set to null.\n- `status` requires `expected_status`, the status you loaded; the change is refused with a conflict if the team's status has changed since.\n- Changing `is_mfa_required` requires a recently verified multi-factor sign-in.\n- Moving to a plan that bills a payment method requires a valid payment method on the team.\n- `billing_email` cannot use the `@tailglow.io` domain.\n- `logo_url` must be a JPEG or PNG base64 data URI of at most 5 MB.\n\n## Delete Team\n\n### Endpoint\n\nSchedule a team for deletion.\n\n```http\nDELETE /v1/teams/:team_id\n```\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Response\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Teams cannot be deleted with an API key. Use a signed-in user session.\n- Only the team Owner can delete a team.\n- The team must be active. Contact support to delete a suspended or blocked team.\n- Deletion is scheduled, not immediate. Permanent removal happens 40 days later. The team's API keys are deleted, its ingest keys are revoked, and its servers are scaled to zero.\n- Members lose access to the team at once but stay signed in to their other teams. Until permanent removal, an Owner can restore the team by updating it with `deleted_at` set to null.\n\n## List Team Members\n\n### Endpoint\n\nRetrieve a list of members in a team.\n\n```http\nGET /v1/teams/:team_id/users\n```\n\n**Scope:** `users:read`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the team members. Optional. Defaults to `\"name\"`. Allowed values: `\"created_at\"`, `\"name\"`.\n\n- **`name`** [`TextFilter`](/api/teams#text-filter)\n Filter by member name, written as `operator:value`. For example `contains:riley` or `starts_with:Ri`. Optional.\n\n- **`status`** [`UserStatus`](/api/users#user-status)\n Filter by member account status, written as `operator:value`. For example `in:active,blocked`. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: User[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can only list members of the team your credentials are scoped to.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Team Member\n\n### Endpoint\n\nRetrieve a single member of a team.\n\n```http\nGET /v1/teams/:team_id/users/:user_id\n```\n\n**Scope:** `users:read`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n- **`user_id`** `string` -- **Required**\n Unique identifier of the user.\n\n### Response\n\nUser retrieved\n\n```ts\n{\n message: string;\n data: User;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can only retrieve members of the team your credentials are scoped to.\n\n## Add Team Member\n\n### Endpoint\n\nAdd a person to a team. This does not create a standalone user account: an existing Tailglow account is matched by email address, and an account is created for the address only when none exists yet.\n\n```http\nPOST /v1/teams/:team_id/users\n```\n\n**Scope:** `users:write`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the member. Optional. Minimum length: `2`. Maximum length: `60`.\n\n- **`role_id`** `string` -- **Required**\n Role the member holds in this team.\n\n- **`email`** `string (email)` -- **Required**\n Email address of the person to add. They receive a sign-in link at this address, and an account is created for them if they do not already have one.\n\n### Response\n\nNew user was added to the team\n\n```ts\n{\n message: string;\n data: User;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The person receives an email with a sign-in link for the team.\n- Someone who is already a member of the team cannot be added again.\n- Only Owners can assign the Owner role.\n- There is a maximum number of members per team.\n- Blocked and locked accounts cannot be added to a team.\n- You can assign only scopes that your own authorization has.\n\n## Update Team Member\n\n### Endpoint\n\nUpdate a member of a team.\n\n```http\nPOST /v1/teams/:team_id/users/:user_id\n```\n\n**Scope:** `users:write`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n- **`user_id`** `string` -- **Required**\n Unique identifier of the user.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the member. Optional. Minimum length: `2`. Maximum length: `60`.\n\n- **`role_id`** `string`\n Role the member holds in this team. Optional.\n\n### Response\n\nUser updated\n\n```ts\n{\n message: string;\n data: User;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You cannot change your own role.\n- You can only change your own name. Another member's name is theirs to change.\n- You cannot assign a role that holds scopes you do not hold yourself, and only Owners can assign or replace the Owner role.\n- Changing a member's role signs them out and emails them the new role.\n- A member's email address cannot be changed. Remove them and add them again with the new address.\n\n## Remove Team Member\n\n### Endpoint\n\nRemove a member from a team.\n\n```http\nDELETE /v1/teams/:team_id/users/:user_id\n```\n\n**Scope:** `users:delete`\n\n### Path Parameters\n\n- **`team_id`** `string` -- **Required**\n Unique identifier of the team.\n\n- **`user_id`** `string` -- **Required**\n Unique identifier of the user.\n\n### Response\n\nUser removed from team\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You cannot remove yourself from a team. Delete the team instead if you are its last member.\n- You cannot remove a member whose role holds scopes you do not hold yourself, and only Owners can remove another Owner.\n- The last Owner of a team cannot be removed.\n- The member is emailed to let them know they were removed. Their Tailglow account is not deleted, only their membership in this team.\n\n",
|
|
30
30
|
"api/users": "# Users\n\n## User Model\n\n### Fields\n\n- **`object`** `\"user\"`\n\n- **`name`** `string`\n\n- **`email`** `string`\n\n- **`id`** `string`\n\n- **`default_team_id`** `string | null`\n\n The team the user lands in at sign-in; null falls back to the first team they belong to.\n\n- **`created_at`** [`ISODateString`](/api/users#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/users#iso-date-string)\n\n- **`last_active_at`** [`ISODateString | null`](/api/users#iso-date-string)\n\n When the user last made an authenticated request. Updated at most once every few minutes.\n\n- **`profile_url`** `string | null`\n\n URL of the user's avatar image; null when none is set.\n\n- **`role`** [`Role | null`](/api/roles#model)\n\n The user's role on the team the request is scoped to; null when they have none.\n\n- **`status`** [`UserStatus`](/api/users#user-status)\n\n `blocked` and `locked` accounts cannot act; `waitlisted` accounts signed up but have not been\n granted access yet.\n\n- **`default_auth_method`** [`AuthMethod`](/api/users#auth-method)\n\n The sign-in method preselected for the user.\n\n- **`available_auth_methods`** [`AuthMethod[]`](/api/users#auth-method)\n\n The sign-in methods enabled on the account.\n\n- **`is_totp_enabled`** `boolean`\n\n Whether an authenticator app is set up for two-factor sign-in.\n\n- **`totp_backup_codes_count`** `number`\n\n How many unused two-factor backup codes remain.\n\n- **`totp_default_count`** `number`\n\n How many backup codes a full set contains, for showing \"N of M remaining\".\n\n- **`password_updated_at`** [`ISODateString | null`](/api/users#iso-date-string)\n\n When the password was last changed; null when password sign-in is not enabled.\n\n- **`monitor_auto_subscribe`** `boolean`\n\n Whether the user is automatically subscribed to alert emails for monitors newly created in\n their team.\n\n- **`monitor_cooloff_minutes`** `number`\n\n Minimum minutes between alert emails about the same monitor.\n\n- **`product_update_notifications`** `boolean`\n\n Whether the user receives product update emails.\n\n- **`appearance_palette`** `string`\n\n Color palette the dashboard renders in for this user.\n\n- **`appearance_type_set`** `string`\n\n Font pairing the dashboard uses.\n\n- **`appearance_accent`** `string`\n\n Accent color the dashboard uses.\n\n- **`appearance_scale`** `string`\n\n Interface density preference. Applied to dashboard content at tablet and desktop widths; phones\n always render at full size.\n\n- **`default_ai_model`** [`AiChatSelection | null`](/api/users#ai-chat-selection)\n\n Selection a new chat opens on. Null when no preference is set. An unavailable saved selection\n requires an explicit replacement; it never silently changes the payer.\n\n- **`default_ai_effort`** [`AiEffortLevel`](/api/users#ai-effort-level)\n\n How hard the assistant is asked to work on each turn by default.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### Scope\n\n`\"users\" | \"roles\" | \"keys\" | \"records\" | \"projects\" | \"metrics\" | \"views\" | \"billing\" | \"pages\" | \"monitors\" | \"alerts\" | \"chats\" | \"secrets\" | \"servers\" | \"ingest_keys\" | \"sources\" | \"domains\" | \"facets\" | \"drains\" | \"pulls\" | \"checks\" | \"logs\"`\n\n#### ScopeValue\n\n`\"read\" | \"write\" | \"delete\"`\n\n#### UserStatus\n\n`\"active\" | \"blocked\" | \"waitlisted\" | \"locked\"`\n\n#### AuthMethod\n\n`\"magic_link\" | \"password\"`\n\n#### AiChatSelection\n\n`\"claude-opus-5-5\" | \"claude-sonnet-5\" | \"gpt-6-sol\" | \"gpt-5.6-terra\" | \"gpt-6-luna\" | \"claude-opus-5\" | \"gpt-5.6-sol\" | \"gpt-5.6-luna\" | \"claude-opus-4-6\" | \"tailglow\"`\n\nAn explicit choice of Tailglow-managed access or a model using the member's own key.\n\n#### AiEffortLevel\n\n`\"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\"`\n\nHow hard a model works on a turn. A model that accepts a narrower range declares it in\n`AI_MODEL_CONFIGS[model].efforts`.\n\n## List Users\n\n### Endpoint\n\nRetrieve a list of users in the current team.\n\n```http\nGET /v1/users\n```\n\n**Scope:** `users:read`\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the users. Optional. Defaults to `\"name\"`. Allowed values: `\"created_at\"`, `\"name\"`.\n\n- **`name`** [`TextFilter`](/api/users#text-filter)\n Filter by display name. Accepts a filter operator, for example `contains:ada` or `starts_with:ada`. Optional.\n\n- **`status`** [`UserStatus`](/api/users#user-status)\n Filter by status. Accepts a filter operator, for example `in:active,blocked`. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: User[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `after` and `before` are mutually exclusive.\n\n## Retrieve User\n\n### Endpoint\n\nRetrieve a single user.\n\n```http\nGET /v1/users/:user_id\n```\n\n### Path Parameters\n\n- **`user_id`** `string` -- **Required**\n Unique identifier of the user.\n\n### Response\n\nUser retrieved\n\n```ts\n{\n message: string;\n data: User;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can retrieve your own user, or a user who is a member of your team.\n\n## Update User\n\n### Endpoint\n\nUpdate a user's profile and preferences.\n\n```http\nPOST /v1/users/:user_id\n```\n\n### Path Parameters\n\n- **`user_id`** `string` -- **Required**\n Unique identifier of the user.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the user. Optional. Minimum length: `2`. Maximum length: `60`.\n\n- **`default_team_id`** `string`\n ID of the team the user lands in after signing in. Optional.\n\n- **`profile_url`** `string`\n Profile image as a JPEG or PNG base64 data URI. Optional.\n\n- **`email`** `string (email)`\n Email address of the user. Optional.\n\n- **`monitor_auto_subscribe`** `boolean`\n Whether the user is subscribed to new monitors automatically. Optional.\n\n- **`monitor_cooloff_minutes`** `integer`\n Minutes to wait before sending another notification for the same monitor. Between 30 and 1440. Optional. Minimum: `30`. Maximum: `1440`.\n\n- **`product_update_notifications`** `boolean`\n Whether the user receives product update emails. Optional.\n\n- **`appearance_palette`** `string`\n Color palette used by the dashboard. Optional. Allowed values: `\"light\"`, `\"dark\"`, `\"dim\"`, `\"midnight\"`, `\"paper\"`.\n\n- **`appearance_type_set`** `string`\n Font pairing used by the dashboard. Optional. Allowed values: `\"system\"`, `\"grotesk\"`, `\"editorial\"`, `\"geometric\"`.\n\n- **`appearance_accent`** `string`\n Accent color used by the dashboard. Optional. Allowed values: `\"orange\"`, `\"azure\"`, `\"burgundy\"`, `\"ink\"`, `\"emerald\"`, `\"violet\"`.\n\n- **`appearance_scale`** `string`\n Interface density used by the dashboard. Optional. Allowed values: `\"comfortable\"`, `\"cozy\"`, `\"compact\"`.\n\n- **`default_ai_model`** [`AiChatSelection`](/api/users#ai-chat-selection)\n Model a new chat opens on. Null clears the preference. An unavailable selection asks the member to choose again; it never changes who pays automatically. Optional.\n\n- **`default_ai_effort`** [`AiEffortLevel`](/api/users#ai-effort-level)\n How hard the assistant is asked to work on each turn by default. Optional.\n\n- **`role_id`** `string`\n ID of the role that grants the user their permissions. Optional.\n\n- **`default_auth_method`** [`AuthMethod`](/api/users#auth-method)\n Method the user signs in with by default. Optional.\n\n- **`status`** [`UserStatus`](/api/users#user-status)\n Status of the user. Optional.\n\n### Response\n\nUser updated\n\n```ts\n{\n message: string;\n data: User;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can only update your own profile.\n- `email` cannot be changed here. Sending a different address returns an error.\n- `role_id` and `status` are ignored here. Manage a user's role through the team endpoints.\n- `default_team_id` must be a team the user is already a member of.\n- Setting `default_auth_method` to `password` requires password authentication and MFA to be enabled first.\n- `profile_url` images must be at least 256x256 pixels. They are resized and stored as PNG, and the response returns the hosted image URL.\n- `profile_url` images must not exceed 5 MB.\n\n## Enable Password Authentication\n\n### Endpoint\n\nEnable password authentication for a user.\n\n```http\nPOST /v1/users/:user_id/enable_password\n```\n\n### Path Parameters\n\n- **`user_id`** `string` -- **Required**\n Unique identifier of the user.\n\n### Response\n\nPassword auth has been enabled\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can only enable password authentication for your own user.\n- MFA must be enabled before password authentication can be turned on.\n\n## List User Teams\n\n### Endpoint\n\nRetrieve a list of teams a user belongs to.\n\n```http\nGET /v1/users/:user_id/teams\n```\n\n### Path Parameters\n\n- **`user_id`** `string` -- **Required**\n Unique identifier of the user.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the teams. Optional. Defaults to `\"name\"`. Allowed values: `\"created_at\"`, `\"name\"`.\n\n- **`name`** [`TextFilter`](/api/users#text-filter)\n Filter by team name, written as `operator:value`. For example `contains:acme` or `equals:Acme Inc`. Optional.\n\n- **`status`** [`TeamStatus`](/api/teams#team-status)\n Filter by team status, written as `operator:value`. For example `equals:active` or `in:active,delinquent`. Optional.\n\n- **`billing_plan`** [`BillingPlan`](/api/teams#billing-plan)\n Filter by billing plan, written as `operator:value`. For example `in:pro_v1,enterprise_v1`. Optional.\n\n- **`created_at`** [`DateFilter`](/api/users#date-filter)\n Filter by creation date, written as `operator:value`. For example `gt:2026-01-01` or `between:2026-01-01,2026-02-01`. Optional.\n\n- **`deleted_at`** [`NullableDateFilter`](/api/users#nullable-date-filter)\n Filter by scheduled deletion date. Use `null` for teams that are not scheduled for deletion, `not:null` for teams that are. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Team[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can only list your own teams.\n- `after` and `before` are mutually exclusive.\n\n",
|
|
31
31
|
"api/roles": "# Roles\n\n## Role Model\n\n### Fields\n\n- **`object`** `\"role\"`\n\n- **`id`** `string`\n\n- **`name`** `string`\n\n- **`team_id`** `string`\n\n- **`scopes`** [`Scopes`](/api/roles#scopes)\n\n The permissions the role grants: each resource mapped to its allowed actions (`read`, `write`,\n `delete`).\n\n### Referenced Types\n\n#### Scopes\n\n`Record<string, (\"read\" | \"write\" | \"delete\")[]>`\n\nScope names mapped to their permitted actions. Allowed scope keys: `\"users\"`, `\"roles\"`, `\"keys\"`, `\"records\"`, `\"projects\"`, `\"metrics\"`, `\"views\"`, `\"billing\"`, `\"pages\"`, `\"monitors\"`, `\"alerts\"`, `\"chats\"`, `\"secrets\"`, `\"servers\"`, `\"ingest_keys\"`, `\"sources\"`, `\"domains\"`, `\"facets\"`, `\"drains\"`, `\"pulls\"`, `\"checks\"`, `\"logs\"`. Allowed action values: `\"read\"`, `\"write\"`, `\"delete\"`.\n\n#### Scope\n\n`\"users\" | \"roles\" | \"keys\" | \"records\" | \"projects\" | \"metrics\" | \"views\" | \"billing\" | \"pages\" | \"monitors\" | \"alerts\" | \"chats\" | \"secrets\" | \"servers\" | \"ingest_keys\" | \"sources\" | \"domains\" | \"facets\" | \"drains\" | \"pulls\" | \"checks\" | \"logs\"`\n\n#### ScopeValue\n\n`\"read\" | \"write\" | \"delete\"`\n\n## List Roles\n\n### Endpoint\n\nRetrieve a list of roles for the current team.\n\n```http\nGET /v1/roles\n```\n\n**Scope:** `roles:read`\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the roles. Optional. Defaults to `\"name\"`. Allowed values: `\"name\"`.\n\n- **`limit`** `number`\n Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Optional. Defaults to `\"asc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Role[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Role\n\n### Endpoint\n\nRetrieve a single role.\n\n```http\nGET /v1/roles/:role_id\n```\n\n**Scope:** `roles:read`\n\n### Path Parameters\n\n- **`role_id`** `string` -- **Required**\n Unique identifier of the role.\n\n### Response\n\nRole retrieved\n\n```ts\n{\n message: string;\n data: Role;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create Role\n\n### Endpoint\n\nCreate a new role for the current team.\n\n```http\nPOST /v1/roles\n```\n\n**Scope:** `roles:write`\n\n### Request Body\n\n- **`id`** `string`\n Custom identifier for the role. One is generated when omitted. Optional.\n\n- **`name`** `string` -- **Required**\n Display name for the role. Minimum length: `2`. Maximum length: `60`.\n\n- **`scopes`** [`Scopes`](/api/roles#scopes)\n Scope names mapped to their permitted actions. Optional. Allowed scope keys: `\"users\"`, `\"roles\"`, `\"keys\"`, `\"records\"`, `\"projects\"`, `\"metrics\"`, `\"views\"`, `\"billing\"`, `\"pages\"`, `\"monitors\"`, `\"alerts\"`, `\"chats\"`, `\"secrets\"`, `\"servers\"`, `\"ingest_keys\"`, `\"sources\"`, `\"domains\"`, `\"facets\"`, `\"drains\"`, `\"pulls\"`, `\"checks\"`, `\"logs\"`. Allowed action values: `\"read\"`, `\"write\"`, `\"delete\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Role;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `id` cannot be `role_owner`, which is reserved.\n- Omit `id` and one is generated for you.\n- You can assign only scopes that your own authorization has.\n- The built-in `role_owner` role cannot be created, updated, or deleted.\n- Human roles must include `projects:read`.\n\n## Update Role\n\n### Endpoint\n\nUpdate an existing role.\n\n```http\nPOST /v1/roles/:role_id\n```\n\n**Scope:** `roles:write`\n\n### Path Parameters\n\n- **`role_id`** `string` -- **Required**\n Unique identifier of the role.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the role. Optional. Minimum length: `2`. Maximum length: `60`.\n\n- **`scopes`** [`Scopes`](/api/roles#scopes)\n Scope names mapped to their permitted actions. Optional. Allowed scope keys: `\"users\"`, `\"roles\"`, `\"keys\"`, `\"records\"`, `\"projects\"`, `\"metrics\"`, `\"views\"`, `\"billing\"`, `\"pages\"`, `\"monitors\"`, `\"alerts\"`, `\"chats\"`, `\"secrets\"`, `\"servers\"`, `\"ingest_keys\"`, `\"sources\"`, `\"domains\"`, `\"facets\"`, `\"drains\"`, `\"pulls\"`, `\"checks\"`, `\"logs\"`. Allowed action values: `\"read\"`, `\"write\"`, `\"delete\"`.\n\n### Response\n\nYour role has been updated.\n\n```ts\n{\n message: string;\n data: Role;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A role's `id` cannot be changed. Every user assignment points at it, so a rename would orphan them.\n- You can assign only scopes that your own authorization has.\n- The built-in `role_owner` role cannot be created, updated, or deleted.\n- Human roles must include `projects:read`.\n\n## Delete Role\n\n### Endpoint\n\nDelete a role from the current team.\n\n```http\nDELETE /v1/roles/:role_id\n```\n\n**Scope:** `roles:delete`\n\n### Path Parameters\n\n- **`role_id`** `string` -- **Required**\n Unique identifier of the role.\n\n### Response\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Roles with assigned users cannot be deleted. Reassign those users before deleting the role.\n- The built-in `role_owner` role cannot be created, updated, or deleted.\n\n",
|
|
32
32
|
"api/keys": "# API Keys\n\n## API Key Model\n\n### Fields\n\n- **`object`** `\"key\"`\n\n- **`id`** `string`\n\n- **`last4`** `string`\n\n The last four characters of the key, for telling keys apart. The full secret is only returned\n at creation.\n\n- **`created_at`** [`ISODateString`](/api/keys#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/keys#iso-date-string)\n\n- **`scopes`** [`Scopes`](/api/keys#scopes)\n\n What the key may do: each resource mapped to its allowed actions (`read`, `write`, `delete`).\n\n- **`team_id`** `string`\n\n- **`name`** `string`\n\n- **`updated_by`** `string | null`\n\n Who last changed the key, including at creation.\n\n- **`last_used_at`** [`ISODateString | null`](/api/keys#iso-date-string)\n\n When the key last authenticated a request, accurate to five minutes. Null until it is used. A\n key created before Tailglow began recording key use shows null until its next request.\n\n### Referenced Types\n\n#### Scopes\n\n`Record<string, (\"read\" | \"write\" | \"delete\")[]>`\n\nScope names mapped to their permitted actions. Allowed scope keys: `\"users\"`, `\"roles\"`, `\"keys\"`, `\"records\"`, `\"projects\"`, `\"metrics\"`, `\"views\"`, `\"billing\"`, `\"pages\"`, `\"monitors\"`, `\"alerts\"`, `\"chats\"`, `\"secrets\"`, `\"servers\"`, `\"ingest_keys\"`, `\"sources\"`, `\"domains\"`, `\"facets\"`, `\"drains\"`, `\"pulls\"`, `\"checks\"`, `\"logs\"`. Allowed action values: `\"read\"`, `\"write\"`, `\"delete\"`.\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### Scope\n\n`\"users\" | \"roles\" | \"keys\" | \"records\" | \"projects\" | \"metrics\" | \"views\" | \"billing\" | \"pages\" | \"monitors\" | \"alerts\" | \"chats\" | \"secrets\" | \"servers\" | \"ingest_keys\" | \"sources\" | \"domains\" | \"facets\" | \"drains\" | \"pulls\" | \"checks\" | \"logs\"`\n\n#### ScopeValue\n\n`\"read\" | \"write\" | \"delete\"`\n\n## List API Keys\n\n### Endpoint\n\nRetrieve a list of API keys for the current team.\n\n```http\nGET /v1/keys\n```\n\n**Scope:** `keys:read`\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the API keys. Optional. Defaults to `\"created_at\"`. Allowed values: `\"created_at\"`, `\"name\"`.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Key[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `after` and `before` are mutually exclusive.\n\n## Retrieve API Key\n\n### Endpoint\n\nRetrieve a single API key.\n\n```http\nGET /v1/keys/:key_id\n```\n\n**Scope:** `keys:read`\n\n### Path Parameters\n\n- **`key_id`** `string` -- **Required**\n Unique identifier of the key.\n\n### Response\n\nKey retrieved\n\n```ts\n{\n message: string;\n data: Key;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create API Key\n\n### Endpoint\n\nCreate a new API key for the current team.\n\n```http\nPOST /v1/keys\n```\n\n**Scope:** `keys:write`\n\n### Request Body\n\n- **`name`** `string` -- **Required**\n Display name for the API key. Minimum length: `2`. Maximum length: `60`.\n\n- **`scopes`** [`Scopes`](/api/keys#scopes)\n Scope names mapped to their permitted actions. Optional. Allowed scope keys: `\"users\"`, `\"roles\"`, `\"keys\"`, `\"records\"`, `\"projects\"`, `\"metrics\"`, `\"views\"`, `\"billing\"`, `\"pages\"`, `\"monitors\"`, `\"alerts\"`, `\"chats\"`, `\"secrets\"`, `\"servers\"`, `\"ingest_keys\"`, `\"sources\"`, `\"domains\"`, `\"facets\"`, `\"drains\"`, `\"pulls\"`, `\"checks\"`, `\"logs\"`. Allowed action values: `\"read\"`, `\"write\"`, `\"delete\"`.\n\n### Response\n\nYou'll only be able to see your API key one time.\n\n```ts\n{\n message: string;\n data: Key & { api_key: string };\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n#### Additional Response Fields\n\n- **`api_key`** `string` -- **Required**\n The full API key. Returned once, when the key is created, and never again. Store it at that point; afterwards only `last4` is available.\n\n### Comments\n\n- You can assign only scopes that your own authorization has.\n\n## Update API Key\n\n### Endpoint\n\nUpdate an existing API key.\n\n```http\nPOST /v1/keys/:key_id\n```\n\n**Scope:** `keys:write`\n\n### Path Parameters\n\n- **`key_id`** `string` -- **Required**\n Unique identifier of the key.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the API key. Optional.\n\n- **`scopes`** [`Scopes`](/api/keys#scopes)\n Scope names mapped to their permitted actions. Optional. Allowed scope keys: `\"users\"`, `\"roles\"`, `\"keys\"`, `\"records\"`, `\"projects\"`, `\"metrics\"`, `\"views\"`, `\"billing\"`, `\"pages\"`, `\"monitors\"`, `\"alerts\"`, `\"chats\"`, `\"secrets\"`, `\"servers\"`, `\"ingest_keys\"`, `\"sources\"`, `\"domains\"`, `\"facets\"`, `\"drains\"`, `\"pulls\"`, `\"checks\"`, `\"logs\"`. Allowed action values: `\"read\"`, `\"write\"`, `\"delete\"`.\n\n### Response\n\nYour API key has been updated.\n\n```ts\n{\n message: string;\n data: Key;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can assign only scopes that your own authorization has.\n\n## Delete API Key\n\n### Endpoint\n\nDelete an API key from the current team.\n\n```http\nDELETE /v1/keys/:key_id\n```\n\n### Path Parameters\n\n- **`key_id`** `string` -- **Required**\n Unique identifier of the key.\n\n### Response\n\nYour API key has been deleted.\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Deleting a key needs `keys:delete`, except that a key can always delete itself. `tglow logout` uses this to remove the key it signed in with.\n\n",
|
|
33
33
|
"api/domains": "# Domains\n\n## Domain Model\n\n### Fields\n\n- **`object`** `\"domain\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `dom_`.\n\n- **`team_id`** `string`\n\n- **`domain`** `string`\n\n The hostname the customer added, stored lowercase.\n\n- **`ownership_type`** [`DnsRecordType`](/api/domains#dns-record-type)\n\n The record proving the customer controls this hostname. Always a TXT at\n `_tailglow-verify.{domain}`, required even for a domain with no purpose, which needs nothing\n else. Verification is per-hostname: it proves nothing about the parent domain or a sibling.\n\n- **`ownership_name`** `string`\n\n- **`ownership_value`** `string`\n\n- **`ownership_status`** [`DnsRecordStatus`](/api/domains#dns-record-status)\n\n What the last lookup found. `unchecked` means no lookup has run yet, which is different from\n `missing` (looked up, definitively not there).\n\n- **`ownership_actual`** `string | null`\n\n What DNS actually returned. Null when the record is absent or has not been looked up.\n\n- **`ownership_checked_at`** [`ISODateString | null`](/api/domains#iso-date-string)\n\n- **`ownership_verified`** `boolean`\n\n Whether Tailglow currently treats the ownership record as good. True while it resolves, and it\n stays true for 24 hours after it stops so a brief DNS problem does not unverify a working\n domain.\n\n- **`ownership_failed_at`** [`ISODateString | null`](/api/domains#iso-date-string)\n\n When the ownership record had been missing long enough that Tailglow stops serving the domain.\n Null while ownership holds, including during the grace period that follows a failed check.\n\n- **`routing_type`** [`DnsRecordType | null`](/api/domains#dns-record-type)\n\n The record routing traffic to Tailglow, or null while the domain has no purpose and so asks for\n no record. Always a CNAME at the hostname itself.\n\n- **`routing_name`** `string | null`\n\n- **`routing_value`** `string | null`\n\n- **`routing_status`** [`DnsRecordStatus`](/api/domains#dns-record-status)\n\n What the last lookup found. A routing record whose target has since moved reads as `unchecked`:\n the stored answer described the old target, so it says nothing about the current one.\n\n- **`routing_actual`** `string | null`\n\n- **`routing_checked_at`** [`ISODateString | null`](/api/domains#iso-date-string)\n\n- **`routing_verified`** `boolean`\n\n Whether Tailglow currently treats the routing record as good. True while it resolves, and it\n stays true for seven days after it stops so a DNS change in progress does not take a live\n domain out of service.\n\n- **`certificate_status`** [`CertificateStatus`](/api/domains#certificate-status)\n\n Progress of the TLS certificate Tailglow provisions once routing is verified.\n\n- **`certificate_error`** `string | null`\n\n Why the last certificate attempt failed; null unless `certificate_status` is `failed`.\n\n- **`certificate_provisioned_at`** [`ISODateString | null`](/api/domains#iso-date-string)\n\n When the current certificate was issued.\n\n- **`purpose`** [`DomainTrafficPurpose | null`](/api/domains#domain-traffic-purpose)\n\n What this domain serves, or null when it serves nothing yet. A domain serves ingest traffic or\n status pages, never both.\n\n- **`is_sso_enabled`** `boolean`\n\n Whether members with an email at this domain sign in through SSO. Adds no DNS record.\n\n- **`lifecycle_action`** [`DomainLifecycleAction | null`](/api/domains#domain-lifecycle-action)\n\n Infrastructure work Tailglow is currently doing for this domain; null when idle.\n\n- **`lifecycle_status`** [`DomainLifecycleStatus | null`](/api/domains#domain-lifecycle-status)\n\n How that work is progressing.\n\n- **`lifecycle_error`** `string | null`\n\n Why the last attempt failed. Tailglow retries automatically.\n\n- **`last_checked_at`** [`ISODateString | null`](/api/domains#iso-date-string)\n\n When this domain was last checked. A domain can be checked once every 30 seconds; per-record\n timing lives on `ownership_checked_at` and `routing_checked_at`.\n\n- **`created_at`** [`ISODateString`](/api/domains#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/domains#iso-date-string)\n\n- **`updated_by`** `string | null`\n\n The user who last changed this domain.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### DnsRecordType\n\n`\"txt\" | \"cname\"`\n\n#### DnsRecordStatus\n\n`\"unchecked\" | \"valid\" | \"invalid\" | \"missing\" | \"unavailable\"`\n\n#### CertificateStatus\n\n`\"none\" | \"provisioning\" | \"active\" | \"failed\"`\n\n#### DomainTrafficPurpose\n\n`\"ingest\" | \"pages\"`\n\nWhat a domain serves. Stored on the domain.\n\n#### DomainLifecycleAction\n\n`\"attach\" | \"reconcile\" | \"disable_ingest\" | \"disable_pages\" | \"delete_domain\" | \"delete_team\"`\n\n#### DomainLifecycleStatus\n\n`\"pending\" | \"processing\" | \"failed\"`\n\n## List Domains\n\n### Endpoint\n\nRetrieve a list of custom domains for the current team.\n\n```http\nGET /v1/domains\n```\n\n**Scope:** `domains:read`\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the domains. Optional. Defaults to `\"created_at\"`. Allowed values: `\"created_at\"`, `\"domain\"`, `\"certificate_status\"`.\n\n- **`ownership_verified`** `string`\n Filter domains by ownership verification state. Optional. Allowed values: `\"true\"`, `\"false\"`.\n\n- **`certificate_status`** [`CertificateStatus`](/api/domains#certificate-status)\n Filter domains by the status of their TLS certificate. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Domain[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `ownership_verified` accepts the strings `true` and `false`. Any other value is rejected.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Domain\n\n### Endpoint\n\nRetrieve a single custom domain, including the DNS records needed to verify and route it.\n\n```http\nGET /v1/domains/:domain_id\n```\n\n**Scope:** `domains:read`\n\n### Path Parameters\n\n- **`domain_id`** `string` -- **Required**\n Unique identifier of the domain.\n\n### Response\n\nDomain retrieved\n\n```ts\n{\n message: string;\n data: Domain;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create Domain\n\n### Endpoint\n\nAdd a custom domain to the current team.\n\n```http\nPOST /v1/domains\n```\n\n**Scope:** `domains:write`\n\n### Request Body\n\n- **`domain`** `string` -- **Required**\n The custom domain to add, for example `analytics.example.com`. Stored in lowercase. Minimum length: `1`. Maximum length: `255`.\n\n- **`purpose`** [`DomainTrafficPurpose`](/api/domains#domain-traffic-purpose)\n Traffic purpose to configure for this domain. Use `ingest` to receive analytics data, or `pages` to serve status pages. Leave it out to add the domain without a purpose and choose one later. Optional.\n\n### Response\n\n```ts\n{\n message: string;\n data: Domain;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The number of custom domains a team can add is limited by its billing plan.\n- A hostname can only be registered once. Adding one that already exists returns a conflict.\n- Some hostnames are reserved and cannot be added.\n- `domain` cannot contain a wildcard (`*`). Add each hostname you want to serve as its own domain.\n- `domain` must be a valid hostname such as `analytics.example.com`. Labels may contain letters, digits and hyphens, cannot start or end with a hyphen, and the top level domain must be at least two letters.\n\n## Check Domain DNS\n\n### Endpoint\n\nLook up every DNS record this domain needs and report what is currently there.\n\n```http\nPOST /v1/domains/:domain_id/check\n```\n\n**Scope:** `domains:write`\n\n### Path Parameters\n\n- **`domain_id`** `string` -- **Required**\n Unique identifier of the domain.\n\n### Response\n\n```ts\n{\n message: string;\n data: Domain;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A check that completes always returns 200. A record that does not resolve is a result, not an error, so read the outcome from `ownership_status` and `routing_status` rather than the status code.\n- Ownership is checked first. Routing is only checked once ownership has been verified and the domain has a traffic purpose enabled.\n- A record that has never been looked up reports `unchecked`, which is not the same as `missing`.\n- A domain can be checked at most once every 30 seconds.\n\n## Update Domain\n\n### Endpoint\n\nUpdate a domain's traffic purpose or SSO assignment.\n\n```http\nPOST /v1/domains/:domain_id\n```\n\n**Scope:** `domains:write`\n\n### Path Parameters\n\n- **`domain_id`** `string` -- **Required**\n Unique identifier of the domain.\n\n### Request Body\n\n- **`purpose`** [`DomainTrafficPurpose`](/api/domains#domain-traffic-purpose)\n What this domain serves. Ownership must be verified before a purpose can be set, and `null` takes the domain out of service. Omit to leave it unchanged. Optional.\n\n- **`is_sso_enabled`** `boolean`\n Whether SSO is assigned to this domain. Optional.\n\n### Response\n\n```ts\n{\n message: string;\n data: Domain;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Domain ownership must be verified before a purpose can be set.\n- A domain serves either ingest traffic or pages. Send `purpose: null` to take it out of service before setting the other, rather than switching between them directly.\n- Setting `purpose` to null queues infrastructure teardown. The domain keeps reporting its old purpose until that work completes.\n- Only one lifecycle operation runs at a time. If another is in progress, retry shortly.\n\n## Delete Domain\n\n### Endpoint\n\nDelete a custom domain from the current team.\n\n```http\nDELETE /v1/domains/:domain_id\n```\n\n**Scope:** `domains:delete`\n\n### Path Parameters\n\n- **`domain_id`** `string` -- **Required**\n Unique identifier of the domain.\n\n### Response\n\n```ts\n{\n message: string;\n data: null;\n status: 202;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Deletion runs in the background. The domain remains listed with a pending lifecycle state until its edge configuration has been removed.\n\n",
|
|
34
|
-
"api/servers": "# Servers\n\n## Server Model\n\n### Fields\n\n- **`object`** `\"server\"`\n\n- **`id`** `string`\n\n- **`project_id`** `string`\n\n- **`project_name`** `string`\n\n Display name of that project.\n\n- **`team_id`** `string`\n\n- **`name`** `string | null`\n\n Generated friendly name, for example `swift-hawk`.\n\n- **`sku`** `string`\n\n The server's size, as a named bundle of CPU and memory, for example `v1-1cpu-2gb`.\n\n- **`status`** [`ServerStatus`](/api/servers#server-status)\n\n Lifecycle state: `provisioning` until the server first comes up, then `active`. A paused\n free-trial server is `draining` while it finishes the data it accepted, then `idle`. A server\n removed by scaling down is `terminating` until it has finished that data and been deleted.\n\n- **`provisioning_phase`** [`ServerProvisioningPhase | null`](/api/servers#server-provisioning-phase)\n\n Why provisioning is still in flight (e.g. \"awaiting_capacity\"); null once active.\n\n- **`ordinal`** `number`\n\n The server's stable position in the project's fleet, starting at 0.\n\n- **`created_at`** [`ISODateString`](/api/servers#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/servers#iso-date-string)\n\n- **`heartbeat_at`** [`ISODateString | null`](/api/servers#iso-date-string)\n\n When the server last reported its vitals. Servers report every 30 seconds; null means it has\n never reported.\n\n- **`cpu_percent`** `number`\n\n CPU usage the server last reported, from 0 to 100. Zero until the first report.\n\n- **`memory_percent`** `number`\n\n Memory usage the server last reported, from 0 to 100. Zero until the first report.\n\n- **`spool_percent`** `number | null`\n\n Actual PVC fullness 0-100, derived as max(bytes, inodes), or null before the first heartbeat.\n\n- **`spool_bytes_percent`** `number | null`\n\n Actual PVC byte fullness 0-100, or null before the first heartbeat.\n\n- **`spool_inodes_percent`** `number | null`\n\n Actual PVC inode (file-count) fullness 0-100, or null before the first heartbeat.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### ServerStatus\n\n`\"provisioning\" | \"active\" | \"upgrading\" | \"draining\" | \"idle\" | \"terminating\"`\n\n#### ServerProvisioningPhase\n\n`\"allocating\" | \"awaiting_capacity\" | \"attaching_storage\" | \"pulling_image\" | \"starting\"`\n\n## Server Event Model\n\n### Fields\n\n- **`object`** `\"server_rollout_event\"`\n\n- **`id`** `string`\n\n- **`project_id`** `string`\n\n- **`started_at`** [`ISODateString`](/api/servers#iso-date-string)\n\n When the deploy of the project's servers began.\n\n- **`completed_at`** [`ISODateString | null`](/api/servers#iso-date-string)\n\n When the deploy finished; null while it is still in progress.\n\n- **`image_tag`** `string | null`\n\n Version tag the deploy moves the project's servers to.\n\n- **`phase`** `string | null`\n\n The most recent step the deploy reached; null before the first step is recorded.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n## List Servers\n\n### Endpoint\n\nRetrieve a list of servers in a project.\n\n```http\nGET /v1/projects/:project_id/servers\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the servers. Optional. Defaults to `\"created_at\"`. Allowed values: `\"created_at\"`, `\"status\"`.\n\n- **`status`** [`ServerStatus`](/api/servers#server-status)\n Filter by server status. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Server[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A server is a dedicated machine that receives and processes the project's data. Every project runs its own fleet.\n- A server that is still coming up is returned with a status of `provisioning`, and a server on its way out with a status of `terminating`.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Server\n\n### Endpoint\n\nRetrieve a single server.\n\n```http\nGET /v1/projects/:project_id/servers/:server_id\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`server_id`** `string` -- **Required**\n Unique identifier of the server.\n\n### Response\n\nServer retrieved\n\n```ts\n{\n message: string;\n data: Server;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Scale Servers\n\n### Endpoint\n\nScale a project's fleet to a desired total number of servers.\n\n```http\nPOST /v1/projects/:project_id/servers\n```\n\n**Scope:** `servers:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Request Body\n\n- **`count`** `integer` -- **Required**\n Total number of servers the project should run after this request. This is the desired total, not the number of servers to add or remove. The maximum is the team's per-project server limit, which is set by its plan. Minimum: `0`.\n\n### Response 200\n\n```ts\n{\n message: string;\n data: Server[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Response 202\n\n```ts\n{\n message: string;\n data: Server[];\n status: 202;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `count` is the total number of servers the project should end up running, not the number to add. A project already running two servers reaches three by sending `count: 3`.\n- Scaling down is the same call with a lower `count`, and `count: 0` takes the whole fleet down. Tailglow chooses which servers to retire and drains them first, so they stay in the response with a status of `terminating` until they finish.\n- There is no endpoint that deletes an individual server. Removing capacity is always a scale request with a lower `count`.\n- Scaling up requires the team to have a payment method, and `count` must stay within the team's per-project server limit.\n- The response is the project's whole fleet after the request, not only the servers that changed.\n- Requesting the count the project already runs, with nothing in flight, returns a `409`.\n- During a platform update, a scale request is queued and answered with a `202`; it is applied once the update finishes, and the project's `queued_server_count` shows it meanwhile. A project that has no servers yet still gets its first server immediately.\n- Adding servers to a project that has not finished moving to the current platform version is queued the same way, even with no update in progress. Removing servers is not held back by it.\n- Requesting the count the project currently runs while a change is queued cancels the queued change, or returns a `409` if that change has already started. A newer request replaces an older queued one.\n\n## Update Server\n\n### Endpoint\n\nRename a server.\n\n```http\nPOST /v1/projects/:project_id/servers/:server_id\n```\n\n**Scope:** `servers:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`server_id`** `string` -- **Required**\n Unique identifier of the server.\n\n### Request Body\n\n- **`name`** `string` -- **Required**\n Display name for the server. Minimum length: `1`. Maximum length: `64`.\n\n### Response\n\nServer updated\n\n```ts\n{\n message: string;\n data: Server;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `name` is the only editable field on a server. It is a display label and does not change how data is routed or how much capacity the server has.\n- Renaming never adds or removes servers. Use `POST /v1/projects/:project_id/servers` to change how many servers the project runs.\n\n## Retrieve Server Vitals\n\n### Endpoint\n\nRetrieve CPU, memory, and buffer usage for a server over a time range.\n\n```http\nGET /v1/projects/:project_id/servers/:server_id/vitals\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`server_id`** `string` -- **Required**\n Unique identifier of the server.\n\n### Query Parameters\n\n- **`start_at`** [`ISODateString`](/api/servers#iso-date-string)\n Start time (ISO format, default: 12 hours ago). Optional.\n\n- **`end_at`** [`ISODateString`](/api/servers#iso-date-string)\n End time (ISO format, default: now). Optional.\n\n- **`interval`** `string`\n Aggregation interval. Optional. Defaults to `\"minute\"`. Allowed values: `\"minute\"`, `\"hour\"`.\n\n### Response\n\nVitals retrieved\n\n```ts\n{\n message: string;\n data: TimeseriesAggregation;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The response carries Worker CPU, Worker Memory, Buffer, Bytes, and Inodes series, plus ingress CPU and memory when available, bucketed by `interval`.\n- Older points come from archived data and recent points come from the running server, stitched into one continuous timeline. A server replaced by a platform deploy does not break the series.\n- Without `start_at` and `end_at` the range is the last 12 hours.\n\n## Retrieve Server Pipeline\n\n### Endpoint\n\nRetrieve queue depth, throughput, and error counts for a server over a time range.\n\n```http\nGET /v1/projects/:project_id/servers/:server_id/pipeline\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`server_id`** `string` -- **Required**\n Unique identifier of the server.\n\n### Query Parameters\n\n- **`start_at`** [`ISODateString`](/api/servers#iso-date-string)\n Start time (ISO format, default: 12 hours ago). Optional.\n\n- **`end_at`** [`ISODateString`](/api/servers#iso-date-string)\n End time (ISO format, default: now). Optional.\n\n- **`interval`** `string`\n Aggregation interval. Optional. Defaults to `\"minute\"`. Allowed values: `\"minute\"`, `\"hour\"`.\n\n### Response\n\nPipeline retrieved\n\n```ts\n{\n message: string;\n data: TimeseriesAggregation;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Pending averages valid queued records, Throughput counts drained records, and Errors counts rejected requests and dropped frames. Older pending history without record counts is unknown.\n- Takes the same range and interval parameters as the vitals endpoint, so the two can be read over the same window.\n- Without `start_at` and `end_at` the range is the last 12 hours.\n\n## List Server Events\n\n### Endpoint\n\nRetrieve the platform events recorded for a server's project over a time range.\n\n```http\nGET /v1/projects/:project_id/servers/:server_id/events\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`server_id`** `string` -- **Required**\n Unique identifier of the server.\n\n### Query Parameters\n\n- **`start_at`** [`ISODateString`](/api/servers#iso-date-string) -- **Required**\n Earliest event date and time to return (ISO format).\n\n- **`end_at`** [`ISODateString`](/api/servers#iso-date-string) -- **Required**\n Latest event date and time to return (ISO format).\n\n- **`type`** `string`\n Filter by event type. Optional. Allowed values: `\"rollout\"`.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: ServerRolloutEvent[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- An event is a Tailglow deploy of the project's servers. Requesting events over the same window as the vitals and pipeline endpoints shows whether a change in those charts lines up with a deploy.\n- `start_at` and `end_at` are both required.\n- An event whose `completed_at` is `null` is still in progress.\n- Events belong to the project, so every server in the project returns the same list.\n- `after` and `before` are mutually exclusive.\n\n",
|
|
34
|
+
"api/servers": "# Servers\n\n## Server Model\n\n### Fields\n\n- **`object`** `\"server\"`\n\n- **`id`** `string`\n\n- **`project_id`** `string`\n\n- **`project_name`** `string`\n\n Display name of that project.\n\n- **`team_id`** `string`\n\n- **`name`** `string | null`\n\n Generated friendly name, for example `swift-hawk`.\n\n- **`sku`** `string`\n\n The server's size, as a named bundle of CPU and memory, for example `v1-1cpu-2gb`.\n\n- **`status`** [`ServerStatus`](/api/servers#server-status)\n\n Lifecycle state: `provisioning` until the server first comes up, then `active`. A server is\n `upgrading` while a platform update replaces it. A paused free-trial server is `draining` while\n it finishes the data it accepted, then `idle`. A server removed by scaling down is\n `terminating` until it has finished that data and been deleted.\n\n- **`provisioning_phase`** [`ServerProvisioningPhase | null`](/api/servers#server-provisioning-phase)\n\n Why provisioning is still in flight (e.g. \"awaiting_capacity\"); null once active.\n\n- **`ordinal`** `number`\n\n The server's stable position in the project's fleet, starting at 0.\n\n- **`is_deploy_surge`** `boolean`\n\n True for the temporary server a platform update adds. It takes each server's records while that\n server is replaced, is not billed, and is removed when the update finishes.\n\n- **`created_at`** [`ISODateString`](/api/servers#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/servers#iso-date-string)\n\n- **`heartbeat_at`** [`ISODateString | null`](/api/servers#iso-date-string)\n\n When the server last reported its vitals. Servers report every 30 seconds; null means it has\n never reported.\n\n- **`cpu_percent`** `number`\n\n CPU usage the server last reported, from 0 to 100. Zero until the first report.\n\n- **`memory_percent`** `number`\n\n Memory usage the server last reported, from 0 to 100. Zero until the first report.\n\n- **`spool_percent`** `number | null`\n\n Actual PVC fullness 0-100, derived as max(bytes, inodes), or null before the first heartbeat.\n\n- **`spool_bytes_percent`** `number | null`\n\n Actual PVC byte fullness 0-100, or null before the first heartbeat.\n\n- **`spool_inodes_percent`** `number | null`\n\n Actual PVC inode (file-count) fullness 0-100, or null before the first heartbeat.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### ServerStatus\n\n`\"provisioning\" | \"active\" | \"upgrading\" | \"draining\" | \"idle\" | \"terminating\"`\n\n#### ServerProvisioningPhase\n\n`\"allocating\" | \"awaiting_capacity\" | \"attaching_storage\" | \"pulling_image\" | \"starting\"`\n\n## Server Event Model\n\n### Fields\n\n- **`object`** `\"server_rollout_event\"`\n\n- **`id`** `string`\n\n- **`project_id`** `string`\n\n- **`started_at`** [`ISODateString`](/api/servers#iso-date-string)\n\n When the deploy of the project's servers began.\n\n- **`completed_at`** [`ISODateString | null`](/api/servers#iso-date-string)\n\n When the deploy finished; null while it is still in progress.\n\n- **`image_tag`** `string | null`\n\n Version tag the deploy moves the project's servers to.\n\n- **`phase`** `string | null`\n\n The most recent step the deploy reached; null before the first step is recorded.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n## List Servers\n\n### Endpoint\n\nRetrieve a list of servers in a project.\n\n```http\nGET /v1/projects/:project_id/servers\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the servers. Optional. Defaults to `\"created_at\"`. Allowed values: `\"created_at\"`, `\"status\"`.\n\n- **`status`** [`ServerStatus`](/api/servers#server-status)\n Filter by server status. Optional.\n\n- **`is_deploy_surge`** `string`\n Set true to list the temporary server a platform update adds instead of the project's own servers. Optional. Allowed values: `\"true\"`, `\"false\"`, `\"1\"`, `\"0\"`.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Server[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A server is a dedicated machine that receives and processes the project's data. Every project runs its own fleet.\n- A server that is still coming up is returned with a status of `provisioning`, and a server on its way out with a status of `terminating`.\n- During a platform update, the server being replaced is returned with a status of `upgrading`. The `status` filter matches the status stored before the update, so it finds that server under `active`.\n- A platform update also adds a temporary server that takes each server's records while that server is replaced. It is not billed and is left out of this list; `is_deploy_surge=true` lists it instead.\n- `is_deploy_surge` accepts `true`, `false`, `1` or `0`.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Server\n\n### Endpoint\n\nRetrieve a single server.\n\n```http\nGET /v1/projects/:project_id/servers/:server_id\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`server_id`** `string` -- **Required**\n Unique identifier of the server.\n\n### Response\n\nServer retrieved\n\n```ts\n{\n message: string;\n data: Server;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Scale Servers\n\n### Endpoint\n\nScale a project's fleet to a desired total number of servers.\n\n```http\nPOST /v1/projects/:project_id/servers\n```\n\n**Scope:** `servers:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Request Body\n\n- **`count`** `integer` -- **Required**\n Total number of servers the project should run after this request. This is the desired total, not the number of servers to add or remove. The maximum is the team's per-project server limit, which is set by its plan. Minimum: `0`.\n\n### Response 200\n\n```ts\n{\n message: string;\n data: Server[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Response 202\n\n```ts\n{\n message: string;\n data: Server[];\n status: 202;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `count` is the total number of servers the project should end up running, not the number to add. A project already running two servers reaches three by sending `count: 3`.\n- Scaling down is the same call with a lower `count`, and `count: 0` takes the whole fleet down. Tailglow chooses which servers to retire and drains them first, so they stay in the response with a status of `terminating` until they finish.\n- There is no endpoint that deletes an individual server. Removing capacity is always a scale request with a lower `count`.\n- Scaling up requires the team to have a payment method, and `count` must stay within the team's per-project server limit.\n- The response is the project's whole fleet after the request, not only the servers that changed.\n- Requesting the count the project already runs, with nothing in flight, returns a `409`.\n- During a platform update, a scale request is queued and answered with a `202`; it is applied once the update finishes, and the project's `queued_server_count` shows it meanwhile. A project that has no servers yet still gets its first server immediately.\n- Adding servers to a project that has not finished moving to the current platform version is queued the same way, even with no update in progress. Removing servers is not held back by it.\n- Requesting the count the project currently runs while a change is queued cancels the queued change, or returns a `409` if that change has already started. A newer request replaces an older queued one.\n\n## Update Server\n\n### Endpoint\n\nRename a server.\n\n```http\nPOST /v1/projects/:project_id/servers/:server_id\n```\n\n**Scope:** `servers:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`server_id`** `string` -- **Required**\n Unique identifier of the server.\n\n### Request Body\n\n- **`name`** `string` -- **Required**\n Display name for the server. Minimum length: `1`. Maximum length: `64`.\n\n### Response\n\nServer updated\n\n```ts\n{\n message: string;\n data: Server;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `name` is the only editable field on a server. It is a display label and does not change how data is routed or how much capacity the server has.\n- Renaming never adds or removes servers. Use `POST /v1/projects/:project_id/servers` to change how many servers the project runs.\n\n## Retrieve Server Vitals\n\n### Endpoint\n\nRetrieve CPU, memory, and buffer usage for a server over a time range.\n\n```http\nGET /v1/projects/:project_id/servers/:server_id/vitals\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`server_id`** `string` -- **Required**\n Unique identifier of the server.\n\n### Query Parameters\n\n- **`start_at`** [`ISODateString`](/api/servers#iso-date-string)\n Start time (ISO format, default: 12 hours ago). Optional.\n\n- **`end_at`** [`ISODateString`](/api/servers#iso-date-string)\n End time (ISO format, default: now). Optional.\n\n- **`interval`** `string`\n Aggregation interval. Optional. Defaults to `\"minute\"`. Allowed values: `\"minute\"`, `\"hour\"`.\n\n### Response\n\nVitals retrieved\n\n```ts\n{\n message: string;\n data: TimeseriesAggregation;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The response carries Worker CPU, Worker Memory, Buffer, Bytes, and Inodes series, plus ingress CPU and memory when available, bucketed by `interval`.\n- Older points come from archived data and recent points come from the running server, stitched into one continuous timeline. A server replaced by a platform deploy does not break the series.\n- Without `start_at` and `end_at` the range is the last 12 hours.\n\n## Retrieve Server Pipeline\n\n### Endpoint\n\nRetrieve queue depth, throughput, and error counts for a server over a time range.\n\n```http\nGET /v1/projects/:project_id/servers/:server_id/pipeline\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`server_id`** `string` -- **Required**\n Unique identifier of the server.\n\n### Query Parameters\n\n- **`start_at`** [`ISODateString`](/api/servers#iso-date-string)\n Start time (ISO format, default: 12 hours ago). Optional.\n\n- **`end_at`** [`ISODateString`](/api/servers#iso-date-string)\n End time (ISO format, default: now). Optional.\n\n- **`interval`** `string`\n Aggregation interval. Optional. Defaults to `\"minute\"`. Allowed values: `\"minute\"`, `\"hour\"`.\n\n### Response\n\nPipeline retrieved\n\n```ts\n{\n message: string;\n data: TimeseriesAggregation;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Pending averages valid queued records, Throughput counts drained records, and Errors counts rejected requests and dropped frames. Older pending history without record counts is unknown.\n- Takes the same range and interval parameters as the vitals endpoint, so the two can be read over the same window.\n- Without `start_at` and `end_at` the range is the last 12 hours.\n\n## List Server Events\n\n### Endpoint\n\nRetrieve the platform events recorded for a server's project over a time range.\n\n```http\nGET /v1/projects/:project_id/servers/:server_id/events\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`server_id`** `string` -- **Required**\n Unique identifier of the server.\n\n### Query Parameters\n\n- **`start_at`** [`ISODateString`](/api/servers#iso-date-string) -- **Required**\n Earliest event date and time to return (ISO format).\n\n- **`end_at`** [`ISODateString`](/api/servers#iso-date-string) -- **Required**\n Latest event date and time to return (ISO format).\n\n- **`type`** `string`\n Filter by event type. Optional. Allowed values: `\"rollout\"`.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: ServerRolloutEvent[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- An event is a Tailglow deploy of the project's servers. Requesting events over the same window as the vitals and pipeline endpoints shows whether a change in those charts lines up with a deploy.\n- `start_at` and `end_at` are both required.\n- An event whose `completed_at` is `null` is still in progress.\n- Events belong to the project, so every server in the project returns the same list.\n- `after` and `before` are mutually exclusive.\n\n",
|
|
35
35
|
"api/drains": "# Drains\n\n## Drain Model\n\n### Fields\n\n- **`object`** `\"drain\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `drn_`.\n\n- **`project_id`** `string`\n\n- **`team_id`** `string`\n\n- **`name`** `string`\n\n- **`collection_id`** `string | null`\n\n The collection whose records are forwarded. Null when the drain reads from a view.\n\n- **`view_id`** `string | null`\n\n The view whose records are forwarded. Null when the drain reads from a collection.\n\n- **`destination_url`** `string`\n\n The HTTPS URL records are delivered to.\n\n- **`headers_configured`** `string[]`\n\n Names of the headers sent with every delivery. The values are stored encrypted and are never\n returned, so this shows what was configured without revealing the secrets.\n\n- **`body_format`** [`DrainBodyFormat`](/api/drains#drain-body-format)\n\n The shape of each delivered request body.\n\n- **`compression`** [`DrainCompression`](/api/drains#drain-compression)\n\n The compression applied to each delivered request body.\n\n- **`backfill_enabled`** `boolean`\n\n Whether records that already existed when the drain first activated were delivered too. Decided\n once at first activation and cannot be changed afterwards.\n\n- **`block_on_failure`** `boolean | null`\n\n Wait for acknowledgement before sending later deliveries. Defaults to false. Null when this\n setting is not available for the drain.\n\n- **`exclude_fields`** `string[]`\n\n Top-level field names removed from every record before delivery, for destinations that reject\n fields they do not expect. Empty means nothing is removed.\n\n- **`status`** [`DrainStatus`](/api/drains#drain-status)\n\n Where the drain is in its lifecycle, from unverified through delivering or paused.\n\n- **`schedule_cron`** `string`\n\n Cron expression setting how often undelivered records are looked for, evaluated in UTC. A drain\n that is behind keeps delivering without waiting for the next run, so this controls how quickly\n new records are picked up rather than how fast a backlog clears.\n\n- **`next_send_at`** [`ISODateString | null`](/api/drains#iso-date-string)\n\n When the drain is next due to look for records. Null means no time is set rather than nothing\n scheduled: on an active drain it is due immediately, and on a stopped one it is ignored.\n Whether a drain runs at all is `status`.\n\n- **`verified_at`** [`ISODateString | null`](/api/drains#iso-date-string)\n\n When the destination was proven reachable. Null until the drain is verified.\n\n- **`last_success_at`** [`ISODateString | null`](/api/drains#iso-date-string)\n\n When the destination last accepted a delivery. Null if it never has.\n\n- **`last_failure_at`** [`ISODateString | null`](/api/drains#iso-date-string)\n\n When a delivery last failed. Null if none ever has.\n\n- **`consecutive_failure_count`** `number`\n\n Failed attempts since the last successful delivery.\n\n- **`last_error`** `string | null`\n\n What went wrong on the most recent failed delivery. Null if none ever has.\n\n- **`last_batch_size`** `number`\n\n How many records were in the most recent successful delivery.\n\n- **`records_sent_hourly`** `DrainHourlyRecords[]`\n\n The last 24 hours of delivery volume, oldest first. Always 24 entries, so an hour with no\n deliveries reads as a zero rather than being absent, and a drain that has never sent still\n returns a full window of zeros. Sum the counts for \"records sent in the last 24 hours\".\n\n- **`created_at`** [`ISODateString`](/api/drains#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/drains#iso-date-string)\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### DrainBodyFormat\n\n`\"ndjson\" | \"json\"`\n\n#### DrainCompression\n\n`\"none\" | \"gzip\"`\n\n#### DrainStatus\n\n`\"draft\" | \"pending_verification\" | \"active\" | \"paused\" | \"disabled\"`\n\n## List Drains\n\n### Endpoint\n\nRetrieve a list of drains for a project.\n\n```http\nGET /v1/projects/:project_id/drains\n```\n\n**Scope:** `drains:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field the results are ordered by. Optional. Defaults to `\"created_at\"`. Allowed values: `\"name\"`, `\"created_at\"`.\n\n- **`status`** [`DrainStatus`](/api/drains#drain-status)\n Return only drains in this state. Optional.\n\n- **`collection_id`** `string`\n Return only drains that read from this collection. Optional.\n\n- **`view_id`** `string`\n Return only drains that read from this view. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Drain[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Drain\n\n### Endpoint\n\nRetrieve a single drain.\n\n```http\nGET /v1/projects/:project_id/drains/:drain_id\n```\n\n**Scope:** `drains:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`drain_id`** `string` -- **Required**\n Unique identifier of the drain.\n\n### Response\n\nDrain retrieved\n\n```ts\n{\n message: string;\n data: Drain;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create Drain\n\n### Endpoint\n\nCreate a drain that forwards records from one collection or view to an external destination.\n\n```http\nPOST /v1/projects/:project_id/drains\n```\n\n**Scope:** `drains:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Request Body\n\n- **`name`** `string` -- **Required**\n Name shown for this drain. Minimum length: `2`. Maximum length: `128`.\n\n- **`collection_id`** `string | null`\n Collection whose records are forwarded. Set this or `view_id`, not both. Optional.\n\n- **`view_id`** `string | null`\n View whose records are forwarded. Set this or `collection_id`, not both. Optional.\n\n- **`destination_url`** `string` -- **Required**\n HTTPS URL records are delivered to. Must resolve to a public address. Minimum length: `1`. Maximum length: `2048`.\n\n- **`headers`** `object | null`\n Headers sent with every delivery, for authenticating to the destination. Stored encrypted and never returned; responses list only the header names, as `headers_configured`. Optional.\n\n- **`body_format`** [`DrainBodyFormat`](/api/drains#drain-body-format)\n Shape of each delivered request body. Optional.\n\n- **`compression`** [`DrainCompression`](/api/drains#drain-compression)\n Compression applied to each delivered request body. Optional.\n\n- **`backfill_enabled`** `boolean`\n Whether records that already existed when the drain first activates are delivered too. Decided once at first activation and cannot be changed afterwards. Optional.\n\n- **`block_on_failure`** `boolean`\n Wait for acknowledgement before sending later deliveries. Defaults to false. Check block_on_failure in the response for availability; null means the setting is unavailable. Optional.\n\n- **`exclude_fields`** `string[]`\n Top-level field names stripped from every record before delivery, for destinations that reject fields they do not expect. Optional.\n\n- **`schedule_cron`** `string`\n Cron expression setting how often undelivered records are looked for, evaluated in UTC. Defaults to hourly. A drain that is behind keeps delivering without waiting for the next run, so this sets how quickly new records are picked up, not how fast a backlog clears. Optional. Minimum length: `1`. Maximum length: `128`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Drain;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The destination must be reachable over HTTPS at a public address. Private, loopback and link-local addresses are rejected.\n- A new drain starts unverified and sends nothing. Verify it before it delivers records.\n- A destination that is Tailglow ingest or a domain this team has verified is trusted, so it skips verification and is activated on create.\n- A trusted destination that would write back into the same collection the drain reads from is rejected, because it would loop.\n- Exactly one of `collection_id` or `view_id` must be set. A drain reads from one container.\n- `headers` accepts at most 20 entries.\n- `exclude_fields` accepts at most 50 entries, each a top-level key of at most 256 characters. Dots, whitespace, wildcards and newlines are rejected. Entries are trimmed and duplicates removed.\n- `schedule_cron` accepts a five-field expression evaluated in UTC. Second-level precision and hashed fields are rejected; the shortest supported gap is one minute.\n\n## Update Drain\n\n### Endpoint\n\nUpdate a drain's name, payload format, compression, or excluded fields.\n\n```http\nPOST /v1/projects/:project_id/drains/:drain_id\n```\n\n**Scope:** `drains:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`drain_id`** `string` -- **Required**\n Unique identifier of the drain.\n\n### Request Body\n\n- **`name`** `string`\n Name shown for this drain. Optional. Minimum length: `2`. Maximum length: `128`.\n\n- **`body_format`** [`DrainBodyFormat`](/api/drains#drain-body-format)\n Shape of each delivered request body. Optional.\n\n- **`compression`** [`DrainCompression`](/api/drains#drain-compression)\n Compression applied to each delivered request body. Optional.\n\n- **`block_on_failure`** `boolean`\n Wait for acknowledgement before sending later deliveries. Defaults to false. Check block_on_failure in the response for availability; null means the setting is unavailable. Optional.\n\n- **`exclude_fields`** `string[]`\n Top-level field names stripped from every record before delivery, for destinations that reject fields they do not expect. Optional.\n\n- **`schedule_cron`** `string`\n Cron expression setting how often undelivered records are looked for, evaluated in UTC. Defaults to hourly. A drain that is behind keeps delivering without waiting for the next run, so this sets how quickly new records are picked up, not how fast a backlog clears. Optional. Minimum length: `1`. Maximum length: `128`.\n\n### Response\n\nDrain updated\n\n```ts\n{\n message: string;\n data: Drain;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `destination_url`, `headers` and `backfill_enabled` cannot be changed. Changing the URL or headers would invalidate the proof of access established at verification, and backfill is decided once when the drain first activates. Delete the drain and create a new one instead.\n- At least one field must be provided.\n- Any field not listed here is rejected, including `destination_url`, `headers` and `backfill_enabled`, which cannot be changed after create.\n- `exclude_fields` accepts at most 50 entries, each a top-level key of at most 256 characters. Dots, whitespace, wildcards and newlines are rejected. Entries are trimmed and duplicates removed.\n- `schedule_cron` accepts a five-field expression evaluated in UTC. Second-level precision and hashed fields are rejected; the shortest supported gap is one minute.\n\n## Delete Drain\n\n### Endpoint\n\nDelete a drain and stop all delivery to its destination.\n\n```http\nDELETE /v1/projects/:project_id/drains/:drain_id\n```\n\n**Scope:** `drains:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`drain_id`** `string` -- **Required**\n Unique identifier of the drain.\n\n### Response\n\nDrain deleted\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Verify Drain\n\n### Endpoint\n\nSend a verification marker to the drain's destination and return the destination's response.\n\n```http\nPOST /v1/projects/:project_id/drains/:drain_id/verify\n```\n\n**Scope:** `drains:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`drain_id`** `string` -- **Required**\n Unique identifier of the drain.\n\n### Response 200 (1)\n\nDrain auto-verified — destination is infrastructure you control.\n\n```ts\n{\n message: string;\n data: Drain;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Response 200 (2)\n\nVerification marker sent\n\n```ts\n{\n message: string;\n data: { drain: Drain; response: { status: number; status_text: string; ok: boolean; response_time_ms: number; body_preview: string; } };\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n#### Additional Response Fields\n\n- **`drain`** [`Drain`](/api/drains#model) -- **Required**\n The drain as it stands after the verification attempt.\n\n- **`response`** `{ status: number; status_text: string; ok: boolean; response_time_ms: number; body_preview: string; }` -- **Required**\n What the destination replied with, including a non-2xx status.\n\n### Comments\n\n- Proving control of the destination takes two steps. This one delivers a marker token to the destination; read that token from what the destination received and send it back to `verify_confirm`.\n- The marker token is never included in this response. Reading it from the destination is what proves access, so echoing it here would defeat the check.\n- A destination that has since become trusted infrastructure is activated immediately and returns the drain with no marker sent.\n- The `response` field reports what the destination replied with, including non-2xx statuses, so a misconfigured endpoint can be diagnosed without reading logs.\n\n## Confirm Drain Verification\n\n### Endpoint\n\nConfirm a drain by sending back the marker token the destination received.\n\n```http\nPOST /v1/projects/:project_id/drains/:drain_id/verify_confirm\n```\n\n**Scope:** `drains:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`drain_id`** `string` -- **Required**\n Unique identifier of the drain.\n\n### Request Body\n\n- **`token`** `string` -- **Required**\n The marker token the destination received from the verify request. Minimum length: `1`. Maximum length: `128`.\n\n### Response\n\nDrain verified\n\n```ts\n{\n message: string;\n data: Drain;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Send the token that arrived at the destination from `verify`. A correct token activates the drain and it begins delivering records.\n\n## Pause Drain\n\n### Endpoint\n\nPause a drain so it stops delivering records without losing its verification.\n\n```http\nPOST /v1/projects/:project_id/drains/:drain_id/pause\n```\n\n**Scope:** `drains:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`drain_id`** `string` -- **Required**\n Unique identifier of the drain.\n\n### Response\n\nDrain paused\n\n```ts\n{\n message: string;\n data: Drain;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A paused drain keeps its position, so resuming continues from where it stopped rather than re-sending or skipping records.\n\n## Resume Drain\n\n### Endpoint\n\nResume a paused drain and continue delivering records from where it stopped.\n\n```http\nPOST /v1/projects/:project_id/drains/:drain_id/resume\n```\n\n**Scope:** `drains:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`drain_id`** `string` -- **Required**\n Unique identifier of the drain.\n\n### Response\n\nDrain resumed\n\n```ts\n{\n message: string;\n data: Drain;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Send Drain Sample\n\n### Endpoint\n\nSend a sample payload to the drain's destination and return the destination's response.\n\n```http\nPOST /v1/projects/:project_id/drains/:drain_id/sample\n```\n\n**Scope:** `drains:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`drain_id`** `string` -- **Required**\n Unique identifier of the drain.\n\n### Response\n\n```ts\n{\n message: string;\n data: { status: number; status_text: string; ok: boolean; response_time_ms: number; body_preview: string };\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n#### Additional Response Fields\n\n- **`status`** `number` -- **Required**\n The HTTP status the destination replied with.\n\n- **`status_text`** `string` -- **Required**\n The status text that came with it.\n\n- **`ok`** `boolean` -- **Required**\n Whether the destination accepted the request.\n\n- **`response_time_ms`** `number` -- **Required**\n How long the destination took to reply, in milliseconds.\n\n- **`body_preview`** `string` -- **Required**\n The start of the destination's response body, for diagnosing a rejection.\n\n### Comments\n\n- Use this to check the destination accepts the payload shape before or after activating the drain. The sample is not part of the record stream and does not move the drain's position.\n- A non-2xx reply is returned rather than raised, so the status and body preview can be read directly from the response.\n- The sample is built from the drain's newest records, so while the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n\n",
|
|
36
36
|
"api/pulls": "# Pulls\n\n## Pull Model\n\n### Fields\n\n- **`object`** `\"pull\"`\n\n Always `\"pull\"`.\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `pul_`.\n\n- **`project_id`** `string`\n\n The project this pull belongs to.\n\n- **`team_id`** `string`\n\n The team that owns the pull.\n\n- **`name`** `string`\n\n Display name for the pull.\n\n- **`source_id`** `string`\n\n The source fetched records are written to.\n\n- **`source_name`** `string | null`\n\n Display name of that source.\n\n- **`collection_slug`** `string | null`\n\n The collection within the source that records land in. Defaults to a slug of the pull name.\n\n- **`endpoint_url`** `string`\n\n The HTTPS URL that is fetched on each run.\n\n- **`method`** [`PullMethod`](/api/pulls#pull-method)\n\n The HTTP method used to fetch the endpoint.\n\n- **`headers_configured`** `string[]`\n\n Names of the headers sent with every fetch. The values are stored encrypted and are never\n returned, so this shows what was configured without revealing the secrets.\n\n- **`request_body`** `string | null`\n\n The request body sent when the method is `post`. Null for `get`.\n\n- **`json_records_path`** `string | null`\n\n Dot path to the array of records inside a JSON response, such as `data` or `data.result`. Null\n when the response is stored exactly as it arrives.\n\n- **`schedule_cron`** `string`\n\n Cron expression setting when the endpoint is fetched, evaluated in UTC.\n\n- **`status`** [`PullStatus`](/api/pulls#pull-status)\n\n The current state of the pull.\n\n- **`next_fetch_at`** [`ISODateString | null`](/api/pulls#iso-date-string)\n\n When the next fetch is due. Null when the pull is paused or disabled, because nothing is\n scheduled: a time here would promise a fetch that will not happen.\n\n- **`missed_slot_count`** `number`\n\n How many scheduled slots have elapsed without a fetch, since the pull was created or last\n resumed. Almost always because the project had no healthy server at the time.\n \n Missed slots are counted rather than fetched late. A pull records whatever its endpoint serves\n at the moment of the request, so a delayed fetch would return current data stamped with a time\n it does not describe. A rising count next to a healthy `last_success_at` means the schedule is\n finer than the project's capacity has been able to keep up with.\n\n- **`last_attempt_at`** [`ISODateString | null`](/api/pulls#iso-date-string)\n\n When the endpoint was last fetched, whether or not it succeeded.\n\n- **`last_success_at`** [`ISODateString | null`](/api/pulls#iso-date-string)\n\n When records were last fetched and accepted.\n\n- **`last_failure_at`** [`ISODateString | null`](/api/pulls#iso-date-string)\n\n When a fetch last failed.\n\n- **`consecutive_failure_count`** `number`\n\n How many fetches have failed in a row. Resets to zero on the next success.\n\n- **`last_error`** `string | null`\n\n The error from the most recent failed fetch.\n\n- **`last_error_stage`** [`PullErrorStage | null`](/api/pulls#pull-error-stage)\n\n Which step of the most recent failed fetch went wrong.\n\n- **`last_http_status`** `number | null`\n\n The HTTP status the endpoint returned on the most recent fetch.\n\n- **`last_response_time_ms`** `number | null`\n\n How long the most recent fetch took, in milliseconds.\n\n- **`last_record_count`** `number`\n\n How many records the most recent successful fetch produced.\n\n- **`records_pulled_hourly`** `PullHourlyRecords[]`\n\n The last 24 hours of fetch volume, oldest first. Always 24 entries, so an hour with no fetches\n reads as a zero rather than being absent, and a pull that has never run still returns a full\n window of zeros. Sum the counts for \"records pulled in the last 24 hours\".\n\n- **`created_at`** [`ISODateString`](/api/pulls#iso-date-string)\n\n When the pull was created.\n\n- **`updated_at`** [`ISODateString`](/api/pulls#iso-date-string)\n\n When the pull was last changed.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### PullMethod\n\n`\"get\" | \"post\"`\n\n#### PullStatus\n\n`\"active\" | \"paused\" | \"disabled\"`\n\n#### PullErrorStage\n\n`\"request\" | \"response\" | \"parse\" | \"ingest\"`\n\n## Pull Test Result\n\n### Fields\n\n- **`ok`** `boolean`\n\n Whether the endpoint responded successfully and the response could be read as records.\n\n- **`status`** `number`\n\n The HTTP status the endpoint returned. Zero when no response was received at all.\n\n- **`status_text`** `string`\n\n The HTTP status text the endpoint returned.\n\n- **`response_time_ms`** `number`\n\n How long the fetch took, in milliseconds.\n\n- **`byte_length`** `number`\n\n How many bytes the endpoint returned.\n\n- **`record_count`** `number`\n\n How many records the response would produce. Zero when the test failed.\n\n- **`body_preview`** `string`\n\n The first 2 KB of the response body.\n\n- **`error_stage`** [`PullErrorStage | null`](/api/pulls#pull-error-stage)\n\n Which step went wrong. Null when the test succeeded.\n\n- **`error`** `string | null`\n\n What went wrong. Null when the test succeeded.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### PullErrorStage\n\n`\"request\" | \"response\" | \"parse\" | \"ingest\"`\n\n## List Pulls\n\n### Endpoint\n\nRetrieve a list of pulls.\n\n```http\nGET /v1/projects/:project_id/pulls\n```\n\n**Scope:** `pulls:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field the results are ordered by. Optional. Defaults to `\"created_at\"`. Allowed values: `\"name\"`, `\"created_at\"`, `\"last_success_at\"`.\n\n- **`status`** `string`\n Return only pulls in this state. Optional. Allowed values: `\"active\"`, `\"paused\"`, `\"disabled\"`.\n\n- **`source_id`** `string`\n Return only pulls that write to this source. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Pull[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Pull\n\n### Endpoint\n\nRetrieve a single pull.\n\n```http\nGET /v1/projects/:project_id/pulls/:pull_id\n```\n\n**Scope:** `pulls:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`pull_id`** `string` -- **Required**\n Unique identifier of the pull.\n\n### Response\n\nPull retrieved\n\n```ts\n{\n message: string;\n data: Pull;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create Pull\n\n### Endpoint\n\nCreate a scheduled pull in a project.\n\n```http\nPOST /v1/projects/:project_id/pulls\n```\n\n**Scope:** `pulls:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Request Body\n\n- **`name`** `string` -- **Required**\n Name shown for this pull. Minimum length: `2`. Maximum length: `128`.\n\n- **`source_id`** `string` -- **Required**\n Source that fetched records are written to. Minimum length: `1`.\n\n- **`collection_slug`** `string | null`\n Collection within the source that records land in. Leave unset and it is derived from the pull name, so each pull gets its own stream. Optional.\n\n- **`endpoint_url`** `string` -- **Required**\n HTTPS URL fetched on every run. Must resolve to a public address. Minimum length: `1`. Maximum length: `2048`.\n\n- **`method`** `string`\n HTTP method used to fetch the endpoint. Optional. Defaults to `\"get\"`. Allowed values: `\"get\"`, `\"post\"`.\n\n- **`headers`** `object | null`\n Headers sent with every fetch, for authenticating to the endpoint. Stored encrypted and never returned; responses list only the header names, as `headers_configured`. Optional.\n\n- **`request_body`** `string | null`\n Body sent with every fetch. Only valid when `method` is `post`, for endpoints that answer queries over POST. Optional.\n\n- **`json_records_path`** `string | null`\n Dot path to the array of records inside a JSON response, for example `data` or `data.result`. Leave unset to store the response exactly as it arrives. Optional.\n\n- **`schedule_cron`** `string`\n Cron expression setting when the endpoint is fetched, evaluated in UTC. Defaults to every minute, which is also the shortest supported gap. Optional. Defaults to `\"* * * * *\"`. Minimum length: `1`. Maximum length: `128`.\n\n### Response\n\nPull created\n\n```ts\n{\n message: string;\n data: Pull;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Pull names are unique within a project.\n- The endpoint must be reachable over HTTPS at a public address. Addresses inside private or reserved ranges are rejected, and the check is repeated on every fetch, not just at create.\n- A pull is created `active` even if the project has no servers yet. It starts fetching as soon as a server exists; an empty fleet delays the first fetch rather than changing the status.\n- An endpoint answering with a Prometheus HTTP Service Discovery document is followed rather than stored. Each discovered target is fetched, its metrics parsed into records, and the target's labels merged onto them so targets stay distinguishable. This is what makes an endpoint that hands out short-lived signed URLs work on a schedule: the discovery response is re-read on every run, so the credentials are always current.\n- Discovered targets are fetched over HTTPS at public addresses only, checked with the same guard as the endpoint itself, so a discovery response cannot direct a pull at a private address.\n- A pull follows at most 50 discovered targets and fails if the document lists more, rather than fetching some and silently omitting the rest. Individual targets are retried; if some still fail, the run succeeds with the records it did collect and reports how many targets did not.\n- `request_body` is only accepted when `method` is `post`. Sending one with a `get` pull is rejected rather than ignored, so a half-configured pull fails at create time instead of quietly fetching the wrong thing.\n- `headers` accepts at most 20 entries. A header whose value is `null` is ignored here, since on create there is nothing yet for it to remove. Names are matched case-insensitively, so two spellings of one name are stored as a single header.\n- `schedule_cron` must be a valid cron expression. It is evaluated in UTC, and the finest granularity is one minute.\n- `collection_slug` must be lowercase, start with a letter or digit, and may otherwise contain digits, letters, hyphens and underscores.\n- `json_records_path` is for endpoints that wrap their rows in an envelope. An endpoint returning `{\"data\": [{...}, {...}]}` stores one record containing the whole response unless you set the path to `data`, which stores the two records instead. Nested keys are joined with dots (`data.result`). Object keys only: array indexes and wildcards are not supported.\n- `json_records_path` applies only to JSON responses. JSONL, CSV, TSV and Prometheus responses already produce one record per row, so leave it unset for those.\n- A fetch fails with a `parse` error when the path is missing from the response or does not resolve to a list or object, rather than falling back to storing the whole document. Use the test endpoint to check a path against a live response before saving.\n\n## Update Pull\n\n### Endpoint\n\nUpdate a pull.\n\n```http\nPOST /v1/projects/:project_id/pulls/:pull_id\n```\n\n**Scope:** `pulls:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`pull_id`** `string` -- **Required**\n Unique identifier of the pull.\n\n### Request Body\n\n- **`name`** `string`\n Name shown for this pull. Optional. Minimum length: `2`. Maximum length: `128`.\n\n- **`collection_slug`** `string | null`\n Collection within the source that records land in. Leave unset and it is derived from the pull name, so each pull gets its own stream. Optional.\n\n- **`endpoint_url`** `string`\n HTTPS URL fetched on every run. Must resolve to a public address. Optional. Minimum length: `1`. Maximum length: `2048`.\n\n- **`method`** `string`\n HTTP method used to fetch the endpoint. Optional. Allowed values: `\"get\"`, `\"post\"`.\n\n- **`headers`** `object | null`\n Headers sent with every fetch, for authenticating to the endpoint. Stored encrypted and never returned; responses list only the header names, as `headers_configured`. Optional.\n\n- **`request_body`** `string | null`\n Body sent with every fetch. Only valid when `method` is `post`, for endpoints that answer queries over POST. Optional.\n\n- **`json_records_path`** `string | null`\n Dot path to the array of records inside a JSON response, for example `data` or `data.result`. Leave unset to store the response exactly as it arrives. Optional.\n\n- **`schedule_cron`** `string`\n Cron expression setting when the endpoint is fetched, evaluated in UTC. Defaults to every minute, which is also the shortest supported gap. Optional. Minimum length: `1`. Maximum length: `128`.\n\n### Response\n\nPull updated\n\n```ts\n{\n message: string;\n data: Pull;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Changes take effect on the next fetch; a fetch already in flight finishes under the old configuration.\n- A changed `schedule_cron` takes effect from the next fetch onward.\n- At least one field must be provided.\n- Any field not listed here is rejected, including `source_id`, which cannot be changed after create. Create a new pull to write into a different source.\n- Clearing `request_body` is required before changing `method` from `post` to `get`; a pull cannot keep a body it would never send.\n- `headers` is a patch, not a replacement, because the values are write-only and never returned. A name mapped to a string adds or replaces that header, a name mapped to `null` removes it, and a name you do not mention keeps its stored value. Omit the field to leave every header alone, or send `null` in place of the object to remove all of them.\n- `headers` names are matched case-insensitively, so patching `authorization` replaces a stored `Authorization` rather than adding a second header. The spelling you send is the one stored and sent.\n- `headers` accepts at most 20 entries, counted after the patch is applied.\n- `schedule_cron` must be a valid cron expression, evaluated in UTC.\n- Send `json_records_path` as `null` to stop unwrapping and store responses whole again.\n\n## Delete Pull\n\n### Endpoint\n\nDelete a pull.\n\n```http\nDELETE /v1/projects/:project_id/pulls/:pull_id\n```\n\n**Scope:** `pulls:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`pull_id`** `string` -- **Required**\n Unique identifier of the pull.\n\n### Response\n\nPull deleted\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Records the pull already wrote are not deleted. They belong to the source and stay queryable.\n\n## Pause Pull\n\n### Endpoint\n\nPause a pull so it stops fetching.\n\n```http\nPOST /v1/projects/:project_id/pulls/:pull_id/pause\n```\n\n**Scope:** `pulls:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`pull_id`** `string` -- **Required**\n Unique identifier of the pull.\n\n### Response\n\nPull paused\n\n```ts\n{\n message: string;\n data: Pull;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Only an active pull can be paused. Pausing an already-paused pull succeeds and changes nothing.\n\n## Resume Pull\n\n### Endpoint\n\nResume a paused pull.\n\n```http\nPOST /v1/projects/:project_id/pulls/:pull_id/resume\n```\n\n**Scope:** `pulls:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`pull_id`** `string` -- **Required**\n Unique identifier of the pull.\n\n### Response\n\nPull resumed\n\n```ts\n{\n message: string;\n data: Pull;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Resumes a pull you paused, and also re-enables one the system disabled after a long failure streak. Resuming clears the failure count, so it starts from a clean slate.\n\n## Test Pull\n\n### Endpoint\n\nFetch the endpoint once without storing anything.\n\n```http\nPOST /v1/projects/:project_id/pulls/:pull_id/test\n```\n\n**Scope:** `pulls:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`pull_id`** `string` -- **Required**\n Unique identifier of the pull.\n\n### Response\n\n```ts\n{\n message: string;\n data: PullTestResult;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Nothing is ingested and no health field or schedule is changed, so this is safe to run against a live pull.\n- The response reports the HTTP status, how long the fetch took, how many records the configured format would produce, and the first 2 KB of the body.\n\n",
|
|
37
37
|
"api/checks": "# Checks\n\n## Check Model\n\n### Fields\n\n- **`object`** `\"check\"`\n\n- **`id`** `string`\n\n- **`project_id`** `string`\n\n- **`team_id`** `string`\n\n- **`name`** `string`\n\n- **`source_id`** `string`\n\n The source that observations are written to. Nothing else is stored.\n\n- **`source_name`** `string | null`\n\n- **`collection_slug`** `string`\n\n The collection within the source that observations land in. Give several checks the same slug\n to chart them together, one strip per check under a worst-of overall row.\n\n- **`endpoint_url`** `string`\n\n The HTTPS URL requested on every run.\n\n- **`method`** [`CheckMethod`](/api/checks#check-method)\n\n- **`headers_configured`** `string[]`\n\n Names of the headers sent with every request. The values are stored encrypted and are never\n returned, so this shows what was configured without revealing the secrets.\n\n- **`request_body`** `string | null`\n\n The request body sent when the method is `post`. Null for `get`.\n\n- **`schedule_cron`** `string`\n\n Cron expression setting when the endpoint is requested, evaluated in UTC.\n\n- **`check_retry_seconds`** `number | null`\n\n How long to wait before one confirming request settles a failed run. Null records the first\n result as it stands, which turns a single flake into a minute of recorded downtime.\n\n- **`status`** [`CheckStatus`](/api/checks#check-status)\n\n The current state. A check is only ever stopped by you pausing it or by the team falling out of\n good standing. A failing endpoint never stops it, because the outage is the thing it is there\n to record.\n\n- **`next_fetch_at`** [`ISODateString | null`](/api/checks#iso-date-string)\n\n When the next run is due. Null while the check is paused or disabled.\n\n- **`missed_slot_count`** `number`\n\n How many scheduled slots have elapsed without a run, since the check was created or last\n resumed. Almost always because the project had no healthy server at the time.\n \n Missed slots are counted rather than run late: a slot describes the endpoint at one moment, so\n a late request would report the wrong minute. These are gaps in the uptime record, not\n downtime.\n\n- **`last_up`** `boolean | null`\n\n Whether the most recent run found the endpoint up, meaning it answered with a 2xx. Null until\n the first run settles, and null again after a resume, which is what separates a check nothing\n has measured yet from one measured to be down.\n\n- **`results_hourly`** `CheckHourlyResults[]`\n\n The last 24 hours of results, oldest first. Always 24 entries, so an hour with no runs reads as\n zeros rather than being absent, and a check that has never run still returns a full window. `up\n / (up + down)` over the window is the uptime for that period.\n\n- **`last_checked_at`** [`ISODateString | null`](/api/checks#iso-date-string)\n\n When the endpoint was last requested, whether or not it answered. Cleared by a resume.\n\n- **`last_success_at`** [`ISODateString | null`](/api/checks#iso-date-string)\n\n When the endpoint last returned a 2xx. Survives a pause, so it dates an ongoing outage.\n\n- **`last_failure_at`** [`ISODateString | null`](/api/checks#iso-date-string)\n\n When the endpoint last failed to return a 2xx. Survives a pause.\n\n- **`consecutive_failure_count`** `number`\n\n How many runs have failed in a row. Resets on the next success. Reporting only: the count never\n stops the check, however high it climbs.\n\n- **`last_error`** `string | null`\n\n The error from the most recent failed run.\n\n- **`last_error_stage`** [`CheckErrorStage | null`](/api/checks#check-error-stage)\n\n Which step of the most recent failed run went wrong.\n\n- **`last_http_status`** `number | null`\n\n The HTTP status returned by the most recent run. Null when nothing answered.\n\n- **`last_response_time_ms`** `number | null`\n\n How long the most recent run took, in milliseconds.\n\n- **`created_at`** [`ISODateString`](/api/checks#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/checks#iso-date-string)\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### CheckMethod\n\n`\"get\" | \"post\"`\n\nStored as plain strings rather than Postgres enums: nothing queries by either, so a database enum\nwould only buy DDL on every added value. `CheckStatus` stays an enum because it IS queried, and\nbacks the dispatch index.\n\n#### CheckStatus\n\n`\"active\" | \"paused\" | \"disabled\"`\n\n#### CheckErrorStage\n\n`\"request\" | \"response\"`\n\n## Check Test Result\n\n### Fields\n\n- **`ok`** `boolean`\n\n Whether the endpoint answered with a 2xx.\n\n- **`status`** `number`\n\n The HTTP status the endpoint returned. Zero when no response was received at all.\n\n- **`status_text`** `string`\n\n- **`response_time_ms`** `number`\n\n How long the request took, in milliseconds.\n\n- **`byte_length`** `number`\n\n How many bytes the endpoint returned.\n\n- **`body_preview`** `string`\n\n The first 2 KB of the response body, for confirming you reached the endpoint you meant to.\n\n- **`error_stage`** [`CheckErrorStage | null`](/api/checks#check-error-stage)\n\n Which step went wrong. Null when the test succeeded.\n\n- **`error`** `string | null`\n\n What went wrong. Null when the test succeeded.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### CheckErrorStage\n\n`\"request\" | \"response\"`\n\n## List Checks\n\n### Endpoint\n\nRetrieve a list of checks.\n\n```http\nGET /v1/projects/:project_id/checks\n```\n\n**Scope:** `checks:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field the results are ordered by. Optional. Defaults to `\"created_at\"`. Allowed values: `\"name\"`, `\"created_at\"`.\n\n- **`status`** `string`\n Return only checks in this state. Optional. Allowed values: `\"active\"`, `\"paused\"`, `\"disabled\"`.\n\n- **`source_id`** `string`\n Return only checks that write their observations to this source. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Check[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Check\n\n### Endpoint\n\nRetrieve a single check.\n\n```http\nGET /v1/projects/:project_id/checks/:check_id\n```\n\n**Scope:** `checks:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`check_id`** `string` -- **Required**\n Unique identifier of the check.\n\n### Response\n\nCheck retrieved\n\n```ts\n{\n message: string;\n data: Check;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create Check\n\n### Endpoint\n\nCreate a scheduled check in a project.\n\n```http\nPOST /v1/projects/:project_id/checks\n```\n\n**Scope:** `checks:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Request Body\n\n- **`name`** `string` -- **Required**\n Name shown for this check. Minimum length: `2`. Maximum length: `128`.\n\n- **`source_id`** `string` -- **Required**\n Source that observations are written to. Nothing else about the response is stored. Minimum length: `1`.\n\n- **`collection_slug`** `string` -- **Required**\n Collection within the source that observations land in. Give several checks the same slug to chart them together, one strip per check under a worst-of overall row. Minimum length: `1`. Maximum length: `64`.\n\n- **`endpoint_url`** `string` -- **Required**\n HTTPS URL requested on every run. Must resolve to a public address. Minimum length: `1`. Maximum length: `2048`.\n\n- **`method`** `string`\n HTTP method used to request the endpoint. Optional. Defaults to `\"get\"`. Allowed values: `\"get\"`, `\"post\"`.\n\n- **`headers`** `object | null`\n Headers sent with every request, for authenticating to the endpoint. Stored encrypted and never returned; responses list only the header names, as `headers_configured`. Optional.\n\n- **`request_body`** `string | null`\n Body sent with every request. Only valid when `method` is `post`, for health endpoints that answer queries over POST. Optional.\n\n- **`schedule_cron`** `string`\n Cron expression setting when the endpoint is requested, evaluated in UTC. Defaults to every minute, which is also the shortest supported gap. Optional. Defaults to `\"* * * * *\"`. Minimum length: `1`. Maximum length: `128`.\n\n- **`check_retry_seconds`** `integer | null`\n Seconds to wait before one confirming request settles a failed run. Send `null` to record the first result as it stands. Optional. Defaults to `10`.\n\n### Response\n\nCheck created\n\n```ts\n{\n message: string;\n data: Check;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The endpoint must be reachable over HTTPS at a public address. Addresses inside private or reserved ranges are rejected, and the check is repeated on every run, not just at create.\n- A check is created `active` even if the project has no servers yet. It starts running as soon as a server exists; an empty fleet delays the first run rather than changing the status.\n- The response body is never stored or parsed. Each run writes one observation recording whether the endpoint answered, its status code and how long it took.\n- A failing endpoint never stops a check, however long the failure lasts. Only pausing it, or the team falling out of good standing, stops one.\n- `request_body` is only accepted when `method` is `post`. Sending one with a `get` check is rejected rather than ignored, so a half-configured check fails at create time instead of quietly measuring the wrong request.\n- `headers` accepts at most 20 entries. A header whose value is `null` is ignored here, since on create there is nothing yet for it to remove. Names are matched case-insensitively, so two spellings of one name are stored as a single header.\n- `schedule_cron` must be a valid cron expression. It is evaluated in UTC, and the finest granularity is one minute.\n- `collection_slug` is required, and must be lowercase, start with a letter or digit, and may otherwise contain digits, letters, hyphens and underscores. Naming it here is what lets several checks deliberately share one collection and chart together; renaming the check later never moves the series, because the slug is stored rather than derived.\n- `check_retry_seconds` is between 1 and 20, and defaults to 10. A failed run waits this long and requests once more before it is recorded, so a single flake does not become a recorded minute of downtime. Send `null` to record the first result as it stands.\n- Each run writes one observation to `collection_slug` carrying `check_id`, `check_name`, `checked_at`, `up`, `http_status`, `response_time_ms`, `error_stage`, `error_message` and `attempts`. `up` is 1 when the endpoint returned a 2xx and 0 when it did not answer or answered with an error status, so the average of `up` over a period is the uptime for that period.\n- `check_name` is the name the check carried when the run was recorded, so a chart grouped on `check_id` can label its series with something readable. Renaming the check does not rewrite observations already stored.\n- `checked_at` is the scheduled minute rather than the moment the observation was written, so a run delayed by a confirming request still lands in the minute it describes.\n- `name` must be unique within the project, so a check is identifiable in a list and in the strip it charts as.\n\n## Update Check\n\n### Endpoint\n\nUpdate a check.\n\n```http\nPOST /v1/projects/:project_id/checks/:check_id\n```\n\n**Scope:** `checks:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`check_id`** `string` -- **Required**\n Unique identifier of the check.\n\n### Request Body\n\n- **`name`** `string`\n Name shown for this check. Optional. Minimum length: `2`. Maximum length: `128`.\n\n- **`collection_slug`** `string | null`\n Collection within the source that observations land in. Give several checks the same slug to chart them together, one strip per check under a worst-of overall row. Optional.\n\n- **`endpoint_url`** `string`\n HTTPS URL requested on every run. Must resolve to a public address. Optional. Minimum length: `1`. Maximum length: `2048`.\n\n- **`method`** `string`\n HTTP method used to request the endpoint. Optional. Allowed values: `\"get\"`, `\"post\"`.\n\n- **`headers`** `object | null`\n Headers sent with every request, for authenticating to the endpoint. Stored encrypted and never returned; responses list only the header names, as `headers_configured`. Optional.\n\n- **`request_body`** `string | null`\n Body sent with every request. Only valid when `method` is `post`, for health endpoints that answer queries over POST. Optional.\n\n- **`schedule_cron`** `string`\n Cron expression setting when the endpoint is requested, evaluated in UTC. Defaults to every minute, which is also the shortest supported gap. Optional. Minimum length: `1`. Maximum length: `128`.\n\n- **`check_retry_seconds`** `integer | null`\n Seconds to wait before one confirming request settles a failed run. Send `null` to record the first result as it stands. Optional.\n\n### Response\n\nCheck updated\n\n```ts\n{\n message: string;\n data: Check;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Changes take effect on the next run; a run already in flight finishes under the old configuration.\n- A changed `schedule_cron` takes effect from the next run onward.\n- At least one field must be provided.\n- Any field not listed here is rejected, including `source_id`, which cannot be changed after create. Create a new check to record into a different source.\n- Clearing `request_body` is required before changing `method` from `post` to `get`; a check cannot keep a body it would never send.\n- `headers` is a patch, not a replacement, because the values are write-only and never returned. A name mapped to a string adds or replaces that header, a name mapped to `null` removes it, and a name you do not mention keeps its stored value. Omit the field to leave every header alone, or send `null` in place of the object to remove all of them.\n- `headers` names are matched case-insensitively, so patching `authorization` replaces a stored `Authorization` rather than adding a second header. The spelling you send is the one stored and sent.\n- `headers` accepts at most 20 entries, counted after the patch is applied.\n- `schedule_cron` must be a valid cron expression, evaluated in UTC. Changing it restarts the missed-slot count from the edit, since slots before it belonged to a different schedule.\n- Send `check_retry_seconds` as `null` to stop confirming failures and record the first result as it stands.\n- Changing `collection_slug` starts a new series. Observations already written stay where they are, so a chart over the old collection stops at the edit.\n\n## Delete Check\n\n### Endpoint\n\nDelete a check.\n\n```http\nDELETE /v1/projects/:project_id/checks/:check_id\n```\n\n**Scope:** `checks:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`check_id`** `string` -- **Required**\n Unique identifier of the check.\n\n### Response\n\nCheck deleted\n\n```ts\n{\n message: string;\n data: null;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Observations the check already wrote are not deleted. They belong to the source and stay queryable, so a chart of past uptime keeps working.\n\n## Pause Check\n\n### Endpoint\n\nPause a check so it stops running.\n\n```http\nPOST /v1/projects/:project_id/checks/:check_id/pause\n```\n\n**Scope:** `checks:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`check_id`** `string` -- **Required**\n Unique identifier of the check.\n\n### Response\n\nCheck paused\n\n```ts\n{\n message: string;\n data: Check;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Only an active check can be paused. Pausing an already-paused check succeeds and changes nothing.\n- A pause is a gap in the uptime record rather than downtime. Nothing is recorded for the minutes it covers.\n\n## Resume Check\n\n### Endpoint\n\nResume a paused check.\n\n```http\nPOST /v1/projects/:project_id/checks/:check_id/resume\n```\n\n**Scope:** `checks:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`check_id`** `string` -- **Required**\n Unique identifier of the check.\n\n### Response\n\nCheck resumed\n\n```ts\n{\n message: string;\n data: Check;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Resumes a check you paused. The failure count starts again from zero.\n- A check the system stopped while the team was not in good standing is not resumable here and returns 400. It starts again on its own once the team is back in good standing.\n\n## Test Check\n\n### Endpoint\n\nRequest the endpoint once without recording anything.\n\n```http\nPOST /v1/projects/:project_id/checks/:check_id/test\n```\n\n**Scope:** `checks:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`check_id`** `string` -- **Required**\n Unique identifier of the check.\n\n### Response\n\n```ts\n{\n message: string;\n data: CheckTestResult;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Nothing is recorded and no health field or schedule is changed, so this is safe to run against a live check. The result does not appear in the uptime history.\n- The response reports the HTTP status, how long the request took, and the first 2 KB of the body so you can confirm you reached the endpoint you meant to.\n- There is no confirming retry here, unlike a scheduled run: a test reports what happened rather than settling a verdict.\n\n",
|
|
38
|
-
"api/logs": "# Logs\n\n## Log Model\n\n### Fields\n\n- **`object`** `\"log\"`\n\n- **`action`** `string`\n\n What happened, as a short code such as `sign_in` or `user.mfa_reset`.\n\n- **`success`** `boolean`\n\n Whether the attempted action succeeded. Failed attempts, like a bad sign-in, are recorded too.\n\n- **`message`** `string | null`\n\n- **`team_id`** `string | null`\n\n Null on sign-in entries that are not tied to a team.\n\n- **`actor_id`** `string | null`\n\n Who acted: the user id, or the API key id when a key acted. See `actor_type`. Null when\n Tailglow acted automatically.\n\n- **`actor_type`** `string | null`\n\n One of `AUTH_SUBJECTS`. Whether a user or an API key acted.\n\n- **`actor_name`** `string | null`\n\n Display name of the actor as it was WHEN THE ACTION HAPPENED, so a renamed or deleted actor\n still reads correctly. Reads `Tailglow` on entries Tailglow made, whether automatically or by a\n Tailglow operator; `actor_context` tells the two apart.\n\n- **`actor_context`** `string | null`\n\n `app` for a member or API key, `admin` when a Tailglow operator acted on this team, and\n `system` when Tailglow acted automatically, for example when a payment arrived. Decide who\n acted from this field, not from `actor_name`, which members choose themselves.\n\n- **`actor_role_id`** `string | null`\n\n Role the actor held at the time. Null on entries a customer reads about an operator.\n\n- **`actor_session_id`** `string | null`\n\n Session the action belongs to, so one actor's run of changes can be grouped.\n\n- **`resource_type`** `string | null`\n\n What the entry is about: the user who signed in, or the row that changed.\n\n- **`resource_id`** `string | null`\n\n Identifier of the resource named by `resource_type`.\n\n- **`ip_address`** `string | null`\n\n- **`device`** `string | null`\n\n Readable device summary
|
|
39
|
-
"guides/agents": "# Set up Tailglow with an agent\n\nThis page is written for AI agents such as Claude Code, Codex, Cursor and Cowork that are helping someone set up Tailglow. People are welcome to read it too.\n\nTailglow turns the data a product sends into live charts and pages. Data arrives at a **source** and lands in **collections**. **Views** reshape it, and **metrics** count it; charts, **pages** and **monitors** are built on metrics. Every project runs on its own **server**, which Tailglow provisions and operates.\n\n## Start by asking\n\nUnless the person has already said, ask what they want to see first. Each answer maps to one path in step 4:\n\n| They want | Path |\n| ------------------------------------------------------- | ------------------------------- |\n| Visitors, page views and clicks in a web app | [Web app](#web-app) |\n| Events, jobs or errors from a backend | [Backend](#backend) |\n| Screens and errors in a React Native or Expo app | [React Native](#react-native) |\n| To know that a site or API is up | [Uptime check](#uptime-check) |\n| Data they already have: files, exports, another service | [Anything else](#anything-else) |\n\n## 1. Sign in\n\n```bash\nnpx @tailglow/cli login\n```\n\nWithout a terminal attached, which is how an agent runs it, the command prints a link and a code, remembers the sign-in, and exits. Show both to the person. They sign in, or create an account if they have none (the first 3 days are free and need no card), check that the page shows the same code, and approve. Then finish signing in:\n\n```bash\nnpx @tailglow/cli login --wait\n```\n\n`--wait` waits up to a minute. Exit code `75` means the person has not approved yet: run it again. Exit code `1` with status `unreachable` means the API could not be reached: the sign-in is kept, so run `login --wait` again once the connection is back. Status `consumed` means the credential was already handed out: start over with `login --new`. Any other exit code `1` means the sign-in was denied, expired or withdrawn: start over with `login`.\n\nThis signs the CLI in as the person, with what their role allows in each of their teams, starting in the team they choose. To use an API key for one team instead, with only the permissions the person picks, add `--method api-key`; that team needs a project first.\n\nWhen Tailglow is at capacity, a brand-new account joins a waitlist instead of signing in, so the sign-in cannot be approved and `--wait` keeps exiting `75` until it expires. If the person sees the waitlist, say so and stop until they are let in.\n\nThe credential is saved to `~/.tailglow/config.json` and the CLI uses it from then on. You never need to see it: do not ask for it, print it, or put it in code. `tglow logout` removes it.\n\nTo keep the CLI around, install it once. It needs Node 18.3 or newer:\n\n```bash\nnpm install -g @tailglow/cli\ntglow whoami\n```\n\nIn CI, set `TAILGLOW_API_KEY` to an API key instead of logging in.\n\n## 2. Learn the CLI as you go\n\nEvery command has the shape `tglow <resource> <command> [flags]`, and the CLI documents itself:\n\n```bash\ntglow help # every resource and the global flags\ntglow sources # the commands on one resource\ntglow sources create --help --json # one command's flags, types, required fields and rules\n```\n\nRead `--help --json` before you call a command for the first time. It is generated from the same validation the API enforces, so it is always current, and its `notes` carry rules a field list cannot.\n\n- Flags are field names in kebab-case: `repeat_window_minutes` is `--repeat-window-minutes`.\n- IDs in the path are flags too: `--source-id src_x`, never a positional argument.\n- Output is JSON whenever it is piped. Read `.data`, and `.pagination.next_cursor` to continue a list.\n- Set the project once so you can leave it off every command: `tglow config set project prj_...`\n\nThe CLI also carries these docs, so you do not need to fetch them. `tglow docs` prints the index and `tglow docs guides/sdk` prints one page, as markdown even when piped.\n\n## 3. Pick a project\n\n```bash\ntglow projects list\ntglow projects create --name \"Acme web\"\ntglow config set project prj_...\n```\n\nReuse a project when the person already has one for this app. A new team's first project comes with a free trial server, which starts in the background: `tglow servers list` shows `provisioning` until it is `active`.\n\n## 4. Connect data\n\nMost paths need a source and an ingest key:\n\n```bash\ntglow sources create --name \"Web app\"\ntglow ingest_keys create --source-id src_...\n```\n\nThe ingest key's `url` is the address to send records to, with the key already in it. It is `null` until the project's server is active; retrieve the key again once it is. An ingest key can only write records into its own source, so it is safe in client-side code. Keep it in the app's environment configuration anyway, so it can be replaced without a code change.\n\n### Web app\n\n```bash\nnpm install @tailglow/browser\n```\n\n```javascript\nimport { Tailglow } from \"@tailglow/browser\";\n\n// The ingest key's url, read from the app's public environment configuration.\nconst tg = new Tailglow({ url: TAILGLOW_INGEST_URL });\n\ntg.track(\"signup\", { plan: \"pro\" });\ntg.identify(\"user_123\");\n```\n\nPage views, clicks in elements marked `data-telemetry`, outbound clicks, uncaught errors and performance signals are collected automatically. A site without a build step can use the script tag instead: see [Drop-in script](/guides/quickstart#drop-in-script). In Next.js, SvelteKit or another framework that renders on the server, create the SDK in the browser only, once: see [Frameworks](/guides/sdk#frameworks).\n\nThe SDK ignores `localhost` by default. To test locally, pass `excludeLocalhost: false` and tag the traffic, for example `context: { environment: \"development\" }`, so it can be filtered out of real charts.\n\n### Backend\n\n```bash\nnpm install @tailglow/core\n```\n\n```javascript\nimport { TailglowCore } from \"@tailglow/core\";\n\nconst tg = new TailglowCore({ url: process.env.TAILGLOW_INGEST_URL });\n\ntg.track(\"job_completed\", { job_id: \"abc\" });\nawait tg.flush();\n```\n\nNothing is collected automatically on a backend. Flush before the process or request ends; in a serverless function, see [Serverless functions](/guides/sdk#serverless-functions). Node 22 or newer, or Bun. Any other language can [send records over HTTP](#anything-else).\n\n### React Native\n\nInstall `@tailglow/react-native` and follow [React Native](/guides/sdk#react-native-tailglowreact-native) in the SDK guide. The app passes the SDK its `AppState`, `Platform`, storage and navigation, which the guide shows in one file.\n\n### Uptime check\n\nA check needs no code. It fetches a public HTTPS address on a schedule and records whether it answered:\n\n```bash\ntglow checks create --name \"Homepage\" --source-id src_... --collection-slug uptime --endpoint-url https://acme.com\ntglow checks test --check-id chk_...\n```\n\n`test` fetches once and stores nothing. Checks that share a collection chart together, one row each, which is what a status page is. See [Checks](/guides/checks).\n\n### Anything else\n\nPOST JSON, JSONL, CSV, TSV or plain text to the ingest URL. Add `&collection=<name>` to choose where the records land; the collection is created the first time data arrives for it.\n\n```bash\ncurl -X POST \"$TAILGLOW_INGEST_URL&collection=orders\" \\\n -H \"Content-Type: application/json\" \\\n -d '[{\"order_id\":\"o_1\",\"total\":42.5,\"created_at\":\"2026-09-27T12:00:00Z\"}]'\n```\n\n`202 Accepted` means the records are stored and will be processed. Retry on `5xx` and on network errors; a `4xx` means the request itself has to change. One request can carry up to 20 MiB. See [Sending data](/guides/sending-data).\n\n## 5. Confirm data arrived\n\n```bash\ntglow collections list --source-id src_...\ntglow collections list-docs --collection-id col_...\n```\n\nThe browser and backend SDKs write to collections named `events`, `errors` and `logs`. A collection appears with its first record.\n\n## 6. Build a chart\n\nA view is written for a settled record shape. By default Tailglow settles a shape when it arrives again at least 60 minutes after it was first seen, or when 100 or more records of it arrive in one batch. Check with:\n\n```bash\ntglow collections list-schemas --collection-id col_...\n```\n\nIf the person confirms a candidate shape is real data rather than a one-off test, settle it by hand:\n\n```bash\ntglow collections update-schema --collection-id col_... --schema-version-id csv_... --state settled\n```\n\nThen create a view on the collection and declare the fields it should produce, based on the records you sampled in step 5. A metric can only use fields the view declares, and its time field has to be one of them, typed `date`. This example reads the SDK's `events` collection:\n\n```bash\ntglow views create --collection-id col_... --name \"Product events\" \\\n --output-schema '[{\"name\":\"event_time\",\"type\":\"date\"},{\"name\":\"type\",\"type\":\"string\"},{\"name\":\"plan\",\"type\":\"string\"}]'\n```\n\nField types are `string`, `number`, `boolean`, `date`, `array` and `null`. Tailglow writes the transform that fills those fields from the records. Add `--hint` only for a rule it cannot read from field names, such as \"classify orders above 500 as large\". Choose the fields with care: once the transform is written, the view's fields cannot be changed in place.\n\nRetrieve the view until its `status` is `active` before telling the person the chart is ready. If it is `error`, read `error_message`. If `waiting_shapes_count` is above 0, `tglow views list-deferrals --view-id view_...` shows what it is waiting on.\n\n```bash\ntglow views retrieve --view-id view_...\n```\n\nThen build up from the view. The `events` collection holds page views and clicks as well as your own events, so filter a metric to the event it counts:\n\n```bash\ntglow metrics create --view-id view_... --name \"Signups per day\" --timestamp-field event_time \\\n --filters '?type=equals:signup' --ui-chart-value count --ui-chart-interval day\ntglow pages create --name \"Product\"\ntglow components create --page-id page_... --metric-ids mtrc_...,mtrc_...\n```\n\nRead `--help --json` on each command for the rest of its fields.\n\nPeople who would rather describe a chart in words can ask the assistant in the Tailglow dashboard.\n\n## Rules\n\n- Ask before any delete. Without a terminal attached, `delete` runs immediately and never asks.\n- Ask before changing servers or billing. Servers cost money.\n- Never print, log or commit the CLI credential. Ingest keys are fine in client-side code.\n- Send only fields that `--help --json` lists. The API rejects unknown fields.\n\n## More\n\n- The same pages without a network call: `tglow docs`, then `tglow docs <path>` for any page it lists\n- Index of the docs for agents: [docs.tailglow.io/llms.txt](https://docs.tailglow.io/llms.txt)\n- Every page in one file: [docs.tailglow.io/llms-full.txt](https://docs.tailglow.io/llms-full.txt)\n- Any docs page as markdown: add `.md`, for example [docs.tailglow.io/guides/sdk.md](https://docs.tailglow.io/guides/sdk.md)\n- Guides: [JavaScript SDK](/guides/sdk), [Sending data](/guides/sending-data), [Checks](/guides/checks), [TGL](/guides/tgl), [Metrics](/guides/metrics), [Pages](/guides/pages), [CLI](/guides/cli)\n",
|
|
38
|
+
"api/logs": "# Logs\n\n## Log Model\n\n### Fields\n\n- **`object`** `\"log\"`\n\n- **`action`** `string`\n\n What happened, as a short code such as `sign_in` or `user.mfa_reset`.\n\n- **`success`** `boolean`\n\n Whether the attempted action succeeded. Failed attempts, like a bad sign-in, are recorded too.\n\n- **`message`** `string | null`\n\n- **`team_id`** `string | null`\n\n Null on sign-in entries that are not tied to a team.\n\n- **`actor_id`** `string | null`\n\n Who acted: the user id, or the API key id when a key acted. See `actor_type`. Null when\n Tailglow acted automatically.\n\n- **`actor_type`** `string | null`\n\n One of `AUTH_SUBJECTS`. Whether a user or an API key acted.\n\n- **`actor_name`** `string | null`\n\n Display name of the actor as it was WHEN THE ACTION HAPPENED, so a renamed or deleted actor\n still reads correctly. Reads `Tailglow` on entries Tailglow made, whether automatically or by a\n Tailglow operator; `actor_context` tells the two apart.\n\n- **`actor_context`** `string | null`\n\n `app` for a member or API key, `admin` when a Tailglow operator acted on this team, and\n `system` when Tailglow acted automatically, for example when a payment arrived. Decide who\n acted from this field, not from `actor_name`, which members choose themselves.\n\n- **`actor_role_id`** `string | null`\n\n Role the actor held at the time. Null on entries a customer reads about an operator.\n\n- **`actor_session_id`** `string | null`\n\n Session the action belongs to, so one actor's run of changes can be grouped.\n\n- **`resource_type`** `string | null`\n\n What the entry is about: the user who signed in, or the row that changed.\n\n- **`resource_id`** `string | null`\n\n Identifier of the resource named by `resource_type`.\n\n- **`ip_address`** `string | null`\n\n- **`device`** `string | null`\n\n Readable device summary, for example `Chrome 126 on macOS` from a browser's user agent, or\n `Tailglow CLI 0.8.1 via Ghostty on macOS` from what the CLI reports about itself.\n\n- **`user_agent`** `string | null`\n\n- **`created_at`** [`ISODateString`](/api/logs#iso-date-string)\n\n- **`type`** [`LogType`](/api/logs#log-type)\n\n Which kind of entry this is: `auth` for sign-in activity, `audit` for changes to resources,\n `billing` for changes to the team's billing.\n\n- **`details`** [`AuthLogDetails`](/api/logs#auth-details-model) | [`AuditLogDetails`](/api/logs#audit-details-model) | [`BillingLogDetails`](/api/logs#billing-details-model)\n\n Extra context whose shape follows `type`: `AuthLogDetails` for sign-ins, `AuditLogDetails` for\n changes, `BillingLogDetails` for billing.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### LogType\n\n`\"auth\" | \"admin_auth\" | \"audit\" | \"billing\"`\n\n#### BillingPlan\n\n`\"pro_v1\" | \"enterprise_v1\"`\n\n## Auth Log Details Model\n\n### Fields\n\n- **`email`** `string`\n\n The email address the sign-in was attempted with.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n## Audit Log Details Model\n\n### Fields\n\n- **`changed_fields`** `string[]`\n\n The fields the request changed, sorted and de-duplicated.\n\n- **`endpoint`** `string | null`\n\n The API endpoint the change came through.\n\n- **`request_client`** `string | null`\n\n Which surface made the request: the API directly, the dashboard, the admin portal, or the AI\n assistant.\n\n- **`client_version`** `string | null`\n\n Version of the client that made the request, when it reported one.\n\n- **`ai_chat_id`** `string | null`\n\n Set when the AI assistant made the change; identifies the chat it happened in.\n\n- **`[key: string]`** `unknown`\n\n Any additional key recorded with the entry.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n## Billing Log Details Model\n\n### Fields\n\n- **`amount_cents`** `number`\n\n Amount in cents, such as an invoice total or a failed payment.\n\n- **`stripe_invoice_id`** `string`\n\n Stripe's id for the invoice, as shown on the invoice list.\n\n- **`period_start_at`** [`ISODateString`](/api/logs#iso-date-string)\n\n First moment of the billing month the entry is about.\n\n- **`plan_from`** [`BillingPlan`](/api/logs#billing-plan)\n\n- **`plan_to`** [`BillingPlan`](/api/logs#billing-plan)\n\n- **`payment_method_brand`** `string`\n\n- **`payment_method_last4`** `string`\n\n- **`discount_id`** `string`\n\n- **`discount_percent`** `number`\n\n- **`discount_amount_cents`** `number`\n\n- **`line_item_type`** `string`\n\n The invoice line a discount applies to, such as `compute` or `storage`.\n\n- **`available_ai_tokens_before`** `number`\n\n Tailglow AI tokens available to the team before the entry's change.\n\n- **`available_ai_tokens_after`** `number`\n\n Tailglow AI tokens available to the team after the entry's change.\n\n- **`trial_ends_at`** [`ISODateString`](/api/logs#iso-date-string)\n\n When the free trial ends after the entry's change.\n\n- **`days`** `number`\n\n Days a staff extension added to the free trial. Only on extensions recorded before they were\n counted in server-hours; later ones carry `added_server_hours`.\n\n- **`added_server_hours`** `number`\n\n Server-hours staff added to the free trial.\n\n- **`server_hours`** `number`\n\n Server-hours of credit the entry added. For a trial extension, this includes any time a server\n ran between a trial's end and the extension, which the extension also covers.\n\n- **`request_id`** `string`\n\n Identifies the staff request behind a trial extension, which is applied only once.\n\n- **`has_payment_method`** `boolean`\n\n For a trial that ended: whether a payment method was on file, so its servers kept running.\n\n- **`invoice_id`** `string`\n\n For a delinquency change: the invoice that caused it, or whose payment cleared it.\n\n### Referenced Types\n\n#### ISODateString\n\n`ISODateString`\n\nAn ISO 8601 date-time string returned at the JSON API boundary.\n\n#### BillingPlan\n\n`\"pro_v1\" | \"enterprise_v1\"`\n\n## List Logs\n\n### Endpoint\n\nRetrieve activity logs available to the current authorization.\n\n```http\nGET /v1/logs\n```\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the logs. Optional. Defaults to `\"created_at\"`. Allowed values: `\"created_at\"`.\n\n- **`start_at`** [`ISODateString`](/api/logs#iso-date-string)\n Earliest log date and time to return. Optional.\n\n- **`end_at`** [`ISODateString`](/api/logs#iso-date-string)\n Latest log date and time to return. Optional.\n\n- **`team_id`** `string`\n Filter by team ID. Optional.\n\n- **`user_id`** `string`\n Filter by user ID. Optional.\n\n- **`type`** [`LogType`](/api/logs#log-type)\n Filter by log type. Optional.\n\n- **`limit`** `number`\n Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.\n\n- **`after`** `string`\n Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.\n\n- **`before`** `string`\n Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.\n\n- **`sort`** `string`\n Sort direction for the result set. Optional. Defaults to `\"desc\"`. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: Log[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- You can retrieve your own authentication logs without `logs:read`. Billing logs require `billing:read`; all other log reads require `logs:read`.\n- Without `type`, the list includes every type you can read. `admin_auth` logs exist only on Tailglow's own admin team.\n- `after` and `before` are mutually exclusive.\n\n",
|
|
39
|
+
"guides/agents": "# Set up Tailglow with an agent\n\nThis page is written for AI agents such as Claude Code, Codex, Cursor and Cowork that are helping someone set up Tailglow. People are welcome to read it too.\n\nTailglow turns the data a product sends into live charts and pages. Data arrives at a **source** and lands in **collections**. **Views** reshape it, and **metrics** count it; charts, **pages** and **monitors** are built on metrics. Every project runs on its own **server**, which Tailglow provisions and operates.\n\n## Start by asking\n\nUnless the person has already said, ask what they want to see first. Each answer maps to one path in step 4:\n\n| They want | Path |\n| ------------------------------------------------------- | ------------------------------- |\n| Visitors, page views and clicks in a web app | [Web app](#web-app) |\n| Events, jobs or errors from a backend | [Backend](#backend) |\n| Screens and errors in a React Native or Expo app | [React Native](#react-native) |\n| To know that a site or API is up | [Uptime check](#uptime-check) |\n| Data they already have: files, exports, another service | [Anything else](#anything-else) |\n\n## 1. Sign in\n\n```bash\nnpx @tailglow/cli@latest login\n```\n\nWithout a terminal attached, which is how an agent runs it, the command prints a link and a code, remembers the sign-in, and exits. Show both to the person. They sign in, or create an account if they have none (the first 3 days are free and need no card), check that the page shows the same code, and approve. Then finish signing in:\n\n```bash\nnpx @tailglow/cli@latest login --wait\n```\n\n`--wait` waits up to a minute. Exit code `75` means the person has not approved yet: run it again. Exit code `1` with status `unreachable` means the API could not be reached: the sign-in is kept, so run `login --wait` again once the connection is back. Status `consumed` means the credential was already handed out: start over with `login --new`. Any other exit code `1` means the sign-in was denied, expired or withdrawn: start over with `login`.\n\nThis signs the CLI in as the person, with what their role allows in each of their teams, starting in the team they choose. To use an API key for one team instead, with only the permissions the person picks, add `--method api-key`; that team needs a project first.\n\nWhen Tailglow is at capacity, a brand-new account joins a waitlist instead of signing in, so the sign-in cannot be approved and `--wait` keeps exiting `75` until it expires. If the person sees the waitlist, say so and stop until they are let in.\n\nThe credential is saved to `~/.tailglow/config.json` and the CLI uses it from then on. You never need to see it: do not ask for it, print it, or put it in code. `tglow logout` removes it.\n\nTo keep the CLI around, install it. It needs Node 22 or newer:\n\n```bash\nnpm install -g @tailglow/cli\ntglow whoami\n```\n\nWhen a command ends with a line saying a newer `tglow` is available, update before you continue: run `npm install -g @tailglow/cli@latest`, or use `npx @tailglow/cli@latest` if you run it through npx. An older CLI is missing the newest commands, and its `--help` and `tglow docs` describe only the version you have.\n\nIn CI, set `TAILGLOW_API_KEY` to an API key instead of logging in.\n\n## 2. Learn the CLI as you go\n\nEvery command has the shape `tglow <resource> <command> [flags]`, and the CLI documents itself:\n\n```bash\ntglow help # every resource and the global flags\ntglow sources # the commands on one resource\ntglow sources create --help --json # one command's flags, types, required fields and rules\n```\n\nRead `--help --json` before you call a command for the first time. It is generated from the same validation the API enforces, as of the CLI version you have installed, and its `notes` carry rules a field list cannot.\n\n- Flags are field names in kebab-case: `repeat_window_minutes` is `--repeat-window-minutes`.\n- IDs in the path are flags too: `--source-id src_x`, never a positional argument.\n- Output is JSON whenever it is piped. Read `.data`, and `.pagination.next_cursor` to continue a list.\n- Set the project once so you can leave it off every command: `tglow config set project prj_...`\n\nThe CLI also carries these docs, so you do not need to fetch them. `tglow docs` prints the index and `tglow docs guides/sdk` prints one page, as markdown even when piped.\n\n## 3. Pick a project\n\n```bash\ntglow projects list\ntglow projects create --name \"Acme web\"\ntglow config set project prj_...\n```\n\nReuse a project when the person already has one for this app. A new team's first project comes with a free trial server, which starts in the background: `tglow servers list` shows `provisioning` until it is `active`.\n\n## 4. Connect data\n\nMost paths need a source and an ingest key:\n\n```bash\ntglow sources create --name \"Web app\"\ntglow ingest_keys create --source-id src_...\n```\n\nThe ingest key's `url` is the address to send records to, with the key already in it. It is `null` until the project's server is active; retrieve the key again once it is. An ingest key can only write records into its own source, so it is safe in client-side code. Keep it in the app's environment configuration anyway, so it can be replaced without a code change.\n\n### Web app\n\n```bash\nnpm install @tailglow/browser\n```\n\n```javascript\nimport { Tailglow } from \"@tailglow/browser\";\n\n// The ingest key's url, read from the app's public environment configuration.\nconst tg = new Tailglow({ url: TAILGLOW_INGEST_URL });\n\ntg.track(\"signup\", { plan: \"pro\" });\ntg.identify(\"user_123\");\n```\n\nPage views, clicks in elements marked `data-telemetry`, outbound clicks, uncaught errors and performance signals are collected automatically. A site without a build step can use the script tag instead: see [Drop-in script](/guides/quickstart#drop-in-script). In Next.js, SvelteKit or another framework that renders on the server, create the SDK in the browser only, once: see [Frameworks](/guides/sdk#frameworks).\n\nThe SDK ignores `localhost` by default. To test locally, pass `excludeLocalhost: false` and tag the traffic, for example `context: { environment: \"development\" }`, so it can be filtered out of real charts.\n\n### Backend\n\n```bash\nnpm install @tailglow/core\n```\n\n```javascript\nimport { TailglowCore } from \"@tailglow/core\";\n\nconst tg = new TailglowCore({ url: process.env.TAILGLOW_INGEST_URL });\n\ntg.track(\"job_completed\", { job_id: \"abc\" });\nawait tg.flush();\n```\n\nNothing is collected automatically on a backend. Flush before the process or request ends; in a serverless function, see [Serverless functions](/guides/sdk#serverless-functions). Node 22 or newer, or Bun. Any other language can [send records over HTTP](#anything-else).\n\n### React Native\n\nInstall `@tailglow/react-native` and follow [React Native](/guides/sdk#react-native-tailglowreact-native) in the SDK guide. The app passes the SDK its `AppState`, `Platform`, storage and navigation, which the guide shows in one file.\n\n### Uptime check\n\nA check needs no code. It fetches a public HTTPS address on a schedule and records whether it answered:\n\n```bash\ntglow checks create --name \"Homepage\" --source-id src_... --collection-slug uptime --endpoint-url https://acme.com\ntglow checks test --check-id chk_...\n```\n\n`test` fetches once and stores nothing. Checks that share a collection chart together, one row each, which is what a status page is. See [Checks](/guides/checks).\n\n### Anything else\n\nPOST JSON, JSONL, CSV, TSV or plain text to the ingest URL. Add `&collection=<name>` to choose where the records land; the collection is created the first time data arrives for it.\n\n```bash\ncurl -X POST \"$TAILGLOW_INGEST_URL&collection=orders\" \\\n -H \"Content-Type: application/json\" \\\n -d '[{\"order_id\":\"o_1\",\"total\":42.5,\"created_at\":\"2026-09-27T12:00:00Z\"}]'\n```\n\n`202 Accepted` means the records are stored and will be processed. Retry on `5xx` and on network errors; a `4xx` means the request itself has to change. One request can carry up to 20 MiB. See [Sending data](/guides/sending-data).\n\n## 5. Confirm data arrived\n\n```bash\ntglow collections list --source-id src_...\ntglow collections list-docs --collection-id col_...\n```\n\nThe browser and backend SDKs write to collections named `events`, `errors` and `logs`. A collection appears with its first record.\n\n## 6. Build a chart\n\nA view is written for a settled record shape. By default Tailglow settles a shape when it arrives again at least 60 minutes after it was first seen, or when 100 or more records of it arrive in one batch. Check with:\n\n```bash\ntglow collections list-schemas --collection-id col_...\n```\n\nIf the person confirms a candidate shape is real data rather than a one-off test, settle it by hand:\n\n```bash\ntglow collections update-schema --collection-id col_... --schema-version-id csv_... --state settled\n```\n\nThen create a view on the collection and declare the fields it should produce, based on the records you sampled in step 5. A metric can only use fields the view declares, and its time field has to be one of them, typed `date`. This example reads the SDK's `events` collection:\n\n```bash\ntglow views create --collection-id col_... --name \"Product events\" \\\n --output-schema '[{\"name\":\"event_time\",\"type\":\"date\"},{\"name\":\"type\",\"type\":\"string\"},{\"name\":\"plan\",\"type\":\"string\"}]'\n```\n\nField types are `string`, `number`, `boolean`, `date`, `array` and `null`. Tailglow writes the transform that fills those fields from the records. Add `--hint` only for a rule it cannot read from field names, such as \"classify orders above 500 as large\". Choose the fields with care: once the transform is written, the view's fields cannot be changed in place.\n\nRetrieve the view until its `status` is `active` before telling the person the chart is ready. If it is `error`, read `error_message`. If `waiting_shapes_count` is above 0, `tglow views list-deferrals --view-id view_...` shows what it is waiting on.\n\n```bash\ntglow views retrieve --view-id view_...\n```\n\nThen build up from the view. The `events` collection holds page views and clicks as well as your own events, so filter a metric to the event it counts:\n\n```bash\ntglow metrics create --view-id view_... --name \"Signups per day\" --timestamp-field event_time \\\n --filters '?type=equals:signup' --ui-chart-value count --ui-chart-interval day\ntglow pages create --name \"Product\"\ntglow components create --page-id page_... --metric-ids mtrc_...,mtrc_...\n```\n\nRead `--help --json` on each command for the rest of its fields.\n\nPeople who would rather describe a chart in words can ask the assistant in the Tailglow dashboard.\n\n## Rules\n\n- Ask before any delete. Without a terminal attached, `delete` runs immediately and never asks.\n- Ask before changing servers or billing. Servers cost money.\n- Never print, log or commit the CLI credential. Ingest keys are fine in client-side code.\n- Send only fields that `--help --json` lists. The API rejects unknown fields.\n\n## More\n\n- The same pages without a network call: `tglow docs`, then `tglow docs <path>` for any page it lists\n- Index of the docs for agents: [docs.tailglow.io/llms.txt](https://docs.tailglow.io/llms.txt)\n- Every page in one file: [docs.tailglow.io/llms-full.txt](https://docs.tailglow.io/llms-full.txt)\n- Any docs page as markdown: add `.md`, for example [docs.tailglow.io/guides/sdk.md](https://docs.tailglow.io/guides/sdk.md)\n- Guides: [JavaScript SDK](/guides/sdk), [Sending data](/guides/sending-data), [Checks](/guides/checks), [TGL](/guides/tgl), [Metrics](/guides/metrics), [Pages](/guides/pages), [CLI](/guides/cli)\n",
|
|
40
40
|
"guides/quickstart": "# Quickstart\n\nUsing a coding agent? Paste this prompt into Claude Code, Codex, Cursor, or Cowork:\n\n```prompt\nSet up Tailglow using https://tailglow.io/agents.md\n```\n\nGet a web app sending data to Tailglow. This is the typical setup: a browser app that auto-collects page views, marked-section engagement and clicks, outbound clicks, uncaught errors, and performance measurements, with your own custom events on top.\n\nYou need one value: the **ingest URL** from the source's **Ingest** tab, which already carries its key (for example `https://us11.ingest.tailglow.io/prj_xxx?key=tg_ingest_your_key`). The [Ingest Key](/guides/ingest-key) guide shows how to reach it.\n\nCopying the key on its own and passing it separately still works, and a key passed that way overrides one carried in the URL.\n\n## Add the SDK\n\nChoose one. Both do exactly the same thing; use the drop-in tag if you do not have a build step.\n\n### Drop-in script\n\nPaste one tag into your `<head>`. It self-initializes and starts collecting immediately, no build step:\n\n```html\n<script\n defer\n src=\"https://cdn.tailglow.io/tglow.js\"\n data-url=\"https://us11.ingest.tailglow.io/prj_xxx?key=tg_ingest_your_key\"\n></script>\n```\n\n### npm\n\n```bash\nbun add @tailglow/browser\n# or: npm install @tailglow/browser\n```\n\n```javascript\nimport { Tailglow } from \"@tailglow/browser\";\n\nconst tg = new Tailglow({\n url: \"https://us11.ingest.tailglow.io/prj_xxx?key=tg_ingest_your_key\"\n});\n```\n\nEither way, auto-collection starts the moment the SDK loads: page views, engagement and clicks inside elements marked with `data-telemetry`, outbound-link clicks, uncaught errors, performance measurements, and a one-time device snapshot. The default path is cookieless and does not read or write persistent browser storage. Assess consent and disclosure requirements for the data you collect and the jurisdictions where you operate.\n\n## Track your own events\n\nSend anything meaningful in your product. The first argument is the event name, the second is any properties you want to attach:\n\n```javascript\ntg.track(\"signup\", { plan: \"pro\" });\ntg.track(\"purchase\", { amount: 99, currency: \"USD\" });\n```\n\nWith the drop-in tag, call the global `tglow` the same way:\n\n```javascript\ntglow(\"track\", \"signup\", { plan: \"pro\" });\n```\n\nOr track clicks straight from your markup, no JavaScript:\n\n```html\n<button data-tglow-event=\"signup\" data-tglow-method=\"github\">Sign up</button>\n```\n\n## Identify the signed-in user\n\nWhen your app knows who the user is (from your own auth), attach their ID. Events already queued earlier in the session are backfilled with it:\n\n```javascript\ntg.identify(\"user_123\");\n```\n\n## See it on localhost\n\nBy default the SDK skips `localhost` so dev traffic does not pollute your analytics. To watch events flow while developing, opt in and tag them as dev so you can filter them out later:\n\n```javascript\nconst tg = new Tailglow({\n url: \"https://us11.ingest.tailglow.io/prj_xxx\",\n key: \"tg_ingest_your_key\",\n excludeLocalhost: false,\n context: { environment: \"development\" }\n});\n```\n\nTrigger a page view or a `track()` call, then confirm the records land in your project.\n\n## Next steps\n\n- **[JavaScript SDK](/guides/sdk)**: the full reference, covering error capture, identity, context, configuration, and record shapes.\n- **Backend, Electron, and React Native**: the same core runs server-side and in native apps. See [Runtimes](/guides/sdk#runtimes) in the SDK guide.\n",
|
|
41
41
|
"guides/concepts": "# Concepts\n\nUsing a coding agent? Paste this prompt into Claude Code, Codex, Cursor, or Cowork:\n\n```prompt\nSet up Tailglow using https://tailglow.io/agents.md\n```\n\n## Introduction\n\nTailglow separates the ingest boundary from the data boundary. The core concepts are:\n\n- **Sources**: Authentication and ingest configuration for a project.\n- **Collections**: Logical streams of records within a source. Collections own schema versions, files, and raw records.\n- **Views**: Transformed data derived from a collection or produced by joining views.\n- **Metrics**: Aggregated measurements and charts built from views.\n\n## Sources and collections\n\nA source is the place you configure ingest keys. Its **Ingest** tab displays the project's default endpoint after one has been provisioned and any available team ingest domains. If neither exists, the tab says **No ingest endpoints configured**. The project and team domains own those routing endpoints; the ingest key selects the source. A new source starts with no collections.\n\nTailglow chooses one collection for each ingest request. A valid `?collection=charges` query parameter picks it, and a request without one goes to the source's `catchall` collection. Fields inside the records, including `type`, never choose the collection. Every record in a JSON array or JSONL request goes to that collection.\n\nA source supports up to 100 collections. New collection names route to `catchall` after the source reaches that guardrail. A burst of simultaneous requests can briefly create more than 100 collections.\n\n## Records\n\nRecords are the events you send to an ingest endpoint. Tailglow accepts JSON, JSONL, CSV, TSV, PSV, SSV, and plain text. Raw collection records can be sampled as soon as they arrive, whatever the state of their schema version.\n\nJSON records need no fixed wrapper. Tailglow normalizes tabular and text input into JSON records, renames unsafe property keys, adds delivery metadata, and replaces embedded image data with artifact references.\n\n## Schema versions\n\nEvery record that reaches a collection is read for its shape: the paths it carries and the type at each path, down to the collection's depth rule. The first time a shape is seen, the collection registers a schema version for it and stamps every file holding that shape with the version. A version's number is permanent. Its canonical shape can still refine in place: a path first seen blank learns its type, and the paths a wider record carries are added once that shape has come back or arrived in bulk, so a single stray key never changes a version. A rule change can merge two versions into one, and the merged version's files move to the survivor. A record that is not a JSON object, or an empty one, has no shape to register: it is stored and kept, but it never joins a schema version and no view reads it.\n\nA version is in one of three states:\n\n- **Candidate**: the shape has been seen once. Its records are stored, but views leave the shape out until it settles: each view lists it under **Pending shapes** as Settling and indexes its records once it settles. Charts mention those records without marking their period incomplete.\n- **Settled**: the shape was seen again after the collection's recurrence gap, arrived in one batch with at least the bulk row count, or a person settled it. Only settled versions reach views.\n- **Stale**: a candidate that was not seen again within the stale window. Its rows stay stored but unclassified, and views stop listing it as pending. A stale shape that returns settles.\n\nEach view decides separately whether an existing TGL script can read a version. Changes under fields a script never reads reuse the script at once. A change from `name: string` to `name: {first, last}` needs a new decision for a view that reads the name, while a view that only reads `run_id` continues.\n\nWhen a version needs a mapping, an existing script whose input paths and types match exactly takes priority. Among equally compatible scripts, Tailglow prefers the mapping assigned to the highest schema version number. A generated script that exactly matches a validated earlier script can keep that earlier mapping. Reusing a script for a new version leaves existing versions' assignments unchanged. A script chosen for one version is still validated before it is copied to other versions.\n\nTailglow can also adapt a unique ID path such as `user_id` becoming `user.id`, without AI. The replacement must have the same type, every other input the script reads must match exactly, and the script must pass the view's mapping checks. Filters and expressions keep their behavior using the new path. Ambiguous paths, changed types, paths requiring an array element choice, and ID paths hidden by the version's recorded grouping or depth limits still need a mapping decision. Changing grouping or depth rules does not recover fields hidden in earlier observations. Fields seen in a version's records but still waiting to enter its schema also prevent an automatic ID move. Versions without enough retained observation history also need a decision before an ID path can be replaced.\n\nNulls and empty values never make a new version: a version first seen with `user_id: null` refines in place once strings arrive. Absent keys follow the collection's optional keys switch. On automatic, a record whose keys are a subset of a version's keys joins that version. On manual, absence is structural, and a record missing a key is a distinct version. A mapping written while the path had no values is flagged on the view with the refined paths, and re-authoring it clears the notice. See [Schema settings](/guides/sources#schema-settings) for the rules and settings a collection carries.\n\n## Views\n\nViews reshape data with [TGL](/guides/tgl). In the **Views** page, click **New View** and choose one of the available creation modes:\n\n- **Collection**: Choose a **Source Collection**, optionally define an output schema and a generation hint, then click **Create View**. Tailglow generates transforms for the collection's settled schema versions.\n- **View**: This is a join builder, not a simple parent-derived view. Choose a base view, add one or more lookup views, and select the fields that join them.\n\nThe view's **Overview** shows how source versions flow through scripts into the output schema and linked metrics, drains, or views. Narrow panels use a connected vertical layout. More than two scripts become a counted group; open it for the script list, or open a source group to choose an individual version. Failed and processing scripts remain visible in the group's status. Counts describe the loaded data; **Version coverage** lets you load more versions.\n\nThe saved **Generation hint** is shown directly below the diagram on desktop and mobile. Change it in the view's Settings. If none is saved, the diagram says **No generation hint added.** Users who can edit the view can click the **+** beside it to open Settings and add a hint.\n\nYou can inspect transformed data on a view's **Records** tab.\n\n## Metrics\n\nMetrics aggregate data from a view. Choose a timestamp field, optional grouping, filters, and value or unique-count fields. Use metrics to chart counts, numeric values, and grouped trends, then attach monitors when you need alerts. Events with overridden timestamps that land far in the past are held for review rather than charted silently; [Late data](/guides/metrics#late-data) explains the window.\n\n## Summary\n\nSources authenticate and configure ingest. Collections own the raw data and its schema versions. Views transform or join collection data one schema version at a time. Metrics aggregate view output for analysis and monitoring.\n",
|
|
42
42
|
"guides/auth": "# Authentication\n\nTailglow supports email sign-in and passwords. Each browser keeps its own session, which you can\nreview or revoke from your profile.\n\n## Email sign-in\n\nEnter your email address and Tailglow emails you a sign-in link and an 8-character code. This\npasswordless option is the default for new accounts.\n\n- The link and the code only work in the browser where you asked for them. If you open the email on\n another device, such as your phone, type the code into the browser where you started.\n- Both expire after 10 minutes and work once. Requesting a new email replaces the previous link and\n code.\n- If you open the link in another tab of the same browser, the tab where you started tells you to\n finish signing in there, including your MFA code if your account uses MFA. Once you have, it shows\n that you are signed in. You can close it at any point.\n- You can request up to 5 sign-in emails in 15 minutes, and up to 20 in a day.\n\n## Password\n\nPasswords are optional and must be at least 16 characters long. Use a password manager to generate\nand store yours.\n\nYou must enable multi-factor authentication (MFA) before adding a password. To change your password,\nopen your profile's **Security** tab and provide a valid MFA code.\n\n## Browser sessions\n\nSigning in creates a separate session for that browser. Signing in somewhere new does not sign out\nyour other browsers, and closing a browser does not end its session.\n\nOpen **Profile** > **Security**, then find **Devices** to review active sessions and sign out a\nspecific browser. You can have up to 25 active browser sessions. If you exceed that limit, Tailglow\nsigns out the least recently active session. A session you have not used for six months, in a\nbrowser or a terminal, ends on its own, and you sign in again there.\n\nClearing Tailglow site data signs out that browser locally. The session may remain listed in the\n**Devices** section until you revoke it.\n\n## CLI sessions\n\nApproving a `tglow login` in the browser gives that terminal a session of its own. It appears under\n**Devices** with a **CLI** label and the machine's name. Like a browser session, it can act in any of\nyour teams; it starts in the one you chose when you approved it. A CLI session carries the MFA\nverification of the browser session that approved it.\n\nCLI sessions are counted separately from browser sessions, up to 10 active at once; signing in from\nanother terminal beyond that ends the least recently used one. Revoking it under **Devices**,\nsigning out of every device, changing your password, resetting MFA, or running `tglow logout` in\nthat terminal ends it immediately.\n\nA CLI session cannot change how your account signs in: MFA, passwords and sessions are managed\nhere, in the browser. See [CLI](/guides/cli#authenticating).\n\n## Multi-factor authentication\n\nTailglow uses time-based one-time passwords (TOTP) for MFA. You will need an authenticator app that\ncan scan a QR code and generate six-digit codes.\n\n### Set up MFA\n\n1. Open **Profile** and select **Security**.\n2. In **Multi-factor Authentication**, open the actions menu and select **Enable MFA**.\n3. Scan the QR code with your authenticator app.\n4. Click **Confirm MFA**.\n5. Enter the six-digit code from your authenticator app and click **Submit**.\n6. Save the backup codes, then click **I've copied my backup codes**.\n\nBackup codes are shown only once. Store them somewhere secure, such as a password manager. Each code\ncan be used once, and Tailglow emails you when a backup code is used to sign in.\n\nYou can regenerate backup codes from **Profile** > **Security**. Regenerating them requires a valid\nMFA code and invalidates the previous set.\n\n### Disable MFA\n\nYou can disable MFA from **Profile** > **Security** with an authenticator code or backup code. If\npassword sign-in is enabled, disabling MFA also disables password sign-in. You can continue using\nemail sign-in. Disabling MFA also signs you out of every browser, including the one you are using.\n\n### Team-required MFA\n\nTeam owners can require MFA for all members. If a team requires MFA, Tailglow prompts members to set\nit up before they can access that team.\n\nMFA verification applies to one browser session at a time.\n\n## Sign out and security activity\n\nSigning out normally ends only the current browser session. From **Profile** > **Security** you can:\n\n- Use the **Devices** section to sign out another browser or every browser.\n- Review authentication history, including the action, time, device, and IP address.\n\n## Roles and permissions\n\nYour role in each team determines which features and actions you can access. Team owners can manage\nroles from **Team Settings** > **Roles**. See [Permissions](/guides/permissions) for how access levels\nand role presets work.\n\nYour selected team is specific to each browser, so another signed-in browser can remain on a\ndifferent team.\n",
|
|
43
43
|
"guides/servers": "# Servers\n\nServers receive and process data for a project. A source's **Ingest** tab shows the project's endpoint when one is available.\n\n## Trial and paid servers\n\nThe free trial starts one server in your team's first project and grants the team 72 server-hours of compute credit. A payment method is required to add more servers. Credits are shared across projects: two running servers use the same 72-hour balance in 36 hours. Manage Servers shows the monthly cost before you confirm a change.\n\nThe trial starts when the trial server first comes up. Without a payment method, the server pauses when its 72 server-hours run out, and Manage Servers cannot scale it to zero before then. Billable usage ends at the trial deadline, including when cleanup runs late. With a card, the trial ends when the team's server-hours run out, and servers keep running, using any remaining credits before paid usage begins. Unused credits do not expire. Tailglow staff can add server-hours to a trial, which also brings back a paused trial server. See [Billing](/guides/billing#free-server-trial) for month boundaries, resume rules, and the AI and storage allowances every team has.\n\n## Manage server count\n\n1. Open **Servers** in the project sidebar.\n2. Click **Manage Servers**.\n3. Set the desired server count.\n4. Review the consequence and confirm it.\n\nScaling up uses **Add server** or **Add servers** and starts provisioning after you confirm. Scaling down, including scaling to zero, requires a hold-to-confirm action. Scaling to zero stops ingestion, so events sent while no server is running are not stored or recoverable.\n\n### Scaling during a platform update\n\nTailglow updates every server to new platform versions one project at a time, reserving the capacity each update needs before it starts. While an update is in progress, a change to the server count is queued instead of applied: Manage Servers confirms the request, the Servers page shows the queued count, and the change starts automatically when the update finishes. Adding servers to a project that has not finished moving to the current platform version is queued the same way; removing servers is not.\n\nTo cancel a queued change, select **Cancel queued change** on the Servers page, or set the count back to the number of servers the project runs. A newer request replaces an older one. A project with no servers yet is not held back; its first server starts right away. A paused trial server that resumes after you add a payment method also waits for the update to finish.\n\n## Statuses\n\nA new server starts as **provisioning** and becomes **active** after setup. Scaling down changes it to **terminating** while Tailglow drains and removes it. Scaling up later creates a new server.\n\n## Server analytics\n\nServer detail pages show resource usage for the selected server. **Project Ingest Pipeline** shows pending records, throughput, and errors across every server in the project, including temporary deployment servers. You can select a time range or brush across a chart to focus on a smaller window.\n\nThe current queue counts valid spooled records awaiting write, including records being processed. It refreshes independently of the selected chart window. When queues change during the observation, Tailglow counts the durable records it can confirm and marks the pending count with **≈**. If a current server's queue cannot be read or a concurrent write's outcome is unknown, the queue is unavailable instead of showing an uncertain total. A stale reading means the last complete or estimated snapshot is too old to confirm the current queue. The single-line summary shows processed records from the last completed minute alongside the current pending count. The error button appears when the selected chart range contains recorded errors; errors count rejected requests and dropped frames.\n\nThe summary shows a skeleton while its first request loads and keeps existing readings visible during refreshes. Delayed or unavailable measurements are labeled separately, so an available queue or processed count remains visible. A retry control appears after a failed refresh or when neither current reading is available.\n\nHistorical pending values are average queue occupancy, so they can contain fractions. Older history recorded in request frames has gaps in the pending-record series.\n\nThroughput is counted in the minute each spool batch completes. Delayed sampling or shutdown uploads preserve that minute, even when resource measurements for it are unavailable. This does not reconstruct missing historical measurements.\n\nMissing measurements appear as gaps, including when a server's contribution cannot be read. Pending, throughput, and errors can be available independently. Live polling pauses in background tabs.\n\nThe analytics API supports windows up to 48 hours with minute buckets or 30 days with hour buckets. Future end times are limited to the current time.\n\nMeasured values in the current chart bucket show **(so far)** until the bucket ends. For example, at 9:35 an hourly bucket starting at 9:00 is still in progress.\n",
|
|
44
44
|
"guides/ingest-key": "# Ingest Key\n\n## Getting Your Key\n\nYour ingest key is located in your source settings. Select a project, open **Settings**, choose the **Sources** tab, and click the edit button for a source. In the source drawer, select the **Ingest** tab. The **Ingest Keys** section lets you view and manage the source's keys. Each source can have multiple ingest keys.\n\n## Prefix\n\nYour ingest key always begins with the prefix `tg_ingest_`.\n\n## Usage\n\n### Query Parameter (Recommended)\n\nThe simplest way to authenticate is by including your ingest key as a `key` query parameter in the URL. This works across all platforms -- browsers, backends, and mobile apps.\n\n```bash\ncurl -X POST \"https://us11.ingest.tailglow.io/prj_xxx?key=tg_ingest_123\" \\\n -H \"Content-Type: text/plain\" \\\n -d '{\"records\": []}' # Your records here\n```\n\nReplace the URL with your project's ingest endpoint, found in your source settings.\n\nEven though the payload is JSON, setting `Content-Type: text/plain` is safe because the ingest server auto-detects the format. For browser-based clients, this is especially important: `text/plain` is a [simple content type](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests), so browsers skip the CORS preflight OPTIONS request entirely, sending one request instead of two.\n\nThis method also works with `navigator.sendBeacon`, which does not support custom headers:\n\n```javascript\nnavigator.sendBeacon(\n \"https://us11.ingest.tailglow.io/prj_xxx?key=tg_ingest_123\",\n JSON.stringify({ event: \"page_view\", path: \"/home\" })\n);\n```\n\n### Authorization Header\n\nYou can also include your ingest key in the `Authorization` header using the `Bearer` or `Key` prefix:\n\n```bash\ncurl -X POST https://us11.ingest.tailglow.io/prj_xxx \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Key tg_ingest_123\" \\\n -d '{\"records\": []}' # Your records here\n```\n\nNote that from browsers, using a custom `Authorization` header with `Content-Type: application/json` will trigger a CORS preflight request, doubling the number of requests.\n\n## Verify the connection\n\nSend a `GET` request to the same complete ingest URL before uploading. It verifies the key for the destination project and checks whether that server can currently accept data. No request body is needed, and no records or collections are created.\n\n```bash\ncurl -i \"https://us11.ingest.tailglow.io/prj_xxx?key=tg_ingest_123\"\n```\n\nA successful check returns `200 OK` with:\n\n```json\n{ \"status\": \"ready\", \"project_id\": \"prj_xxx\" }\n```\n\nThe `Authorization` header works for verification too. Use the same key and destination you will use for the upload.\n\n| HTTP status | Meaning | What to do |\n| ----------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |\n| `200` | The key is accepted for this project and the server is ready to receive data. | Send your data with `POST` to the same URL. |\n| `401` | The key is missing, invalid, disabled, or belongs to a different project. | Copy the complete ingest URL again from the source's Ingest tab. |\n| `503` | The server cannot currently verify the key or accept data. | Check the response's `error.code` and `message`; wait for the `Retry-After` interval before checking again. |\n\nError responses use the same envelope as uploads, with a top-level `message` and the error code in `error.code`. A `503` with `ingest_auth_unavailable` means the key could not be verified, rather than that it is invalid. `invalid_server_state` means the server is starting or draining; `ingest_spool_full` means its storage writer or capacity is unavailable. A network error or an unreachable project route also means readiness has not been confirmed.\n\nVerification uses the same authentication cache as uploads, and responses are marked `Cache-Control: no-store`. It can update both the key's recent-use time and the project's recent-activity indicator without ingesting any records. A successful check describes the server at that moment: it does not reserve capacity or validate the file you plan to send. Wait for each upload's `202 Accepted` response to confirm durable receipt; downstream processing happens afterward.\n\nThe infrastructure `/health/*` endpoints and CORS `OPTIONS` responses do not verify ingest keys. Use this authenticated `GET` request to check your destination.\n\n## Security\n\nYour ingest key can send data and verify readiness on your ingest server; it cannot read stored records. While it's important to keep this key private, it's acceptable to include it in frontend code if you're using the ingest server for frontend metrics and analytics.\n\n## Managing Your Keys\n\nEach source can have multiple ingest keys. You can create new keys and delete old ones as needed from your source settings. When you need to rotate a key, simply create a new one, update your services to use it, and then delete the old key.\n\n## Key and Team Status\n\nAn ingest key works only when its team status also permits ingestion:\n\n- **Active team**: An active key on an active team works normally.\n- **Delinquent team**: For a non-Enterprise team, an uncollectible or later-unpaid invoice can make the team delinquent. Active ingest keys are then disabled until the payment issue is resolved. A single failed payment attempt does not by itself apply this state.\n- **Restricted team**: The team is read-only and ingestion is disabled until the restriction is resolved.\n- **Blocked team**: The team cannot be accessed and ingestion is disabled. Contact support to resolve this.\n\nAn ingest request is accepted only when both the key and its team are active. If the team becomes `delinquent`, `restricted`, or `blocked`, the key stops accepting data. Update your payment information or contact support as appropriate to restore access.\n",
|
|
45
|
-
"guides/sending-data": "# Sending Data\n\nSend records to the full ingest endpoint copied from a source's **Ingest** tab:\n\n```url\nhttps://{region}.ingest.tailglow.io/{project_id}\n```\n\nInclude an ingest key as the endpoint requires. Do not substitute a generic ingest hostname for your project endpoint.\n\n## Check before uploading\n\nSend a `GET` request to your complete ingest URL, including its key, to verify authentication and current readiness without sending records. A `200 OK` response with `status: \"ready\"` confirms the key and destination are ready for an upload. See [Verify the connection](/guides/ingest-key#verify-the-connection) for examples and failure responses.\n\nUpload with `POST` to the same address and wait for each request's acknowledgement. Readiness can change after the check.\n\n## Upload a file in the app\n\nOpen **Collections → Upload data** to choose an existing collection or create one. From inside a collection, **Upload data** selects that collection for you. Choose an ingest key and an XML, JSONL, CSV, TSV, or JSON file. The app detects the format from a bounded content sample and previews the first records. A sampled preview does not validate the whole file or guarantee every field has been seen.\n\nFor XML, choosing the file is enough. The app automatically imports each entry directly inside the document's root, including different entry types. Nested content stays together. Root attributes and meaningful text are retained in a separate document record; a document containing only its root becomes one record. Each XML record has `name`, `attributes`, and ordered `children`, with attribute values preserved as strings. Line-break indentation in element-only containers is removed; meaningful text, inline spaces, whitespace-only values, CDATA, character references and `xml:space=\"preserve\"` are retained.\n\n**Advanced options** lets you override the detected format or import only part of an XML file using a **Custom XML path**, such as `/HealthData/Record`. Paths use literal element names, including any namespace prefix, rather than XPath expressions. No path selection is required for a normal upload.\n\nXML imports support UTF-8 XML 1.0 with predefined and numeric entities. Namespace names are preserved without resolving namespace URIs. DOCTYPE declarations are framed but not applied; external resources and declared entities are not loaded. HTML is not supported.\n\nCSV and TSV require UTF-8 with unique, non-empty column headers. JSONL requires one JSON object per non-empty line. Whole-document JSON accepts an object or an array of objects, up to 50 MiB. Export Excel or Numbers spreadsheets as CSV first.\n\nStreaming XML, JSONL, CSV and TSV have no total file-size cap in the picker. The browser reads bounded chunks and uploads batches of at most 500 records or 1 MiB. Each serialized record, including its newline, must fit within 1 MiB. Keep the tab open while importing. Large files still require time to read and send, and the destination's normal ingest limits apply.\n\nAfter the preview, choose **Import records** to start. Each record is checked before its batch is sent. You can optionally check the full file first to catch file errors before sending anything. A failed check prevents import; changing the format or XML path creates a new preview. The app also verifies the destination before sending records. If the destination check fails, fix it or choose another key, then select **Check again**.\n\nSource dates stay in the records.
|
|
45
|
+
"guides/sending-data": "# Sending Data\n\nSend records to the full ingest endpoint copied from a source's **Ingest** tab:\n\n```url\nhttps://{region}.ingest.tailglow.io/{project_id}\n```\n\nInclude an ingest key as the endpoint requires. Do not substitute a generic ingest hostname for your project endpoint.\n\n## Check before uploading\n\nSend a `GET` request to your complete ingest URL, including its key, to verify authentication and current readiness without sending records. A `200 OK` response with `status: \"ready\"` confirms the key and destination are ready for an upload. See [Verify the connection](/guides/ingest-key#verify-the-connection) for examples and failure responses.\n\nUpload with `POST` to the same address and wait for each request's acknowledgement. Readiness can change after the check.\n\n## Upload a file in the app\n\nOpen **Collections → Upload data** to choose an existing collection or create one. From inside a collection, **Upload data** selects that collection for you. Choose an ingest key and an XML, JSONL, CSV, TSV, or JSON file. The app detects the format from a bounded content sample and previews the first records. A sampled preview does not validate the whole file or guarantee every field has been seen.\n\nFor XML, choosing the file is enough. The app automatically imports each entry directly inside the document's root, including different entry types. Nested content stays together. Root attributes and meaningful text are retained in a separate document record; a document containing only its root becomes one record. Each XML record has `name`, `attributes`, and ordered `children`, with attribute values preserved as strings. Line-break indentation in element-only containers is removed; meaningful text, inline spaces, whitespace-only values, CDATA, character references and `xml:space=\"preserve\"` are retained.\n\n**Advanced options** lets you override the detected format or import only part of an XML file using a **Custom XML path**, such as `/HealthData/Record`. Paths use literal element names, including any namespace prefix, rather than XPath expressions. No path selection is required for a normal upload.\n\nXML imports support UTF-8 XML 1.0 with predefined and numeric entities. Namespace names are preserved without resolving namespace URIs. DOCTYPE declarations are framed but not applied; external resources and declared entities are not loaded. HTML is not supported.\n\nCSV and TSV require UTF-8 with unique, non-empty column headers. JSONL requires one JSON object per non-empty line. Whole-document JSON accepts an object or an array of objects, up to 50 MiB. Export Excel or Numbers spreadsheets as CSV first.\n\nStreaming XML, JSONL, CSV and TSV have no total file-size cap in the picker. The browser reads bounded chunks and uploads batches of at most 500 records or 1 MiB. Each serialized record, including its newline, must fit within 1 MiB. Keep the tab open while importing. Large files still require time to read and send, and the destination's normal ingest limits apply.\n\nAfter the preview, choose **Import records** to start. Each record is checked before its batch is sent. You can optionally check the full file first to catch file errors before sending anything. A failed check prevents import; changing the format or XML path creates a new preview. The app also verifies the destination before sending records. If the destination check fails, fix it or choose another key, then select **Check again**.\n\nSource dates stay in the records. Tailglow records when each batch arrives; views and charts can use a source date field instead, such as a historical start time.\n\nThe rows animate as the server acknowledges them. **Stop import** waits for the current request and sends no further batches. Upload requests are not retried automatically; a network failure can leave the last batch's delivery uncertain. A late file error can also stop an import after some batches have arrived. The result reports accepted records and any affected batch range. Check delivery before starting another import, since repeating it can create duplicates. Accepted records may take a moment to appear in the collection.\n\n## Import a file with the CLI\n\nThe CLI requires Node 22 or newer. Scan and validate work locally without credentials:\n\n```bash\ntglow import scan ./export.xml\ntglow import scan ./export.xml --record-path /HealthData/Record\ntglow import validate ./export.xml\n```\n\n`scan` reports a bounded sample, schema, XML paths and possible timestamp fields. Add `--full` to scan to the end. XML uses the same automatic record selection as the app. `validate` reads the entire file. An optional `--record-path` narrows the import; XML outside that path is still checked for syntax errors. `--format xml|jsonl|json|csv|tsv` overrides automatic format detection.\n\nImport into an explicit collection using the complete ingest URL and expected project:\n\n```bash\ntglow import ./export.xml \\\n --ingest-url 'https://{region}.ingest.tailglow.io/{project_id}?key={ingest_key}' \\\n --project '{project_id}' \\\n --collection health_records\n```\n\nTo import only Apple Health measurements, add `--record-path /HealthData/Record`. Without that filter, the file may also contain metadata and other record types. Each record keeps its own dates, such as `startDate`, for views and charts to use.\n\nAlternatively, provide `--project`, `--source-id` and `--ingest-key-id` to resolve that exact key using your configured API credentials. No source or key is chosen automatically.\n\nBy default, upload previews a bounded sample and validates records as it sends batches. It reads through one open file descriptor and stops if it detects a change to the input. Do not edit the file during import. A late file error can leave earlier batches accepted.\n\nAdd `--validate-first` to check a private snapshot completely before uploading that same snapshot. This needs temporary disk space approximately equal to the input size while memory stays bounded. The snapshot is removed after success, failure or cancellation. Separate `scan` and `validate` commands do not upload anything or reserve a later import.\n\nProgress goes to stderr. `--json`, or piping stdout, produces a machine-readable final report. The total row count remains unknown until a full check or clean EOF. Ctrl-C cancels work and reports accepted records plus any uncertain in-flight batch. Only `202 Accepted` acknowledges a batch. The CLI stops on a failed or uncertain delivery and does not retry automatically or resume an earlier import.\n\n## Server formats\n\nThe ingest server accepts JSON objects or arrays, JSONL, CSV, TSV, PSV, SSV, and plain text. JSON needs no wrapper. The app and CLI convert XML into JSONL locally before sending it; the server does not accept raw XML. Input is normalized before storage, so tabular and text payloads become records and dangerous keys or embedded images may be transformed.\n\n```bash\ncurl -X POST \"https://{region}.ingest.tailglow.io/{project_id}?key={ingest_key}\" \\\n -H \"Content-Type: text/plain\" \\\n -d '{\"method\":\"POST\",\"path\":\"/v1/users\",\"status\":200}'\n```\n\n## Response and delivery\n\nA successful request returns `202 Accepted`. Tailglow then processes the records in the background.\n\nWait for that response, use the SDK queue, or otherwise record delivery failures. You do not need to wait for downstream processing, but a request can be lost if the caller exits before delivery completes.\n\nThe maximum server request body is 20 MiB (20,971,520 bytes). A larger body is rejected with `413 Payload Too Large`; split it into smaller requests. File imports use the smaller 1 MiB batch limit described above.\n\n## Errors and retries\n\nA `202 Accepted` response means the data is durably accepted: it is queued on your server and retried internally until it lands. Accepted data is never dropped.\n\nAn explicit ingest rejection means nothing was recorded from that request. A lost response can leave delivery uncertain. A `4xx` response means the request itself must change, so fix the key, payload, or size before resending.\n\nThe official SDKs retry transient failures automatically with backoff. App and CLI file imports stop and report instead, allowing you to check an uncertain batch before deciding whether to resend it. Customer-supplied idempotency keys are not supported by this import flow.\n\nRefused requests appear on the server page's Ingest Pipeline chart, grouped by reason. The error drawer marks each group as retryable or needing a change to the request. The full reason list is on the [Enums](/api/enums#ingest-pipeline-error-reasons) page.\n",
|
|
46
46
|
"guides/sdk": "# JavaScript SDK\n\nTailglow's JavaScript SDK reports **events**, **errors**, and **logs** to your project from anywhere JavaScript runs: a browser app, a Node or Bun backend, an edge function, Electron, or React Native. A shared core handles queueing, batching, retry attempts, redaction, and sampling. A thin per-runtime package adds the auto-collection and lifecycle wiring that platform needs.\n\nThe mental model is small: you `track()` events and `identify()` users, and the SDK routes every record into one of a few **collections** (`events`, `errors`, `logs`), stamping each with a `type` that says what it is. Batching, retries, redaction, and delivery attempts are handled for you on every runtime; browser and React Native apps also collect page views, errors, and more automatically.\n\nNew here? The **[Quickstart](/guides/quickstart)** gets a web app reporting in about two minutes. This page is the full reference.\n\n## Runtimes\n\n| Runtime | Package | Automatic out of the box |\n| ------------------------------------ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Browser** (web apps, static sites) | `@tailglow/browser` | Page views, tagged-section activity, declarative and outbound clicks, uncaught errors, performance signals, device snapshot, plus best-effort exit flush via `sendBeacon` |\n| **Backend** (Node, Bun, edge) | `@tailglow/core` | Nothing automatic: you `track()` explicitly and flush before the process or request ends |\n| **Electron** | renderer uses `@tailglow/browser`, main uses `@tailglow/core` | Renderer behaves like a browser after allowing `file://`; main behaves like a backend |\n| **React Native / Expo** | `@tailglow/react-native` | JS errors and supported rejection hooks, console, and device once modules are injected; screen views after `attachNavigation()`; best-effort persistence and flush with AppState and storage |\n\nEvery package exposes the same core API: `track`, `captureException`, `identify`, `setContext`, `flush`, and friends. What changes per runtime is which events are collected for you and how exit or shutdown flushing is wired. ESM only, no CJS bundle. Runtime floors: Node `>=22`, Bun any, React Native `>=0.74`, evergreen browsers (Chrome and Firefox 90+, Safari 14+, Edge 90+), and recent edge runtimes.\n\n## Installation\n\n```bash\nbun add @tailglow/browser\n# or\nnpm install @tailglow/browser\n```\n\n## Drop-in `<script>` (no build step)\n\nFor static sites and snippet-paste integrations, the IIFE bundle self-initializes from `data-*` attributes on its own `<script>` tag and registers `window.tglow` as a function. Drop the loader into your page:\n\n```html\n<script\n defer\n src=\"https://cdn.tailglow.io/tglow.js\"\n data-key=\"tg_ingest_your_key\"\n data-url=\"https://{region}.ingest.tailglow.io/{project_id}\"\n></script>\n```\n\nOptionally, add a pre-load stub before the loader so calls made before the SDK finishes loading are queued and replayed on init:\n\n```html\n<script>\n // Optional pre-load stub. Calls made before the SDK loads are replayed on init.\n window.tglow =\n window.tglow ||\n function () {\n (window.tglow.q = window.tglow.q || []).push(arguments);\n };\n\n tglow(\"track\", \"signup\", { plan: \"pro\" });\n tglow(\"identify\", \"user_123\");\n</script>\n```\n\nOverride the default collection routing via additional data-attrs (rare; defaults are `events` / `errors` / `logs`):\n\n```html\n<script\n defer\n src=\"https://cdn.tailglow.io/tglow.js\"\n data-key=\"tg_ingest_...\"\n data-url=\"https://{region}.ingest.tailglow.io/{project_id}\"\n data-collection-events=\"myapp_events\"\n data-collection-errors=\"myapp_errors\"\n data-collection-logs=\"myapp_logs\"\n></script>\n```\n\nSet all three to the same slug to merge everything into one timeline collection.\n\n### Declarative event tracking\n\n`data-tglow-event=\"<name>\"` fires `tg.track(\"<name>\", props)` on click and routes to the configured `events` collection with `type: \"<name>\"`. Other `data-tglow-*` attributes become props on the record.\n\n```html\n<button data-tglow-event=\"signup\" data-tglow-method=\"github\">Sign up with GitHub</button>\n<a href=\"/pricing\" data-tglow-event=\"pricing_click\" data-tglow-source=\"hero\">See pricing</a>\n```\n\n## Initializing\n\nThe full browser walkthrough is in the [Quickstart](/guides/quickstart). Below is the minimal init per runtime.\n\n### Browser\n\n```javascript\nimport { Tailglow } from \"@tailglow/browser\";\n\nconst tg = new Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\"\n});\n\n// Auto-collection starts immediately: page views, section visibility,\n// errors, performance signals, and device info.\n\ntg.track(\"signup\", { plan: \"pro\" });\ntg.identify(\"user_123\");\n```\n\n### Node / Bun (via `@tailglow/core`)\n\n```javascript\nimport { TailglowCore } from \"@tailglow/core\";\n\nconst tg = new TailglowCore({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\"\n});\n\ntg.track(\"job_completed\", { job_id: \"abc\" });\n\nprocess.on(\"SIGTERM\", async () => {\n await tg.flush();\n await tg.destroy();\n});\n```\n\n### React Native (`@tailglow/react-native`)\n\n```javascript\n// App.tsx\nimport AsyncStorage from \"@react-native-async-storage/async-storage\";\nimport { NavigationContainer, useNavigationContainerRef } from \"@react-navigation/native\";\nimport { createAsyncStorageAdapter, Tailglow } from \"@tailglow/react-native\";\nimport { useEffect } from \"react\";\nimport { AppState, Dimensions, NativeModules, Platform } from \"react-native\";\n\nconst tg = new Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\",\n appState: AppState,\n platform: Platform,\n dimensions: Dimensions.get(\"window\"),\n nativeConstants: NativeModules.PlatformConstants,\n storageAdapter: createAsyncStorageAdapter(AsyncStorage)\n});\n\nexport default function App() {\n const navigationRef = useNavigationContainerRef();\n useEffect(() => {\n tg.attachNavigation(navigationRef); // → automatic page_view on every screen change\n }, []);\n return <NavigationContainer ref={navigationRef}>{/* screens */}</NavigationContainer>;\n}\n```\n\nWith the modules wired above, JS errors, console activity, a one-time `device` record, and persist-and-flush on `AppState` background are automatic. Unhandled rejection capture is best-effort because it depends on the rejection hooks exposed by the current Hermes, JSC, and bundler combination. The AsyncStorage adapter restores up to the newest 1,000 persisted records after a cold start, but duplicate delivery is possible if a record was sent before its persisted snapshot was cleared. The one wire-up the SDK cannot infer is your navigation library, hence `attachNavigation(navigationRef)`: React Native has no universal navigation primitive. Expo Router uses React Navigation underneath, so the same call covers both (pass the ref from `useNavigationContainerRef()`).\n\nThis package captures JS-layer behavior only. Native crashes (iOS Swift, Android Kotlin) are out of scope, and even fatal JS crashes are best-effort since the runtime may die before the AsyncStorage write finishes. Screen-view records land in the `events` collection as `type: \"page_view\"` like the browser, but with `from_screen` / `to_screen` / `params` instead of `from_path` / `to_path` / `url`; query both platforms together and expect both field sets.\n\n## Frameworks\n\nAny framework can use the SDK. In an existing app, four things decide whether it reports correctly.\n\n### Next.js and SvelteKit\n\nBoth frameworks render pages on the server before the browser runs them. `@tailglow/browser` needs a browser: created during server rendering, it logs an error and records nothing. Keep one instance in a module, and create it the first time the app runs in the browser:\n\n```javascript\n// lib/tailglow.js\nimport { Tailglow } from \"@tailglow/browser\";\n\nlet tg = null;\n\nexport function getTailglow() {\n tg ??= new Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\"\n });\n return tg;\n}\n\n// On sign-out or an account switch: the next getTailglow() starts a fresh instance.\nexport async function resetTailglow() {\n const previous = tg;\n tg = null;\n // destroy() removes the old collectors before it awaits anything, then sends what is queued.\n await previous?.destroy();\n}\n```\n\nIn SvelteKit, call it from the root layout:\n\n```svelte\n<!-- src/routes/+layout.svelte -->\n<script>\n import { getTailglow } from \"$lib/tailglow.js\";\n import { onMount } from \"svelte\";\n\n let { children } = $props();\n\n onMount(() => {\n getTailglow();\n });\n</script>\n\n{@render children()}\n```\n\nIn Next.js (App Router), call it from a client component, and render `<TailglowInit />` once inside `<body>` in `app/layout.jsx`:\n\n```javascript\n// app/tailglow-init.jsx\n\"use client\";\n\nimport { useEffect } from \"react\";\nimport { getTailglow } from \"../lib/tailglow.js\";\n\nexport function TailglowInit() {\n useEffect(() => {\n getTailglow();\n }, []);\n return null;\n}\n```\n\nElsewhere in browser code, call `getTailglow().track(...)`. Page views follow client-side navigation on their own.\n\n### One instance per page\n\nEvery instance installs its own collectors, so two instances count every page view, click and error twice. Create it once, as above, never in a component body or an effect that can run again. Do not destroy it in an effect's cleanup either: React's Strict Mode runs the cleanup and the effect a second time in development, and the module would keep a destroyed instance.\n\n### Serverless functions\n\n`@tailglow/core` sends records in batches every 30 seconds by default. A serverless function can be frozen as soon as it returns, with records still queued. In a Node or Bun function, such as AWS Lambda, create the instance outside the handler, so warm invocations share it, and flush before returning:\n\n```javascript\nimport { TailglowCore } from \"@tailglow/core\";\n\nconst tg = new TailglowCore({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\"\n});\n\nexport async function handler(event) {\n tg.track(\"order_placed\", { order_id: event.order_id });\n await tg.flush();\n return { statusCode: 200 };\n}\n```\n\nWhere the platform offers `waitUntil`, pass it `tg.flush()` instead, so the response is not held up. A script that exits on its own should end with `await tg.destroy()`: until then, the SDK's send timer keeps the process running.\n\nCloudflare Workers allow no timers or random values at module scope, and the SDK starts both when it is created. Create it inside the handler there, and hand its shutdown to `waitUntil`:\n\n```javascript\nimport { TailglowCore } from \"@tailglow/core\";\n\nexport default {\n async fetch(request, env, ctx) {\n const tg = new TailglowCore({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\"\n });\n tg.track(\"api_request\", { path: new URL(request.url).pathname });\n ctx.waitUntil(tg.destroy());\n return new Response(\"ok\");\n }\n};\n```\n\n### Fitting into an existing app\n\n- **Sign-in.** Call `identify()` where the app learns who is signed in, and again on each page load of a signed-in session, because the browser SDK keeps no identity between loads. On sign-out or an account switch, `await resetTailglow()` so the next `getTailglow()` starts a fresh instance; [Identity changes](#identity-changes-logout--login--org-switch) explains why there is no reset in place.\n- **Existing error reporting.** With `autoErrors` on, Tailglow and any other error reporter both receive every uncaught error. Keep both, or set `autoErrors: false` and send Tailglow only the errors you choose.\n\n## Identity model\n\nBrowser identity is **ephemeral by default**. The SDK stores no `device_id`, `user_id`, or session identifier in cookies or browser storage. A `user_id` is stable across loads only when your app calls `identify()` again from its own authenticated session.\n\n| Layer | Source |\n| ------------ | -------------------------------------------------------------------------------------------------- |\n| `session_id` | In-memory, regenerated after `sessionTimeout` of inactivity |\n| `user_id` | `identify(user_id)`. Same-session records can be backfilled until their request body is serialized |\n| `device_id` | `setDeviceId(id)`. Opt-in, customer-supplied (mobile/desktop) |\n\nSticky sampling resolves in cascade: `user_id` → `device_id` → `session_id`. Without an identifier above session, sampling resets per session (the privacy-preserving default).\n\n## Public API\n\n### Collections model\n\nThe SDK manages three collections (configurable; defaults shown):\n\n| Collection | What lands here |\n| ---------- | --------------------------------------------------------------------------------------------------------------- |\n| `events` | Every `track()` call and every auto-collected event (page views, clicks, vitals, device snapshots, navigation). |\n| `errors` | `captureException`, `captureMessage`, and `console.error`. |\n| `logs` | Non-error console levels selected by `autoConsole`. By default only `console.warn` emits here. |\n\nRecords carry a `type` field that discriminates within the collection (page_view, click, vital, signup, purchase, or whatever you pass to `track()`). The collection is the routing destination; the `type` is the event kind.\n\n```javascript\ntg.track(\"signup\", { plan: \"pro\" });\n// → POST ?collection=events with { type: \"signup\", plan: \"pro\", session_id: ... }\n\ntg.track(\"purchase\", { amount: 99 });\n// → POST ?collection=events with { type: \"purchase\", amount: 99, session_id: ... }\n```\n\nOverride per call when you need a separate collection (rare, but useful for separating high-volume telemetry or audit records from the default event schema). The override uses the **object form** of `track()`:\n\n```javascript\ntg.track({ type: \"audit_event\", collection: \"audit_log\", actor: \"user_42\" });\n// → POST ?collection=audit_log with { type: \"audit_event\", actor: \"user_42\", ... }\n```\n\nThe string form `track(name, props)` is the 99% case; the object form is reserved for the per-call routing override. There's no third positional argument; pass everything (type, collection, props) in one object.\n\nConfigure the destinations at construction time:\n\n```javascript\nnew Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\",\n collections: {\n events: \"events\", // default; override to namespace per app on a shared source\n errors: \"errors\",\n logs: \"logs\"\n }\n});\n```\n\nSet all three to the same slug to merge everything into one timeline collection:\n\n```javascript\nnew Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\",\n collections: { events: \"timeline\", errors: \"timeline\", logs: \"timeline\" }\n});\n```\n\n### Sending events\n\n`track(name, props?)` (string form)\nThe `name` parameter is the **event type**, stamped as the `type` field on the record. Routes to the configured `collections.events` destination (default: `events`).\n\n`track({ type, collection?, ...props })` (object form)\nUse this when you need to route a single record to a different collection. `type` is required; `collection` overrides the default routing; everything else becomes record props.\n\n```javascript\ntg.track(\"signup\", { plan: \"pro\" });\ntg.track(\"purchase\", { amount: 99, currency: \"USD\" });\n```\n\nCustomer-supplied `props.type` wins on collision: pass `type: \"...\"` explicitly in props to override the SDK's stamp.\n\n### Errors and exceptions\n\n`captureException(err, props?)`\nCapture an exception. Routes to the configured `collections.errors` destination (default: `errors`). The SDK parses the stack into structured frames, computes a stable fingerprint for the client burst limiter and your own filtering, snapshots the breadcrumb buffer, and stamps mechanism metadata. The record's top-level `type` field is the JS error class name (e.g. `\"TypeError\"`); within errors, `type` discriminates error kinds the same way it discriminates event kinds within events.\n\n```javascript\ntry {\n riskyOperation();\n} catch (err) {\n tg.captureException(err, { user_step: \"checkout\" });\n}\n```\n\n`level` defaults to `\"error\"`. Override via props:\n\n```javascript\ntg.captureException(err, { level: \"fatal\", user_step: \"checkout\" });\n```\n\n`fingerprint` is a stable string included in the record and used by the client burst limiter. The SDK computes it from the frame signature; pass an explicit string in props to control which errors share a burst budget or to provide your own downstream grouping key:\n\n```javascript\ntg.captureException(err, { fingerprint: \"payment-flow\" });\n```\n\n`captureMessage(message, props?)`\nCapture a message-style event (no Error to attach). Same destination as `captureException` (the configured `errors` collection); use this for assertion failures, recoverable warnings, or business invariants. Default `level` is `\"info\"`. The record's `type` is `\"Message\"`.\n\n```javascript\ntg.captureMessage(\"payment validator returned null\", { level: \"warning\" });\n```\n\n`addBreadcrumb(entry)`\nPush a breadcrumb manually. Auto-collectors do this for you (console activity, page and screen navigations, section and outbound clicks), but you can leave your own context markers.\n\n```javascript\ntg.addBreadcrumb({\n category: \"navigation\",\n message: \"/home → /checkout\",\n timestamp: Date.now()\n});\n```\n\n#### Auto-captured errors and rejections\n\nWhen `autoErrors` is enabled (default), the browser SDK installs `window.addEventListener(\"error\")` and `unhandledrejection` listeners that route through `captureException` automatically. Same record shape as a manual call; `mechanism.type` differs (`\"uncaught\"` / `\"unhandled_rejection\"` vs. `\"manual\"`).\n\n#### Auto-captured console activity\n\nWhen `autoConsole` uses its default value of `[\"error\", \"warn\"]`, all supported `console.*` calls are wrapped for breadcrumbs, but only warnings and errors emit records:\n\n| Console method | Default effect | Record destination | `type` |\n| --------------------------- | ------------------------------------------------ | ------------------- | ---------------- |\n| `console.log` | breadcrumb only | none | n/a |\n| `console.warn` | record + breadcrumb | `logs` collection | `\"warn\"` |\n| `console.info` | breadcrumb only | none | n/a |\n| `console.debug` | breadcrumb only | none | n/a |\n| `console.error(err: Error)` | `captureException` + breadcrumb | `errors` collection | error class name |\n| `console.error(string)` | `captureMessage` (level: `\"error\"`) + breadcrumb | `errors` collection | `\"Message\"` |\n\nThe wrapper preserves the original console output: DevTools still shows everything as before. The only loss is DevTools' \"source\" column shows the wrapper's file/line instead of the actual caller. This is the standard tradeoff every error tracker makes.\n\nWhen a non-error level is included in `autoConsole`, its record follows these argument-shape rules:\n\n```javascript\nconsole.log(\"user clicked save\");\n// → { type: \"log\", message: \"user clicked save\" }\n\nconsole.log({ user_id: 42, action: \"save\" });\n// → { type: \"log\", user_id: 42, action: \"save\", message: \"{...}\" } (object flattened)\n\nconsole.log(\"user clicked\", { id: 42, name: \"Alice\" });\n// → { type: \"log\", message: \"user clicked {...}\", id: 42, name: \"Alice\" }\n```\n\n#### Burst protection\n\nTo catch runaway loops (the same error firing thousands of times per second), the SDK rate-limits per fingerprint. After 10 events of the same fingerprint within 1 second, the next event triggers a 2-minute cooldown and is dropped. The first event emitted after cooldown carries `burst_suppressed: N` only when additional events were dropped during that cooldown. `N` counts those additional cooldown drops and does not include the event that triggered cooldown.\n\nConfigurable via:\n\n```javascript\nnew Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\",\n errorBurst: { threshold: 10, window_ms: 1000, cooldown_ms: 120_000 }\n});\n```\n\nThe fingerprint counter map is LRU-evicted at 1000 entries to bound memory in long-running sessions.\n\nUnderneath the per-fingerprint limiter is a global token-bucket limiter that caps the total record rate across every type (`track()`, auto-collectors, errors, logs). It protects the client from a runaway emit loop, but it does not guarantee that the ingest server will accept every request. Invalid credentials, malformed or oversized payloads, unavailable capacity, and other server failures can still reject a request. The limiter runs after sticky sampling and before stamping, redaction, size checks, `onBeforeSend`, and queueing. The bucket holds 200 tokens and refills 120 per minute by default; every admitted record spends one token, and when the bucket empties records are dropped. Drops surface as a collapsed `tglow_rate_limited` self-event carrying `limited_count` (records dropped since the last notice) and `window_ms`, emitted at most once per 60 seconds. Tune it with the `rateLimit` option (`burst` must be a finite number at least 1, `perMinute` a finite number at least 0, where `perMinute: 0` means drain-only with no refill), or pass `rateLimit: false` to disable it. Any out-of-range value (including `burst: 0`) falls back to that field's default and logs one `[tglow]` warning, so a bad config can never silently disable the brake or drop everything.\n\n#### `errors` collection record shape\n\n```json\n{\n \"type\": \"TypeError\",\n \"message\": \"Cannot read 'x' of null\",\n \"stack_raw\": \"TypeError: Cannot...\",\n \"frames\": [\n {\n \"filename\": \"https://cdn.example.com/app.min.js\",\n \"function\": \"calculateTotal\",\n \"lineno\": 1,\n \"colno\": 48201,\n \"in_app\": true\n }\n ],\n \"fingerprint\": \"9c3f8a1b\",\n \"mechanism\": { \"type\": \"uncaught\", \"handled\": false },\n \"level\": \"error\",\n \"cause\": { \"type\": \"Error\", \"message\": \"...\", \"frames\": [] },\n \"breadcrumbs\": [\n { \"category\": \"console\", \"level\": \"log\", \"message\": \"...\", \"timestamp\": 1700000000000 }\n ],\n \"sdk\": { \"name\": \"tailglow.browser\", \"version\": \"x.y.z\" }\n}\n```\n\n#### `logs` collection record shape\n\n```json\n{\n \"type\": \"log\",\n \"message\": \"user clicked save\",\n \"session_id\": \"sess_...\",\n \"event_id\": \"...\",\n \"event_time\": \"...\",\n \"sdk\": { \"name\": \"tailglow.browser\", \"version\": \"x.y.z\" }\n}\n```\n\n### Identity & context\n\n`identify(user_id)`: set the user ID. Pre-identify records from the same session are backfilled while they remain buffered or in an in-flight SDK batch that has not yet been serialized. A request body already serialized for `fetch` cannot be changed and remains anonymous.\n\n`unidentify()`: clear the current user ID. Equivalent to `identify(\"\")` but explicit.\n\n`setDeviceId(id)`: set a customer-supplied device ID for subsequent records. The SDK does not persist it; mobile or desktop apps are responsible for supplying the same stable identifier again on a later launch. Browsers should normally leave this unset.\n\n`rotateSession()`: force a new session ID at an explicit boundary (workflow finished, end-of-flow). Returns the new ID.\n\n`setContext(ctx)`: merge sticky context. Every key here is stamped on every subsequent record (only if the record doesn't already have that key). Values must be JSON-serializable (functions and `undefined` are dropped). The context bag is capped at 64 top-level keys and 8192 serialized bytes. A non-object value or array is ignored without a warning. An object patch that cannot be serialized, resolves to a non-object through `toJSON`, or would exceed either cap is rejected as a whole, leaving the existing bag unchanged, and the SDK logs one `[tglow]` warning for that violation kind. Common conventions are `release`, `environment`, `dist`, `region`, `plan`, but the SDK doesn't reserve any of them; they're just sticky fields.\n\n`clearContext()`: drop everything previously added via `setContext`.\n\n```javascript\ntg.identify(\"usr_123\");\ntg.setContext({\n release: \"v2.1.0\",\n environment: \"production\",\n email: \"alice@example.com\",\n plan: \"pro\",\n region: \"us-west\",\n subscription: { tier: \"pro\", seats: 12 }\n});\n```\n\nTailglow doesn't impose a specific shape on your context: flat or nested, whatever queries naturally for your data model. Mid-session updates (e.g. CodePush bumps `release`, an Electron auto-update flips `dist`, a UI toggle changes `environment`) are just `setContext({...})` calls. There are no special-case setters.\n\n#### Sticky-sample identity transition\n\nSampling key cascades `user_id` → `device_id` → `session_id`. An anonymous user (sampled by `session_id`) who later calls `identify()` shifts to `user_id`-keyed sampling, and the verdict can flip in or out mid-session. Identify before any tracking when you need the sampling verdict to stay keyed to the user for the whole session.\n\n#### Identity changes (logout / login / org switch)\n\nThere is no `reset()` method. The pattern is flush, destroy, reconstruct:\n\n```javascript\nawait tg.flush(); // attempt to drain pending records under the old identity\nawait tg.destroy(); // remove listeners, stop the queue interval\ntg = new Tailglow({ ...sdkConfig });\n```\n\n### Opt-out and privacy\n\nThe default browser SDK path is cookieless and uses no persistent browser storage. Whether you need consent or a banner still depends on what you collect, how you use it, and the laws that apply to you. Disclose collection in your privacy policy and honor Do Not Track / Global Privacy Control (the browser SDK does by default, configurable).\n\nTo let a user turn analytics off:\n\n`optOut()`: clear buffered records and make the core drop new records until `optIn()`. It does not cancel a request body already handed to `fetch`, and browser collectors remain installed. Some collectors can retain observations while opted out and emit them after a later `optIn()`. Call `await tg.flush()` first only if you intentionally want to attempt delivery of buffered records before opting out.\n`optIn()` / `isOptedOut()`: resume tracking / check status.\n\n```javascript\ntg.optOut(); // drop buffered records and stop accepting new records in the core\ntg.optIn(); // resume\n```\n\nFor consent gating, construct the SDK only after the user accepts. On revocation, call `optOut()` first to clear buffered records, then call `destroy()` to remove collectors and listeners. Neither call can cancel a request body already handed to `fetch`. Create a new SDK instance if the user later grants consent again.\n\n```javascript\nif (userAccepted) tg = new Tailglow({ ...config });\n// later, to revoke:\ntg.optOut();\nawait tg.destroy();\n```\n\n### Lifecycle\n\n`flush()`: async; attempt to flush all queued records and wait for the current flush cycle. Retryable failures that exhaust their transport attempts remain queued.\n`destroy()`: async; attempt one final queue flush, remove platform listeners, and stop the interval. It does not guarantee delivery after a permanent or exhausted transient failure.\n`getStats()`: `{queueCount, queueBytes, sentCount, lastSendAt, lastSendOk}` for production debugging. Queue count and bytes cover the buffered queue, not records currently in an in-flight batch.\n\n#### Exit-time delivery\n\nBrowser tab-hide delivery via `navigator.sendBeacon` is best-effort. `@tailglow/core` does not install Node process listeners, so wire your own shutdown handler and await `flush()` and `destroy()` while the runtime can still perform network work. Those calls wait for the SDK's delivery attempts, but they cannot guarantee delivery when the network or ingest service keeps failing.\n\n## Configuration\n\nAll options have sensible defaults. Only `url` is required, and it may already carry the key.\n\n### Core options (all packages)\n\n| Option | Type | Default | Description |\n| -------------------------- | --------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `url` | `string` | required | Full project ingest endpoint, such as `https://{region}.ingest.tailglow.io/{project_id}`. Copy it from the source's Ingest tab, where it already carries `?key=`. |\n| `key` | `string` | optional | Ingest key (starts with `tg_ingest_`). Omit it when `url` carries one; supplying it overrides the carried key. |\n| `enabled` | `boolean` | `true` | Environment gate. When `false`, normal capture, identity, context, lifecycle, and diagnostic methods are inert or return neutral values. No timers, network, queue, or collectors are created. With `debug: true`, construction logs one disabled diagnostic. Do not call core-only transport or queue escape hatches such as `getTransport()` or `drainQueue()` on a disabled `TailglowCore`. |\n| `collections` | `{ events?, errors?, logs? }` | `{ events: \"events\", errors: \"errors\", logs: \"logs\" }` | Routing destinations. `events` = `track()` + event auto-collectors; `errors` = `captureException`/`captureMessage`/uncaught errors/`console.error`; `logs` = the other enabled console wrappers. Set all three to the same slug to merge into one timeline collection. |\n| `context` | `Record<string, unknown>` | - | Initial sticky context (JSON-serializable values only). Every key is stamped on every record unless the record already has that key. Mergeable later via `setContext()`. Capped at 64 top-level keys and 8192 serialized bytes. A non-object value or array is ignored silently; an object that cannot serialize to an in-cap JSON object is rejected whole and logs one warning for that violation kind. |\n| `flushInterval` | `number` | `30000` | Auto-flush interval in ms |\n| `flushSize` | `number` | `100` | Auto-flush at this record count |\n| `maxBatchBytes` | `number` | `15000000` | Approximate serialized-byte target used to split queued records into outgoing batches. A single record can exceed it when `maxRecordBytes` is configured higher. |\n| `maxQueueSize` | `number` | `10000` | Maximum records in the buffered queue. In-flight batches are tracked separately, so this is not a strict process-wide record or memory cap. The oldest buffered record is dropped when the buffer is full. |\n| `maxRecordBytes` | `number` | `1000000` (1MB) | Drop records larger than this (serialized JSON). Emits a collapsed `tglow_record_dropped` self-event, at most one per drop reason (size or unserializable) per 60s. `dropped_count` spans every collection dropped for that reason in the window, and the other `dropped_*` fields reflect the most recent drop. Pass `Infinity` to disable. |\n| `sessionTimeout` | `number` | `1800000` | Session timeout (30 min) |\n| `onSessionRotate` | `(info) => void` | - | Fires after the session ID rotates (timeout or `rotateSession()`), with a snapshot of the ended session: `previous_session_id`, `reason`, `started_at`, `last_activity_at`. |\n| `maxRetries` | `number` | `3` | Retry attempts after network errors and 5xx, 408, or 429 responses. Honors `Retry-After`. |\n| `retryDelay` | `number` | `1000` | Initial retry delay (doubles each attempt) |\n| `sampleRate` | `number` | `1.0` | Sticky sampling rate (0 to 1) |\n| `deviceId` | `string` | - | Customer-supplied device ID for this SDK instance. The caller owns persistence across launches. |\n| `userId` | `string` | - | Initial user ID |\n| `storageAdapter` | `StorageAdapter` | - | Async storage adapter for non-browser persistence |\n| `onBeforeSend` | `(record) => record \\| null` | - | Filter/enrich records, return null to drop |\n| `onTransportError` | `(error, batch) => void` | - | Fires for a collection group rejected by a non-retryable 4xx and for unexpected exceptions in the SDK send path, with the affected records. Exhausted retryable network, 408, 429, and 5xx failures are requeued without calling this hook. |\n| `redact.enabled` | `boolean` | `true` | Master redaction switch |\n| `redact.redactEmails` | `boolean` | `true` | Redact email-shaped strings |\n| `redact.redactFields` | `string[]` | - | Drop these top-level fields |\n| `redact.allowFields` | `string[]` | - | Restrict top-level fields to an allowlist. Internal `__tg_*` routing plus `event_id`, `event_time`, `session_id`, `user_id`, `device_id`, and `sdk` pass automatically. Include `type` in the allowlist if you want to preserve it. |\n| `redact.stripQueryStrings` | `boolean` | `false` | Strip the query string and hash from the top-level `url`, `referrer`, and `href` fields (rewrites to origin plus pathname). Only `http` and `https` URLs are rewritten; other schemes (`ftp:`, `data:`, `blob:`, etc.) and unparseable values pass through unchanged. Opt in. Leaves nested values and breadcrumbs untouched. |\n| `errorBurst` | `object` | see above | Per-fingerprint client-side rate limit. Keys: `threshold` (default 10), `window_ms` (1000), `cooldown_ms` (120000), `max_entries` (1000). |\n| `rateLimit` | `{ burst?, perMinute? } \\| false` | `{ burst: 200, perMinute: 120 }` | Global token-bucket rate limit across every record type, enforced after sticky sampling and before stamping or queueing. Each admitted record spends one token; when the bucket empties records are dropped and a collapsed `tglow_rate_limited` self-event reports the count (at most once per 60s). `burst` must be a finite number at least 1 and `perMinute` a finite number at least 0 (`perMinute: 0` is drain-only); invalid values fall back to defaults with one warning. Pass `false` to disable. |\n| `breadcrumbBuffer` | `number` | `100` | Ring buffer size for breadcrumbs. |\n| `debug` | `boolean` | `false` | Console logging |\n\n**Environment gating.** Set `enabled: false` for environments where you don't want to report, such as unprovisioned previews, CI, or local development. A disabled instance starts no timers, opens no network connections, queues nothing, and installs no auto-collectors. Normal public wrapper methods remain safe and diagnostic getters return neutral values. `debug: true` deliberately logs one disabled diagnostic, while the default `debug: false` path stays silent even when `url` and `key` are empty. The recommended pattern is `enabled: env === \"production\" && Boolean(key)`.\n\n### Browser-only options (`@tailglow/browser`)\n\n| Option | Type | Default | Description |\n| ----------------------- | ------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `autoPageViews` | `boolean` | `true` | SPA navigation tracking |\n| `autoSections` | `boolean` | `true` | `data-telemetry` element visibility + clicks |\n| `autoErrors` | `boolean` | `true` | Uncaught errors and unhandled rejections |\n| `autoVitals` | `boolean` | `true` | Lightweight browser performance signals named LCP, CLS, INP, FCP, and TTFB. These are SDK approximations, not standards-compliant Core Web Vitals calculations. |\n| `autoDevice` | `boolean` | `true` | Send device info on init |\n| `autoSessionSummary` | `boolean` | `true` | Emit `session_summary` records (engaged time, pages viewed, entry/exit paths) on tab hide/unload and session rotation. Requires `autoPageViews`. Each emission carries cumulative totals for its `session_id`: read the last record per session. |\n| `autoConsole` | `Array<\"log\"\\|\"warn\"\\|\"info\"\\|\"debug\"\\|\"error\">` | `[\"error\",\"warn\"]` | Console levels that emit records on the wire. Levels not listed still feed breadcrumbs. `[]` disables wrapping entirely. |\n| `spaMode` | `\"auto\" \\| \"history\" \\| \"hash\" \\| \"off\"` | `\"auto\"` | SPA navigation strategy |\n| `routeContext` | `() => object \\| null` | - | Called at every page_view emission; returned fields (route template, params) merge into the record. Auto-collected fields win on collision. Use it to stamp a stable router route ID for downstream views and metrics. |\n| `sectionAttribute` | `string` | `\"data-telemetry\"` | Attribute name for section tracking |\n| `intersectionThreshold` | `number` | `0.5` | Visibility ratio for \"seen\" |\n| `honorDnt` | `boolean` | `true` | Honor `navigator.doNotTrack` |\n| `honorGpc` | `boolean` | `true` | Honor `navigator.globalPrivacyControl` |\n| `excludeLocalhost` | `boolean` | `true` | Skip tracking on localhost / file:// |\n| `respectDevOptOut` | `boolean` | `false` | Read `tglow_ignore` localStorage flag |\n\n## Privacy defaults\n\nThe browser SDK is designed to operate without persistent client storage:\n\n| Default | Behavior |\n| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Honor DNT | If `navigator.doNotTrack === \"1\"`, the SDK is fully disabled. |\n| Honor GPC | If `navigator.globalPrivacyControl === true`, the SDK is fully disabled. |\n| Skip localhost | The SDK does not track on `localhost`, `127.0.0.1`, `0.0.0.0`, or `file://`. |\n| No browser storage | The default browser path does **not** read or write cookies, localStorage, sessionStorage, or IndexedDB. |\n| Redaction | URL token patterns and email-shaped strings are redacted before send, recursively through at most eight container levels. Strings longer than 4,096 characters are not scanned. |\n\nThe `tglow_ignore` developer escape hatch (set `localStorage.setItem(\"tglow_ignore\", \"true\")` in your browser) is opt-in via `respectDevOptOut: true`. Useful for staging/dev builds.\n\n### Console capture\n\nDefault `autoConsole: [\"error\", \"warn\"]`: only severity-flagged console calls become records on the wire. `log` / `info` / `debug` are still wrapped (so they feed the breadcrumb buffer that attaches to the next captured error), but don't emit records by themselves. This matches Sentry-style \"errors with context\" behavior out of the box, while leaving the data-lake firehose one config away.\n\nTo broaden capture, list more levels. To disable entirely, pass `[]`.\n\n```javascript\n// Default: errors and warnings as records, all levels in breadcrumbs\nnew Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\"\n});\n\n// Errors only on the wire, breadcrumbs still capture from log/warn/info/debug\nnew Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\",\n autoConsole: [\"error\"]\n});\n\n// Capture everything (data-lake mode)\nnew Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\",\n autoConsole: [\"log\", \"warn\", \"info\", \"debug\", \"error\"]\n});\n\n// Disable entirely: no wrapping, no breadcrumbs from console\nnew Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\",\n autoConsole: []\n});\n```\n\nRouting per level when emitted:\n\n- `log` / `warn` / `info` / `debug` → configured `logs` collection with `type: \"<level>\"`\n- `error(Error)` → `captureException` → configured `errors` collection with `type` = error class name\n- `error(string)` → `captureMessage` (level: `\"error\"`) → configured `errors` collection with `type: \"Message\"`\n\n**PII**: whatever your code logs can be sent. Automatic URL-token and email redaction scans strings up to 4,096 characters and traverses at most eight container levels, including typical breadcrumb messages, frame filenames, and nested objects within those limits. `redact.redactFields` and `redact.allowFields` apply only to top-level fields. For stricter guarantees, remove sensitive data at the source or in `onBeforeSend` instead of relying only on automatic redaction.\n\n**Opt-out**: breadcrumbs are dropped while the SDK is opted out (`tg.optOut()`). Pre-opt-out activity does not leak into post-opt-in captures.\n\n## Testing on localhost\n\nBy default the SDK skips tracking on `localhost`, `127.0.0.1`, `0.0.0.0`, and `file://` so your local dev runs don't pollute production analytics. To verify the integration end-to-end during development, opt back in with `excludeLocalhost: false`.\n\n### ESM\n\n```javascript\nimport { Tailglow } from \"@tailglow/browser\";\n\nconst tg = new Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_your_key\",\n excludeLocalhost: false, // enable on localhost\n context: { environment: \"development\" }, // tag dev events so they're filterable\n debug: true // log SDK activity to the console\n});\n```\n\n### Drop-in script\n\n```html\n<script\n defer\n src=\"https://cdn.tailglow.io/tglow.js\"\n data-key=\"tg_ingest_your_key\"\n data-url=\"https://{region}.ingest.tailglow.io/{project_id}\"\n data-exclude-localhost=\"false\"\n data-environment=\"development\"\n data-debug=\"true\"\n></script>\n```\n\n(`data-release`, `data-environment`, and `data-dist` flow into `context` automatically. For richer context, call `tglow(\"setContext\", {...})` after the script loads.)\n\n### Recommended: tag dev events with a distinct `environment`\n\nWhen you enable localhost tracking, the events flow into the same ingest as production. Set `context: { environment: \"development\" }` (or `\"staging\"`, `\"local\"`, whatever you want) so you can filter dev events out of production dashboards or build a separate \"Dev events\" view. Without this, your local records show up next to real customer events and skew metrics.\n\nA common pattern is to drive it from the build environment. This example uses Vite's `import.meta.env`; adapt the environment API for your bundler:\n\n```javascript\nconst environment = import.meta.env.MODE;\n\nnew Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_...\",\n excludeLocalhost: import.meta.env.PROD,\n context: { environment }\n});\n```\n\n### Want to disable in dev too? Use `tglow_ignore`\n\nThe `tglow_ignore` localStorage flag is a per-browser kill-switch. Useful when you're running an env where the SDK is enabled but you specifically don't want your own events captured (QA accounts, automated tests, your own dev session). The SDK only reads it when configured with `respectDevOptOut: true`:\n\n```javascript\nnew Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_...\",\n respectDevOptOut: true\n});\n```\n\nThen in the browser console: `localStorage.setItem(\"tglow_ignore\", \"true\")`. SDK will be disabled on next load.\n\n## Auto-collected events (browser)\n\nBrowser event auto-collectors route page views, clicks, vitals, device snapshots, and similar records to the configured `events` collection with a `type` discriminator. Uncaught errors route to `errors`; enabled console wrappers route warnings and other non-error console records to `logs`, while `console.error` routes to `errors`. Use the `type` field when building views or metrics to distinguish `page_view`, `click`, `vital`, and other record kinds.\n\n### Page views: `type = \"page_view\"`\n\n```json\n{\n \"type\": \"page_view\",\n \"from_path\": \"/\",\n \"to_path\": \"/pricing\",\n \"duration_ms\": 4500,\n \"nav_type\": \"push\",\n \"url\": \"https://example.com/pricing\"\n}\n```\n\nThe URL field uses `<link rel=\"canonical\">` if present, otherwise `window.location.href`.\n\n### Section visibility & clicks\n\n`type = \"section_view\"` | `\"click\"` | `\"outbound_click\"` | `\"scroll_depth\"`.\n\n```html\n<section data-telemetry=\"hero\">...</section>\n```\n\n```json\n// section_view\n{ \"type\": \"section_view\", \"section\": \"hero\", \"path\": \"/\", \"duration_ms\": 8200 }\n// click\n{ \"type\": \"click\", \"section\": \"pricing\", \"path\": \"/\", \"tag\": \"button\", \"text\": \"Sign up\" }\n// outbound_click\n{ \"type\": \"outbound_click\", \"href\": \"https://github.com/...\", \"text\": \"View on GitHub\", \"path\": \"/\" }\n// scroll_depth\n{ \"type\": \"scroll_depth\", \"depth_percent\": 85, \"path\": \"/\" }\n```\n\n### Errors: `errors` collection\n\n```json\n{\n \"type\": \"TypeError\",\n \"message\": \"Cannot read 'x' of null\",\n \"stack_raw\": \"TypeError: Cannot read 'x' of null\\n at ...\",\n \"frames\": [\n {\n \"filename\": \"https://cdn.example.com/app.min.js\",\n \"function\": \"calculateTotal\",\n \"lineno\": 1,\n \"colno\": 48201,\n \"in_app\": true\n }\n ],\n \"fingerprint\": \"9c3f8a1b\",\n \"mechanism\": { \"type\": \"uncaught\", \"handled\": false },\n \"level\": \"error\",\n \"breadcrumbs\": [\n { \"category\": \"console\", \"level\": \"log\", \"message\": \"...\", \"timestamp\": 1700000000000 }\n ]\n}\n```\n\n`type` here is the JS error class name. Same shape from manual capture (`captureException`), the auto-collector (`window.addEventListener(\"error\", ...)` / `unhandledrejection`), and the console wrapper (`console.error(err)`); only `mechanism.type` differs (`\"manual\"` / `\"uncaught\"` / `\"unhandled_rejection\"` / `\"console\"`).\n\n### Performance signals: `type = \"vital\"`\n\n```json\n{ \"type\": \"vital\", \"vital_name\": \"LCP\", \"vital_value\": 1200, \"path\": \"/\" }\n{ \"type\": \"vital\", \"vital_name\": \"CLS\", \"vital_value\": 0.05, \"path\": \"/\" }\n```\n\nThese are lightweight performance measurements, not standards-compliant Core Web Vitals. LCP uses the latest observed largest-contentful-paint start time. CLS sums layout-shift values without recent input across the page lifetime, rather than applying the standard session-window algorithm. INP is the maximum observed event duration, rather than the standard interaction percentile calculation. FCP and TTFB come directly from paint and navigation performance entries.\n\n### Device: `type = \"device\"`\n\nSent once on init. Includes browser, OS, device type, screen, viewport, language, timezone, connection (effectiveType / downlink / rtt), referrer source, and UTM parameters.\n\n### Session summary: `type = \"session_summary\"`\n\n```json\n{\n \"type\": \"session_summary\",\n \"session_id\": \"sess_Abc123\",\n \"engaged_ms\": 145200,\n \"pages_viewed\": 7,\n \"entry_path\": \"/\",\n \"exit_path\": \"/pricing\",\n \"reason\": \"hide\"\n}\n```\n\nEmitted on tab hide/unload (`reason: \"hide\"` / `\"unload\"`), on session rotation (`\"rotate\"`), and on `destroy()` (`\"destroy\"`). Each emission carries **cumulative** totals for its `session_id`: totals only grow within a session, so read the last record per session (for example, a metric grouped by `session_id` with the last-value chart mode). `engaged_ms` counts only active, visible time, matching the page_view `duration_ms` semantics. Disable with `autoSessionSummary: false`.\n\n## Record metadata\n\nThe SDK stamps this metadata when it is available, but **only if the customer hasn't already set the field**. Customer-explicit values always win on collision.\n\n| Field | Description |\n| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `event_id` | Unique 16-character correlation ID. The client uses it to avoid some repeat sends within one live SDK instance, but the ingest service does not deduplicate by this field. Duplicate records remain possible, and overriding it does not provide server-side idempotency. |\n| `session_id` | SDK's session ID, renews after `sessionTimeout`. Customer can override. |\n| `event_time` | ISO 8601 timestamp of when the SDK queued the record. Customer can override (occurred-at). Overridden times older than the project's late data window are held for a manual re-backfill instead of charting automatically; see [Late data](/guides/metrics#late-data). |\n| `user_id` | Included after `identify()` or `userId` is set. Customer can override per-record (B2B/CRM use cases). |\n| `device_id` | Included after `setDeviceId()` or `deviceId` is set. Customer can override. |\n| `type` | Event kind within the collection (e.g. `\"page_view\"`, `\"signup\"`, `\"TypeError\"`). Stamped from `track(name, ...)`'s `name` argument or by auto-collectors. Customer can override per-record. |\n| `sdk` | SDK package name and version. Included unless the customer provides an `sdk` field. |\n\nPlus any sticky fields from `context` (constructor) or `setContext()`: those also follow the customer-wins rule (only stamp if the record doesn't already have that key).\n\nThe collection (routing destination) is **not** a field on the record. It's the URL slug in `?collection=<events|errors|logs|...>`. The record's `type` field discriminates the kind of thing within that collection.\n\n### Reserved field names\n\nThese names are reserved for SDK metadata. You can still use them as your own fields and your value will appear on the wire instead of the SDK's. But by convention, treat them as SDK-controlled and use distinct names if you mean something different (e.g. `subject_id` instead of `user_id` to track a CRM contact distinct from the authenticated app user).\n\n```\nevent_id, event_time, session_id, user_id, device_id, type, sdk\n```\n\n## Transport\n\nAll requests go to `POST {url}?key={key}&collection={slug}` with `Content-Type: text/plain`. There is no `Authorization` header and no custom request headers; this is a CORS \"simple request\" with no preflight, so cross-origin installs have no `OPTIONS` round-trip.\n\nFailed requests retry on 5xx, 408, and 429. With the default `maxRetries: 3` and `retryDelay: 1000`, the three retry waits are 1s, 2s, and 4s. Raising `maxRetries` adds later waits such as 8s, capped at 30s. The transport honors the `Retry-After` response header and drops a collection group on other 4xx responses.\n\nOn page hide, the browser SDK attempts best-effort delivery with `navigator.sendBeacon`. A `true` return means the browser accepted the data for transfer, not that the ingest service received it. If the browser rejects a buffered beacon and the page remains alive, the SDK requeues those records for a later fetch attempt.\n\n## Hooks\n\n`onBeforeSend` intercepts records before they are queued. Return the record (optionally modified) to keep it, or `null` to drop it. The internal `__tg_collection` field carries the collection routing; read it to filter by collection.\n\n```javascript\nconst tg = new Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_...\",\n onBeforeSend: (record) => {\n if (\n record.__tg_collection === \"errors\" &&\n record.frames?.some((f) => f.filename?.includes(\"third-party\"))\n ) {\n return null;\n }\n record.app_build = \"abc123\";\n return record;\n }\n});\n```\n\n## Pipeline order\n\nThe common path for records accepted by `track()` or `capture()` is:\n\n```\nsession touch → sticky sampling → rateLimit gate → stamp → redact → maxRecordBytes check → onBeforeSend → queue → flush → transport\n```\n\n- Error and message capture first packages the error, computes its fingerprint, snapshots breadcrumbs, and applies the per-fingerprint `errorBurst` gate. Records that pass then enter the common path above.\n- Sticky sampling runs before the global `rateLimit` bucket. A record either gate drops is never stamped, redacted, size-checked, passed to `onBeforeSend`, or queued. Rate-limit drops surface as a collapsed `tglow_rate_limited` self-event.\n- `onBeforeSend` sees the post-redaction record. Set `redact.enabled: false` if your hook needs raw payloads.\n- `maxRecordBytes` runs before `onBeforeSend`; oversized records never reach the hook.\n- `onTransportError` fires for immediate non-retryable 4xx rejections and unexpected exceptions in the send path. Exhausted retryable failures are requeued without calling the hook.\n\n## Lifecycle without auto-collectors\n\nIf you want browser-side queueing + visibility flush + sendBeacon delivery but **not** the auto-collectors (Electron renderer, embedded WebView, etc.), disable them all and use `track` manually:\n\n```javascript\nconst tg = new Tailglow({\n url: \"https://{region}.ingest.tailglow.io/{project_id}\",\n key: \"tg_ingest_...\",\n autoPageViews: false,\n autoSections: false,\n autoErrors: false,\n autoVitals: false,\n autoDevice: false,\n autoConsole: [],\n excludeLocalhost: false\n});\n// Visibility/beforeunload flush via sendBeacon stays active.\n```\n\n`excludeLocalhost: false` is required for Electron renderer pages loaded from `file://`, which the browser package excludes by default.\n\n## Typed event schemas\n\nAugment `@tailglow/core`'s `TailglowEventTypes` interface. This is the canonical location whether you use the browser package, the React Native package, or core directly. Event types not in the schema fall through with `Record<string, unknown>`.\n\nThe augmented keys describe the `type` field of records sent to the configured `events` collection, not a separate collection per key.\n\n```ts\ndeclare module \"@tailglow/core\" {\n interface TailglowEventTypes {\n signup: { plan: \"free\" | \"pro\" };\n purchase: { amount: number; currency: string };\n }\n}\n\ntg.track(\"signup\", { plan: \"pro\" }); // ✓ typed\ntg.track(\"signup\", { plan: \"wrong\" }); // ✗ TS error\ntg.track(\"anything_else\", { whatever: true }); // ✓ falls back to Record<string, unknown>\n```\n\n## Platform support\n\n| Platform | Auto-collection | Lifecycle | Storage |\n| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| Browser (`@tailglow/browser`) | Page views, tagged sections and clicks, declarative and outbound clicks, JS errors, console breadcrumbs plus error/warn records, performance signals, device | Best-effort `visibilitychange` / `beforeunload` delivery through `sendBeacon` | In-memory by default |\n| Node / Bun / edge (`@tailglow/core`) | None. Call `track()` and capture methods explicitly | No automatic process or request hook. The caller must await lifecycle methods when needed | In-memory unless a `storageAdapter` is provided |\n| React Native (`@tailglow/react-native`) | JS errors, best-effort rejection hooks, console breadcrumbs plus error/warn records; screen views after `attachNavigation()`; device when modules are provided | Injected `AppState` starts best-effort persist, then flush, when leaving active | Optional `createAsyncStorageAdapter(AsyncStorage)`, newest 1,000 records |\n| Electron renderer | Use `@tailglow/browser`; set `excludeLocalhost: false` for `file://` | Same best-effort lifecycle as browser | In-memory by default |\n| Electron main | Use `@tailglow/core` | Customer owns the quit lifecycle and must keep the process alive for awaited delivery attempts | In-memory unless a customer adapter is provided |\n",
|
|
47
|
-
"guides/teams": "# Teams\n\n## Overview\n\nA team is the topmost level of organization in Tailglow.\nYou can have multiple projects within a team, and you can add different users to each team.\n\n## Creating a Team\n\nA new account starts without a team. After profile setup, the **Set up your team & project** onboarding form creates the first team and project when you click **Create Team & Project**. To create another team later, open the team switcher in the top toolbar, select **Create New Team**, and complete the same form.\n\n## Team Settings\n\nTo access your team settings, click your team name in the top toolbar. This opens the team overview, where you can view all projects. Select the settings icon next to **Add New**.\n\n### General\n\nIn the **General** settings, you can:\n\n- Change your team name.\n- View your team's status.\n- Update your team's logo.\n- Delete your team.\n\nIf you delete your team, it will not be removed immediately. Instead, every
|
|
47
|
+
"guides/teams": "# Teams\n\n## Overview\n\nA team is the topmost level of organization in Tailglow.\nYou can have multiple projects within a team, and you can add different users to each team.\n\n## Creating a Team\n\nA new account starts without a team. After profile setup, the **Set up your team & project** onboarding form creates the first team and project when you click **Create Team & Project**. To create another team later, open the team switcher in the top toolbar, select **Create New Team**, and complete the same form.\n\n## Team Settings\n\nTo access your team settings, click your team name in the top toolbar. This opens the team overview, where you can view all projects. Select the settings icon next to **Add New**.\n\n### General\n\nIn the **General** settings, you can:\n\n- Change your team name.\n- View your team's status.\n- Update your team's logo.\n- Delete your team.\n\nIf you delete your team, it will not be removed immediately. Instead, every member loses access to\nthe team, every ingest key is revoked, every API key is deleted, the team's servers are scaled to\nzero, and permanent removal is scheduled for forty days later. Members stay signed in to their other\nteams. Nothing is billed while a team is archived. Your projects and their data are kept for the\nwhole window. If you need the team deleted immediately, contact support.\n\nOnly an Owner can do this. In the **Delete Team** card, click **Schedule For Deletion**, then\nconfirm by typing your team's name and the phrase **Schedule To Delete**. Both fields ignore case\nand surrounding spaces. Every member loses access as soon as you confirm, so the confirmation is\ndeliberately heavier than the one on other resources.\n\nTo restore an archived team, an Owner selects it from the team menu, or signs in when it is their\nonly team, and clicks **Restore Team** on the **Recover** page. Selecting the team on its own does\nnot restore it. Restoring does not reactivate the revoked ingest keys or bring back the servers, so\nadd active ingest keys to your sources and servers to your projects before sending data again.\n\n### Statuses\n\nThere are four statuses for your team:\n\n- **Active**: Your team is active and can be accessed by all team members.\n- **Delinquent**: Tailglow emails your billing contact after a failed payment. A non-Enterprise team can become delinquent if an invoice becomes uncollectible or remains unpaid into a later billing cycle. Ingestion is paused until the issue is resolved.\n- **Restricted**: Your team is read-only and ingest keys are disabled. Billing updates remain available so an Owner can address billing. An Owner cannot delete a restricted team through the application and must contact support.\n- **Blocked**: Your account has been blocked, typically due to a violation of our terms of service. You will no longer be able to access the account.\n\n### Users\n\nIn the **Users** settings, you can manage everyone who has access to your team. All users will have access to all projects within the team.\n\n### Security\n\nIn the **Security** settings, you can:\n\n- **Require MFA**: Turn on **Require Multi-factor authentication** to require every team member to enable MFA before accessing the team. Only the Owner can change this setting, and the Owner must have verified MFA first.\n\n### Billing\n\nIn the **Billing** settings, you can:\n\n- **Billing Email**: Set a billing-specific email address for invoices and payment notifications.\n- **Billing Plan**: View and manage your current billing plan.\n- **Billing Address**: Set your team's billing address for invoices.\n\n### Roles\n\nIn the **Roles** settings, you can create roles for your team members. By default, there are two roles: **Owner** and **Member**. The built-in **Owner** role cannot be created, updated, or deleted, and there must always be at least one owner.\n\nThe **Quick fill** actions are **Common**, **Full access**, and **Clear**. You can adjust the\npermissions before saving. **Clear** removes optional access while retaining the project read access\nrequired for team members.\n",
|
|
48
48
|
"guides/users": "# Users\n\n## Overview\n\nUsers are individuals who have access to a specific team within Tailglow. Each user has one role in that team, which determines their permissions. By managing users effectively, you can ensure proper access control for your projects.\n\n## Adding Users\n\nTo add a new user to your team:\n\n1. Click your team name in the top toolbar, then click **Team Settings** on the team overview.\n2. Go to the **Users** tab.\n3. Click the plus button in the **Users** card.\n4. Enter the user's name, email address, and assign a role.\n5. Click **Add User**. Tailglow adds the user to the team immediately and emails them. Someone new\n to Tailglow gets a button that opens the sign-in page with their email filled in. Someone who\n already uses Tailglow gets a link to the app.\n\n## Managing Users\n\nIn the **Users** tab of your team settings, you can:\n\n- **View all users**: See a list of all users currently in your team.\n- **Review activity**: See `Recently active` when a user used Tailglow within the last five minutes,\n or the relative time of their last activity otherwise. Activity is global and may have occurred\n in another team.\n- **Edit roles**: Change the role of a user by selecting from the predefined roles or custom roles you've created. If you change a user's role, they will be notified via email and signed out.\n- **Remove users**: Click the user's edit button, then hold the trash button labeled **Hold to remove user** until the action confirms.\n\n## Updating Your Profile\n\nTo update your own profile, click on your image in the top right corner and select **Profile**. Here, you can update your name, preferences, and profile image.\n\n## Roles and Permissions\n\nUsers' access and capabilities within the team are determined by their assigned roles. Each role has specific permissions. By default, Tailglow includes two roles:\n\n- **Owner**: Full access to all features, including team and project management.\n- **Member**: Can view data but has limited permissions for modifying settings or projects.\n\nYou can also create custom roles under the **Roles** tab in the settings to provide more tailored permissions.\n\nUse **Common**, **Full access**, or **Clear** as a starting point when creating a custom role, then\nadjust its permissions before saving. See [Permissions](/guides/permissions) for details.\n\n## Notification Preferences\n\nEach user can configure their notification preferences from **Profile** > **Notifications**:\n\n- **Auto-subscribe to new monitors**: Automatically subscribe to notifications for newly created monitors.\n- **Product Updates**: Choose whether to receive noteworthy product and feature announcements.\n",
|
|
49
49
|
"guides/projects": "# Projects\n\n## Overview\n\nA project in Tailglow represents a container for your sources, metrics, and pages. Projects are used to organize data for specific purposes, such as a product, application, or environment. Each project has its own settings, while a member's team role scopes govern access across the team's projects.\n\n## Creating a Project\n\nTo create a new project:\n\n1. Navigate to your **Team Overview** by selecting your team name in the top toolbar.\n2. Click the **\"Add New\"** button in the top-right corner.\n3. Enter the project name and click \"Create Project\".\n4. Once created, the project will appear in your **Team Overview**.\n\n## Project Settings\n\nTo access a project's settings:\n\n1. Select a project from the sidebar.\n2. Navigate to the **Settings** tab.\n\n### General Settings\n\nIn the general settings, you can:\n\n- Rename your project.\n- Update the project start date, which Tailglow uses as the starting point for displayed charts.\n- Set the late data window, which controls how far back an event with an overridden timestamp can\n reach and still enter charts automatically. See\n [Late data](/guides/metrics#late-data).\n- Delete the project.\n **Note**: Deleting a project schedules it for permanent removal seven days later. You can\n restore the project from the settings page any time before that date. Ingest keeps running for the\n whole window, so a restored project carries on with nothing missing.\n\n### Sources\n\nSources own ingest configuration and ingest keys. Collections under a source own the records, files, and schemas. You can manage sources and their ingest keys from **Settings** > **Sources** in your project.\n\n### Views\n\nIn this section, you can:\n\n- Create, edit, and delete views that use TGL to transform collection or view inputs and join data when configured.\n\n## Sending Data to a Project\n\nEach ingest key authenticates a source. Tailglow routes an accepted request to a collection under that source using the request's collection selection or the source's default routing. To send data:\n\n- Send data to your project's ingest endpoint (found in your source settings), e.g. `https://us11.ingest.tailglow.io/prj_xxx`.\n- Include a source's ingest key in the `key` query parameter, or use the `Authorization` header.\n\nRefer to the [Ingest Key](/guides/ingest-key) documentation for detailed steps on sending data.\n\n## Deleting a Project\n\nIf a project is no longer needed and its data is no longer relevant, you can delete it. Deleting a project will:\n\n- Schedule permanent removal of all data, settings, and configurations for seven days later.\n- Allow you to restore the project any time before that removal date.\n- Offer a **Delete Permanently** option in project settings, if you would rather not wait out the\n window. That one cannot be undone.\n- Keep accepting data until the removal date, so restoring the project loses nothing.\n\nTo schedule a project for deletion:\n\n1. Open the project's **Settings** tab and select **General**.\n2. In the **Delete Project** card, press and hold **Hold to Schedule Deletion** until it completes.\n\nThe project settings page then shows a red banner counting down to the removal date. Click\n**Restore Project** there at any point before it to cancel.\n\nTo remove the project immediately instead:\n\n1. Click **Delete Permanently** in that banner.\n2. Type the project name, then type **Delete permanently**.\n3. Click **Delete permanently**. This cannot be undone.\n\n## Best Practices\n\n- **Use separate projects for different environments**: For example, create distinct projects for production, staging, and development environments.\n- **Organize projects by product or feature**: If you have multiple products, consider creating one project per product for better data separation and management.\n- **Monitor project activity**: Regularly check activity logs and metrics to ensure the project is running as expected.\n",
|
|
50
50
|
"guides/sources": "# Sources\n\nSources organize the data sent to a project. Each source has its own ingest keys and settings, and contains collections of related records.\n\n## Create and configure a source\n\n1. Open project **Settings** and select **Sources**.\n2. Click the plus button.\n3. Enter a name and click **Create Source**.\n4. In the source drawer, open **Ingest** to copy an ingest endpoint or manage keys.\n\nCreating a source does not guarantee that a key was created. Use **Add Key** in the Ingest tab if you need one.\n\n## Collections and schema detection\n\nSources begin with no collections. Tailglow chooses one collection for each ingest request: the `?collection=` value when it is valid, otherwise the source's `catchall` collection. Fields inside the records, including `type`, never choose the collection. All records in a JSON array or JSONL request use that collection.\n\nCollection names are normalized before they are matched, so `?collection=Charges`, `?collection=CHARGES`, and `?collection=charges` all resolve to the same `charges` collection. Normalizing trims the value, lowercases it, replaces every character outside `a-z`, `0-9`, `_`, and `-` with a hyphen, collapses repeated hyphens, and truncates to 64 characters. A value with no letters or digits left after that is ignored rather than rejected, and the request goes to `catchall`. The collection's `name` field carries its display casing.\n\nA source supports up to 100 collections. New collection names route to `catchall` after the source reaches that guardrail. A burst of simultaneous requests can briefly create more than 100 collections.\n\nEach collection detail page lists schema versions with their state (candidate, settled, or stale), rows, files, and last-seen time. Use the eye button to inspect a version's fields and types. **Version details** shows why it settled and its dates; the drawer also shows whether a field has appeared as null, empty, or absent. Choose **Settle version** in the drawer when you trust a candidate or stale shape and do not want to wait for it to recur. A view's **Pending shapes** card offers the same action on a shape that is still settling. Settling is one way: a settled version never goes back to candidate. [Concepts](/guides/concepts#schema-versions) explains the states. The aggregate **Schema Tree** remains on the collection page to compare fields across versions.\n\nWith **Automatic** detection, Tailglow groups changing keys when existing mappings can keep working. It leaves a proposal unchanged when a mapping reads inside that path, or when a mapping's reads cannot be determined. There is no approval task in Automatic mode. With **Manual** detection, the **Schema proposals** card shows suggested groups. Its eye button opens the proposed changes, including the remaining shapes and tracked child paths. Choose **Group these keys** to apply it or **Leave unchanged** to close it. The drawer warns when an existing mapping may need updating. These changes require `sources:write` access.\n\n## Schema settings\n\nThe collection's **Overview** includes a **Schema recognition** summary. Its edit button, or the gear in the page header, opens the settings drawer. **Recognition**, **Settlement**, and **Limits** group the options; choose **Save changes** to apply edits. Closing the drawer discards unsaved edits. Rules and settings are separate on purpose. Changing a rule re-keys the collection: every version and unclassified shape is re-signed under the new rules, and shapes that now match are merged into one version, which keeps its files and mappings. Before a rule change lands, a confirmation shows how many shapes the collection holds now and how many it would hold under the new rules. Changing a setting re-keys nothing.\n\nRules:\n\n- **Grouped paths** keep arbitrary children out of schema identity while every raw value is still stored. Enter one path per line, such as `/metadata`. Escape a literal slash in a key as `~1` and a literal tilde as `~0`.\n- **Tracked child paths** restore tracking for selected children of an opaque path, such as `/metadata/customer_id`.\n- **Maximum depth** sets how many levels of nesting take part in identity. The default is 5. A mapping can read five levels deep, so raising this past 5 records deeper shapes without making them readable.\n- **Nulls and absent values**, **Optional keys**, and **Detection** are each **Automatic** or **Manual**. Automatic can accept null and absent mapping inputs when at least one other read has matching typed evidence. A missing ID with an ambiguous or unsafe replacement blocks that script from being reused. Automatic also lets a record whose keys are a subset of a version's keys join that version and applies a safe opaque-path proposal from the detector on its own. Manual waits for a person at each of those points.\n\nSettings:\n\n- **Recurrence gap**: how long after first sight a shape must be seen again before its version settles. The default is 60 minutes.\n- **Bulk rows**: a single batch carrying at least this many rows of a shape settles its version at once. The default is 100, and 0 turns the bulk path off.\n- **Stale after**: how long a candidate waits to be seen again before it goes stale. The default is 24 hours.\n- **Settled versions limit**, **Candidate pool**, and **Alias pool** bound how many settled versions, candidate and stale versions, and cached signatures a collection may hold. Past a bound, new shapes stay unclassified with their signatures kept.\n- **Files per batch**: how many collection files one ingest batch may open at once for this collection. A batch with more versions than that is written in more than one pass, so every shape with a version still lands in a file of its own version. Shapes past the candidate pool get the same number of files per batch, the shapes with the most rows first, so they can be linked to a version once the pool has room; the rows of shapes past that budget are kept in the batch's unclassified file and cannot be linked to a version later.\n- **Unclassified retention**: how many days the files of shapes that never settled are kept before they are deleted. That covers unclassified files and the files of stale versions, and a stale version with nothing left under it is deleted with them, together with the pairings views held on it. Leave it empty to keep them forever.\n\nFor a version a view cannot map automatically, you can supply a hint, provide a script, or exclude that version from a particular view with a recorded reason.\n\nAn ingest key's recent-use time indicates that requests reached the source. It does not mean every view and chart has finished processing them.\n\n## Deleting a source\n\nUse the hold-to-delete control in the source drawer to schedule deletion. The source remains visible\nwith a line-through name and a **Deleting** chip. Hover the chip or open the source drawer to see how\nlong remains before permanent removal. Tailglow schedules removal for seven days later.\n\nBefore that date, open the source drawer and click **Restore Source** to cancel deletion. The source\nkeeps accepting data for the whole window, so a restored source carries on with nothing missing.\n\nThe same drawer offers **Delete Permanently** if you would rather not wait. It asks you to type the\nsource name, destroys every collection and record under it immediately, and cannot be undone.\n\nPermanent removal deletes the source, its collections and data, and any views built from those\ncollections. Existing results from joins that used one of those views remain. If the removed view\nwas the join's main input, the join stops receiving new records; if it was a lookup, new records are\nno longer enriched by it. Reconfigure or remove affected joins before permanent removal.\n",
|
|
51
51
|
"guides/records": "# Views\n\nViews reshape data from a collection or combine existing views. Use them to prepare data for analysis, charts, and monitors. See [Sources](/guides/sources) for how incoming data is organized into collections.\n\n## Create a view\n\n1. Open **Views** and click **New View**.\n2. In the **Collection** tab, select a **Source Collection**.\n3. Enter a **View Name**. You can also add an **Output Schema** or **Generation Hint**.\n4. Click **Create View**.\n\nTo combine existing views instead, use the **View** tab and choose a base view with one or more lookup views.\n\n## Inspect a view\n\nOpen a view and select **Records** to inspect its transformed output.\n\n## View status\n\nThe status chip can show **Empty**, **Preparing**, **Indexing**, **Needs TGL**, **Active**, or\n**Error**. **Preparing** and **Indexing** mean Tailglow is building the view. **Needs TGL** means the\nview needs a transform script for a schema version. **Error** means generation or indexing needs\nattention.\n",
|
|
52
|
-
"guides/metrics": "# Metrics & Monitors\n\nMetrics aggregate a view's data into charts. A metric needs a name, view, and timestamp field. Grouping and filters are optional.\n\n## Create a metric\n\n1. Open **Metrics** and click **New Metric**.\n2. Choose a view and timestamp field. The UI selects the first eligible date or string output field by default.\n3. Optionally choose **Group By**, a numeric value field, a unique-count field, and filters.\n4. Click **Create Metric**.\n\nTailglow backfills existing view data, then updates the metric from new data. The status indicates whether it is initializing, waiting for transforms, backfilling, active, in error, or cancelled.\n\n## Partial results and waiting shapes\n\nCharts show available results with a compact coverage control in the header. Open it to see whether records need mapping or recovery work, or completeness is simply unverified. Unverified historical or time coverage can remain even when the source view has no pending shapes and its scripts are live. An amber region on Cartesian charts marks incomplete results with known mapping, recovery, or exclusion reasons; empty ranges show a no-data state. Records in a shape that has not settled yet are listed in the coverage control but do not mark results incomplete; they are added once the shape settles. Trends and forecasts use verified complete buckets; they are unavailable when coverage is unknown or too little complete data remains. Monitors and storage sealing continue to use the verified boundary.\n\nUse **Inspect view**, then **Inspect** on a waiting shape, to see missing fields, changed types, possible substitutions, and the fields each candidate TGL reads. The drawer lists the shape's fields and types, using full paths for nested fields. **Activity details** shows its state, dates, source, stored rows, and schema observations. Pending shapes also show the next automatic check or requested hint. A small deferral-evidence count is not the total number of ingested records.\n\nExcluding a version records a decision for that view and version, including future matching records. It does not erase existing materialized output. Charts retain an exclusion notice; other views make independent decisions.\n\nWaiting for a schema mapping does not create a new late-data decision. Catch-up preserves original admission. Sealed history is rebuilt by re-backfilling the metric: a change to its data feed, or the re-backfill action, wipes its rollups and rebuilds them from the retained rows. Adding rows to the hot tier is not enough.\n\n## Metric settings\n\nMetric settings are organized into four tabs. **Display** covers display value, time range, chart type, value unit, empty-interval handling, the maximum series shown, a time dimension (hour of day, weekday, or hour of week) for breakdown and heatmap charts, the bar order for breakdown charts, Compact Values, which rounds tooltip readings to compact notation such as 321.3M and is on by default, and Y Axis Min and Max, which pin either end of the y axis on line, area, and bar charts. Leave a bound empty and that side keeps fitting the data. **Analysis** covers trend lines, forecasting, and alert markers. **Colors** covers how series are pinned and painted. The metric's name is edited in the settings header. Tailglow chooses the interval from the selected time range.\n\nThe **Query** tab lets you change the view, timestamp field, grouping, values, and filters. Changing it rebuilds the metric.\n\nTailglow supports line, area, bar, pie, scatter, radar, stat, gauge, calendar, uptime, breakdown, heatmap, and table charts. Trend lines and forecasts are available for line, area, and bar charts. Alert markers are available for line, area, bar, pie, radar, breakdown, and calendar charts. With trend lines on, the tooltip reports each bucket's distance from the trend, green above and red below, and a single-series chart adds the overall growth per period. A line whose direction the data does not support carries a small warning triangle instead.\n\nCalendar and uptime charts render time itself, so they read best over day-sized buckets. A calendar shades one cell per day (per hour when the range is a week or less) by value, and a metric with several series sums them into one calendar. An uptime strip colors one cell per bucket using the value rules from the **Colors** tab; a metric with several series draws one strip per series plus an Overall row carrying the worst value of any series in each bucket. An uptime chart in By Value mode with no rules yet offers a one-click preset: 99.9 and above green, 99 and above amber, anything lower red. Click any cell on either chart to zoom into that period.\n\nBreakdown, heatmap, and table charts summarize the whole range instead of drawing it over time. A breakdown draws one bar per group, largest first, and folds the groups past the series limit into one Other bar; a time scrubber under it replays the range bucket by bucket, as a pie's does. With a time dimension, a breakdown draws one column per hour of the day or per weekday, and a heatmap places every hour of the week on a grid. Time dimensions fold the metric's hourly data on the server in the time zone you are viewing in, so colleagues in different zones each see their own clock, and they cover at most 400 days. A table lists the series with their value, share of the total, and trend, and pages through them when there are more than fit.\n\n## Forecasting\n\nA forecast extends a metric's best fit line past your latest data, so the chart shows where the metric is heading.\n\nTurn it on from the **Analysis** tab of the metric's settings with **Show Forecast**. **Default Horizon** sets how far ahead the chart looks: Next 7 days, Next 30 days, Next 90 days, End of quarter, End of year, or Next year. Relative horizons resolve when the chart is viewed, so a metric set to Next 30 days always looks 30 days past the day you open it. **Model** sets the shape of the projection. Auto follows whichever model fits the current data best, or you can pin Linear, Exponential, Logarithmic, Logistic, or Sinusoidal. Either way, the list marks the model that currently fits your data best.\n\nEvery line, area, and bar chart in the app carries a forecast control in its header, whether or not the metric has forecasting saved. Use it to change the horizon or the model for your own view. The metric's saved settings do not change, and neither does what anyone else sees. **Use metric default** returns you to the saved settings, and your choice lasts only for the current visit.\n\nThe projected part of the chart is drawn dashed, with a shaded band around it covering the range of likely values. The band is always shown with a forecast. In the tooltip, projected buckets add labeled columns: the estimate appears as a small chip on a track, with the bucket's bounds beside it. A bucket that is still filling in shows the value recorded so far under Now, with Est. and Max for the projected landing and ceiling; a fully projected bucket shows Min and Max around the estimate. Buckets that are fully measured keep the plain tooltip.\n\nWhen a fitted model explains little of a series' history, or its first projection leaps far from recent values, that series carries a small warning triangle in the tooltip: the forecast still draws, but read it as low confidence.\n\nForecasts need a line, area, or bar chart. On a bar chart every visible series gets its own fit, and the projected buckets draw as hatched ghost bars in place of the dashed line and band, stacked in the same order as the bars. The tooltip reports each series' projection, plus a Total row when there are several series. On a stacked line or area chart the forecast projects the stack's total.\n\n## Late data\n\nMost data arrives in order and charts simply fill forward. Data sent live is never late: an event without a timestamp of its own is stamped on arrival and always charts automatically.\n\nOnly events with an overridden timestamp, such as the SDK's `event_time` field, can land in the past. This is normal when importing history, replaying a queue, or batching in an integration. How far back such an event may reach and still enter charts automatically is your project's late data window.\n\n### The late data window\n\nOpen project **Settings**, then find the **Late Data** card under General. The window is a number of minutes, counted back from the newest data already in your charts:\n\n- **Within the window**, a late event folds into charts automatically. Nothing to do.\n- **Beyond the window**, the event is stored and counted, but stays out of charts until you fold it in with a re-backfill.\n\nThe default is 120 minutes, which covers most integrations. Set it to 0 to hold everything that arrives behind your charts: each such event is surfaced instead of charted. The maximum is 129,600 minutes (90 days).\n\nThe window is a promise about charts, not about storage. Late events are kept in your views either way; only their aggregation waits. Records, exports, and drains are unaffected.\n\nChanging the window applies to events that arrive after the change. Events already held under the old window stay in **Late Data Waiting** until you re-backfill them.\n\n### Re-backfilling late data\n\nWhen events are held out of charts, a **Late Data Waiting** card appears in the project's general settings. Each row is one batch: the view it belongs to, how many events were held, and the event-time range they span.\n\nClick **Re-backfill** to fold a batch in. The row shows **Queued**, and the events appear in charts as the fold completes, back to the earliest event in the batch.\n\nVery large batches can appear in this list even when their events are inside the window. Folding a batch that touches many hours of history is expensive on a busy metric, so a batch beyond roughly a hundred thousand events, or one spread across more than 48 distinct hours, waits for the same one-click confirmation instead of folding silently. The window itself never changes on its own: only you decide what enters charts automatically.\n\n### Choosing a window\n\nThe default suits the common pattern of backfilling history first and then running live. If your integrations regularly submit data that is hours or days old, widen the window so those events chart without manual steps. If you report on recent data and want it locked quickly, narrow the window or set it to 0 and treat **Late Data Waiting** as your review queue.\n\n## Monitors\n\nOpen a metric and use its monitor control to manage monitors. Click the add icon to create one. A metric can have up to 15 monitors.\n\n### Auto and Manual\n\n**Auto** turns a description such as \"Alert me if no data arrives for 10 minutes\" into a monitor. Tailglow fills in every setting, lists the choices it had to assume, and opens the form for you to review before saving. When the description leaves something out that it cannot fill in, such as a threshold, it asks a question instead of guessing. Each team can draft a limited number of monitors per hour. **Manual** opens the same form empty.\n\n### The condition\n\nEach check takes the metric's data in a window, computes one **statistic**, and compares it with a **threshold** in the metric's unit. The statistics are count, sum, average, minimum, maximum, last value, median, 95th and 99th percentile, and unique values. Every statistic covers the whole window: an average over 10 minutes averages every reading in those 10 minutes, and **All data combined** combines every series before computing it.\n\nTo detect that a feed has stopped, choose **No data**, then a window such as **10 minutes**. It checks that no readings arrived in that window. A measured value of zero still counts as data.\n\nFor averages, extremes, the last value and percentiles, choose what a window without readings means: **Skip the check** (the default) leaves any alert as it is, and **Count it as 0** compares 0 with the threshold. Counts, sums and unique values are always 0 for an empty window.\n\n### Scope and windows\n\nUse **All data combined** for one value across the whole metric, **Any series** for one alert while any series matches (the alert records which series opened it), or **Each series separately** for a separate alert per series. Series filters narrow the series being watched.\n\nWindows have presets and a **Custom** duration in whole minutes. **All time** starts where the metric's all-time chart starts and is unavailable for **No data**.\n\n### When an alert opens\n\n- **On the first matching check** opens an alert as soon as a check matches.\n- **After holding for a duration** opens once the condition has matched for that long. A gap between checks longer than the hold starts it over.\n- **After several matching checks** opens once enough checks match within a window. The matches need not be in a row, and the alert ends once the window holds fewer matches than required.\n\nThe **Evaluation frequency** sets how often the rule is checked, as a preset or a cron expression in UTC (for example `0 9 * * 1-5` for weekdays at 09:00), at most once a minute. Tailglow refuses a hold or a number of matching checks that the frequency could never reach. **Alert length** keeps an alert open **Until the condition clears** or records an **Instant event** for a milestone that does not clear on its own. An **Expires at** date stops the monitor after that date.\n\n### What a check waits for\n\nA check runs only on complete data, so a processing delay never reads as missing data. A new metric is checked as soon as its history has finished loading, and checks are skipped while a metric rebuilds, not replayed afterwards. Pausing a monitor ends its open alerts; resuming it starts from the next check without going back over the paused time. Changing the rule ends open alerts, while changing only the frequency keeps them. Deleting the data behind a metric ends its alerts.\n\nThe preview checks the most recent window with the same rules and shows each series' value and whether it matches. It saves nothing, sends nothing, and does not simulate holds or repeated checks.\n\nA blinking red indicator means an alert is currently open. Ended alerts remain shaded on the chart as history, with their original condition and end time, but do not keep the indicator blinking.\n",
|
|
52
|
+
"guides/metrics": "# Metrics & Monitors\n\nMetrics aggregate a view's data into charts. A metric needs a name, view, and timestamp field. Grouping and filters are optional.\n\n## Create a metric\n\n1. Open **Metrics** and click **New Metric**.\n2. Choose a view and timestamp field. The UI selects the first eligible date or string output field by default.\n3. Optionally choose **Group By**, a numeric value field, a unique-count field, and filters.\n4. Click **Create Metric**.\n\nTailglow backfills existing view data, then updates the metric from new data. The status indicates whether it is initializing, waiting for transforms, backfilling, active, in error, or cancelled.\n\n## Partial results and waiting shapes\n\nCharts show available results with a compact coverage control in the header. Open it to see whether records need mapping or recovery work, or completeness is simply unverified. Unverified historical or time coverage can remain even when the source view has no pending shapes and its scripts are live. An amber region on Cartesian charts marks incomplete results with known mapping, recovery, or exclusion reasons; empty ranges show a no-data state. Records in a shape that has not settled yet are listed in the coverage control but do not mark results incomplete; they are added once the shape settles. Trends and forecasts use verified complete buckets; they are unavailable when coverage is unknown or too little complete data remains. Monitors and storage sealing continue to use the verified boundary.\n\nUse **Inspect view**, then **Inspect** on a waiting shape, to see missing fields, changed types, possible substitutions, and the fields each candidate TGL reads. The drawer lists the shape's fields and types, using full paths for nested fields. **Activity details** shows its state, dates, source, stored rows, and schema observations. Pending shapes also show the next automatic check or requested hint. A small deferral-evidence count is not the total number of ingested records.\n\nExcluding a version records a decision for that view and version, including future matching records. It does not erase existing materialized output. Charts retain an exclusion notice; other views make independent decisions.\n\nWaiting for a schema mapping does not create a new late-data decision. Catch-up preserves original admission. Sealed history is rebuilt by re-backfilling the metric: a change to its data feed, or the re-backfill action, wipes its rollups and rebuilds them from the retained rows. Adding rows to the hot tier is not enough.\n\n## Metric settings\n\nMetric settings are organized into four tabs. **Display** covers display value, time range, chart type, value unit, empty-interval handling, the maximum series shown, a time dimension (hour of day, weekday, or hour of week) for breakdown and heatmap charts, the bar order for breakdown charts, Compact Values, which rounds tooltip readings to compact notation such as 321.3M and is on by default, and Y Axis Min and Max, which pin either end of the y axis on line, area, and bar charts. Leave a bound empty and that side keeps fitting the data. **Analysis** covers trend lines, forecasting, and alert markers. **Colors** covers how series are pinned and painted. The metric's name is edited in the settings header. Tailglow chooses the interval from the selected time range.\n\nThe **Query** tab lets you change the view, timestamp field, grouping, values, and filters. Changing it rebuilds the metric.\n\nTailglow supports line, area, bar, pie, scatter, radar, stat, gauge, calendar, uptime, breakdown, heatmap, and table charts. Trend lines and forecasts are available for line, area, and bar charts. Alert markers are available for line, area, bar, pie, radar, breakdown, and calendar charts. With trend lines on, the tooltip reports each bucket's distance from the trend, green above and red below. A single-series chart also says whether the series is growing, shrinking, or roughly flat, and, where the size can be measured, how fast. That reading follows the typical bucket, so a few spikes barely move it, and once a chart covers a few days, weeks, or years it compares each hour, weekday, or month with the same one in other cycles. It names a direction only when the data clearly shows one, and says roughly flat only when the data rules out any change larger than its usual spread or the whole series moves too little to matter; otherwise it says no clear direction. A range too short to tell gets no reading.\n\nCalendar and uptime charts render time itself, so they read best over day-sized buckets. A calendar shades one cell per day (per hour when the range is a week or less) by value, and a metric with several series sums them into one calendar. An uptime strip colors one cell per bucket using the value rules from the **Colors** tab; a metric with several series draws one strip per series plus an Overall row carrying the worst value of any series in each bucket. An uptime chart in By Value mode with no rules yet offers a one-click preset: 99.9 and above green, 99 and above amber, anything lower red. Click any cell on either chart to zoom into that period.\n\nBreakdown, heatmap, and table charts summarize the whole range instead of drawing it over time. A breakdown draws one bar per group, largest first, and folds the groups past the series limit into one Other bar; a time scrubber under it replays the range bucket by bucket, as a pie's does. With a time dimension, a breakdown draws one column per hour of the day or per weekday, and a heatmap places every hour of the week on a grid. Time dimensions fold the metric's hourly data on the server in the time zone you are viewing in, so colleagues in different zones each see their own clock, and they cover at most 400 days. A table lists the series with their value, share of the total, and trend, and pages through them when there are more than fit.\n\n## Forecasting\n\nA forecast extends a metric's best fit line past your latest data, so the chart shows where the metric is heading.\n\nTurn it on from the **Analysis** tab of the metric's settings with **Show Forecast**. **Default Horizon** sets how far ahead the chart looks: Next 7 days, Next 30 days, Next 90 days, End of quarter, End of year, or Next year. Relative horizons resolve when the chart is viewed, so a metric set to Next 30 days always looks 30 days past the day you open it. **Model** sets the shape of the projection. Auto follows whichever model fits the current data best, or you can pin Linear, Exponential, Logarithmic, Logistic, or Sinusoidal. Either way, the list marks the model that currently fits your data best.\n\nEvery line, area, and bar chart in the app carries a forecast control in its header, whether or not the metric has forecasting saved. Use it to change the horizon or the model for your own view. The metric's saved settings do not change, and neither does what anyone else sees. **Use metric default** returns you to the saved settings, and your choice lasts only for the current visit.\n\nThe projected part of the chart is drawn dashed, with a shaded band around it covering the range of likely values. The band is always shown with a forecast. In the tooltip, projected buckets add labeled columns: the estimate appears as a small chip on a track, with the bucket's bounds beside it. A bucket that is still filling in shows the value recorded so far under Now, with Est. and Max for the projected landing and ceiling; a fully projected bucket shows Min and Max around the estimate. Buckets that are fully measured keep the plain tooltip.\n\nWhen a fitted model explains little of a series' history, or its first projection leaps far from recent values, that series carries a small warning triangle in the tooltip: the forecast still draws, but read it as low confidence.\n\nForecasts need a line, area, or bar chart. On a bar chart every visible series gets its own fit, and the projected buckets draw as hatched ghost bars in place of the dashed line and band, stacked in the same order as the bars. The tooltip reports each series' projection, plus a Total row when there are several series. On a stacked line or area chart the forecast projects the stack's total.\n\n## Late data\n\nMost data arrives in order and charts simply fill forward. Data sent live is never late: an event without a timestamp of its own is stamped on arrival and always charts automatically.\n\nOnly events with an overridden timestamp, such as the SDK's `event_time` field, can land in the past. This is normal when importing history, replaying a queue, or batching in an integration. How far back such an event may reach and still enter charts automatically is your project's late data window.\n\n### The late data window\n\nOpen project **Settings**, then find the **Late Data** card under General. The window is a number of minutes, counted back from the newest data already in your charts:\n\n- **Within the window**, a late event folds into charts automatically. Nothing to do.\n- **Beyond the window**, the event is stored and counted, but stays out of charts until you fold it in with a re-backfill.\n\nThe default is 120 minutes, which covers most integrations. Set it to 0 to hold everything that arrives behind your charts: each such event is surfaced instead of charted. The maximum is 129,600 minutes (90 days).\n\nThe window is a promise about charts, not about storage. Late events are kept in your views either way; only their aggregation waits. Records, exports, and drains are unaffected.\n\nChanging the window applies to events that arrive after the change. Events already held under the old window stay in **Late Data Waiting** until you re-backfill them.\n\n### Re-backfilling late data\n\nWhen events are held out of charts, a **Late Data Waiting** card appears in the project's general settings. Each row is one batch: the view it belongs to, how many events were held, and the event-time range they span.\n\nClick **Re-backfill** to fold a batch in. The row shows **Queued**, and the events appear in charts as the fold completes, back to the earliest event in the batch.\n\nVery large batches can appear in this list even when their events are inside the window. Folding a batch that touches many hours of history is expensive on a busy metric, so a batch beyond roughly a hundred thousand events, or one spread across more than 48 distinct hours, waits for the same one-click confirmation instead of folding silently. The window itself never changes on its own: only you decide what enters charts automatically.\n\n### Choosing a window\n\nThe default suits the common pattern of backfilling history first and then running live. If your integrations regularly submit data that is hours or days old, widen the window so those events chart without manual steps. If you report on recent data and want it locked quickly, narrow the window or set it to 0 and treat **Late Data Waiting** as your review queue.\n\n## Monitors\n\nOpen a metric and use its monitor control to manage monitors. Click the add icon to create one. A metric can have up to 15 monitors.\n\n### Auto and Manual\n\n**Auto** turns a description such as \"Alert me if no data arrives for 10 minutes\" into a monitor. Tailglow fills in every setting, lists the choices it had to assume, and opens the form for you to review before saving. When the description leaves something out that it cannot fill in, such as a threshold, it asks a question instead of guessing. Each team can draft a limited number of monitors per hour. **Manual** opens the same form empty.\n\n### The condition\n\nEach check takes the metric's data in a window, computes one **statistic**, and compares it with a **threshold** in the metric's unit. The statistics are count, sum, average, minimum, maximum, last value, median, 95th and 99th percentile, and unique values. Every statistic covers the whole window: an average over 10 minutes averages every reading in those 10 minutes, and **All data combined** combines every series before computing it.\n\nTo detect that a feed has stopped, choose **No data**, then a window such as **10 minutes**. It checks that no readings arrived in that window. A measured value of zero still counts as data.\n\nFor averages, extremes, the last value and percentiles, choose what a window without readings means: **Skip the check** (the default) leaves any alert as it is, and **Count it as 0** compares 0 with the threshold. Counts, sums and unique values are always 0 for an empty window.\n\n### Scope and windows\n\nUse **All data combined** for one value across the whole metric, **Any series** for one alert while any series matches (the alert records which series opened it), or **Each series separately** for a separate alert per series. Series filters narrow the series being watched.\n\nWindows have presets and a **Custom** duration in whole minutes. **All time** starts where the metric's all-time chart starts and is unavailable for **No data**.\n\n### When an alert opens\n\n- **On the first matching check** opens an alert as soon as a check matches.\n- **After holding for a duration** opens once the condition has matched for that long. A gap between checks longer than the hold starts it over.\n- **After several matching checks** opens once enough checks match within a window. The matches need not be in a row, and the alert ends once the window holds fewer matches than required.\n\nThe **Evaluation frequency** sets how often the rule is checked, as a preset or a cron expression in UTC (for example `0 9 * * 1-5` for weekdays at 09:00), at most once a minute. Tailglow refuses a hold or a number of matching checks that the frequency could never reach. **Alert length** keeps an alert open **Until the condition clears** or records an **Instant event** for a milestone that does not clear on its own. An **Expires at** date stops the monitor after that date.\n\n### What a check waits for\n\nA check runs only on complete data, so a processing delay never reads as missing data. A new metric is checked as soon as its history has finished loading, and checks are skipped while a metric rebuilds, not replayed afterwards. Pausing a monitor ends its open alerts; resuming it starts from the next check without going back over the paused time. Changing the rule ends open alerts, while changing only the frequency keeps them. Deleting the data behind a metric ends its alerts.\n\nThe preview checks the most recent window with the same rules and shows each series' value and whether it matches. It saves nothing, sends nothing, and does not simulate holds or repeated checks.\n\nA blinking red indicator means an alert is currently open. Ended alerts remain shaded on the chart as history, with their original condition and end time, but do not keep the indicator blinking.\n",
|
|
53
53
|
"guides/pages": "# Pages\n\n## Overview\n\nPages are customizable dashboards that display one or more metrics in a single view. They refresh automatically while you are viewing a live time range and can be shared publicly or served through a custom domain.\n\n## Creating a Page\n\nTo create a new page:\n\n1. Navigate to the **Pages** section in the sidebar.\n2. Click the **\"New Page\"** button.\n3. Configure the following fields:\n - **Name**: Enter a descriptive name for your page, like \"Quarterly Dashboard\" or \"Performance\".\n - **Time Range**: This synchronizes the time range of all metrics on the page.\n - **Data Interval**: Choose the interval used to group the data. It defaults to **Day**. Intervals that do not suit the time range are disabled. A custom range disables intervals too fine for its length, such as **Minute** across a month.\n4. Click **Add Page** to save and generate your new dashboard.\n\n## Time ranges and live updates\n\nUse the range picker to choose **Last hour**, **Last 7 days**, **Last 30 days**, **Last 90 days**, **Last year**, **All time**, or a custom selection. A page can also have a different saved default range, which the picker lists alongside these.\n\nRelative ranges, including All time, follow the present. Charts refresh every minute, so the newest bucket keeps growing until it ends. If you brush into history or choose fixed custom dates, that selection stays still and automatic refresh pauses. Choose a relative range again to follow new data.\n\nThe status page created during onboarding starts with Last hour and one-minute intervals, with uptime and latency together on one card. Its first charts may take a few minutes to prepare. Setup also creates an **Endpoint down** monitor that emails you after 3 failed checks in a row. The page is initially private: **Open your status page** opens the internal page, and **Publish page** takes you to Sharing settings. Turn on **Publish page** and save when you want others to see it.\n\nThe newest buckets show the data counted so far until they are complete. Uptime and calendar\ncells show them with gray stripes, and hovering one shows **So far**. Line, area, and bar charts\nmark them the same way, and their tooltips label the value **(so far)**. If processing falls\nbehind, every bucket it has not finished is marked, not only the current one. Older gaps remain\n**No data**. Pie, radar, and breakdown charts label a snapshot that is still filling **(so far)**, and refresh\npauses while you scrub through older snapshots. Stats and gauges show the metric's value over the\nwhole range: an average over every reading, the lowest or highest reading, the latest reading, or a\ntotal for counts and sums (percentiles are estimated from each bucket's own percentile). They read\n**so far** while the range includes a bucket that is still filling. In a merged card,\nwhere they show the bucket under the cursor, that bucket reads **so far** only while it is still\nfilling.\n\n## Adding Metrics\n\nOnce your page is created:\n\n1. Click the **Settings** button on your page.\n2. Navigate to the **Layout** tab.\n3. Click the **Add Metric** button. You'll see a dropdown of all of your available metrics.\n4. Select the metrics you want to display on the page. Each one starts on a card of its own, and a metric can appear on only one card of a page.\n\nEach metric keeps its own chart settings on the page, including the trend lines and forecast saved on its **Analysis** tab. When forecasts are enabled on both the page and the metric, public line, area, and bar charts show a forecast button. Visitors can change the horizon and model, hide the forecast, or return to the metric default. These choices affect only their current view; they do not change the saved settings. On a merged card, one forecast button covers every metric that forecasts and starts at the longest horizon among them.\n\n## Layout & Sizing\n\nYou can customize the layout of your page by rearranging and resizing metric cards:\n\n- **Drag and Drop**: Drag metrics to rearrange their order on the page.\n- **Card Sizing**: Toggle each metric between full-width and half-width. Use full-width for metrics that need more space (like detailed line charts) and half-width to display two metrics side by side.\n- **Merge Charts**: Click the merge button next to **Save changes**, select the cards to combine, and confirm the merge. A merged card shows up to six metrics on one shared time axis, and hovering any of its charts moves the cursor on all of them. Line, area, and bar charts with the same unit share one chart, and a second unit gets its own axis on the right. Stats, gauges, pies, and calendars sit above the charts, and uptime strips line up with them.\n- **Split and Rename**: Remove a metric from a merged card to move it back to a card of its own. Use the pencil on a merged card to give it a name; without one, the card is titled with its metrics' names.\n\n## Visibility & Sharing\n\nPages can be shared publicly so that anyone with the link can view them, without requiring authentication.\n\n- **Public**: Toggle public visibility from the page settings. When enabled, the page is accessible via a public URL.\n- **Access Code**: Optionally require a shared access code. Visitors enter it once per browser. Changing or clearing the code requires visitors to enter it again.\n- **Show Trend Lines On Public Page**: Show or hide trend lines across the public page. Enabled by default; each metric must also have trend lines enabled.\n- **Show Forecasts On Public Page**: Show or hide forecasts and their controls across the public page. Enabled by default; each metric must also have forecasting enabled.\n- **Show Alerts On Public Page**: Enable this setting to show monitor alert markers on the public page. Alerts are hidden by default and must also be enabled on the underlying metric.\n- **Custom Domain**: You can serve pages through your own branded hostname (e.g., `status.yourcompany.com`). Configure a Page domain once in the **Domains** section of team settings. Every public page owned by the team is available from that hostname at its permanent generated slug. You can also give a page a readable path such as `/weekly-health` independently on each custom domain, or set that path to `/` so the page opens at the hostname root.\n\n## What Visitors See\n\nA public page opens with a header. On your own domain it shows your team logo and name; on a\nTailglow address it shows a **Made with Tailglow** link instead. Visitors can show every time on the\npage in their own time zone or in UTC, and switch between light and dark mode. These choices change\nonly their own view and are remembered in their browser.\n\n## Best Practices\n\n- **Use pages to monitor key metrics**: Pages are a great way to keep an eye on the most important metrics for your team or project.\n- **Share pages with stakeholders**: Public pages let stakeholders stay informed without needing a Tailglow account.\n- **Customize pages for different teams**: Create different pages for different teams to focus on the metrics that matter most to them.\n- **Use card sizing strategically**: Put high-level summary metrics (stat, gauge) at half-width and detailed time-series charts (line, area) at full-width for the best readability.\n",
|
|
54
54
|
"guides/custom-domains": "# Custom Domains\n\n## Overview\n\nCustom domains allow you to serve your Tailglow pages and ingest data through your own branded domain instead of the default Tailglow URLs. For example, instead of using `pages.tailglow.io/your-page`, you can use `analytics.yourdomain.com/your-page`.\n\nYou can use custom domains for:\n\n- **Custom Pages**: Serve your public status pages and dashboards from your own domain.\n- **Custom Ingest**: Send analytics data through your own domain for first-party tracking.\n\nCustom domains are available on the Enterprise plan.\nA team can have up to 10 custom domains in total across Ingest and Page domains.\n\nEach hostname has one traffic purpose. You can configure several Ingest domains and several Page domains for the same team.\n\n## Adding a Custom Domain\n\nTo add a custom domain to your team:\n\n1. Navigate to your **Team Settings** page.\n2. Click the **\"Domains\"** tab.\n3. In either the **Ingest domains** or **Page domains** table, click the add button.\n4. Enter your custom domain (e.g., `analytics.yourdomain.com`).\n5. Click **Add Ingest Domain** or **Add Page Domain**, matching the table you chose.\n\nThe table you add the hostname from determines its purpose. After adding it, verify domain ownership and configure routing through a two-step DNS process.\n\n## DNS Verification\n\nSetting up a custom domain is a two-step process: first verify ownership, then configure traffic routing.\n\n### Step 1: Ownership Verification (TXT Record)\n\nBefore your domain can be used, you must verify that you own it by adding a TXT record:\n\n1. Go to your DNS provider's management console.\n2. Add a TXT record:\n - **Name/Host**: The full name shown in Tailglow. For `analytics.yourdomain.com`, it is `_tailglow-verify.analytics.yourdomain.com`.\n - **Value**: The verification token shown in your domain settings\n3. Wait for DNS propagation. Timing depends on your provider and the record's TTL.\n4. Click **Check DNS** in the drawer, or **Check Now** beside the domain status, to check the TXT record.\n\n### Step 2: Routing Setup\n\nOnce ownership is verified, configure routing so traffic reaches Tailglow:\n\n**CNAME Record**\n\nA domain that serves traffic has to be a subdomain such as `analytics.yourdomain.com`. A root domain\nlike `yourdomain.com` cannot hold a CNAME, so it can be verified but not used for ingest or pages.\n\n1. Go to your DNS provider's management console.\n2. Add a CNAME record:\n - **Name/Host**: The name shown in your domain settings. Your provider may display the same host relative to the DNS zone, such as `analytics`.\n - **Value/Target**: The CNAME target shown in your domain settings.\n3. Wait for DNS propagation.\n4. Click **Check DNS** to check the routing configuration.\n\nIf your DNS provider offers proxying, use **DNS only** while setting up the domain. The hostname must\npoint directly to the target shown in Tailglow so its certificate can be provisioned and renewed. A\nproxied record can leave certificate provisioning stuck or failed.\n\n### Ongoing Checks\n\nTailglow re-checks every record hourly, not just during setup. Both records have to keep resolving\nfor the domain to keep working.\n\nIf a record stops resolving, the domain keeps serving while you fix it:\n\n- **Ownership (TXT)**: 24 hours\n- **Routing (CNAME)**: 7 days\n\nPut the record back within that window and nothing is interrupted. Past it, Tailglow stops serving\nthe domain until the record resolves again. Adding it back and clicking **Check DNS** restores\nservice on the next successful check.\n\nA check that cannot reach your DNS provider at all does not count against these windows. Only a\nlookup that completes and finds the record missing or pointing somewhere else starts the clock.\n\n## SSL Certificate Provisioning\n\nOnce your domain is verified and routing is configured, Tailglow requests an SSL certificate. The certificate status can report:\n\n- **None**: No certificate has been requested yet (pre-verification).\n- **Provisioning**: Certificate provisioning is in progress after routing verification.\n- **Active**: The certificate is provisioned and your domain is ready for HTTPS traffic.\n- **Failed**: Certificate provisioning failed. Check the error message in your domain settings for details.\n\nCertificate provisioning often takes a few minutes, but it can take longer. Certificates renew\nautomatically while the required DNS records continue to point directly to Tailglow.\n\n## Traffic Purpose\n\nThe table used to create a domain assigns its traffic purpose, and one hostname can serve only one\npurpose at a time. In **Domain Details**, the **Used for** chip shows the active purpose.\n\nAn Ingest domain can be used as a first-party endpoint for sending analytics data. It keeps the ingest hostname under your domain and may reduce blocking by tools that specifically target known third-party analytics hosts.\n\nA Page domain serves every public page owned by the team. Each Page's permanent generated slug works on the Tailglow hostname and every custom hostname. A Page can also be given a readable path on one specific domain, such as `status.yourcompany.com/weekly-health`, and another Page domain can choose a different path for the same Page.\n\nSet that path to `/` to serve the Page at the hostname's root, so `status.yourcompany.com` opens it directly. Only one Page can hold a given path on a domain, including the root. Paths are a single segment: `/weekly-health` is valid, `/team/weekly-health` is not.\n\nPages served from your own hostname remove Tailglow branding, show your team logo and name at the top of the page, and use the logo as the browser tab icon (favicon). You can update both in your team settings.\n\nTo remove the active purpose without deleting the hostname, open **Domain Details** and click **Stop\nusing for Ingest** or **Stop using for Pages**. Tailglow explains what the change affects and asks\nyou to hold the confirm button, because the teardown cannot be cancelled once it starts. The change\ncan take time to finish.\n\n## Troubleshooting\n\n### DNS Verification Failed\n\nIf verification fails:\n\n- **Check DNS propagation**: Use a tool like [whatsmydns.net](https://www.whatsmydns.net) to verify your records have propagated.\n- **Verify record values**: Ensure the CNAME target exactly matches what Tailglow shows.\n- **Check for conflicting records**: Remove any conflicting A or AAAA records if using CNAME.\n- **Wait and retry**: Propagation time depends on your DNS provider and the record's TTL.\n\n### Certificate Provisioning Stuck\n\nIf certificate status shows \"provisioning\" for more than 24 hours:\n\n- **Verify DNS is correct**: Certificate provisioning requires DNS to be properly configured.\n- **Check for CAA records**: If you have CAA records, ensure they allow certificate issuance.\n- **Contact support**: If issues persist, reach out to support@tailglow.io.\n\n### Domain Shows \"Failed\"\n\nIf certificate provisioning failed:\n\n- Check the error message in your domain settings.\n- Verify DNS configuration is still correct.\n- Try removing and re-adding the domain.\n\n## Removing a Custom Domain\n\nTo remove a custom domain:\n\n1. Navigate to **Team Settings** > **Domains**.\n2. Click the edit button for the domain you want to remove.\n3. Hold the trash button labeled **Hold to remove domain** until the action confirms.\n\nRemoval can take time, and the row remains visible with a pending state until it finishes. Public Pages stop using the hostname as soon as removal begins, while other services may take longer. Do not rely on the hostname after starting removal. Adding it again later requires new ownership verification.\n\n## Best Practices\n\n- **Use dedicated subdomains**: Hostnames such as `ingest.yourdomain.com` and `status.yourdomain.com` make each traffic purpose explicit. Page domains require a subdomain.\n- **Keep DNS records**: Don't remove DNS records after verification. They are re-checked hourly, and removing one eventually takes the domain out of service. See [Ongoing Checks](#ongoing-checks).\n- **Monitor certificate status**: Check your domain status periodically to ensure certificates remain active.\n- **Plan for propagation**: When adding new domains, allow time for DNS propagation before relying on them in production.\n",
|
|
55
55
|
"guides/api-keys": "# API Keys\n\n## Overview\n\nAPI keys allow you to authenticate with the Tailglow API programmatically, without a user session. They are useful for backend integrations, CI/CD pipelines, and scripts that need to interact with Tailglow on behalf of your team.\n\nAPI keys are separate from **ingest keys**. Ingest keys are used to send data to your ingest servers, while API keys are used to interact with the Tailglow management API (e.g., creating projects, managing views, querying metrics).\n\n## Creating an API Key\n\nTo create a new API key:\n\n1. Navigate to your **Team Settings**.\n2. Select the **API Keys** tab.\n3. Click the plus button labeled **Create an API key**.\n4. Configure the following fields:\n - **Name**: A descriptive name for the key (2-60 characters).\n - **Quick fill**: Optionally start with **Common** or **Full access**, or click **Clear** to remove all selected scopes.\n - **Scopes**: Choose which permissions the key should have. You can only assign permissions that you yourself have.\n5. Click **Create API Key**.\n\nThe API key will be displayed once after creation. Copy and store it securely -- you will not be able to see it again.\n\n## Using an API Key\n\nInclude the API key in the `Authorization` header of your requests:\n\n```bash\ncurl -X GET \"https://api.tailglow.io/v1/projects\" \\\n -H \"Authorization: Bearer tg_api_your_api_key\"\n```\n\nYou can also create a key from the terminal with `tglow login --method api-key`: approve it in the browser, choose its permissions, and the CLI stores it. See [CLI](/guides/cli#authenticating).\n\n## Key Properties\n\n- **last4**: The last 4 characters of the key, for identification.\n- **scopes**: The permissions assigned to the key.\n- **last_used_at**: When the key last authenticated a request, accurate to five minutes.\n\n## Managing API Keys\n\nFrom the **API Keys** tab in your team settings, you can:\n\n- **View all keys**: See each key's name, last 4 characters, and when it was last used. A key created before Tailglow began recording use shows **No recorded use** until its next request.\n- **Update a key**: Change the name or scopes of an existing key.\n- **Delete a key**: Permanently revoke a key. This takes effect immediately.\n\n## Security\n\n- API keys inherit the scopes you assign to them. Follow the principle of least privilege -- only grant the permissions the key needs.\n- Quick-fill choices only preselect scopes in the creation form. Review them before saving and remove any\n permission the integration does not need.\n- The available quick-fill choices are **Common** and **Full access**. **Clear** removes all scopes.\n- Unlike human roles, API keys do not require `projects:read`. A key may be limited to one narrow\n operation.\n- Rotate keys periodically by creating a new key, updating your integrations, and deleting the old one.\n- Never commit API keys to version control. Use environment variables or a secrets manager.\n",
|
|
@@ -57,7 +57,7 @@ export const DOCS_PAGES = {
|
|
|
57
57
|
"guides/changelog": "# Changelog\n\nNo changelog entries yet.\n",
|
|
58
58
|
"guides/chat": "# Chat\n\n## Overview\n\nChat is an AI assistant built into Tailglow. Ask questions about your metrics, views, collections, and records in natural language, across any project on your team. Chat uses your account's permissions for supported Tailglow actions, so it can only see and change what you can.\n\n## Getting started\n\n**Tailglow** uses AI managed by Tailglow. When several models are available, use the model picker to choose one. Click the model and effort label to change your selection. When only one model is available, this opens its effort choices directly. Free tokens are shared across your team and its projects. They apply first, then usage is billed to the team if it has a payment method or is on the Enterprise plan.\n\nTo use your own provider account, add an Anthropic or OpenAI key in **Team Settings > Agents**, then select that provider's model in Chat. Adding or deleting a key does not switch your conversation's payer: it keeps the model of its latest answer. If that model becomes unavailable, choose another model explicitly. Before your first message, Chat starts on your saved model when it's available; otherwise it starts on Tailglow or another model you can use.\n\nThe **Tailglow** name stays the same when the underlying model changes. To see your free-token balance and monthly allowance, choose **Usage and allowance** in the chat settings menu, or type `/usage`. When fewer than 1 million free tokens remain, the balance also appears beside the model picker; click it to open the same view. **Pricing and details** there, or the information drawer in Agents settings, shows the current provider, model, and supported token rates. The displayed rates include a 15% service fee; no additional service fee is added afterward.\n\nEvery new team starts with 10 million Tailglow AI tokens. When your team's latest monthly invoice is paid, **Enterprise teams receive a reset to 100 million tokens**, including teams that pay by invoice without a saved payment method. **Pro teams with a payment method receive a reset to 10 million tokens**. This is included, not a prepaid purchase or an additional charge. Unused AI credits, including higher balances, are replaced by the monthly allowance. Usage since the end of the invoiced month counts against it. Pro teams without a payment method keep their remaining tokens but receive no monthly reset.\n\nChat history remains accessible when sending needs action. Without a model, or without enough credits on a Pro team with no payment method, the composer explains what to change. Your draft is kept. Provider-key models do not spend Tailglow credits. Teams with a payment method, and Enterprise teams, can continue after their free balance reaches zero; that usage is billed.\n\n## Using Chat\n\nOpen Chat with **Command + /** or the chat icon. It is available on every page: inside a project, and on team pages such as billing, users, and roles.\n\n1. Open Chat.\n2. Type a question and press Enter.\n\nChat reads the page you are on, so a question about \"this view\" or \"this metric\" means the one in front of you. If you move to a different project mid-conversation, Chat notices and keeps going. When that makes a request ambiguous, it asks rather than guessing.\n\nResponses stream as they are generated. You can attach up to five images to a message: JPEG, PNG, or WebP, each up to 5 MB. If you close the panel or the tab, the answer keeps going and is waiting when you come back.\n\nChat is one continuous conversation per team. Choose the model and effort with the picker under the composer. While an answer is running, the composer shows **Esc to stop**: press **Esc** (or **Command + Backspace**) to stop the answer. On a touch device, tap **Stop**.\n\nThe settings button under the composer opens the chat menu: **Verbose reasoning**, **Usage and allowance**, **Memories**, **Watching**, and **New topic**. Type `/` in the composer for the same commands: `/new` starts a new topic, `/watching` opens the Watching view, `/memories` opens the assistant's notes, `/usage` shows your token balance, and `/reasoning` opens the settings menu.\n\nWhile an answer runs, one line shows what the assistant is working on, and its reasoning and tool calls stay hidden. Turn on **Verbose reasoning** to show them in the conversation.\n\n### Example questions\n\n- \"What are the top 5 endpoints by error rate this week?\"\n- \"Show me the trend of page views over the last 30 days.\"\n- \"Why did latency spike yesterday at 3pm?\"\n- \"Create the same metric in my sandbox project.\"\n- \"Which roles can delete metrics?\"\n- \"Tell me when the signups metric finishes backfilling, then add it to the Growth page.\"\n\n## Watches\n\nSome work takes a while: a metric backfilling its history, a view catching up, a pull or a check running. Ask Chat to tell you when it finishes, or to do something next (\"build the page once both metrics are ready\"), and it sets a **watch**. It says what it is waiting for and what it will do. You can keep talking about other things in the meantime.\n\nA watch keeps checking in Tailglow even when your browser is closed. Changes are usually noticed within seconds, and every watch is also checked at least once a minute. When everything it watches is ready, or something fails, the assistant picks the conversation back up and carries out the plan it stated, using the model that was selected when the watch was set.\n\n- A watch looks at up to 5 things, and a conversation can have up to 5 running at once.\n- A watch stops after 2 hours unless you ask for a different limit, from 15 minutes up to 24 hours. When time runs out, the assistant reports what is unfinished and can set a new watch, for up to 24 hours from the first one.\n- If a watch cannot read what it is watching three times in a row, it stops and the assistant explains what went wrong.\n- A watch acts with your permissions. If you leave the team or sign out of all devices, your running watches stop.\n\nThe eye button under the composer shows how many watches are running, or how many finished today when none is. Click it, choose **Watching** in the settings menu, or type `/watching` to see each one's progress, what the assistant plans to do next, and its latest check. Choose **Stop watching** to end one.\n\n## Notifications\n\nWhen an answer finishes while the chat panel is closed, a notice appears and the chat icon shows a dot. If the tab is in the background, its title shows how many answers are waiting. When you open the panel, a **New** line marks where the unread answers begin.\n\n## Topics, memory, and search\n\nUse `/new` to start a new topic when you change subjects. The assistant starts fresh, so earlier details no longer crowd its context. Everything above the divider stays in the conversation, and the assistant can still find it with search.\n\nThe assistant keeps short notes about things worth remembering, such as your default project or how you like results presented. It saves them on its own and tells you in its answer when it does. It treats them as hints and checks them against your current data. Ask it to remember or forget something at any time. To see every note, add one, edit one, or delete one, choose **Memories** in the settings menu or type `/memories`.\n\nWhen you refer to something from earlier, the assistant searches the conversation by keywords and by meaning, then reads the relevant part. Past results of its tool calls are not kept in full, so it reads current data again instead of relying on an old copy.\n\n## Context and billing\n\nFor long conversations, the assistant also summarizes older work near a 250,000-token context target. With your own provider key, it summarizes at the lower of that target and 60% of the model's context window, and the summary runs on your key. Recent exchanges, complete tool calls and results, and your current request remain available alongside that summary. The full transcript stays in your conversation.\n\nCompaction runs between model calls. If a summary fails, the assistant retains the original context and uses additional headroom toward a 400,000-token operating limit, subject to the provider's capacity. It tries to reduce older context before asking you to shorten an unusually large request. Automatic compaction on Tailglow is covered by Tailglow and does not spend your free tokens.\n\nToken billing uses the provider's reported usage for every generation, including tool continuations and the work the assistant does when a watch finishes. Input, cached input, cache writes, and output are separate categories; reasoning is already included in output. An estimate used to fit context is never used to calculate your bill. Stopping a response can leave a provider generation finishing in the background; its reported usage still counts. If usage cannot be recovered, the unknown remainder is covered by Tailglow, while the answer already received remains available.\n\nYour conversation, notes, and watches belong to your user account within a team. Other team members cannot see them.\n\n## Permissions\n\nChat access is controlled by the **Chats** permission in your role. Read access opens the conversation, its watches, and its notes; write access sends messages and adds or edits notes; and delete access stops watches and removes notes.\n",
|
|
59
59
|
"guides/checks": "# Checks\n\nA check fetches an endpoint on a schedule and records whether it answered. It is the fastest thing in Tailglow to set up, and the only one that needs nothing else to exist first: give it a URL and you have real data within a minute, without instrumenting anything or sending us a single event.\n\nUse a check when you want to know that something is up. Use a [pull](/guides/pulls) when you want what the endpoint returns.\n\n## Set up a check\n\nPick a source for the results to land in, name the check, and give it the HTTPS URL to watch. Everything else has a working default.\n\n**Schedule** decides how often it runs, down to once a minute. **Collection** decides where the results land inside the source, and you name it yourself, because which collection a check writes to is the thing that decides what it charts alongside.\n\nPoint several checks at the same collection and they chart together: one strip per check, with an overall row above them taking the worst result of any check in each bucket. That is a status page. Give each check its own collection to keep them apart.\n\n**Test check** on the Activity tab fetches the endpoint once, right now, and tells you what came back. It stores nothing, so use it freely while you get the configuration right.\n\n## What gets recorded\n\nEvery run writes exactly one row, whether it succeeded or not:\n\n| Field | Meaning |\n| ------------------ | -------------------------------------------------------------------- |\n| `check_id` | Which check ran |\n| `checked_at` | The scheduled time of the run, not the moment the row was written |\n| `up` | `1` when the endpoint answered with a 2xx, `0` when it did not |\n| `http_status` | The status it answered with, or `0` when nothing answered |\n| `response_time_ms` | How long the request took |\n| `error_stage` | `request` when nothing answered, `response` when it answered wrongly |\n| `error_message` | What went wrong |\n| `attempts` | `2` when a failure was confirmed by a second check |\n\nBecause `up` is a number, the average of `up` across a period is the uptime for that period, as a fraction between 0 and 1. A view that multiplies it by 100 first reads as a percentage instead, which is the form thresholds are written in: the Colors tab's uptime preset expects `99` and `99.9`, not `0.99` and `0.999`. Views built by setup carry that field already, as `uptime_percent`.\n\nA run that was never dispatched writes no row at all. Tailglow did not ask the endpoint anything, so it makes no claim about whether it was up, and the gap renders as no data rather than as downtime.\n\n## Confirming failures\n\n**Confirm failures after** waits the given number of seconds and checks once more before recording a failure. A single refused connection is a flake as often as an outage, and without this one blip becomes a red cell on your chart. When the second attempt succeeds, the run is recorded as up.\n\n## Charting uptime\n\n1. Create a view over the checks collection.\n2. Create a metric on that view: display value **Average**, over the `up` field. Group by `check_id` if several checks share the collection.\n3. Set the chart type to **Uptime**, colour mode **By value**, and use the uptime preset in the Colors tab. Rules are read worst-first, so red comes before amber before green.\n4. Add a monitor that opens **After holding for a duration** if you want an alert. The chart shows every bucket; the monitor decides what counts as an outage worth waking someone for.\n\n## A check never stops itself\n\nThis is the difference between a check and a pull. A pull that has been failing for 72 hours is switched off, because a collector with nothing to collect is only wasting requests. A check keeps going for as long as the outage lasts, because documenting that outage is the entire job. Your chart stays honest through the worst week you have.\n\nA check stops only when you pause it. It runs on your project's server, so while the project has no running server it shows **Not running** with the time it last ran.\n\n## Limits\n\nEach request times out after 20 seconds, and a confirmation attempt after 10. A POST request body is capped at 4 KB.\n\n## Security\n\nEndpoints must be public HTTPS addresses. Tailglow re-validates the address on every single run, not just when you save the check, and rejects private, loopback and link-local targets. That re-validation is what stops a hostname that resolved publicly at save time from being repointed at an internal address later.\n\nHeaders are stored encrypted and never returned. Responses list only the header names you configured.\n",
|
|
60
|
-
"guides/cli": "# CLI\n\n`tglow` is the Tailglow command line. Every endpoint in the [API reference](/api) is a command, because both are generated from the same source: the API's own routes and validations. A new endpoint becomes a new command with
|
|
60
|
+
"guides/cli": "# CLI\n\n`tglow` is the Tailglow command line. Every endpoint in the [API reference](/api) is a command, because both are generated from the same source: the API's own routes and validations. A new endpoint becomes a new command in the next CLI release with nothing written by hand, and the CLI cannot describe an API that does not exist.\n\nIt is built for two readers at once. In a terminal it prints tables and asks before deleting. Piped, redirected, or run by an agent, it emits JSON and never prompts.\n\n## Installation\n\n```bash\nnpm install -g @tailglow/cli\n```\n\nNode 22 or newer. `bun add -g @tailglow/cli` and `pnpm add -g @tailglow/cli` work the same way.\n\nVerify it:\n\n```bash\ntglow --version\n```\n\nPrefer a shorter name? Alias it rather than installing over one, since `tg` belongs to other tools on many systems:\n\n```bash\nalias tg=tglow\n```\n\n### Updating\n\n```bash\nnpm install -g @tailglow/cli@latest\n```\n\nOnce a day, `tglow` asks npm's public registry for the latest version. When yours is older, it prints one line to stderr after the command's output, so JSON on stdout is unaffected. It skips the check when the `CI` or `TAILGLOW_NO_UPDATE_CHECK` environment variable is set, and stays quiet when the registry cannot be reached or a command fails with `--json` output.\n\n## Authenticating\n\nIn a terminal, running `tglow` with no command offers sign-in when no credential is configured for the selected API. Choose your account, an API key, or Cancel. `tglow help` and `tglow --help` always show help. When a credential is configured, or without an interactive terminal, `tglow` shows help.\n\nYou can also start sign-in explicitly:\n\n```bash\ntglow login\n```\n\nIt asks how you want to sign in, opens the approval page in your browser, and counts down to the code's expiry, ten minutes after creation. Check that the page shows the code your terminal printed, then approve.\n\nAt expiry, the CLI makes one final poll to collect an approval given just before the deadline. If the code expired, the interactive wait ends cleanly; run `tglow login` to try again. If the server still reports pending, it keeps the sign-in and shows `tglow login --wait` to resume. These normal interactive endings exit `0`. A poll in progress can finish after expiry so an approved credential is not lost.\n\nAn approved request can be collected for up to fifteen minutes after approval. `tglow login --wait` waits up to one minute at a time and exits `75` while still pending; it can be run again.\n\n- **Sign in with your Tailglow account** gives the CLI a session that acts as you, with what your role allows in each of your teams. It starts in the team you choose; `--team` or `tglow config set team` switches it. It is listed as **CLI** under **Profile > Security**, ends when you sign out of every device, and ends on its own after six months unused.\n- **Sign in with an API key** creates a key for one team, named after this machine, with the permissions you choose. The team needs a project first.\n\n`--method account` or `--method api-key` skips the question.\n\nWithout a terminal, for example when an agent runs it, `tglow login` prints the link and the code and exits. Approve the sign-in, then run `tglow login --wait` to finish. It exits `0` once signed in, `75` while approval is still pending (run it again), and `1` if the sign-in was denied, expired or withdrawn, or the API could not be reached (the sign-in is kept, so run it again once the connection is back).\n\n```bash\ntglow whoami # who the CLI acts as, in which team, with which credential\ntglow logout # end the session, or delete the key, that tglow login created\n```\n\nThe credential is stored in `~/.tailglow/config.json`, readable only by you, and is only ever sent to the API that issued it.\n\n### API keys\n\nAn API key you already have works too. Create one in the dashboard under **Team settings**, then store it once:\n\n```bash\ntglow config set api_key tg_api_...\n```\n\nA stored key is never printed back. Credentials are tried in this order:\n\n| Source | Use it for |\n| -------------------------- | -------------------------------------------- |\n| `--api-key <key>` | A single call. Visible in your shell history |\n| `TAILGLOW_API_KEY` | CI, containers, and anything scripted |\n| `tglow login` | Your own machine |\n| `tglow config set api_key` | A key you created in the dashboard |\n\nIn CI, prefer the environment variable. A key passed as a flag appears in the process list.\n\n## Getting around\n\nThe shape is always the same:\n\n```bash\ntglow <resource> <command> [flags]\n```\n\nThree levels of help, and between them they are the whole manual:\n\n```bash\ntglow help # every resource, and the flags that apply everywhere\ntglow drains # every command on drains\ntglow drains create --help # every flag, its type, and its accepted values\n```\n\nCommand help is generated from the same validations the API enforces, so a required field is required here for the same reason it is required there.\n\n## Reading the docs\n\nEvery guide and API reference page ships inside the CLI as markdown:\n\n```bash\ntglow docs # the index of every page\ntglow docs guides/sdk # one page\ntglow docs /api/drains.md # any link from the index works as it is\n```\n\nThe pages match the version of the CLI you have installed, so they describe the commands it can run, and they work without a network connection. Each page ends with a link to its latest version online.\n\nUnlike other commands, `tglow docs` prints markdown even when its output is piped, because the reader is usually an agent. Add `--json` to get `{ path, url, content }` for a page, or `{ index, pages, online }` for the index.\n\n## Working with a project\n\nMost commands act on a project. Set it once instead of repeating it:\n\n```bash\ntglow config set project prj_...\ntglow metrics list\n```\n\n`--project` overrides the stored default for a single call. Every other id has to be named explicitly: only `project` and `team` fall back to configuration, so nothing in your environment can quietly become the target of a delete.\n\n## Flags\n\nField names are snake_case in the API and kebab-case on the command line: `repeat_window_minutes` becomes `--repeat-window-minutes`. Path parameters are flags too, never positional.\n\n```bash\ntglow sources create --project prj_x --name \"Web events\" # a string\ntglow pages create --project prj_x --name Status --is-public # true\ntglow pages create --project prj_x --name Status --is-public=false\ntglow metrics create --project prj_x --group-by user_id,country # a list\ntglow views create-join --project prj_x --joins '[{\"lookup_view_id\":\"view_b\",\"alias\":\"b\",\"base_field\":\"user_id\",\"lookup_field\":\"id\"}]'\n```\n\nA boolean is passed alone for true, or written attached for false. Anything typed `object`, `object[]`, or `Scopes` takes JSON as a single argument, and the command's `--help` lists the keys it takes.\n\n## Output\n\nA terminal gets a table. Everything else gets JSON, so `| jq` needs no flag:\n\n```bash\ntglow projects list | jq -r '.data[].name'\n```\n\nJSON is the API's response as it was sent, envelope included: read `.data` for results and `.pagination.next_cursor` to continue. `--json` forces it when you want JSON in a terminal.\n\nFailures go to stderr and follow the same mode, so a pipeline reading stdout never has to tell a result apart from an explanation of why there is none.\n\n## Long lists\n\nList commands paginate. `--all` follows the cursor until the results run out or `--max` is reached:\n\n```bash\ntglow metrics list --project prj_x --all --max 500\n```\n\nThe cap defaults to 10,000 and exists because records and collection documents are unbounded. A run that stops early prints the exact command to continue with. Under `--json`, `--all` returns `{ data, truncated, next_cursor }` instead, so a caller can resume on its own with `--after`.\n\n`--all` owns paging, so it does not combine with `--limit` or `--before`. Use `--limit` on its own to size a single page.\n\n## Deleting\n\nIn a terminal, a delete asks first. `--yes` skips the question.\n\n**Without a terminal, a delete runs immediately and is never confirmed.** That is deliberate, so scripts and agents are not blocked on a question they cannot answer, but it means a delete in CI happens the moment it is called.\n\nMost resources delete softly: the response tells you when permanent removal happens.\n\n## Scripting it\n\nThe CLI is designed to be driven by other programs, including AI agents:\n\n```bash\nexport TAILGLOW_API_KEY=tg_api_...\ntglow metrics create --help --json # the full contract for one command, as data\n```\n\n`--help --json` returns the command's flags, types, requiredness, accepted values, and the syntax for each one. An agent can read that and construct a valid call without any other documentation.\n",
|
|
61
61
|
"guides/drains": "# Webhook drains\n\nA drain sends collection or view records to an external HTTPS endpoint. You can choose the body\nformat, compression, headers, and whether to include existing records. A project can have up to 10\ndrains.\n\n## Set up a drain\n\n1. Open project **Settings**, then **Drains**.\n2. Click the plus button. The drawer is titled **New drain**.\n3. Select a collection or view, then provide a name, destination URL, optional headers, body format, compression, delivery schedule, and whether to backfill historical data.\n4. Create the drain.\n5. In **Details**, click **Send verification**. Find the `tailglow_verify` value at your endpoint and paste it back.\n6. The verified drain is paused. Use **Send sample** on its Activity tab if useful, then click **Resume** to begin delivery.\n\nA sample sends the latest five eligible records with `X-Tailglow-Test: true`. It does not advance the drain cursor.\n\n## Schedule\n\nChoose how often the drain looks for new records: every minute, every 5 or 15 minutes, hourly, daily, or a custom cron expression. Schedules are evaluated in UTC, and new drains default to hourly. You can change the schedule at any time.\n\nThe schedule sets how quickly new records are picked up, not how fast a backlog clears. A drain that has fallen behind keeps sending until it catches up instead of waiting for its next run, so a large backfill is never limited by the schedule you choose.\n\nA run that finds nothing to send makes no request, so a fast schedule adds no traffic to a quiet endpoint. It does change the shape of steady traffic: a drain receiving around 10 records a minute sends roughly 1,440 small requests a day on a per-minute schedule, against 24 larger ones on an hourly schedule. Choose whichever suits your destination.\n\n## Saved batches and retries\n\nRequests with an `X-Tailglow-Batch-Id` beginning with `batch_` use saved delivery as described below. For other IDs, deduplicate records individually: retries can contain a changed set of records under the same ID.\n\nEach delivery contains at most 5,000 records and at most 5 MiB of uncompressed serialized data, except that a single oversize record is sent on its own. Tailglow saves the request before sending it. Ordinary retries use the same saved bytes and `X-Tailglow-Batch-Id`, even if new source records arrive or the source files expire.\n\nSaved delivery tries older batches first. If a batch fails, it backs off while later eligible batches can continue. When available in Details, turn on **Wait for each batch** to require a successful 2xx acknowledgement before the next batch is sent. The setting defaults to off. It controls when Tailglow sends requests; it cannot control the destination's internal processing order. Pause the drain and wait for its current request to finish before changing this setting.\n\nThe queue holds up to 32 batches beyond the confirmed cursor, including batches already acknowledged after a gap. When it is full, Tailglow stops reading more source records until the gap clears. The confirmed cursor advances only through a continuous sequence of successful deliveries.\n\nTailglow honors `Retry-After` with a delay between 30 seconds and 30 minutes. Repeated endpoint failures also slow delivery to that endpoint. A failed batch stays saved across pause, disable, and resume. Later successful batches do not hide an older failure: Tailglow warns team owners after six hours and disables the drain after 72 hours of unresolved failure. Explicitly resuming restarts this failure timer and makes pending batches eligible again.\n\n### Receiver deduplication for saved batches\n\nFor saved-batch delivery, treat `X-Tailglow-Batch-Id` as the identity of one fixed, ordered set of records. A timeout means Tailglow did not receive a complete response; your endpoint might already have accepted the request.\n\nTo avoid duplicate effects, atomically save the batch ID with your accepted records and return success for an ID already accepted. Retain that identity for as long as you allow drain retries or resumes. Do not return success for a duplicate that is still being processed unless its records are already durably accepted. The header alone does not prevent duplicates at a receiver that ignores or forgets it.\n\n### Changing fields or encoding\n\nAdding an exclusion removes that field from pending batches before their next request. Restoring an excluded field affects newly produced batches only, because the saved body no longer contains that field. Format and compression changes can also repair a pending body. An already processing request may finish with its previous settings.\n\nRepairs keep the same records, order, count, window headers, and batch ID. They never fill the freed space with additional records. At a receiver that deduplicates by ID, the first accepted representation wins; changing an exclusion does not edit records already accepted there. If your receiver rejects changed content under the same ID, that error remains visible and the batch stays unconfirmed.\n\nIf the source cursor expires, saved batches can still finish. Reading new source records stops because Tailglow cannot prove where to continue. Restore the missing source or create a new drain with the backfill choice you need. Resume preserves the cursor and does not silently replay history.\n\nDeleting a drain stops new requests and asynchronously removes its saved batches. A request already received by the destination cannot be recalled.\n\nA drain runs on your project's server, so while the project has no running server it shows **Not running** with the time it last ran.\n\n## Backfill\n\nWith **Backfill historical data** off, the drain starts from records arriving after its first activation. With it on, the drain catches up existing records before continuing with new ones. This choice cannot be changed after creation.\n\n## Draining to Tailglow\n\nPoint a drain at the destination project's copied ingest endpoint and provide a destination ingest key. A trusted destination in the same project can skip marker verification, but two projects owned by the same team do not automatically verify each other. For a non-trusted destination, find the verification marker in the destination collection's **Records** tab and confirm it in the drain drawer.\n\n## Security\n\nDestinations must be public HTTPS addresses. Tailglow revalidates outbound addresses, rejects private and loopback targets, and times out each request after 30 seconds.\n",
|
|
62
62
|
"guides/notifications": "# Notifications\n\nTailglow emails subscribed users when monitor alerts open. For spanning alerts, it also sends a notification when the alert recovers.\n\n## Manage subscriptions\n\nOpen **Profile** then **Notifications**. The **Auto-subscribe to new monitors** switch subscribes you to monitors created in teams where you are a member. Use the activity button to open **Update Monitor Subscriptions** and choose individual monitors across your teams.\n\nThe switch next to a monitor in its metric's monitor drawer changes whether the monitor is active for everyone. Manage your own email subscriptions from **Profile** > **Notifications**.\n\n## Alert behavior\n\nWhen a monitor condition becomes true, Tailglow opens an alert and emails subscribed users. A spanning alert stays open until the condition clears and then sends a recovery notification. An instant alert opens and ends in the same evaluation. An alert that ends because the monitor's rule changed, the monitor was paused or expired, or its data was deleted ends without an email.\n",
|
|
63
63
|
"guides/permissions": "# Permissions\n\nRoles control what team members can see and change. Manage them from **Team Settings** > **Roles**.\n\n## Access levels\n\nA permission can include one or more access levels:\n\n- **Read**: View the feature and its data.\n- **Write**: Create or update items.\n- **Delete**: Remove items.\n\nThese levels are independent. For example, someone can be allowed to view a project without being\nallowed to change or delete it.\n\n## Permission areas\n\nThe role editor shows the complete, current list of permission areas. Broadly, they cover:\n\n- **Team management**: Users, roles, API keys, billing, and security logs.\n- **Data setup**: Projects, sources, ingest keys, and servers.\n- **Data and monitoring**: Records, views, facets, drains, metrics, monitors, and alerts.\n- **Product features**: Pages, custom domains, Chat, and secrets.\n\nMember roles always retain read access to projects so that project navigation works.\n\n## Create or update a role\n\n1. Open **Team Settings** and select **Roles**.\n2. Create a role or open any existing role except **Owner**.\n3. Select the access levels the role needs.\n4. Save the role.\n\nUse **Quick fill** to start with **Common** or **Full access**, then adjust the selection before\nsaving. **Clear** removes optional access while retaining the project read access required for team\nmembers.\n\n## Missing access\n\nTailglow keeps product sections visible even when your role cannot open their data. Selecting a\nrestricted section shows which permission is missing. Ask a team administrator to grant it.\nActions you cannot perform are hidden or disabled.\n\n## API keys\n\nAPI keys use the same permission areas but are not human roles. They do not require project read\naccess, and **Clear** removes all of their permissions. Give each key only the access its integration\nneeds. See [API Keys](/guides/api-keys) for setup and security guidance.\n",
|
|
@@ -69,5 +69,5 @@ export const DOCS_PAGES = {
|
|
|
69
69
|
"guides/tgl/functions": "# TGL: Functions\n\nBuilt-in functions are the only callable functions in TGL. They do not mutate your input or output. Most return `null` for a wrong type or a missing value, while predicates such as `isEmpty` and `isValidDate` return a boolean and `typeOf` always returns a type name. `now()` is intentionally time-dependent, so it returns a different value as time advances.\n\nThere are five categories: math, string, array, type, and date.\n\n## Math\n\n| Function | Returns |\n| --------------------- | ----------------------------------------- |\n| `add(a, b, ...)` | sum of all arguments (variadic) |\n| `subtract(a, b)` | a minus b |\n| `multiply(a, b, ...)` | product of all arguments (variadic) |\n| `divide(a, b)` | a divided by b (returns `null` if b is 0) |\n| `mod(a, b)` | a modulo b (returns `null` if b is 0) |\n| `abs(n)` | absolute value |\n| `round(n, decimals?)` | rounds to N decimals (default 0) |\n| `floor(n)` | rounds down |\n| `ceil(n)` | rounds up |\n| `min(a, b, ...)` | smallest argument (variadic) |\n| `max(a, b, ...)` | largest argument (variadic) |\n\n```tgl\n$out.tax = round(multiply($row.subtotal, 0.0825), 2)\n$out.bucket = floor(divide($row.age, 10))\n$out.peak = max($row.q1, $row.q2, $row.q3, $row.q4)\n```\n\n### Null handling in math\n\n`add` and `subtract` treat `null` as `0` so accumulator patterns work:\n\n```tgl\n$out.sum = 0\n#each $row.values as $v\n $out.sum = add($out.sum, $v) // null v contributes 0, doesn't break the chain\n/each\n```\n\n`multiply`, `divide`, `mod` short-circuit on `null`. If any argument is `null`, the result is `null`. This catches \"missing field\" cases loudly instead of silently making everything zero.\n\n## String\n\n| Function | Returns |\n| ------------------------------------ | ------------------------------------------ |\n| `lowerCase(s)` | lowercased string |\n| `upperCase(s)` | uppercased string |\n| `trim(s)` | whitespace stripped from both ends |\n| `trimStart(s)` | leading whitespace stripped |\n| `trimEnd(s)` | trailing whitespace stripped |\n| `concat(...)` | joins arguments into one string (variadic) |\n| `length(s_or_array)` | character or element count |\n| `contains(s, search)` | true if `s` contains `search` |\n| `startsWith(s, prefix)` | true if `s` starts with `prefix` |\n| `endsWith(s, suffix)` | true if `s` ends with `suffix` |\n| `substring(s, start, end?)` | slice (negative indices count from end) |\n| `split(s, delimiter, limit?)` | array of pieces |\n| `replace(s, search, replacement)` | replaces first occurrence |\n| `replaceAll(s, search, replacement)` | replaces every occurrence |\n\n```tgl\n$out.email = lowerCase(trim($row.email))\n$out.full_name = concat($row.first, \" \", $row.last)\n$out.domain = split($row.email, \"@\")[1]\n$out.is_gmail = endsWith($row.email, \"@gmail.com\")\n```\n\n`concat` is variadic and converts numbers/booleans to their string form: `concat(\"Hello, \", $row.name, \"! You have \", $row.count, \" messages\")` works directly. If any argument is `null`, the whole result is `null`. Handle missing values with `??` first.\n\n`length` is grapheme-aware on strings: `length(\"👋\")` is `1`, not `2`. It also works on arrays.\n\n## Array\n\n| Function | Returns |\n| ------------------------ | ---------------------------------------- |\n| `includes(array, value)` | true if array contains value |\n| `push(array, value)` | new array with value appended |\n| `prepend(array, value)` | new array with value prepended |\n| `merge(arr1, arr2, ...)` | concatenates arrays |\n| `flat(array, depth?)` | flattens nested arrays (default depth 1) |\n| `unique(array)` | removes duplicate primitives |\n| `join(array, separator)` | joins into a string |\n\nArrays in TGL are immutable from the script's perspective. `push` doesn't mutate the source, it returns a new array. That means the accumulator pattern looks like:\n\n```tgl\n$out.tags = null\n#each $row.raw_tags as $t\n $out.tags = push($out.tags, lowerCase($t))\n/each\n```\n\n`push(null, x)` returns `[x]`, so the loop builds up an array starting from null without needing a separate initialization.\n\nThere are no array literals (`[1, 2, 3]`). Build arrays via `push`/`prepend` or read them off `$row`.\n\n## Type\n\n| Function | Returns |\n| ---------------- | -------------------------------------------------------------------------- |\n| `int(value)` | parsed integer (truncates), `null` if not parseable |\n| `float(value)` | parsed float, `null` if not parseable |\n| `string(value)` | string form of a primitive, `null` for objects/arrays |\n| `bool(value)` | true / false / null (only for explicit boolean-like values) |\n| `typeOf(value)` | \"string\" / \"number\" / \"boolean\" / \"null\" / \"array\" / \"object\" |\n| `isEmpty(value)` | true if value is `null`, `undefined`, `\"\"`, whitespace-only, `[]`, or `{}` |\n\nThe most common use is **numeric coercion** for CSV-ingested data, where everything arrives as a string:\n\n```tgl\n$out.total = add(int($row[\"Quantity\"]), int($row[\"Bonus\"]))\n$out.price = float($row[\"Price\"])\n```\n\n`add(string, number)` returns `null` because `add` is strict about types. `int($row[\"Quantity\"])` parses `\"42\"` into `42`, then `add` works.\n\n`bool` is strict: it accepts `true`/`false`, `1`/`0`, and `\"true\"`/`\"false\"` (case-insensitive). It does NOT treat truthiness loosely: `bool(2)` is `null`, `bool(\"yes\")` is `null`, `bool(\"\")` is `null`. Use `!!$row.value` if you want truthiness; use `bool` when the source is a real boolean encoded in another type.\n\n`isEmpty` is the platform's shared definition of \"missing\"; useful for branching on optional fields. Unlike `??`, which only catches `null` and `undefined`, `isEmpty` also treats empty strings, whitespace-only strings, empty arrays, and empty objects as missing. Real values like `0`, `false`, and `\"0\"` are NOT empty:\n\n```tgl\n// normalize missing-shaped values to null in view output\n#if isEmpty($row.secondary_type)\n $out.secondary_type = null\n#else\n $out.secondary_type = $row.secondary_type\n/if\n```\n\n## Date\n\nDate functions emit **milliseconds since epoch** as plain integers. They accept several input forms,\nincluding ISO strings, Unix seconds or milliseconds, and timestamp objects with seconds and\nnanoseconds.\n\n| Function | Returns |\n| ------------------------------------------------------------ | -------------------------------------- |\n| `now()` | current UTC time in ms |\n| `date(input)` | parses any common date format into ms |\n| `isValidDate(input)` | true if input parses |\n| `addTime(ms, amount, unit)` | shifted timestamp |\n| `startOf(ms, unit)` | start of day/hour/etc. |\n| `endOf(ms, unit)` | end of day/hour/etc. |\n| `year(ms)` / `month(ms)` / `day(ms)` | components (UTC) |\n| `weekday(ms)` / `weekdayName(ms)` | 1=Mon..7=Sun, name string |\n| `hour(ms)` / `minute(ms)` / `second(ms)` / `millisecond(ms)` | components |\n| `formatDate(ms, format)` | formatted string (Luxon tokens) |\n| `toISO(ms)` | ISO 8601 string |\n| `toDateString(ms)` | YYYY-MM-DD |\n| `toTimeString(ms)` | HH:mm:ss |\n| `toMillis(input)` / `toSeconds(input)` | parsed date as ms / whole Unix seconds |\n| `dateDiff(a, b, unit)` | difference in unit (days, hours, etc.) |\n| `isWeekend(ms)` / `isWeekday(ms)` | true/false |\n| `isBefore(a, b)` / `isAfter(a, b)` | comparison |\n\nUnits accepted by `addTime` / `startOf` / `endOf` / `dateDiff`: `year`, `month`, `week`, `day`, `hour`, `minute`, `second`, `millisecond` (with or without a trailing `s`).\n\n```tgl\n// Bucket by week\n$out.week_start = startOf(date($row.created_at), \"week\")\n\n// Days since signup\n$out.tenure_days = dateDiff(now(), date($row.signup_at), \"days\")\n\n// Pretty-print\n$out.label = formatDate(date($row.created_at), \"yyyy-MM-dd HH:mm\")\n```\n\nIf the input doesn't parse, `date()` returns `null`. Date functions that produce a value or formatted string return `null` for invalid input; date predicates such as `isValidDate`, `isWeekend`, and `isBefore` return `false`.\n\nNumeric date input below 10 billion is interpreted as Unix seconds; larger numeric input is interpreted as milliseconds. Both `toMillis` and `toSeconds` apply that parsing rule before converting to their requested unit.\n\n## What's next\n\n- **[Recipes](/guides/tgl/recipes)**: common patterns combining these functions.\n- **[Operators](/guides/tgl/operators)**: comparison, logical, nullish coalescing.\n",
|
|
70
70
|
"guides/tgl/operators": "# TGL: Operators\n\nTGL has comparison, logical, and nullish-coalescing operators. It deliberately doesn't have arithmetic operators. This page also covers TGL's strict-typing semantics, which interact with operators in ways worth knowing about.\n\n## Comparison\n\n```tgl\n$out.is_admin = $row.role == \"admin\"\n$out.is_recent = $row.age < 30\n$out.has_orders = $row.order_count >= 1\n$out.eligible = $row.status != \"banned\"\n```\n\nAvailable: `==`, `!=`, `<`, `>`, `<=`, `>=`. `==` does strict equality (no type coercion, so `\"5\" == 5` is `false`).\n\n## Logical\n\n```tgl\n$out.flagged = $row.suspicious && $row.score < 0.5\n$out.notify = $row.subscribed || $row.opted_in\n$out.is_invalid = !$row.is_valid\n```\n\n`&&` and `||` short-circuit. `!` negates.\n\nTruthiness is standard: `null`, `undefined`, `0`, `\"\"`, `false`, and `NaN` are falsy; everything else is truthy.\n\n## Nullish coalescing: `??`\n\n```tgl\n$out.label = $row.label ?? \"untitled\"\n$out.country = $row.address.country ?? $row.user.country ?? \"US\"\n```\n\n`a ?? b` returns `a` unless `a` is `null` or `undefined`, in which case it returns `b`. Critically, `??` does NOT treat `0`, `\"\"`, or `false` as missing. Only `null`/`undefined` triggers the fallback. Use this when you want a default for \"really missing,\" not for \"falsy.\"\n\n```tgl\n$out.count = $row.count ?? 0 // 0 if count is null\n$out.checked = $row.opted_in ?? false // false if opted_in is null\n```\n\n## What's not here: arithmetic\n\nThere are no `+`, `-`, `*`, `/`, `%` operators. Use the [math functions](/guides/tgl/functions) instead:\n\n```tgl\n$out.total = multiply($row.price, $row.qty)\n$out.with_tax = add($out.total, multiply($out.total, $row.tax_rate))\n$out.average = divide($out.total, $row.count)\n```\n\nThe reason: arithmetic operators in JS-like languages have surprising coercions (`\"5\" + 3` is `\"53\"`, `null + 1` is `1`, `[] + {}` is `\"[object Object]\"`). TGL's function-call form makes the type intent explicit. `add` accepts numbers and treats `null` as `0`; other types return `null`. There is no silent string-numeric mixing.\n\n## Type semantics\n\n### Strict typing\n\nFunctions don't coerce silently. If `add` gets a string, you get `null`:\n\n```tgl\n$out.total = add($row.x, $row.y)\n// row.x = 5, row.y = 3 returns 8\n// row.x = \"5\", row.y = 3 returns null (string isn't a number)\n// row.x = null, row.y = 3 returns 3 (null treated as 0 in add)\n// row.x = 5, row.y = \"abc\" returns null (still null, abc isn't a number)\n```\n\nThis is intentional. Silent type coercion can create subtle data bugs. TGL surfaces a math type mismatch as `null`. If your math involves a numeric string, add an `int(...)` or `float(...)` conversion.\n\n### Null propagation\n\nMost functions return `null` when given `null` (or any wrong type). This propagates: `lowerCase(trim($row.email))` returns `null` cleanly when `email` is missing, instead of throwing.\n\nThe exceptions are documented per-function. Most notably `add` and `subtract`, which treat `null` as `0` to make accumulators ergonomic.\n\n## What's next\n\n- **[Functions](/guides/tgl/functions)**: every built-in by category.\n- **[Recipes](/guides/tgl/recipes)**: real-world patterns.\n",
|
|
71
71
|
"guides/tgl/recipes": "# TGL: Recipes\n\nReal-world patterns. Each recipe is small, copy-pasteable, and combines features from the rest of the guide.\n\n## Filter test users\n\n```tgl\n#if startsWith($row.user_id, \"internal_\")\n $out = null\n/if\n$out = $row\n```\n\n## Drop incomplete records\n\n```tgl\n#if isEmpty($row.email)\n $out = null\n/if\n\n$out.email = lowerCase($row.email)\n$out.name = $row.name\n```\n\n`isEmpty` catches `null`, `\"\"`, whitespace-only strings, `[]`, and `{}` in one shot, so a customer with `\" \"` (a single space) for their email is filtered out the same as one with `null`.\n\n## Empty string to null\n\n```tgl\n#if isEmpty($row[\"Type 2\"])\n $out.secondary_type = null\n#else\n $out.secondary_type = $row[\"Type 2\"]\n/if\n```\n\nWhen a source field might be `\"\"` (especially with CSV ingestion, where empty cells parse as empty strings), use `isEmpty` to coerce blanks to `null`. Rollups treat `null` and `\"\"` as the same series, but downstream consumers (charts, exports, monitoring rules) read cleaner data when blanks are normalized at the transform layer.\n\n## Numeric coercion (CSV to math)\n\n```tgl\n$out.line_total = multiply(float($row[\"Price\"]), int($row[\"Quantity\"]))\n```\n\n## Default values\n\n```tgl\n$out.country = $row.address.country ?? \"US\"\n$out.label = $row.label ?? \"untitled\"\n$out.is_active = $row.is_active ?? true\n```\n\n## Sum line items\n\n```tgl\n$out.total = 0\n#each $row.line_items as $item\n $out.total = add($out.total, multiply($item.price, $item.qty))\n/each\n```\n\n## Build a tag array\n\n```tgl\n$out.tags = null\n#each $row.raw_tags as $tag\n #if !isEmpty($tag)\n $out.tags = push($out.tags, lowerCase(trim($tag)))\n /if\n/each\n```\n\n## Conditional output shape\n\n```tgl\n$out.id = $row.id\n$out.created_at = $row.timestamp\n\n#if $row.type == \"purchase\"\n $out.amount_cents = round(multiply($row.amount, 100))\n $out.currency = $row.currency\n#elseif $row.type == \"refund\"\n $out.amount_cents = round(multiply(subtract(0, $row.amount), 100))\n $out.refund_reason = $row.reason\n#else\n $out = null\n/if\n```\n\n## Bucket by week\n\n```tgl\n$out.week_start = toDateString(startOf(date($row.timestamp), \"week\"))\n$out.event = $row.event\n$out.user_id = $row.user_id\n```\n\n## Normalize an email\n\n```tgl\n$out.email = lowerCase(trim($row.email))\n$out.email_domain = split($out.email, \"@\")[1]\n```\n\n## Limits\n\nTGL enforces hard limits to bound compilation and each record execution. Compile limits apply to the script as a whole. Runtime limits reset for every input record.\n\n| Limit | Default | What happens when exceeded |\n| ---------------------------------- | ---------------------------- | ------------------------------------------------------------------- |\n| Transform script request | 65,536 UTF-16 code units | Request validation error; the script is not compiled or stored |\n| Statement count | 10,000 | Compile error; script rejected at storage time |\n| Expression or control-flow depth | 200 levels | Parse or compile error; script rejected at storage time |\n| Output path nesting | 50 levels | Compile error; script rejected at storage time |\n| String length for one value | 10,485,760 UTF-16 code units | That value becomes `null` |\n| Loop iterations per record | 1,000,000 | The record fails; processing continues with the next record |\n| Output writes per record | 10,000 | The record fails; processing continues with the next record |\n| Output array length | 10,000 | Oversized values become `null`; an out-of-range indexed write fails |\n| Values and containers copied out | 100,000 | The record fails; processing continues with the next record |\n| Cumulative output string/key bytes | 64 MiB | The record fails; processing continues with the next record |\n\nTailglow rejects a transform request longer than 65,536 code units. Other compile limits reject the script before it is saved. Runtime string and array limits can turn an individual value into `null`; iteration, write, and cumulative output limits skip the affected record.\n\n## Errors and validation\n\n### Storage-time validation\n\nWhen you create or update a transform, Tailglow validates the script before saving it as `active`.\nScripts that fail compilation or output-field validation are not stored.\n\nThe output-field check compares the script's statically known top-level writes with the view's output schema. It is skipped for the exact mirror script `$out = $row` and for scripts with a dynamic top-level key such as `$out[$row.key]`, because their field sets cannot be known at compile time. Other scripts are not stored until they pass both compilation and output-field validation.\n\n### Runtime errors\n\nAn invalid input record or a row that reaches a runtime safety limit is skipped while processing continues with the remaining records.\n\nA structural output error, such as assigning an array to the root with `$out = arrayValue`, can mark a live transform as `error` or cause a backfill to fail after retries. Fix and save the script from the transform drawer.\n\n### Per-record skip\n\n`$out = null` is not an error. It's a normal filter signal. The record is dropped silently and the view continues.\n",
|
|
72
|
-
"guides/tgl/syntax": "# TGL: Syntax\n\nThis page covers the basic shape of a TGL script: how to read input, build output, declare scratch variables, and lay out statements.\n\n## `$row`: the input record\n\n`$row` is the record you're transforming. It's a plain JSON object: nested fields, arrays, strings, numbers, booleans, nulls. Read fields with dot or bracket notation:\n\n```tgl\n$out.email = $row.email\n$out.full_name = $row[\"First Name\"] // brackets when the key has spaces\n$out.first_tag = $row.tags[0]\n$out.deep = $row.user.profile.email\n```\n\nReading a missing field returns `null`. TGL doesn't throw on undefined access. Same for chains where a parent is null:\n\n```tgl\n$out.x = $row.does.not.exist // returns null, no error\n```\n\n`$row` is read-only. You can't write to it: `$row.x = 1` is a parse error.\n\n## `$meta`: platform metadata\n\n`$meta` is a read-only object of platform metadata about the record, kept separate from the record's own fields on `$row`. It's how a transform reaches values Tailglow attaches when it ingests the data, rather than values that came in the payload.\n\nOne field is available today:\n\n- `$meta.ingested_at` (string): the platform source time for the record, as an ISO 8601 timestamp. For collection rows, this is the
|
|
72
|
+
"guides/tgl/syntax": "# TGL: Syntax\n\nThis page covers the basic shape of a TGL script: how to read input, build output, declare scratch variables, and lay out statements.\n\n## `$row`: the input record\n\n`$row` is the record you're transforming. It's a plain JSON object: nested fields, arrays, strings, numbers, booleans, nulls. Read fields with dot or bracket notation:\n\n```tgl\n$out.email = $row.email\n$out.full_name = $row[\"First Name\"] // brackets when the key has spaces\n$out.first_tag = $row.tags[0]\n$out.deep = $row.user.profile.email\n```\n\nReading a missing field returns `null`. TGL doesn't throw on undefined access. Same for chains where a parent is null:\n\n```tgl\n$out.x = $row.does.not.exist // returns null, no error\n```\n\n`$row` is read-only. You can't write to it: `$row.x = 1` is a parse error.\n\n## `$meta`: platform metadata\n\n`$meta` is a read-only object of platform metadata about the record, kept separate from the record's own fields on `$row`. It's how a transform reaches values Tailglow attaches when it ingests the data, rather than values that came in the payload.\n\nOne field is available today:\n\n- `$meta.ingested_at` (string): the platform source time for the record, as an ISO 8601 timestamp. For collection rows, this is the time Tailglow received the record. For transforms sourced from a ViewFile, including joins, it is that source file's creation time. Reach for it when your data has no timestamp of its own, or when you want Tailglow's time for the current transform stage.\n\n```tgl\n$out.id = $row.id\n$out.ingested_at = $meta.ingested_at\n```\n\n`$meta` is read-only, the same as `$row`: writing to it (`$meta.x = 1`) is a parse error. Unlike `$row`, reading a field that isn't part of the contract (anything other than `$meta.ingested_at` today) is a compile error that lists the fields you can use, so a typo fails fast instead of quietly returning null.\n\n## `$out`: your output\n\n`$out` starts as `{}` (an empty object) on every record. Build it up:\n\n```tgl\n$out.id = $row.id\n$out.name = lowerCase($row.name)\n$out.created_at = $row.timestamp\n```\n\nWhatever's in `$out` when the script ends is the output record.\n\nYou can also assign wholesale, copying every field:\n\n```tgl\n$out = $row // mirror the entire input\n$out = $row.payload // promote a nested object to top-level\n```\n\nWholesale assignment is filtered. Dangerous keys like `__proto__` are stripped automatically. You don't have to worry about prototype pollution from untrusted source data.\n\n## `$varname`: scratch variables\n\nFor intermediate values you don't want in the output:\n\n```tgl\n$first = upperCase($row.first_name)\n$last = upperCase($row.last_name)\n$out.display = concat($first, \" \", $last)\n```\n\nVariable names must start with a letter or underscore, followed by letters, digits, or underscores. A few names are reserved, including names with a `__` prefix, `constructor`, and `prototype`. Variables are scoped to the script. There's no global state across records.\n\n## Statements\n\nEach statement goes on its own line. Newlines are the separator; semicolons aren't a thing.\n\n```tgl\n$out.a = 1\n$out.b = 2\n```\n\nComments use `//`:\n\n```tgl\n// strip the protocol prefix\n$out.host = replace($row.url, \"https://\", \"\")\n```\n\nThere's no block syntax for grouping statements. Control flow is done with directives. See [Control flow](/guides/tgl/control-flow) for `#if`/`#each`.\n\n## Data types\n\nTGL works on the JSON-shaped data stored by Tailglow: strings, finite numbers, booleans, null,\narrays, and objects. Date functions can parse ISO strings, Unix seconds or milliseconds, and\ntimestamp objects with seconds and nanoseconds.\n\nThere are no:\n\n- Object literals like `{ a: 1 }`. Build outputs by writing to `$out` field by field.\n- Array literals like `[1, 2, 3]`. Use `push(null, x)` to start an array.\n\n## What's next\n\n- **[Control flow](/guides/tgl/control-flow)**: filtering rows, `#if`/`#elseif`/`#else`/`/if`, `#each`.\n- **[Operators](/guides/tgl/operators)**: comparison, logical, `??`.\n- **[Functions](/guides/tgl/functions)**: every built-in.\n"
|
|
73
73
|
};
|