@tailglow/cli 0.9.16 → 0.9.18

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.
@@ -15,17 +15,17 @@ export const DOCS_INDEX = "# Tailglow\n\n> Analytics and monitoring platform. Se
15
15
  export const DOCS_PAGES = {
16
16
  "api/introduction": "# Introduction\n\n## Base URL\n\nThe Tailglow API is built on REST principles. We enforce HTTPS in every request to improve data security, integrity, and privacy. The API does not support HTTP.\n\nAll requests contain the following base URL:\n\n```url\nhttps://api.tailglow.io\n```\n\n## Authentication\n\nTo authenticate with the Tailglow API, send your API key in the `Authorization` header using the `Bearer` scheme. An API key looks like `tg_api_` followed by 70 characters.\n\n```http\nAuthorization: Bearer tg_api_1a2B3c4D5e6F7g8H9i0J...\n```\n\n`Authorization: Key <your key>` is also accepted, as is sending the key on its own with no scheme. Keys issued before the `tg_api_` prefix begin with `sk_` and keep working indefinitely.\n\n## Filtering Data\n\nWhen filtering data, you'll create a query string that will be appended to the URL of your `GET` request. The query string will begin with a `?` and contain key-value pairs separated by `&`.\n\n> Be careful when using \"not\" filters. They can result in a lot of data being returned.\n\nTailglow uses a single canonical operator vocabulary across every filter surface. URL syntax is flat: `?<field>=<op>:<value>`. The full list, per-type allowlists, and per-surface availability matrix live in the [Filter Operators enum](/api/enums#filter-operators).\n\n### Strings\n\n- **`equals`** - `?type=equals:expense`\n- **`not_equals`** - `?type=not_equals:expense`\n- **`starts_with`** - `?type=starts_with:expense`\n- **`not_starts_with`** - `?type=not_starts_with:expense`\n- **`ends_with`** - `?type=ends_with:expense`\n- **`not_ends_with`** - `?type=not_ends_with:expense`\n- **`contains`** - `?type=contains:expense`\n- **`not_contains`** - `?type=not_contains:expense`\n- **`in`** - `?type=in:expense,refund`\n- **`not_in`** - `?type=not_in:expense,refund`\n\nString operators compare case-insensitively, so `?type=equals:GET` and `?type=equals:get` include the same rows.\n\n### Numbers\n\n- **`equals`** - `?value=equals:100`\n- **`not_equals`** - `?value=not_equals:100`\n- **`gt`** (Greater Than) - `?value=gt:100`\n- **`gte`** (Greater Than or Equal) - `?value=gte:100`\n- **`lt`** (Less Than) - `?value=lt:100`\n- **`lte`** (Less Than or Equal) - `?value=lte:100`\n- **`between`** - `?value=between:100,200`\n- **`in`** / **`not_in`** - `?value=in:100,200`\n\n## Pagination\n\nMost `list` endpoints use cursor-based pagination. The first request omits the cursor; subsequent requests echo back the `next_cursor` (or `prev_cursor`) the previous response handed you.\n\n### Request parameters\n\n- **`limit`** `number`\n Maximum number of items to return per page. Defaults to `25`, max `200`.\n- **`after`** `string`\n Opaque cursor from a previous response's `pagination.next_cursor`. Returns the rows after the pivot.\n- **`before`** `string`\n Opaque cursor from a previous response's `pagination.prev_cursor`. Returns the rows before the pivot. Mutually exclusive with `after`.\n- **`sort`** `\"asc\" \\| \"desc\"`\n Sort direction. Cursors are minted under one direction; reusing them under a different sort returns a `400`.\n\n### Response shape\n\nThe `pagination` object on every list response carries:\n\n- **`limit`** `number`: echoed from the request.\n- **`count`** `number`: number of rows in this page (≤ `limit`).\n- **`offset`** `number`: 0-indexed position of the first row across the full result set. Use `offset + 1` for \"Showing 21\" labels.\n- **`sort`** `\"asc\" \\| \"desc\"`: echoed sort direction.\n- **`next_cursor`** `string \\| null`: cursor for the next page. `null` means there are no more rows.\n- **`prev_cursor`** `string \\| null`: cursor for the previous page. `null` means you're on the first page.\n- **`total`** `number \\| null`: exact row count for database-backed lists and records. `null` is returned only when the underlying feed cannot provide a total. For example, collection docs always return `null` because the underlying files aren't enumerated.\n\nCursors are server-minted opaque tokens. Don't construct them yourself; round-trip the values the API gave you.\n\n## Responses\n\nAll of our responses will follow a standard structure, regardless if it's successful or not. Here is an example of the response object.\n\n### Successful Response Example\n\nIf you are listing data, the `data` key will be an array of objects and a pagination object will be present.\nIf you are retrieving, creating, or writing a single object, the `data` key will be an object.\nIf you are deleting an object, the `data` key will be `null`.\n\n```json\n{\n \"message\": \"Data retrieved successfully\",\n \"data\": [],\n \"status\": 200,\n \"error\": null,\n \"pagination\": {\n \"object\": \"pagination\",\n \"limit\": 10,\n \"count\": 10,\n \"offset\": 0,\n \"sort\": \"desc\",\n \"next_cursor\": \"eyJ2IjoxLCJyZXNvdXJjZSI6...\",\n \"prev_cursor\": null,\n \"total\": 42\n },\n \"endpoint\": \"/v1/projects\"\n}\n```\n\n### Error Response Example\n\nYou'll typically see error when request validations fail. The `error` key will contain an object with a `code` key and a `properties` key that will contain an array of objects with an `id` and `message` key. The `id` corresponds to the field that failed validation and the `message` will contain the error message.\n\n```json\n{\n \"message\": \"The request is invalid\",\n \"data\": null,\n \"status\": 400,\n \"error\": {\n \"code\": \"request_invalid\",\n \"properties\": [\n {\n \"id\": \"name\",\n \"message\": \"Name is required\"\n }\n ]\n },\n \"pagination\": null,\n \"endpoint\": \"/v1/projects\"\n}\n```\n",
17
17
  "api/errors": "# Errors\n\n## Variations\n\nWe follow standard HTTP status codes for errors. Here are the variations you might encounter:\n\n### `request_invalid`\n\n- **Status Code:** 400\n- **Message:** \"The request is invalid.\"\n- **How to Resolve:** Check the request payload for missing or incorrect fields.\n\n### `request_no_update`\n\n- **Status Code:** 400\n- **Message:** \"There was nothing new to update.\"\n- **How to Resolve:** Make sure you're submitting new or modified data.\n\n### `request_validation_failed`\n\n- **Status Code:** 400\n- **Message:** \"The request failed validation.\"\n- **How to Resolve:** Ensure all fields meet the required validation rules.\n\n### `unsupported_file_type`\n\n- **Status Code:** 400\n- **Message:** \"Unsupported file type.\"\n- **How to Resolve:** Use one of the supported file formats.\n\n### `auth_missing_credentials`\n\n- **Status Code:** 401\n- **Message:** \"You must provide credentials to access this resource.\"\n- **How to Resolve:** Include your API key in the `Authorization` header.\n\n### `auth_invalid_credentials`\n\n- **Status Code:** 401\n- **Message:** \"Your credentials are invalid.\"\n- **How to Resolve:** Double-check your API key or token.\n\n### `auth_account_disabled`\n\n- **Status Code:** 401\n- **Message:** \"Your account is disabled, please contact support.\"\n- **How to Resolve:** Reach out to support to restore access.\n\n### `auth_missing_permissions`\n\n- **Status Code:** 401\n- **Message:** \"You do not have permission to access this resource.\"\n- **How to Resolve:** Ensure your account has the appropriate permissions.\n\n### `auth_mfa_required`\n\n- **Status Code:** 401\n- **Message:** \"Multi-factor authentication is required.\"\n- **How to Resolve:** Complete MFA verification before accessing this resource.\n\n### `auth_unavailable`\n\n- **Status Code:** 503\n- **Message:** \"Your credentials could not be checked right now; retry shortly.\"\n- **Returned by:** any authenticated endpoint when Tailglow cannot reach a service it needs to check your credentials. Your credentials have not been rejected, and nothing about the request needs to change.\n- **How to Resolve:** Retry the request after a short wait. Do not sign out or replace your API key: this error is temporary.\n\n### `resource_limit_reached`\n\n- **Status Code:** 403\n- **Message:** \"You have reached the limit for this resource.\"\n- **How to Resolve:** Upgrade your plan or wait until the limit resets.\n\n### `forbidden`\n\n- **Status Code:** 403\n- **Message:** \"This action is forbidden.\"\n- **How to Resolve:** Ensure you have proper permissions for this action.\n\n### `resource_not_found`\n\n- **Status Code:** 404\n- **Message:** \"The requested resource was not found.\"\n- **How to Resolve:** Verify the resource ID or endpoint.\n\n### `request_timeout`\n\n- **Status Code:** 408\n- **Message:** \"The request timed out.\"\n- **How to Resolve:** Retry the request or check your connection.\n\n### `rate_limit_exceeded`\n\n- **Status Code:** 429\n- **Message:** \"You have exceeded the rate limit.\"\n- **How to Resolve:** Reduce the frequency of your requests and try again later.\n\n### `resource_locked`\n\n- **Status Code:** 409\n- **Message:** \"The resource is locked.\"\n- **How to Resolve:** Unlock the resource if possible or wait until it becomes available.\n\n### `payload_too_large`\n\n- **Status Code:** 413\n- **Message:** \"The payload was too large for the server to safely handle.\"\n- **How to Resolve:** Reduce the size of the request payload.\n\n### `ingest_spool_full`\n\n- **Status Code:** 503\n- **Message:** \"Server at capacity (REASON); retry shortly\", where REASON names the limiting resource, e.g. a disk-capacity reason such as \"N MiB free below the M MiB byte floor\" or \"inodes P% at/above the C% ceiling\", \"ingest queue full\", or \"storage write failed\".\n- **Returned by:** the ingest endpoint when the server's durable spool cannot accept more records. The response includes a `Retry-After` header (in seconds).\n- **How to Resolve:** Retry the request after the `Retry-After` interval. If the error persists, you've outgrown the server's allocated capacity for this project. Provision a larger spool volume or distribute traffic across additional servers.\n\n### `ingest_auth_unavailable`\n\n- **Status Code:** 503\n- **Message:** \"The ingest key could not be verified; retry shortly.\"\n- **Returned by:** the ingest endpoint when it cannot reach the service that verifies ingest keys. Your key has not been rejected, and nothing about the request needs to change. The response includes a `Retry-After` header (in seconds).\n- **How to Resolve:** Retry the request after the `Retry-After` interval. Tailglow SDKs do this automatically. A key that was verified recently keeps being accepted throughout the outage, so this only affects a key the server has not seen lately.\n\n### `database_error`\n\n- **Status Code:** 500\n- **Message:** \"A database error occurred.\"\n- **How to Resolve:** Try again later or contact support if the issue persists.\n\n### `invalid_server_state`\n\n- **Status Code:** 500\n- **Message:** \"The server is in an invalid state.\"\n- **How to Resolve:** Contact support if the issue persists.\n\n### `plan_limit_reached`\n\n- **Status Code:** 403\n- **Message:** \"You have reached the limit for your current plan.\"\n- **How to Resolve:** Upgrade your plan to increase your limits.\n\n### `server_error`\n\n- **Status Code:** 500\n- **Message:** \"An error occurred.\"\n- **How to Resolve:** Try again later or contact support if the issue persists.\n\n### `not_implemented`\n\n- **Status Code:** 501\n- **Message:** \"This route has not been implemented.\"\n- **How to Resolve:** Contact support or refer to the documentation for available routes.\n",
18
- "api/enums": "# Enums\n\nThis page lists common enum values used across the API.\n\n## Ambiguity Handling Mode\n\nProject `ambiguity_handling` controls what new mappings do across all views when the source data allows more than one defensible reading:\n\n- `decide` (default): choose the mapping best supported by the fields, examples and hint.\n- `ask`: ask for a hint when competing source fields have different plausible meanings.\n\nBoth settings validate scripts against original records. Existing scripts are unchanged when this setting changes.\n\n## Metric Group By\n\nThe `group_by` field accepts any string field names from your view's output schema. There is no fixed set of allowed values.\n\n## Chart Types\n\n- `line`\n- `area`\n- `bar`\n- `pie`\n- `scatter`\n- `radar`\n- `stat`\n- `gauge`\n- `calendar`\n- `uptime`\n- `breakdown`: Bars comparing the range's groups: one per series, or one per hour or weekday when the metric has a [time dimension](#metric-time-dimensions).\n- `heatmap`: A grid of cells shaded by value across a [time dimension](#metric-time-dimensions), such as the 168 hours of a week. Requires a `time_dimension`.\n- `table`: One row per series, summarizing it over the range.\n\n## Chart Color Modes\n\nHow a chart picks colors. The chart type must allow the chosen mode (e.g., gauge supports `by_series` and `by_value`; calendar supports `by_series` and `by_intensity`; breakdown and heatmap support `by_intensity` and `by_series`; table supports `by_series` only).\n\n- `by_series`: A series that matches one of the metric's `ui_chart_series_rules` takes that rule's color, and once the metric has a rule every other series is gray. Without rules, each series takes a color picked from its hash, unless a series above it on the chart uses that color, then a free one. A metric without `group_by` draws one series in `ui_chart_color_base`, blue when it is null.\n- `by_intensity`: One base color (`ui_chart_color_base`); the renderer modulates lightness/opacity by value. Used by calendar / heatmap-style charts.\n- `by_value`: Ordered rules (`ui_chart_color_rules`) map each value to a color. First matching rule wins; no match falls back to system default. Used by gauges and uptime strips.\n\n## Metric Chart Values\n\nDetermines which aggregation is displayed by default on a chart.\n\n- `count`: Number of records.\n- `average`: Average of the record `value` field.\n- `sum`: Sum of the record `value` field.\n- `min`: Minimum of the record `value` field.\n- `max`: Maximum of the record `value` field.\n- `last`: Latest reading in each time bucket per group_by combination. Within the requested range, the last observed value carries forward across complete empty intervals. Use for entity/snapshot data (e.g., deals, users, inventory).\n- `cumulative_sum`: Cumulative sum of the record `value` field over the time range.\n- `cumulative_count`: Cumulative count of records over the time range.\n- `p50`: 50th percentile (median) of the record `value` field.\n- `p95`: 95th percentile of the record `value` field.\n- `p99`: 99th percentile of the record `value` field.\n- `count_unique`: Number of distinct values of the `unique_field`. Exact for small cardinalities (up to 8,192), approximate for larger sets.\n\n## Empty Bucket Handling\n\nControls how time intervals with no data render on a metric's chart. This is a display setting: changing it applies instantly to all historical data with no recalculation.\n\n- `zero`: Empty intervals render as a true 0. The right choice for additive aggregations (`count`, `sum`, `cumulative_*`, `count_unique`) where \"nothing happened\" means zero.\n- `gaps`: Empty intervals render as gaps (`null` values in `series.records`) and are excluded from trend line fitting. The right choice for observational aggregations (`average`, `min`, `max`, percentiles) where an empty interval has no defined value. The `last` chart value is the one exception: it carries the latest measured value forward through empty intervals in both modes.\n\nNew metrics default based on their `ui_chart_value`: additive aggregations get `zero`, observational aggregations get `gaps`. Monitors do not read this setting: a monitor's own [`empty_window_handling`](#monitor-empty-window-handling) decides what an empty window means.\n\n## Chart Colors\n\nPossible values for `ui_chart_color_base` and for the `color` of a rule in `ui_chart_color_rules` or `ui_chart_series_rules`.\n\n- `blue`\n- `teal`\n- `purple`\n- `pink`\n- `green`\n- `amber`\n- `red`\n\n## Analytics Time Ranges\n\nUsed for `time_range` (in aggregation requests) and `ui_chart_time_range` (in the metric object).\n\n- `last_hour`\n- `last_6_hours`\n- `today`\n- `last_24_hours`\n- `yesterday`\n- `this_week`\n- `last_7_days`\n- `last_week`\n- `this_month`\n- `last_30_days`\n- `last_month`\n- `this_quarter`\n- `last_90_days`\n- `last_quarter`\n- `this_year`\n- `last_365_days`\n- `last_year`\n- `all_time`\n- `custom` (Requires specific start/end dates)\n- `next_7_days`\n- `next_30_days`\n- `next_90_days`\n\nA `next_*` range reaches the same distance into the past as it does into the future, so a chart has history to fit a forecast from.\n\n## Forecast Horizons\n\nUsed for `ui_chart_forecast_horizon` (in the metric object). Relative values resolve when the chart is viewed.\n\n- `next_7_days`\n- `next_30_days`\n- `next_90_days`\n- `end_of_quarter`\n- `end_of_year`\n- `next_year`\n\n## Forecast Models\n\nUsed for `ui_chart_forecast_model` (in the metric object). `auto` follows whichever model fits the data best.\n\n- `auto`\n- `linear`\n- `exponential`\n- `logarithmic`\n- `logistic`\n- `sinusoidal`\n\n## Metric Data Intervals\n\nUsed for `interval` (in aggregation requests) and `ui_chart_interval` (in the metric object).\n\n- `minute`\n- `hour`\n- `day`\n- `month`\n\n## Metric Time Dimensions\n\nUsed for `time_dimension` (in the metric object). A metric with one returns its aggregation folded instead of as a timeline: every hour of the range merges into its calendar position, read in the request's `timezone`, and each record key is that position as a decimal string.\n\n- `hour_of_day`: Hours `0` to `23`. Breakdown and heatmap charts.\n- `weekday`: ISO weekdays `1` (Monday) to `7` (Sunday). Breakdown and heatmap charts.\n- `hour_of_week`: The 168 hours of a week, `0` to `167`, numbered `(weekday - 1) * 24 + hour`. Heatmap charts.\n\nA fold covers at most 400 days and cannot use a cumulative chart value.\n\n## Chart Orders\n\nUsed for `ui_chart_order` (in the metric object): the order a chart ranks its series in, which an aggregation request's `sort` can override.\n\n- `natural`: Rank series largest first, and keep a time dimension in its own order, such as hour 0 to 23 or Monday to Sunday.\n- `highest`: Rank series largest first.\n- `lowest`: Rank series smallest first.\n\n`null` is `natural`. `value` is accepted on write and reads back as `highest`.\n\n## View Types\n\nHow a [View](/api/views#model) produces its records. It is set when the view is created and cannot change afterwards.\n\n- `transform`: Default. The view reads one collection or one other view through a [TGL](/guides/tgl) script.\n- `join`: The view starts from a base view and attaches rows from its lookup views, matched on a field.\n\n## View Transform Modes\n\nHow a [View](/api/views#model) acquires a [TGL](/guides/tgl) transform for schema versions discovered after it was created.\n\nBoth modes first try to reuse one of the view's existing scripts. Reuse requires a typed candidate signature and at least one of the script's read paths to be present. A present path whose value is null counts as evidence; an absent path does not. An unknown-only signature is not eligible for reuse. The modes differ only when no existing script can read the new shape:\n\n- `auto`: Tailglow generates a script for the new version.\n- `manual`: The version waits for you to supply a compatible script instead of generating one automatically. A supplied script can wait for input before validation.\n\n## View Transform Conflict Resolutions\n\nControls what happens when editing a collection-backed transform would make the new script incompatible with some schema versions that used the prior script.\n\n- `strand`: Default. Compatible versions adopt the edit; conflicting versions move to queued placeholders so they can receive their own script.\n- `keep`: Compatible versions adopt the edit; conflicting versions remain on the prior script.\n\n## Schema Version States\n\nThe lifecycle of a collection's [schema version](/guides/concepts#schema-versions), from first sight to trust.\n\n- `candidate`: The shape has been seen once. Its records are stored and served immediately by any view mapping that already fits them, but no mapping is written for it yet.\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 are offered to views for authoring, and a settled version never goes back.\n- `stale`: A candidate that was not seen again within the stale window. Its rows stay stored but unclassified. A stale shape that returns settles.\n\n## Schema Settlements\n\nWhat settled a schema version.\n\n- `recurrence`: The shape was seen again after the collection's recurrence gap.\n- `bulk`: One batch carried at least the collection's bulk row count of the shape.\n- `person`: Someone settled the version from the collection page or through the API.\n\n## Schema Rule Modes\n\nHow a collection applies each of its three identity rules: nulls and absent values, optional keys, and the detector. See [Schema settings](/guides/sources#schema-settings).\n\n- `auto`: Tailglow decides. Null and absent values are accepted at any path a mapping reads, a record whose keys are a subset of a version's keys joins that version, and a safe opaque-path proposal from the detector is applied on its own.\n- `manual`: A person decides at each of those points.\n\n## Metric Statuses\n\nThe current status of the metric.\n\n- `initializing`: Metric is being created.\n- `waiting_for_transforms`: Metric is waiting for its source transforms to finish processing.\n- `backfilling`: Historical data is being backfilled (also covers re-backfill triggered by data-feed changes).\n- `active`: Metric is up-to-date and collecting new data.\n- `error`: An error occurred during processing.\n- `cancelled`: The metric, or the view it reads, was deleted while a backfill was still running, so the backfill was cancelled instead of being allowed to finish. Restoring the metric within its 24-hour window leaves it here; re-run the backfill to finish what it missed.\n\n## View Statuses\n\nThe current status of a [View](/api/views#model), derived from its transforms.\n\n- `empty`: The view has no transforms yet.\n- `building`: A transform is waiting for input or processing data, including indexing history.\n- `needs_transform`: A transform script needs to be written or corrected. This can occur in either transform mode.\n- `active`: At least one transform is finished and serving data.\n- `cancelled`: The view was deleted while it was still indexing, so the indexing was cancelled rather than allowed to finish. Deleted views remain readable until permanent removal 24 hours later, and they report this status for that window.\n- `error`: A transform failed to compile, or its indexing failed.\n\n## Metric Values\n\nIndicates which aggregations were calculated in the [Metric Aggregation Model](/api/metrics#aggregation-model).\n\n- `count`\n- `sum`\n- `min`\n- `max`\n\n## Filter Operators\n\nThe canonical operator vocabulary used everywhere filters appear: list endpoint query parameters (`?<field>=<op>:<value>`), metric `filters` (applied at rollup time), facet record queries against indexed views, and the UI filter components.\n\nURL syntax is flat: `?type_1=equals:Fire&limit=50`. Operator names are long-form snake*case except for the industry-standard `gt`/`gte`/`lt`/`lte`/`in`. Every negation has a `not*` prefix. String operators compare case-insensitively (`equals:Get`matches`GET`); numeric, date, and existence operators are unaffected.\n\n| Operator | Semantics |\n| ----------------- | --------------------------------------------- |\n| `equals` | Exact match |\n| `not_equals` | Inverse of `equals` |\n| `gt` / `gte` | Greater than (strict / inclusive) |\n| `lt` / `lte` | Less than (strict / inclusive) |\n| `between` | Inclusive range; value is `min,max` |\n| `contains` | Substring match (strings) |\n| `not_contains` | Inverse of `contains` |\n| `starts_with` | Prefix match (strings) |\n| `not_starts_with` | Inverse of `starts_with` |\n| `ends_with` | Suffix match (strings) |\n| `not_ends_with` | Inverse of `ends_with` |\n| `in` | Scalar field, value in a comma-separated list |\n| `not_in` | Inverse of `in` |\n| `has` | Array field contains the given scalar |\n| `not_has` | Inverse of `has` |\n| `has_any` | Array field intersects a comma-separated list |\n| `exists` | Field has a non-null value |\n| `not_exists` | Field is null or absent |\n\n### Per-type allowlist (UI filter components)\n\n| Field type | Allowed operators |\n| ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |\n| `string` | `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `not_starts_with`, `ends_with`, `not_ends_with`, `in`, `not_in` |\n| `number` | `equals`, `not_equals`, `gt`, `gte`, `lt`, `lte`, `between`, `in`, `not_in` |\n| `date` | `equals`, `not_equals`, `gt`, `gte`, `lt`, `lte`, `between` |\n| `array` | `has`, `not_has`, `has_any` |\n| `boolean` | `equals`, `not_equals` |\n\n### Surface availability\n\nNot every operator is supported by every surface. The metric filter pipeline supports the full vocabulary; CRUD list endpoints reject existence operators in v1 (the per-field nullable-aware validators land separately); facet record queries are limited to the SQLite-indexed subset.\n\n| Operator | Metric filters | CRUD list | Facet records |\n| ----------------- | :------------: | :-------: | :-----------: |\n| `equals` | ✓ | ✓ | ✓ |\n| `not_equals` | ✓ | ✓ | |\n| `gt` | ✓ | ✓ | |\n| `gte` | ✓ | ✓ | ✓ |\n| `lt` | ✓ | ✓ | |\n| `lte` | ✓ | ✓ | ✓ |\n| `between` | ✓ | ✓ | |\n| `contains` | ✓ | ✓ | |\n| `not_contains` | ✓ | ✓ | |\n| `starts_with` | ✓ | ✓ | ✓ |\n| `not_starts_with` | ✓ | ✓ | |\n| `ends_with` | ✓ | ✓ | |\n| `not_ends_with` | ✓ | ✓ | |\n| `in` / `not_in` | ✓ | ✓ | |\n| `has` | ✓ | | |\n| `not_has` | ✓ | | |\n| `has_any` | ✓ | | |\n| `exists` | ✓ | | |\n| `not_exists` | ✓ | | |\n\n### Reserved query parameter keys\n\nSeveral keys are reserved as query-string control parameters across every endpoint. View `output_schema` fields and facet `field_path` values cannot use any of these names:\n\n`limit`, `cursor`, `page`, `sort`, `order_by`, `after`, `before`, `start_at`, `end_at`, `timezone`, `interval`, `search`\n\n## Monitor Statuses\n\nWhether a monitor checks on its schedule ([`status`](/api/monitors#model)).\n\n- `active`: Checks at every scheduled time.\n- `paused`: You stopped the monitor. Pausing ends its open alerts, and resuming starts again at the next scheduled time without checking the paused period. Only you start it again, with Resume Monitor.\n- `disabled`: Tailglow stopped the monitor because its `expires_at` passed. Move the expiry into the future, then resume it.\n\n## Monitor Statistics\n\nWhat a monitor computes over each window ([`statistic`](/api/monitors#model)). Every statistic covers the whole window, and for the `total` scope the whole filtered feed.\n\n- `count`: How many readings arrived. `count` equal to 0 alerts when no data arrives.\n- `sum`: The sum of the readings' values.\n- `average`: The average of the readings' values.\n- `min`: The smallest value.\n- `max`: The largest value.\n- `last`: The most recent reading.\n- `p50`: The median of the values.\n- `p95`: The 95th percentile of the values.\n- `p99`: The 99th percentile of the values.\n- `count_unique`: How many distinct values of the metric's `unique_field` arrived.\n\n## Monitor Operators\n\nHow the statistic is compared with `threshold_value` ([`operator`](/api/monitors#model)).\n\n- `gt`: Above the threshold.\n- `gte`: At or above the threshold.\n- `lt`: Below the threshold.\n- `lte`: At or below the threshold.\n- `eq`: Equal to the threshold.\n- `neq`: Not equal to the threshold.\n\n## Monitor Empty Window Handling\n\nWhat the statistic reads when a window holds no data ([`empty_window_handling`](/api/monitors#model)). The metric's own `empty_bucket_handling` does not affect monitors.\n\n- `zero`: The window reads 0. The only value `count`, `sum` and `count_unique` accept, since an empty window is a measured 0 for them.\n- `no_value`: The window gives nothing to compare, so that check neither opens nor closes an alert. The default for `average`, `min`, `max`, `last` and the percentiles.\n\n## Monitor Window Types\n\nWhich data each check reads ([`window_type`](/api/monitors#model)).\n\n- `rolling`: The `window_minutes` before each scheduled check.\n- `all_time`: Everything from the start of the project up to each check, for totals such as \"signups pass 1 million\". Takes `window_minutes: null`.\n\n## Monitor Scopes\n\nHow the metric's series are checked ([`scope`](/api/monitors#model)).\n\n- `total`: One value for all the watched series combined.\n- `any_series`: One alert that stays open while any series matches. The series that opened it is shown on the alert.\n- `each_series`: A separate alert for every series.\n\n## Monitor Trigger Types\n\nWhen a match opens an alert ([`trigger_type`](/api/monitors#model)).\n\n- `immediate`: The first matching check.\n- `sustained`: Matches that keep holding for `sustained_minutes`. A gap between checks longer than `sustained_minutes` starts the count again.\n- `repeated`: `repeat_count` matching checks within `repeat_window_minutes`. The alert closes once fewer matches remain in a fully checked window and the current check does not match.\n\n## Monitor Alert Modes\n\nHow long an alert stays open ([`alert_mode`](/api/monitors#model)).\n\n- `spanning`: Open until a check finds the condition cleared. The default.\n- `instant`: Opened and ended at once, for a milestone that does not clear on its own. It does not open again until the condition has cleared.\n\n## Monitor Check Statuses\n\nThe outcome of a target's most recent check ([`last_check_status`](/api/monitors#series-state-model)).\n\n- `pending`: Not checked yet.\n- `ready`: The window was complete and the check was applied.\n- `unknown`: The scheduled time was passed over without a verdict, with the reason in `last_check_reason`: the metric is rebuilding (`metric_not_active`) or something it reads from is being deleted (`resource_deleting`).\n- `error`: Reading the window failed. The same scheduled time is checked again on the next run.\n\n## Alert End Reasons\n\nWhy an alert ended ([`end_reason`](/api/alerts#model)). Null while the alert is open. Only `recovered` sends a recovery email.\n\n- `recovered`: A check found the condition cleared.\n- `instant`: An instant alert, which ends as it opens.\n- `monitor_changed`: The monitor's rule was edited.\n- `monitor_paused`: The monitor was paused.\n- `monitor_expired`: The monitor's `expires_at` passed.\n- `resource_deleted`: The metric, or something it reads from, is being deleted.\n- `metric_rebuilt`: The metric was rebuilt from its data.\n- `target_removed`: The series no longer exists.\n\n## Drain Statuses\n\nLifecycle status of a Drain.\n\n- `draft`: Created but never verified. No outbound traffic.\n- `pending_verification`: A verification marker has been sent to the destination and we're waiting for the customer to paste it back via Confirm Verification.\n- `active`: Drain is verified, resumed, and forwarding records on the recurring schedule.\n- `paused`: You stopped the drain, or it was freshly verified (verification intentionally lands paused so you can sample before going live). Only you start it again. Resume via Resume Drain.\n- `disabled`: Tailglow stopped the drain after 72 hours of consecutive failures. Resume the same way after fixing the destination; resuming clears the failure counters.\n\n## Drain Body Formats\n\nWire format for the body of outbound drain POSTs.\n\n- `ndjson`: Newline-delimited JSON. Default. One record per line. Sent with `Content-Type: application/x-ndjson`.\n- `json`: A single JSON array of records. Required by Datadog Logs and similar receivers. Sent with `Content-Type: application/json`.\n\n## Drain Compressions\n\nWire compression for outbound drain POSTs.\n\n- `none`: Default. Body is sent uncompressed and works with any receiver.\n- `gzip`: Body is gzipped and `Content-Encoding: gzip` is added. Receivers MUST decompress `Content-Encoding` for this to work.\n\n## Check Statuses\n\nLifecycle status of a Check ([`status`](/api/checks#model)).\n\n- `active`: Running on its schedule.\n- `paused`: You stopped the check. Nothing is recorded for the minutes a pause covers, so the uptime record shows a gap rather than downtime. Start it again with Resume Check.\n- `disabled`: Tailglow stopped the check because the team is no longer in good standing. A failing endpoint never lands here, however long the failure lasts, because the outage is the thing the check exists to record. Resume the same way once the account is settled.\n\n## Check Methods\n\nThe HTTP method a check uses to request its endpoint ([`method`](/api/checks#model)). Both are observational, so a check can never change anything at the endpoint it watches.\n\n- `get`: Default.\n- `post`: For health endpoints that only answer to POST. Only a `post` check may send a `request_body`.\n\n## Check Error Stages\n\nWhich step of the most recent failed run went wrong ([`last_error_stage`](/api/checks#model)). A check never reads the response body, so there is no parse stage.\n\n- `request`: No usable response arrived. The address was refused by the outbound guard, the request timed out, TLS failed, the connection was reset, or the body ran past the size cap before it finished.\n- `response`: The endpoint answered, but not with a 2xx status.\n\n## Appearance Palettes\n\nThe color palette for a user's app appearance ([`appearance_palette`](/api/users#model)). Sets the light or dark scheme.\n\n- `light`\n- `dark`\n- `dim`: Default. A softer dark scheme.\n- `midnight`\n- `paper`\n\n## Appearance Type Sets\n\nThe font pairing for a user's app appearance ([`appearance_type_set`](/api/users#model)).\n\n- `system`: Default. The operating system's native font stack.\n- `grotesk`\n- `editorial`\n- `geometric`\n\n## Appearance Accents\n\nThe accent color for a user's app appearance ([`appearance_accent`](/api/users#model)).\n\n- `orange`: Default.\n- `azure`\n- `burgundy`\n- `ink`\n- `emerald`\n- `violet`\n\n## Appearance Scales\n\nThe UI scale (density) for a user's app appearance ([`appearance_scale`](/api/users#model)). Applies at tablet and desktop widths; mobile always renders at full size.\n\n- `comfortable`: 100% scale.\n- `cozy`: 90% scale.\n- `compact`: Default. 80% scale, fits more on screen.\n\n## Ingest Pipeline Error Reasons\n\nThe reasons behind the Errors series on a server's Ingest Pipeline chart. Every reason is a refusal issued before acceptance: the sender received an error response and nothing was recorded from that request. Accepted data is never dropped. It is queued on the server and retried until it lands.\n\nReasons where retrying the same request delivers the data. Tailglow SDKs retry these automatically; if you send raw HTTP, retry on any 5xx response:\n\n- `connection_closed`: The sender closed the connection before the upload finished.\n- `server_error`: An unexpected error while handling the request.\n- `spool_full_disk_gate`: The server paused intake to protect its disk from filling.\n- `spool_full_storage_write`: A buffer write failed while accepting the request.\n- `spool_full_ingest_queue`: The intake queue was momentarily full.\n- `spool_full_wal_unavailable`: The write buffer was restarting.\n- `auth_unavailable`: The ingest key could not be verified because the verification service was unreachable. The key itself was not rejected.\n\nReasons where the request itself must change. Retrying the same request is refused again:\n\n- `auth_missing_credentials`: No ingest key was provided.\n- `auth_invalid_credentials`: The ingest key is wrong or revoked.\n- `payload_too_large`: The payload exceeds the size limit.\n- `request_validation_failed`: The request body or parameters are invalid.\n- `frame_invalid`: A frame in the payload could not be parsed.\n- `empty_payload`: The request carried no data.\n- `source_deleted`: The target source no longer exists.\n",
18
+ "api/enums": "# Enums\n\nThis page lists common enum values used across the API.\n\n## Ambiguity Handling Mode\n\nProject `ambiguity_handling` controls what new mappings do across all views when the source data allows more than one defensible reading:\n\n- `decide` (default): choose the mapping best supported by the fields, examples and hint.\n- `ask`: ask for a hint when competing source fields have different plausible meanings.\n\nBoth settings validate scripts against original records. Existing scripts are unchanged when this setting changes.\n\n## Metric Group By\n\nThe `group_by` field accepts any string field names from your view's output schema. There is no fixed set of allowed values.\n\n## Chart Types\n\n- `line`\n- `area`\n- `bar`\n- `pie`\n- `scatter`\n- `radar`\n- `stat`\n- `gauge`\n- `calendar`\n- `uptime`\n- `breakdown`: Bars comparing the range's groups: one per series, or one per hour or weekday when the metric has a [time dimension](#metric-time-dimensions).\n- `heatmap`: A grid of cells shaded by value across a [time dimension](#metric-time-dimensions), such as the 168 hours of a week. Requires a `time_dimension`.\n- `table`: One row per series, summarizing it over the range.\n\n## Chart Color Modes\n\nHow a chart picks colors. The chart type must allow the chosen mode (e.g., gauge supports `by_series` and `by_value`; calendar supports `by_series` and `by_intensity`; breakdown and heatmap support `by_intensity` and `by_series`; table supports `by_series` only).\n\n- `by_series`: A series that matches one of the metric's `ui_chart_series_rules` takes that rule's color, and once the metric has a rule every other series is gray. Without rules, each series takes a color picked from its hash, unless a series above it on the chart uses that color, then a free one. A metric without `group_by` draws one series in `ui_chart_color_base`, blue when it is null.\n- `by_intensity`: One base color (`ui_chart_color_base`); the renderer modulates lightness/opacity by value. Used by calendar / heatmap-style charts.\n- `by_value`: Ordered rules (`ui_chart_color_rules`) map each value to a color. First matching rule wins; no match falls back to system default. Used by gauges and uptime strips.\n\n## Metric Chart Values\n\nDetermines which aggregation is displayed by default on a chart.\n\n- `count`: Number of records.\n- `average`: Average of the record `value` field.\n- `sum`: Sum of the record `value` field.\n- `min`: Minimum of the record `value` field.\n- `max`: Maximum of the record `value` field.\n- `last`: Latest reading in each time bucket per group_by combination. Within the requested range, the last observed value carries forward across complete empty intervals. Use for entity/snapshot data (e.g., deals, users, inventory).\n- `cumulative_sum`: Cumulative sum of the record `value` field over the time range.\n- `cumulative_count`: Cumulative count of records over the time range.\n- `p50`: 50th percentile (median) of the record `value` field.\n- `p95`: 95th percentile of the record `value` field.\n- `p99`: 99th percentile of the record `value` field.\n- `count_unique`: Number of distinct values of the `unique_field`. Exact for small cardinalities (up to 8,192), approximate for larger sets.\n\n## Empty Bucket Handling\n\nControls how time intervals with no data render on a metric's chart. This is a display setting: changing it applies instantly to all historical data with no recalculation.\n\n- `zero`: Empty intervals render as a true 0. The right choice for additive aggregations (`count`, `sum`, `cumulative_*`, `count_unique`) where \"nothing happened\" means zero.\n- `gaps`: Empty intervals render as gaps (`null` values in `series.records`) and are excluded from trend line fitting. The right choice for observational aggregations (`average`, `min`, `max`, percentiles) where an empty interval has no defined value. The `last` chart value is the one exception: it carries the latest measured value forward through empty intervals in both modes.\n\nNew metrics default based on their `ui_chart_value`: additive aggregations get `zero`, observational aggregations get `gaps`. Monitors do not read this setting: a monitor's own [`empty_window_handling`](#monitor-empty-window-handling) decides what an empty window means.\n\n## Chart Colors\n\nPossible values for `ui_chart_color_base` and for the `color` of a rule in `ui_chart_color_rules` or `ui_chart_series_rules`.\n\n- `blue`\n- `teal`\n- `purple`\n- `pink`\n- `green`\n- `amber`\n- `red`\n\n## Analytics Time Ranges\n\nUsed for `time_range` (in aggregation requests) and `ui_chart_time_range` (in the metric object).\n\n- `last_hour`\n- `last_6_hours`\n- `today`\n- `last_24_hours`\n- `yesterday`\n- `this_week`\n- `last_7_days`\n- `last_week`\n- `this_month`\n- `last_30_days`\n- `last_month`\n- `this_quarter`\n- `last_90_days`\n- `last_quarter`\n- `this_year`\n- `last_365_days`\n- `last_year`\n- `all_time`\n- `custom` (Requires specific start/end dates)\n- `next_7_days`\n- `next_30_days`\n- `next_90_days`\n\nA `next_*` range reaches the same distance into the past as it does into the future, so a chart has history to fit a forecast from.\n\n## Forecast Horizons\n\nUsed for `ui_chart_forecast_horizon` (in the metric object). Relative values resolve when the chart is viewed.\n\n- `next_7_days`\n- `next_30_days`\n- `next_90_days`\n- `end_of_quarter`\n- `end_of_year`\n- `next_year`\n\n## Forecast Models\n\nUsed for `ui_chart_forecast_model` (in the metric object). `auto` follows whichever model fits the data best.\n\n- `auto`\n- `linear`\n- `exponential`\n- `logarithmic`\n- `logistic`\n- `sinusoidal`\n\n## Metric Data Intervals\n\nUsed for `interval` (in aggregation requests) and `ui_chart_interval` (in the metric object).\n\n- `minute`\n- `hour`\n- `day`\n- `month`\n\n## Metric Time Dimensions\n\nUsed for `time_dimension` (in the metric object). A metric with one returns its aggregation folded instead of as a timeline: every hour of the range merges into its calendar position, read in the request's `timezone`, and each record key is that position as a decimal string.\n\n- `hour_of_day`: Hours `0` to `23`. Breakdown and heatmap charts.\n- `weekday`: ISO weekdays `1` (Monday) to `7` (Sunday). Breakdown and heatmap charts.\n- `hour_of_week`: The 168 hours of a week, `0` to `167`, numbered `(weekday - 1) * 24 + hour`. Heatmap charts.\n\nA fold covers at most 400 days and cannot use a cumulative chart value.\n\n## Chart Orders\n\nUsed for `ui_chart_order` (in the metric object): the order a chart ranks its series in, which an aggregation request's `sort` can override.\n\n- `natural`: Rank series largest first, and keep a time dimension in its own order, such as hour 0 to 23 or Monday to Sunday.\n- `highest`: Rank series largest first.\n- `lowest`: Rank series smallest first.\n\n`null` is `natural`. `value` is accepted on write and reads back as `highest`.\n\n## View Types\n\nHow a [View](/api/views#model) produces its records. It is set when the view is created and cannot change afterwards.\n\n- `transform`: Default. The view reads one collection or one other view through a [TGL](/guides/tgl) script.\n- `join`: The view starts from a base view and attaches rows from its lookup views, matched on a field.\n\n## View Transform Modes\n\nHow a [View](/api/views#model) acquires a [TGL](/guides/tgl) transform for schema versions discovered after it was created.\n\n`auto` and `manual` first try to reuse one of the view's existing scripts. Reuse requires a typed candidate signature and at least one of the script's read paths to be present. A present path whose value is null counts as evidence; an absent path does not. An unknown-only signature is not eligible for reuse. The two differ only when no existing script can read the new shape:\n\n- `auto`: Tailglow generates a script for the new version.\n- `manual`: The version waits for you to supply a compatible script instead of generating one automatically. A supplied script can wait for input before validation.\n- `explicit`: The version takes only the script you set on it. No existing script is reused and none is generated, even when one of the view's scripts could read the new shape, so every new version waits for its own.\n\n## View Transform Conflict Resolutions\n\nControls what happens when editing a collection-backed transform would make the new script incompatible with some schema versions that used the prior script.\n\n- `strand`: Default. Compatible versions adopt the edit; conflicting versions move to queued placeholders so they can receive their own script.\n- `keep`: Compatible versions adopt the edit; conflicting versions remain on the prior script.\n\n## Schema Version States\n\nThe lifecycle of a collection's [schema version](/guides/concepts#schema-versions), from first sight to trust.\n\n- `candidate`: The shape has been seen once. Its records are stored and served immediately by any view mapping that already fits them, but no mapping is written for it yet.\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 are offered to views for authoring, and a settled version never goes back.\n- `stale`: A candidate that was not seen again within the stale window. Its rows stay stored but unclassified. A stale shape that returns settles.\n\n## Schema Settlements\n\nWhat settled a schema version.\n\n- `recurrence`: The shape was seen again after the collection's recurrence gap.\n- `bulk`: One batch carried at least the collection's bulk row count of the shape.\n- `person`: Someone settled the version from the collection page or through the API.\n\n## Schema Rule Modes\n\nHow a collection applies each of its three identity rules: nulls and absent values, optional keys, and the detector. See [Schema settings](/guides/sources#schema-settings).\n\n- `auto`: Tailglow decides. Null and absent values are accepted at any path a mapping reads, a record whose keys are a subset of a version's keys joins that version, and a safe opaque-path proposal from the detector is applied on its own.\n- `manual`: A person decides at each of those points.\n\n## Metric Statuses\n\nThe current status of the metric.\n\n- `initializing`: Metric is being created.\n- `waiting_for_transforms`: Metric is waiting for its source transforms to finish processing.\n- `backfilling`: Historical data is being backfilled (also covers re-backfill triggered by data-feed changes).\n- `active`: Metric is up-to-date and collecting new data.\n- `error`: An error occurred during processing.\n- `cancelled`: The metric, or the view it reads, was deleted while a backfill was still running, so the backfill was cancelled instead of being allowed to finish. Restoring the metric within its 24-hour window leaves it here; re-run the backfill to finish what it missed.\n\n## View Statuses\n\nThe current status of a [View](/api/views#model), derived from its transforms.\n\n- `empty`: The view has no transforms yet.\n- `building`: A transform is waiting for input or processing data, including indexing history.\n- `needs_transform`: A transform script needs to be written or corrected. This can occur in either transform mode.\n- `active`: At least one transform is finished and serving data.\n- `cancelled`: The view was deleted while it was still indexing, so the indexing was cancelled rather than allowed to finish. Deleted views remain readable until permanent removal 24 hours later, and they report this status for that window.\n- `error`: A transform failed to compile, or its indexing failed.\n\n## Metric Values\n\nIndicates which aggregations were calculated in the [Metric Aggregation Model](/api/metrics#aggregation-model).\n\n- `count`\n- `sum`\n- `min`\n- `max`\n\n## Filter Operators\n\nThe canonical operator vocabulary used everywhere filters appear: list endpoint query parameters (`?<field>=<op>:<value>`), metric `filters` (applied at rollup time), facet record queries against indexed views, and the UI filter components.\n\nURL syntax is flat: `?type_1=equals:Fire&limit=50`. Operator names are long-form snake*case except for the industry-standard `gt`/`gte`/`lt`/`lte`/`in`. Every negation has a `not*` prefix. String operators compare case-insensitively (`equals:Get`matches`GET`); numeric, date, and existence operators are unaffected.\n\n| Operator | Semantics |\n| ----------------- | --------------------------------------------- |\n| `equals` | Exact match |\n| `not_equals` | Inverse of `equals` |\n| `gt` / `gte` | Greater than (strict / inclusive) |\n| `lt` / `lte` | Less than (strict / inclusive) |\n| `between` | Inclusive range; value is `min,max` |\n| `contains` | Substring match (strings) |\n| `not_contains` | Inverse of `contains` |\n| `starts_with` | Prefix match (strings) |\n| `not_starts_with` | Inverse of `starts_with` |\n| `ends_with` | Suffix match (strings) |\n| `not_ends_with` | Inverse of `ends_with` |\n| `in` | Scalar field, value in a comma-separated list |\n| `not_in` | Inverse of `in` |\n| `has` | Array field contains the given scalar |\n| `not_has` | Inverse of `has` |\n| `has_any` | Array field intersects a comma-separated list |\n| `exists` | Field has a non-null value |\n| `not_exists` | Field is null or absent |\n\n### Per-type allowlist (UI filter components)\n\n| Field type | Allowed operators |\n| ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |\n| `string` | `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `not_starts_with`, `ends_with`, `not_ends_with`, `in`, `not_in` |\n| `number` | `equals`, `not_equals`, `gt`, `gte`, `lt`, `lte`, `between`, `in`, `not_in` |\n| `date` | `equals`, `not_equals`, `gt`, `gte`, `lt`, `lte`, `between` |\n| `array` | `has`, `not_has`, `has_any` |\n| `boolean` | `equals`, `not_equals` |\n\n### Surface availability\n\nNot every operator is supported by every surface. The metric filter pipeline supports the full vocabulary; CRUD list endpoints reject existence operators in v1 (the per-field nullable-aware validators land separately); facet record queries are limited to the SQLite-indexed subset.\n\n| Operator | Metric filters | CRUD list | Facet records |\n| ----------------- | :------------: | :-------: | :-----------: |\n| `equals` | ✓ | ✓ | ✓ |\n| `not_equals` | ✓ | ✓ | |\n| `gt` | ✓ | ✓ | |\n| `gte` | ✓ | ✓ | ✓ |\n| `lt` | ✓ | ✓ | |\n| `lte` | ✓ | ✓ | ✓ |\n| `between` | ✓ | ✓ | |\n| `contains` | ✓ | ✓ | |\n| `not_contains` | ✓ | ✓ | |\n| `starts_with` | ✓ | ✓ | ✓ |\n| `not_starts_with` | ✓ | ✓ | |\n| `ends_with` | ✓ | ✓ | |\n| `not_ends_with` | ✓ | ✓ | |\n| `in` / `not_in` | ✓ | ✓ | |\n| `has` | ✓ | | |\n| `not_has` | ✓ | | |\n| `has_any` | ✓ | | |\n| `exists` | ✓ | | |\n| `not_exists` | ✓ | | |\n\n### Reserved query parameter keys\n\nSeveral keys are reserved as query-string control parameters across every endpoint. View `output_schema` fields and facet `field_path` values cannot use any of these names:\n\n`limit`, `cursor`, `page`, `sort`, `order_by`, `after`, `before`, `start_at`, `end_at`, `timezone`, `interval`, `search`\n\n## Monitor Statuses\n\nWhether a monitor checks on its schedule ([`status`](/api/monitors#model)).\n\n- `active`: Checks at every scheduled time.\n- `paused`: You stopped the monitor. Pausing ends its open alerts, and resuming starts again at the next scheduled time without checking the paused period. Only you start it again, with Resume Monitor.\n- `disabled`: Tailglow stopped the monitor because its `expires_at` passed. Move the expiry into the future, then resume it.\n\n## Monitor Statistics\n\nWhat a monitor computes over each window ([`statistic`](/api/monitors#model)). Every statistic covers the whole window, and for the `total` scope the whole filtered feed.\n\n- `count`: How many readings arrived. `count` equal to 0 alerts when no data arrives.\n- `sum`: The sum of the readings' values.\n- `average`: The average of the readings' values.\n- `min`: The smallest value.\n- `max`: The largest value.\n- `last`: The most recent reading.\n- `p50`: The median of the values.\n- `p95`: The 95th percentile of the values.\n- `p99`: The 99th percentile of the values.\n- `count_unique`: How many distinct values of the metric's `unique_field` arrived.\n\n## Monitor Operators\n\nHow the statistic is compared with `threshold_value` ([`operator`](/api/monitors#model)).\n\n- `gt`: Above the threshold.\n- `gte`: At or above the threshold.\n- `lt`: Below the threshold.\n- `lte`: At or below the threshold.\n- `eq`: Equal to the threshold.\n- `neq`: Not equal to the threshold.\n\n## Monitor Empty Window Handling\n\nWhat the statistic reads when a window holds no data ([`empty_window_handling`](/api/monitors#model)). The metric's own `empty_bucket_handling` does not affect monitors.\n\n- `zero`: The window reads 0. The only value `count`, `sum` and `count_unique` accept, since an empty window is a measured 0 for them.\n- `no_value`: The window gives nothing to compare, so that check neither opens nor closes an alert. The default for `average`, `min`, `max`, `last` and the percentiles.\n\n## Monitor Window Types\n\nWhich data each check reads ([`window_type`](/api/monitors#model)).\n\n- `rolling`: The `window_minutes` before each scheduled check.\n- `all_time`: Everything from the start of the project up to each check, for totals such as \"signups pass 1 million\". Takes `window_minutes: null`.\n\n## Monitor Scopes\n\nHow the metric's series are checked ([`scope`](/api/monitors#model)).\n\n- `total`: One value for all the watched series combined.\n- `any_series`: One alert that stays open while any series matches. The series that opened it is shown on the alert.\n- `each_series`: A separate alert for every series.\n\n## Monitor Trigger Types\n\nWhen a match opens an alert ([`trigger_type`](/api/monitors#model)).\n\n- `immediate`: The first matching check.\n- `sustained`: Matches that keep holding for `sustained_minutes`. A gap between checks longer than `sustained_minutes` starts the count again.\n- `repeated`: `repeat_count` matching checks within `repeat_window_minutes`. The alert closes once fewer matches remain in a fully checked window and the current check does not match.\n\n## Monitor Alert Modes\n\nHow long an alert stays open ([`alert_mode`](/api/monitors#model)).\n\n- `spanning`: Open until a check finds the condition cleared. The default.\n- `instant`: Opened and ended at once, for a milestone that does not clear on its own. It does not open again until the condition has cleared.\n\n## Monitor Check Statuses\n\nThe outcome of a target's most recent check ([`last_check_status`](/api/monitors#series-state-model)).\n\n- `pending`: Not checked yet.\n- `ready`: The window was complete and the check was applied.\n- `unknown`: The scheduled time was passed over without a verdict, with the reason in `last_check_reason`: the metric is rebuilding (`metric_not_active`) or something it reads from is being deleted (`resource_deleting`).\n- `error`: Reading the window failed. The same scheduled time is checked again on the next run.\n\n## Alert End Reasons\n\nWhy an alert ended ([`end_reason`](/api/alerts#model)). Null while the alert is open. Only `recovered` sends a recovery email.\n\n- `recovered`: A check found the condition cleared.\n- `instant`: An instant alert, which ends as it opens.\n- `monitor_changed`: The monitor's rule was edited.\n- `monitor_paused`: The monitor was paused.\n- `monitor_expired`: The monitor's `expires_at` passed.\n- `resource_deleted`: The metric, or something it reads from, is being deleted.\n- `metric_rebuilt`: The metric was rebuilt from its data.\n- `target_removed`: The series no longer exists.\n\n## Drain Statuses\n\nLifecycle status of a Drain.\n\n- `draft`: Created but never verified. No outbound traffic.\n- `pending_verification`: A verification marker has been sent to the destination and we're waiting for the customer to paste it back via Confirm Verification.\n- `active`: Drain is verified, resumed, and forwarding records on the recurring schedule.\n- `paused`: You stopped the drain, or it was freshly verified (verification intentionally lands paused so you can sample before going live). Only you start it again. Resume via Resume Drain.\n- `disabled`: Tailglow stopped the drain after 72 hours of consecutive failures. Resume the same way after fixing the destination; resuming clears the failure counters.\n\n## Drain Body Formats\n\nWire format for the body of outbound drain POSTs.\n\n- `ndjson`: Newline-delimited JSON. Default. One record per line. Sent with `Content-Type: application/x-ndjson`.\n- `json`: A single JSON array of records. Required by Datadog Logs and similar receivers. Sent with `Content-Type: application/json`.\n\n## Drain Compressions\n\nWire compression for outbound drain POSTs.\n\n- `none`: Default. Body is sent uncompressed and works with any receiver.\n- `gzip`: Body is gzipped and `Content-Encoding: gzip` is added. Receivers MUST decompress `Content-Encoding` for this to work.\n\n## Check Statuses\n\nLifecycle status of a Check ([`status`](/api/checks#model)).\n\n- `active`: Running on its schedule.\n- `paused`: You stopped the check. Nothing is recorded for the minutes a pause covers, so the uptime record shows a gap rather than downtime. Start it again with Resume Check.\n- `disabled`: Tailglow stopped the check because the team is no longer in good standing. A failing endpoint never lands here, however long the failure lasts, because the outage is the thing the check exists to record. Resume the same way once the account is settled.\n\n## Check Methods\n\nThe HTTP method a check uses to request its endpoint ([`method`](/api/checks#model)). Both are observational, so a check can never change anything at the endpoint it watches.\n\n- `get`: Default.\n- `post`: For health endpoints that only answer to POST. Only a `post` check may send a `request_body`.\n\n## Check Error Stages\n\nWhich step of the most recent failed run went wrong ([`last_error_stage`](/api/checks#model)). A check never reads the response body, so there is no parse stage.\n\n- `request`: No usable response arrived. The address was refused by the outbound guard, the request timed out, TLS failed, the connection was reset, or the body ran past the size cap before it finished.\n- `response`: The endpoint answered, but not with a 2xx status.\n\n## Appearance Palettes\n\nThe color palette for a user's app appearance ([`appearance_palette`](/api/users#model)). Sets the light or dark scheme.\n\n- `light`\n- `dark`\n- `dim`: Default. A softer dark scheme.\n- `midnight`\n- `paper`\n\n## Appearance Type Sets\n\nThe font pairing for a user's app appearance ([`appearance_type_set`](/api/users#model)).\n\n- `system`: Default. The operating system's native font stack.\n- `grotesk`\n- `editorial`\n- `geometric`\n\n## Appearance Accents\n\nThe accent color for a user's app appearance ([`appearance_accent`](/api/users#model)).\n\n- `orange`: Default.\n- `azure`\n- `burgundy`\n- `ink`\n- `emerald`\n- `violet`\n\n## Appearance Scales\n\nThe UI scale (density) for a user's app appearance ([`appearance_scale`](/api/users#model)). Applies at tablet and desktop widths; mobile always renders at full size.\n\n- `comfortable`: 100% scale.\n- `cozy`: 90% scale.\n- `compact`: Default. 80% scale, fits more on screen.\n\n## Ingest Pipeline Error Reasons\n\nThe reasons behind the Errors series on a server's Ingest Pipeline chart. Every reason is a refusal issued before acceptance: the sender received an error response and nothing was recorded from that request. Accepted data is never dropped. It is queued on the server and retried until it lands.\n\nReasons where retrying the same request delivers the data. Tailglow SDKs retry these automatically; if you send raw HTTP, retry on any 5xx response:\n\n- `connection_closed`: The sender closed the connection before the upload finished.\n- `server_error`: An unexpected error while handling the request.\n- `spool_full_disk_gate`: The server paused intake to protect its disk from filling.\n- `spool_full_storage_write`: A buffer write failed while accepting the request.\n- `spool_full_ingest_queue`: The intake queue was momentarily full.\n- `spool_full_wal_unavailable`: The write buffer was restarting.\n- `auth_unavailable`: The ingest key could not be verified because the verification service was unreachable. The key itself was not rejected.\n\nReasons where the request itself must change. Retrying the same request is refused again:\n\n- `auth_missing_credentials`: No ingest key was provided.\n- `auth_invalid_credentials`: The ingest key is wrong or revoked.\n- `payload_too_large`: The payload exceeds the size limit.\n- `request_validation_failed`: The request body or parameters are invalid.\n- `frame_invalid`: A frame in the payload could not be parsed.\n- `empty_payload`: The request carried no data.\n- `source_deleted`: The target source no longer exists.\n",
19
19
  "api/projects": "# Projects\n\n## Project Model\n\n### Fields\n\n- **`object`** `\"project\"`\n\n- **`id`** `string`\n\n Project identifier prefixed with `prj_`.\n\n- **`team_id`** `string`\n\n- **`team_name`** `string`\n\n- **`name`** `string`\n\n- **`ambiguity_handling`** [`AmbiguityHandlingMode`](/api/projects#ambiguity-handling-mode)\n\n What new mappings do when the source data allows more than one defensible reading: `decide`\n picks the most defensible one, `ask` asks you for a hint. Existing scripts are unchanged.\n\n- **`start_at`** [`ISODateString`](/api/projects#iso-date-string)\n\n Metrics collection begins at this time.\n\n- **`created_at`** [`ISODateString`](/api/projects#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/projects#iso-date-string)\n\n- **`deleted_at`** [`ISODateString | null`](/api/projects#iso-date-string)\n\n Scheduled deletion time; `null` when deletion is not scheduled.\n\n- **`last_activity_at`** [`ISODateString | null`](/api/projects#iso-date-string)\n\n Most recent authenticated ingest time; `null` if none has occurred.\n\n- **`late_data_window_minutes`** `number`\n\n How many minutes behind the completeness watermark an event with an overridden timestamp may\n arrive and still be included automatically in older metrics that have not been rebuilt. New and\n rebuilt metrics include accepted late input automatically at any age. For older metrics, 0\n holds late arrivals for manual re-backfill.\n\n- **`server_endpoint`** `string | null`\n\n Project-specific ingest endpoint; `null` while unavailable.\n\n- **`server_count`** `number`\n\n Servers the project runs, including ones that are starting up or paused. Servers being removed\n are not counted.\n\n- **`queued_server_count`** `number | null`\n\n Server count a scale request asked for that is waiting to be applied, because a platform update\n is in progress or the project has not finished updating to the current platform version. `null`\n when nothing is queued.\n\n- **`storage_gb`** `number`\n\n Stored data for this project, in gigabytes of uncompressed data: raw ingested files and\n artifacts, plus view output and minute-level rollups. Recalculated hourly; `billing_updated_at`\n is the last refresh.\n\n- **`billing_period_server_months`** `number`\n\n Server-months this project's servers have accrued in the current calendar-month billing period,\n counted up to the latest hourly billing pass. Every pass recounts it, including an hour that\n keeps the previous `storage_gb`.\n\n- **`billing_updated_at`** [`ISODateString | null`](/api/projects#iso-date-string)\n\n When `storage_gb` was last recalculated; `null` before the first calculation. An hour whose\n recalculation could not read the project's data keeps the previous size and this time.\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#### AmbiguityHandlingMode\n\n`\"decide\" | \"ask\"`\n\n## Late Data Model\n\n### Fields\n\n- **`object`** `\"late_data_file\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `ldf_`.\n\n- **`view_id`** `string`\n\n- **`view_name`** `string`\n\n Display name of the view whose data carried the late events.\n\n- **`excluded_count`** `number`\n\n How many events were kept out of charts because their timestamps fell behind the project's late\n data window. The events themselves are stored; only their aggregation waits.\n\n- **`min_event_at`** [`ISODateString`](/api/projects#iso-date-string)\n\n The earliest event time among the excluded events. A re-backfill folds back to this instant.\n\n- **`max_event_at`** [`ISODateString`](/api/projects#iso-date-string)\n\n The latest event time among the excluded events.\n\n- **`exclusion_below`** [`ISODateString`](/api/projects#iso-date-string)\n\n The boundary the events fell behind. Events at or after it were included normally.\n\n- **`refold_queued_at`** [`ISODateString | null`](/api/projects#iso-date-string)\n\n When a re-backfill was requested for these events; null until requested.\n\n- **`refolded_at`** [`ISODateString | null`](/api/projects#iso-date-string)\n\n When the re-backfill finished folding the events into charts; null until complete.\n\n- **`created_at`** [`ISODateString`](/api/projects#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## Live Pipeline Model\n\n### Fields\n\n- **`object`** `\"project_pipeline_live\"`\n\n- **`observed_at`** [`ISODateString`](/api/projects#iso-date-string)\n\n- **`pending_records`** `number | null`\n\n Valid spooled records awaiting write, including processing. Null when the queue is unknown.\n\n- **`processing_records`** `number | null`\n\n Pending records currently being processed.\n\n- **`pending_observed_at`** [`ISODateString | null`](/api/projects#iso-date-string)\n\n Oldest contributing queue observation, independent of the server heartbeat.\n\n- **`pending_is_estimated`** `boolean`\n\n The queue changed during inspection; available counts are estimates from durable records.\n\n- **`state`** `\"fresh\" | \"stale\" | \"unavailable\"`\n\n Queue freshness across all current project servers, including deployment surges.\n\n- **`contributing_servers`** `number`\n\n- **`fresh_servers`** `number`\n\n- **`window_start_at`** [`ISODateString`](/api/projects#iso-date-string)\n\n Inclusive start of the last completed minute.\n\n- **`window_end_at`** [`ISODateString`](/api/projects#iso-date-string)\n\n Exclusive end of the last completed minute.\n\n- **`throughput_records`** `number | null`\n\n Records processed in the completed minute. Null when any contributor lacks telemetry.\n\n- **`errors`** `number | null`\n\n Rejected requests and dropped frames in the completed minute, not a record count.\n\n- **`pipeline_error_counts`** `Partial | null`\n\n Map from each `PipelineErrorReason` key to its error count for the completed minute. Omitted\n keys had no errors; `null` means telemetry is incomplete.\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#### PipelineErrorReason\n\n`\"auth_missing_credentials\" | \"auth_invalid_credentials\" | \"payload_too_large\" | \"request_validation_failed\" | \"server_error\" | \"spool_full_disk_gate\" | \"spool_full_storage_write\" | \"spool_full_ingest_queue\" | \"spool_full_wal_unavailable\" | \"frame_invalid\" | \"empty_payload\" | \"source_deleted\" | \"connection_closed\" | \"auth_unavailable\"`\n\n## Usage Sample Model\n\n### Fields\n\n- **`object`** `\"usage_sample\"`\n\n- **`sampled_at`** [`ISODateString`](/api/projects#iso-date-string)\n\n When the reading was taken. Readings are hourly, on the hour. With `interval=day` this is the\n timestamp of the day's last reading, not midnight.\n\n- **`collection_gb`** `number`\n\n Gigabytes of raw event data held in the project's collections, measured uncompressed, exactly\n as it is billed.\n\n- **`artifact_gb`** `number`\n\n Gigabytes of artifacts extracted from the project's events, measured uncompressed.\n\n- **`view_gb`** `number`\n\n Gigabytes of view records the project's views produced, measured uncompressed.\n\n- **`rollup_gb`** `number`\n\n Gigabytes of rollup data behind the project's charts, measured uncompressed. Counts the minute\n tier only, matching what invoices charge for.\n\n- **`total_gb`** `number`\n\n The four components added together: everything the project stored at this reading.\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## Attention Item Model\n\n### Fields\n\n- **`object`** `\"attention_item\"`\n\n- **`type`** [`AttentionItemType`](/api/projects#attention-item-type)\n\n- **`tier`** [`AttentionTier`](/api/projects#attention-tier)\n\n- **`resource_id`** `string | null`\n\n The resource to open to act on the condition: the metric behind a firing monitor, the view\n behind a failing transform, otherwise the check, view, metric, drain, pull or server itself.\n Null when the item covers the project as a whole, which is the case for refused requests.\n\n- **`resource_name`** `string | null`\n\n The name shown for the condition, which can belong to a related resource: a firing monitor's\n item names the monitor while `resource_id` opens its metric. Null whenever `resource_id` is\n null.\n\n- **`count`** `number | null`\n\n How much of the condition there is, in the unit that fits it: requests refused (not records)\n over the last 24 hours, or the last hour for capacity, series a monitor is firing on, a view's\n failing transforms, events waiting on a shape change, consecutive failures of a check, pull, or\n drain, or a hot server's average load in percent. Null when the condition has no quantity.\n\n- **`since`** [`ISODateString | null`](/api/projects#iso-date-string)\n\n When the condition started, as accurately as Tailglow knows. Null when nothing on record marks\n a beginning: a check that has never once succeeded, or a server under sustained load, which is\n judged from an average rather than a start.\n\n- **`ended_at`** [`ISODateString | null`](/api/projects#iso-date-string)\n\n When the condition stopped, for a `resolved` item: to the minute within the last hour, to the\n end of the hour before that. Null while the condition is still true, including while a server\n that was refusing has not reported since.\n\n- **`detail`** `string | null`\n\n A short machine-readable qualifier that narrows the type: the refusal reason group, the firing\n monitor's id, or `cpu` or `mem` for the load that made a server hot. A stable identifier meant\n for branching in code, never a sentence to display. Null when the type needs no qualifier.\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#### AttentionItemType\n\n`\"ingest_refusals\" | \"capacity_refusals\" | \"monitor_firing\" | \"check_down\" | \"transform_error\" | \"metric_error\" | \"join_error\" | \"shape_deferral\" | \"pull_suspended\" | \"drain_suspended\" | \"drain_unverified\" | \"servers_hot\"`\n\nThe condition an attention item reports. Every value is a state Tailglow entered on its own, so\nnothing a project deliberately turned off is ever reported here.\n\n#### AttentionTier\n\n`\"losing_data\" | \"firing\" | \"stalled\" | \"suspended\" | \"resolved\"`\n\nHow urgent an attention item is, most urgent first. `losing_data` means requests are being\nrefused right now in a way that loses data unless the sender acts, or were until a server that\nwas refusing stopped reporting. `firing` means something the project asked to be told about is\ncurrently true, or a server has run under sustained heavy load. `stalled` means a pipeline\nstopped making progress and existing data is going stale. `suspended` means Tailglow switched\nsomething off and it stays off until it is fixed. `resolved` means refusals that lost data have\nstopped; the item stays for a while so the loss is still visible, and says when it ended.\n\n## List Projects\n\n### Endpoint\n\nRetrieve a list of projects for the current team.\n\n```http\nGET /v1/projects\n```\n\n**Scope:** `projects:read`\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the projects. Optional. Defaults to `\"name\"`. 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: Project[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- When neither `sort` nor `order_by` is provided, the route defaults `sort` to `\"asc\"`.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Project\n\n### Endpoint\n\nRetrieve a single project.\n\n```http\nGET /v1/projects/:project_id\n```\n\n**Scope:** `projects:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Response\n\nProject retrieved\n\n```ts\n{\n message: string;\n data: Project;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Retrieve Project Pipeline\n\n### Endpoint\n\nRetrieve ingest activity across all of a project's servers.\n\n```http\nGET /v1/projects/:project_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### Query Parameters\n\n- **`start_at`** [`ISODateString`](/api/projects#iso-date-string)\n Start time (ISO format, default: 12 hours ago). Optional.\n\n- **`end_at`** [`ISODateString`](/api/projects#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\nProject pipeline 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- Includes temporary deployment servers and their archived history. Pending averages valid queued records; throughput counts drained records. Older pending history without record counts is unknown.\n\n## Retrieve Live Project Pipeline\n\n### Endpoint\n\nRetrieve the current project queue and ingest totals for the last completed minute.\n\n```http\nGET /v1/projects/:project_id/pipeline/live\n```\n\n**Scope:** `servers:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Response\n\nLive project pipeline retrieved\n\n```ts\n{\n message: string;\n data: ProjectPipelineLive;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Includes temporary deployment servers. Queue freshness is independent of heartbeat freshness. Observations taken while queues change return estimated counts from durable records; unreadable observations or unknown write outcomes return null counts.\n- Throughput and error totals cover the last completed minute. Errors count requests or frames, and totals are null when telemetry is incomplete.\n\n## Create Project\n\n### Endpoint\n\nCreate a new project for the current team.\n\n```http\nPOST /v1/projects\n```\n\n**Scope:** `projects:write`\n\n### Request Body\n\n- **`name`** `string` -- **Required**\n Display name for the project. Minimum length: `1`. Maximum length: `60`.\n\n- **`start_at`** [`ISODateString`](/api/projects#iso-date-string)\n Date and time when the project began collecting data. Optional.\n\n### Response\n\nYour project has been created.\n\n```ts\n{\n message: string;\n data: Project;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Update Project\n\n### Endpoint\n\nUpdate an existing project.\n\n```http\nPOST /v1/projects/:project_id\n```\n\n**Scope:** `projects:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Request Body\n\n- **`ambiguity_handling`** [`AmbiguityHandlingMode`](/api/projects#ambiguity-handling-mode)\n What new mappings do when the source data allows more than one defensible reading. decide picks the most defensible one; ask requests a hint from you instead. Defaults to decide. Existing scripts are unchanged. Optional.\n\n- **`name`** `string`\n Display name for the project. Optional. Minimum length: `1`. Maximum length: `60`.\n\n- **`start_at`** [`ISODateString`](/api/projects#iso-date-string)\n Date and time when the project began collecting data. Optional.\n\n- **`late_data_window_minutes`** `integer`\n Late-arrival window for older metrics that have not been rebuilt. New and rebuilt metrics automatically include accepted late input at any age. For older metrics, events outside this window are kept for manual re-backfill; 0 holds all late arrivals. Maximum 129600 (90 days). Optional. Minimum: `0`. Maximum: `129600`.\n\n- **`deleted_at`** `null`\n Set to null to cancel a scheduled project deletion. Optional.\n\n### Response\n\nYour project has been updated.\n\n```ts\n{\n message: string;\n data: Project;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Delete Project\n\n### Endpoint\n\nSchedule a project for deletion.\n\n```http\nDELETE /v1/projects/:project_id\n```\n\n**Scope:** `projects:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Response\n\n```ts\n{\n message: string;\n data: Project;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The project is hidden immediately and permanently removed 7 days later. Restore it before then by updating it with `deleted_at` set to null.\n\n## Force Delete Project\n\n### Endpoint\n\nDelete a project permanently, without waiting out its restore window.\n\n```http\nDELETE /v1/projects/:project_id/force\n```\n\n**Scope:** `projects:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Response\n\nProject queued for permanent deletion.\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- Works on a live project as well as one already scheduled for deletion.\n- Every source, collection, view, metric and record in the project is destroyed immediately, and its servers are torn down. Nothing here can be restored.\n\n## List Late Data\n\n### Endpoint\n\nRetrieve the events held out of charts by the project's late data window, grouped per data file.\n\n```http\nGET /v1/projects/:project_id/late_data\n```\n\n**Scope:** `projects: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 late data records. Optional. Defaults to `\"created_at\"`. Allowed values: `\"created_at\"`, `\"min_event_at\"`.\n\n- **`refolded`** `string`\n Filter by re-backfill state: false returns records whose events are still excluded from charts, true returns records a completed re-backfill has folded in. Optional. Allowed values: `\"true\"`, `\"false\"`.\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: LateDataFile[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Events with overridden timestamps beyond the late data window are stored and counted here instead of entering charts. Data sent live is never held.\n- Records with `refolded_at` set have already been folded into charts by a completed re-backfill; filter with `refolded=false` for records still waiting.\n- `after` and `before` are mutually exclusive.\n\n## Re-backfill Late Data\n\n### Endpoint\n\nStart a re-backfill that folds one late data record's events into charts.\n\n```http\nPOST /v1/projects/:project_id/late_data/:late_data_file_id/re_backfill\n```\n\n**Scope:** `projects:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`late_data_file_id`** `string` -- **Required**\n Unique identifier of the late data.\n\n### Response\n\nRe-backfill queued. The events will appear in charts as the fold completes.\n\n```ts\n{\n message: string;\n data: LateDataFile;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Idempotent: requesting a re-backfill that is already queued or complete returns the record unchanged.\n- The fold is bounded to the excluded events, so data already in charts is never counted twice. `refolded_at` is set once the fold's durable receipts land.\n\n## Retrieve Project Usage\n\n### Endpoint\n\nRetrieve how much the project stored over a time range.\n\n```http\nGET /v1/projects/:project_id/usage\n```\n\n**Scope:** `projects:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Query Parameters\n\n- **`start_at`** [`ISODateString`](/api/projects#iso-date-string) -- **Required**\n Start of the range, inclusive (ISO format).\n\n- **`end_at`** [`ISODateString`](/api/projects#iso-date-string)\n End of the range, inclusive (ISO format, default: now). Optional.\n\n- **`interval`** `string`\n Reading cadence returned: every hourly reading, or one reading per UTC day. Optional. Defaults to `\"hour\"`. Allowed values: `\"hour\"`, `\"day\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: UsageSample[];\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Readings are taken hourly and are the same measurements the project is billed on. Gigabytes are logical (uncompressed) bytes.\n- `interval=day` returns the last reading of each UTC day rather than an average, so a row is what the project held when the day closed. Its `sampled_at` is that reading's own hour.\n- Readings start when Tailglow began recording them, so a range reaching further back returns nothing for the hours before that.\n- `start_at` must be earlier than `end_at`, and the range cannot be longer than 400 days.\n\n## Retrieve Project Attention\n\n### Endpoint\n\nRetrieve everything in the project that is asking for a human right now.\n\n```http\nGET /v1/projects/:project_id/attention\n```\n\n**Scope:** `projects:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Response\n\n```ts\n{\n message: string;\n data: AttentionItem[];\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- An item appears only when Tailglow entered a state on its own. Anything a project chose, such as a paused pull, a paused drain, or a paused monitor, is never reported here.\n- `tier` orders the list. `losing_data` means requests are being refused right now, within the last 5 minutes: for a reason a retry cannot fix, or for capacity after at least 5 minutes of unbroken refusals. `firing` means a monitor or check the project set up is currently triggered, or a server has run under sustained heavy load. `stalled` means a pipeline stopped making progress and its data is going stale. `suspended` means Tailglow switched something off and it stays off until it is fixed. `resolved` means refusals that lost data have stopped; `ended_at` says when.\n- Items disappear the moment their condition clears, with one exception so a loss is not missed: refusals that stop stay as `resolved`, for up to 24 hours for a key or payload refusal and up to an hour for capacity. A refusal item's `count` covers that whole window, not only the current spell. Refusals only count as stopped once every running server that was refusing has reported since; until then the item stays `losing_data`. Readings from the project's servers are cached briefly, so a condition that just started can take up to a minute to appear.\n- Items cover only resources the caller's key or role can read: without a resource's read permission, its conditions are left out. Refusal and server load items need `servers:read`, and firing monitors need `alerts:read`.\n- Items are already ranked, most urgent first. One item covers each firing monitor, and each view's failing transforms, joins, or shape waits. The response is capped per condition so a project in a bad state returns a readable list rather than every affected resource.\n\n",
20
20
  "api/sources": "# Sources\n\n## Source Model\n\n### Fields\n\n- **`object`** `\"source\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `src_`.\n\n- **`project_id`** `string`\n\n- **`project_name`** `string`\n\n- **`name`** `string`\n\n Unique within its project.\n\n- **`storage_gb`** `number`\n\n Data this source has ingested, in gigabytes of uncompressed data: collection files plus\n uploaded artifacts. Recalculated hourly.\n\n- **`schema_count`** `number`\n\n How many schema versions this source has produced.\n\n- **`collections_count`** `number`\n\n How many collections this source feeds.\n\n- **`deleted_at`** [`ISODateString | null`](/api/sources#iso-date-string)\n\n When the source is scheduled to be deleted; null when it is not.\n\n- **`created_at`** [`ISODateString`](/api/sources#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/sources#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 Sources\n\n### Endpoint\n\nRetrieve a list of sources for a project.\n\n```http\nGET /v1/projects/:project_id/sources\n```\n\n**Scope:** `sources: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 sources. Optional. Defaults to `\"name\"`. Allowed values: `\"created_at\"`, `\"name\"`, `\"updated_at\"`.\n\n- **`project_id`** `string`\n Filter to sources belonging to a single project. Optional.\n\n- **`deleted_at`** [`NullableDateFilter`](/api/sources#nullable-date-filter)\n Filter by scheduled deletion date. Use `null` for sources that are not scheduled for deletion, `not:null` for sources that are. Optional.\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: Source[];\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 Source\n\n### Endpoint\n\nRetrieve a single source.\n\n```http\nGET /v1/projects/:project_id/sources/:source_id\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`source_id`** `string` -- **Required**\n Unique identifier of the source.\n\n### Response\n\nSource retrieved\n\n```ts\n{\n message: string;\n data: Source;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create Source\n\n### Endpoint\n\nCreate a source in a project.\n\n```http\nPOST /v1/projects/:project_id/sources\n```\n\n**Scope:** `sources: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 source. Minimum length: `1`. Maximum length: `128`.\n\n- **`project_id`** `string`\n Project the source belongs to. Ignored when the nested path supplies it. Optional.\n\n### Response\n\nSource created\n\n```ts\n{\n message: string;\n data: Source;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The project is supplied by the path.\n- Source names are unique within a project.\n\n## Update Source\n\n### Endpoint\n\nUpdate a source.\n\n```http\nPOST /v1/projects/:project_id/sources/:source_id\n```\n\n**Scope:** `sources:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`source_id`** `string` -- **Required**\n Unique identifier of the source.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the source. Optional. Minimum length: `1`. Maximum length: `128`.\n\n- **`deleted_at`** `null`\n Send `null` to restore a source that is scheduled for deletion. The deletion date itself is set by the delete endpoint and cannot be written here. Optional.\n\n### Response\n\nSource updated\n\n```ts\n{\n message: string;\n data: Source;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Send `deleted_at` as `null` to restore a source that is scheduled for deletion.\n\n## Delete Source\n\n### Endpoint\n\nSchedule a source for deletion.\n\n```http\nDELETE /v1/projects/:project_id/sources/:source_id\n```\n\n**Scope:** `sources:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`source_id`** `string` -- **Required**\n Unique identifier of the source.\n\n### Response\n\n```ts\n{\n message: string;\n data: Source;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The source is hidden immediately and permanently removed 7 days later. Restore it before then by updating it with `deleted_at` set to null.\n\n## Force Delete Source\n\n### Endpoint\n\nDelete a source permanently, without waiting out its restore window.\n\n```http\nDELETE /v1/projects/:project_id/sources/:source_id/force\n```\n\n**Scope:** `sources:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`source_id`** `string` -- **Required**\n Unique identifier of the source.\n\n### Response\n\nSource queued for permanent deletion.\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- Works on a live source as well as one already scheduled for deletion.\n- Every collection, record and file under the source is destroyed immediately. Nothing here can be restored.\n\n",
21
21
  "api/ingest_keys": "# Ingest Keys\n\n## Ingest Key Model\n\n### Fields\n\n- **`object`** `\"ingest_key\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `ik_`.\n\n- **`source_id`** `string`\n\n The source this key writes into.\n\n- **`source_name`** `string | null`\n\n- **`url`** `string | null`\n\n The Tailglow-issued address to send records to, ready to use. POST your payload to it and\n append `&collection=<name>` to choose where the records land; a collection is created the first\n time data arrives for it. Lasts as long as the project does. Null until the project has a\n provisioned server, which is the only window in which no address reaches it.\n\n- **`custom_urls`** `string[]`\n\n The same source reached through the team's verified custom ingest domains, one entry each,\n every one carrying the key exactly as `url` does. Empty when none are configured. Prefer these\n for browser traffic, where a first-party host survives the blockers that reject a shared\n analytics domain. An entry disappears if its domain is removed or stops resolving.\n\n- **`key`** `string`\n\n The credential embedded in `url`. Publishable, in the sense that it belongs in client-side code\n the way an analytics site tag does. It permits writing records into this source and nothing\n else: it cannot read, and it carries no access to anything outside this source.\n\n- **`status`** [`IngestKeyStatus`](/api/ingest_keys#ingest-key-status)\n\n Whether records sent to this URL are still accepted.\n\n- **`created_at`** [`ISODateString`](/api/ingest_keys#iso-date-string)\n\n- **`last_used_at`** [`ISODateString | null`](/api/ingest_keys#iso-date-string)\n\n When data was last received at this URL; null if it has never been used.\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#### IngestKeyStatus\n\n`\"active\" | \"revoked\" | \"disabled\"`\n\n## List Ingest Keys\n\n### Endpoint\n\nRetrieve every address that can send records to a source.\n\n```http\nGET /v1/projects/:project_id/sources/:source_id/ingest_keys\n```\n\n**Scope:** `ingest_keys:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`source_id`** `string` -- **Required**\n Unique identifier of the source.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field used to order the ingest keys. Optional. Defaults to `\"created_at\"`. Allowed values: `\"created_at\"`, `\"status\"`.\n\n- **`status`** [`IngestKeyStatus`](/api/ingest_keys#ingest-key-status)\n Filter by whether the URL still accepts records. Optional.\n\n- **`project_id`** `string`\n Filter to URLs whose source belongs to a project. Optional.\n\n- **`source_id`** `string`\n Filter to URLs belonging to a single 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: IngestKey[];\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 Ingest Key\n\n### Endpoint\n\nRetrieve a single ingest key.\n\n```http\nGET /v1/projects/:project_id/sources/:source_id/ingest_keys/:ingest_key_id\n```\n\n**Scope:** `ingest_keys:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`source_id`** `string` -- **Required**\n Unique identifier of the source.\n\n- **`ingest_key_id`** `string` -- **Required**\n Unique identifier of the ingest key.\n\n### Response\n\nIngest key retrieved\n\n```ts\n{\n message: string;\n data: IngestKey;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create Ingest Key\n\n### Endpoint\n\nCreate an ingest key for a source. Use this to rotate: create the replacement, deploy it, then delete the old one.\n\n```http\nPOST /v1/projects/:project_id/sources/:source_id/ingest_keys\n```\n\n**Scope:** `ingest_keys:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`source_id`** `string` -- **Required**\n Unique identifier of the source.\n\n### Request Body\n\n- **`source_id`** `string`\n Source the URL writes into. Ignored when the nested path supplies it. Optional.\n\n### Response\n\nIngest key created.\n\n```ts\n{\n message: string;\n data: IngestKey;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The source is supplied by the path.\n- The source has to belong to your team.\n- The full `key` is returned on create and never again. Store it when you receive it.\n\n## Delete Ingest Key\n\n### Endpoint\n\nDelete an ingest key. Records already sent to it are unaffected; the address stops being accepted.\n\n```http\nDELETE /v1/projects/:project_id/sources/:source_id/ingest_keys/:ingest_key_id\n```\n\n**Scope:** `ingest_keys:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`source_id`** `string` -- **Required**\n Unique identifier of the source.\n\n- **`ingest_key_id`** `string` -- **Required**\n Unique identifier of the ingest key.\n\n### Response\n\nIngest key 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- Deletion is immediate. Anything still sending with this key starts failing straight away.\n\n",
22
- "api/collections": "# Collections\n\n## Collection Model\n\n### Fields\n\n- **`object`** `\"collection\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `col_`.\n\n- **`source_id`** `string`\n\n- **`source_name`** `string | null`\n\n- **`project_id`** `string`\n\n- **`slug`** `string`\n\n Permanent identifier used in endpoints. Cannot be changed after creation.\n\n- **`name`** `string`\n\n- **`storage_gb`** `number`\n\n Data stored in this collection's files, in gigabytes of uncompressed data. Recalculated hourly.\n\n- **`schema_count`** `number`\n\n How many schema versions this collection has, in every state.\n\n- **`views_count`** `number`\n\n How many views read from this collection.\n\n- **`drains_count`** `number`\n\n How many drains export from this collection.\n\n- **`schema_opaque_paths`** `string[][]`\n\n Object paths whose contents do not take part in schema identity, one key segment per entry.\n Records that differ only inside an opaque path share one schema version. Changing this re-keys\n the collection.\n\n- **`schema_tracked_paths`** `string[][]`\n\n Children of opaque paths that stay part of schema identity. Changing this re-keys the\n collection.\n\n- **`schema_max_depth`** `number`\n\n How many levels of nesting take part in schema identity. Changing this re-keys the collection.\n\n- **`schema_nulls`** [`SchemaRuleMode`](/api/collections#schema-rule-mode)\n\n `auto` accepts null and absent values where a mapping reads; `manual` waits for a person per\n view.\n\n- **`schema_optional_keys`** [`SchemaRuleMode`](/api/collections#schema-rule-mode)\n\n `auto` lets a record with a subset of a version's keys join it; `manual` treats absence as a\n new shape.\n\n- **`schema_detector`** [`SchemaRuleMode`](/api/collections#schema-rule-mode)\n\n `auto` applies a safe opaque-path proposal on its own; `manual` holds every proposal for a\n person.\n\n- **`schema_recurrence_gap_ms`** `number`\n\n How long after first sight a shape must recur before its version settles, in milliseconds.\n\n- **`schema_bulk_rows`** `number`\n\n A single batch carrying this many rows of a shape settles its version at once; `0` disables it.\n\n- **`schema_stale_after_ms`** `number`\n\n How long a candidate version waits to recur before it goes stale, in milliseconds.\n\n- **`schema_identity_limit`** `number`\n\n How many settled versions the collection may hold; past it, new shapes stay unclassified.\n\n- **`schema_candidate_pool`** `number`\n\n How many candidate and stale versions the collection may hold; past it, new shapes stay\n unclassified.\n\n- **`schema_alias_pool`** `number`\n\n How many exact signature hashes the collection caches for lookup.\n\n- **`schema_writer_limit`** `number`\n\n How many collection files one ingest batch may open for this collection. Shapes past the\n candidate pool get the same number of files per batch, the shapes with the most rows first; the\n rows of shapes past that budget stay unclassified and cannot be linked to a version later.\n\n- **`schema_unclassified_retention_days`** `number | null`\n\n How many days rows without a settled version are kept; null keeps them forever.\n\n- **`deleted_at`** [`ISODateString | null`](/api/collections#iso-date-string)\n\n When the collection is scheduled to be deleted; null when it is not.\n\n- **`created_at`** [`ISODateString`](/api/collections#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/collections#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#### SchemaRuleMode\n\n`\"auto\" | \"manual\"`\n\n## Collection Schema Report\n\n### Fields\n\n- **`object`** `\"collection_schema_report\"`\n\n- **`collection_id`** `string`\n\n- **`rules`** `{ opaque_paths: string[][]; tracked_paths: string[][]; max_depth: number; }`\n\n The rules the report reads the collection's shapes under: the collection's own for the\n retrieve, the rules sent for a preview.\n\n- **`shapes`** `{ before: number; after: number; }`\n\n How many shapes the collection holds under its current rules (`before`) and under the report's\n rules (`after`). Equal unless the report previews a rule change; the difference is what a\n re-key would merge.\n\n- **`proposals`** [`CollectionSchemaProposal[]`](/api/collections#schema-proposal)\n\n Where the detector sees keys being invented, with what applying each proposal would do.\n\n- **`keys_look_like_data`** `boolean`\n\n Whether every shape's top-level keys are its own, such as records keyed by a date. Those keys\n are values, and no opaque path can bring the shapes together.\n\n- **`unclassified`** `{ shapes: number; rows: number; }`\n\n Shapes past the collection's pools, stored with their signature but no version, and the rows\n they carry. Nothing reads them until the pool frees or a rule brings them under a version.\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## Collection Schema Proposal\n\n### Fields\n\n- **`object`** `\"collection_schema_proposal\"`\n\n- **`path`** `string[]`\n\n The path the detector proposes opaque, as key segments.\n\n- **`tracked`** `string[]`\n\n Children of the path every shape carries with one type, which stay in schema identity as\n tracked paths when the proposal is applied.\n\n- **`content_children`** `number`\n\n How many children of the path read as content: keys that keep being invented.\n\n- **`read_by_mapping`** `boolean`\n\n Whether a bound mapping reads inside the path. Applying the proposal would leave that mapping\n unverified, so the detector never applies such a proposal on its own; a person can.\n\n- **`shapes_after`** `number`\n\n How many shapes the collection would hold with this proposal applied.\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 Collections\n\n### Endpoint\n\nRetrieve a list of collections for a project.\n\n```http\nGET /v1/projects/:project_id/collections\n```\n\n**Scope:** `sources: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 collections. Optional. Defaults to `\"name\"`. Allowed values: `\"created_at\"`, `\"name\"`, `\"updated_at\"`.\n\n- **`source_id`** `string`\n Filter to collections under a single source. Optional.\n\n- **`project_id`** `string`\n Filter to collections in a single project. Optional.\n\n- **`deleted_at`** [`NullableDateFilter`](/api/collections#nullable-date-filter)\n Filter by scheduled deletion date. Use `null` for collections that are not scheduled for deletion, `not:null` for collections that are. Optional.\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: Collection[];\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 Collection\n\n### Endpoint\n\nRetrieve a single collection.\n\n```http\nGET /v1/projects/:project_id/collections/:collection_id\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Response\n\nCollection retrieved\n\n```ts\n{\n message: string;\n data: Collection;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create Collection\n\n### Endpoint\n\nCreate a collection under a source.\n\n```http\nPOST /v1/projects/:project_id/collections\n```\n\n**Scope:** `sources: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 collection. Minimum length: `1`. Maximum length: `128`.\n\n- **`slug`** `string`\n Permanent identifier used in ingest URLs. Derived from `name` when omitted. Optional. Minimum length: `1`. Maximum length: `64`.\n\n- **`source_id`** `string` -- **Required**\n Source the collection is created under.\n\n### Response\n\nCollection created\n\n```ts\n{\n message: string;\n data: Collection;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `source_id` is required.\n- `slug` is permanent. It appears in the ingest URL, so pick it deliberately.\n\n## Update Collection\n\n### Endpoint\n\nUpdate a collection.\n\n```http\nPOST /v1/projects/:project_id/collections/:collection_id\n```\n\n**Scope:** `sources:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the collection. Optional. Minimum length: `1`. Maximum length: `128`.\n\n- **`expected_updated_at`** [`ISODateString`](/api/collections#iso-date-string)\n Reject this update if the collection changed since this inspected update time. Optional.\n\n- **`schema_opaque_paths`** `string[][]`\n Object paths whose contents do not take part in schema identity, each written as its key segments (`[\"metadata\", \"id\"]`). A path that names an array covers its elements and their children. Records that differ only inside an opaque path share one schema version; the raw values are still stored. Changing this re-keys the collection. Optional.\n\n- **`schema_tracked_paths`** `string[][]`\n Children of opaque paths that stay part of schema identity, written like `schema_opaque_paths`. Changing this re-keys the collection. Optional.\n\n- **`schema_max_depth`** `integer`\n How many levels of nesting take part in schema identity. Changing this re-keys the collection. Optional. Minimum: `1`. Maximum: `10`.\n\n- **`schema_nulls`** `string`\n `auto` accepts null and absent values at any path a mapping reads; `manual` waits for a person to approve each nullable input per view. Optional. Allowed values: `\"auto\"`, `\"manual\"`.\n\n- **`schema_optional_keys`** `string`\n `auto` lets a record whose keys are a subset of a version's keys join that version; `manual` treats every absent key as a different shape. Optional. Allowed values: `\"auto\"`, `\"manual\"`.\n\n- **`schema_detector`** `string`\n `auto` applies a safe opaque-path proposal from the detector on its own; `manual` holds every proposal for a person. Optional. Allowed values: `\"auto\"`, `\"manual\"`.\n\n- **`schema_recurrence_gap_ms`** `integer`\n How long after first sight a shape must be seen again before its version settles, in milliseconds. Optional. Minimum: `60000`. Maximum: `604800000`.\n\n- **`schema_bulk_rows`** `integer`\n A single batch carrying at least this many rows of a shape settles its version immediately. `0` disables the bulk path. Optional. Minimum: `0`. Maximum: `1000000`.\n\n- **`schema_stale_after_ms`** `integer`\n How long a candidate version waits to be seen again before it goes stale, in milliseconds. Optional. Minimum: `3600000`. Maximum: `7776000000`.\n\n- **`schema_identity_limit`** `integer`\n How many settled schema versions the collection may hold. Past it, new shapes stay unclassified with their signatures kept. Optional. Minimum: `1`. Maximum: `4096`.\n\n- **`schema_candidate_pool`** `integer`\n How many candidate and stale versions the collection may hold. Past it, new shapes stay unclassified. Optional. Minimum: `1`. Maximum: `8192`.\n\n- **`schema_alias_pool`** `integer`\n How many exact signature hashes the collection remembers for fast lookup. Past it, a new hash is still resolved but not cached. Optional. Minimum: `16`. Maximum: `100000`.\n\n- **`schema_writer_limit`** `integer`\n How many collection files one ingest batch may open for this collection. Shapes past the candidate pool get the same number of files per batch, the shapes with the most rows first; the rows of shapes past that budget stay unclassified and cannot be linked to a version later. Optional. Minimum: `1`. Maximum: `256`.\n\n- **`schema_unclassified_retention_days`** `integer | null`\n How many days rows without a settled schema version are kept before they are deleted. `null` keeps them forever. Optional.\n\n- **`deleted_at`** `null`\n Send `null` to restore a collection that is scheduled for deletion. The deletion date itself is set by the delete endpoint and cannot be written here. Optional.\n\n### Response\n\nCollection updated\n\n```ts\n{\n message: string;\n data: Collection;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `slug` cannot be changed after creation.\n- Send `deleted_at` as `null` to restore a collection that is scheduled for deletion.\n\n## Delete Collection\n\n### Endpoint\n\nSchedule a collection for deletion.\n\n```http\nDELETE /v1/projects/:project_id/collections/:collection_id\n```\n\n**Scope:** `sources:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Response\n\n```ts\n{\n message: string;\n data: Collection;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The collection is hidden immediately and permanently removed 7 days later. Restore it before then by updating it with `deleted_at` set to null.\n\n## Force Delete Collection\n\n### Endpoint\n\nDelete a collection permanently, without waiting out its restore window.\n\n```http\nDELETE /v1/projects/:project_id/collections/:collection_id/force\n```\n\n**Scope:** `sources:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Response\n\nCollection queued for permanent deletion.\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- Works on a live collection as well as one already scheduled for deletion.\n- Every record, schema version and file in the collection is destroyed immediately.\n\n## List Collection Schemas\n\n### Endpoint\n\nRetrieve the schema versions for a collection.\n\n```http\nGET /v1/projects/:project_id/collections/:collection_id/schemas\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Query Parameters\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- **`state`** `string`\n Return only schema versions in this state. Optional. Allowed values: `\"candidate\"`, `\"settled\"`, `\"stale\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: CollectionSchemaVersion[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `state` is `candidate` from first sight, `settled` once the shape recurs or arrives in bulk, and `stale` when a candidate is not seen again. Only a settled version is offered to views for authoring.\n- `after` and `before` are mutually exclusive.\n\n## Update Collection Schema\n\n### Endpoint\n\nSettle a schema version by hand, so views can author against it before its shape recurs.\n\n```http\nPOST /v1/projects/:project_id/collections/:collection_id/schemas/:schema_version_id\n```\n\n**Scope:** `sources:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n- **`schema_version_id`** `string` -- **Required**\n Unique identifier of the schema.\n\n### Request Body\n\n- **`state`** `string` -- **Required**\n The only state a person sets. `settled` trusts the version for view authoring now, without waiting for its shape to recur; a settled version never goes back. Allowed values: `\"settled\"`.\n\n### Response\n\nSchema version settled\n\n```ts\n{\n message: string;\n data: CollectionSchemaVersion;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `state` accepts only `settled`; a settled version never returns to candidate or stale.\n- A version that is already settled is left as it is and responds with `request_no_update`.\n\n## Retrieve Collection Schema Report\n\n### Endpoint\n\nRetrieve the detector's view of a collection: the opaque paths it proposes, whether the top-level keys are data, and how many shapes sit unclassified past the collection's pools.\n\n```http\nGET /v1/projects/:project_id/collections/:collection_id/schema_report\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Response\n\nCollection schema report retrieved\n\n```ts\n{\n message: string;\n data: CollectionSchemaReport;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A proposal a bound mapping reads inside is reported and never applied on its own, whatever the detector switch; apply it by adding its path to the collection's `schema_opaque_paths` and its tracked children to `schema_tracked_paths`.\n- `shapes.before` and `shapes.after` are equal here; the preview is where they differ.\n\n## Preview Collection Schema Rules\n\n### Endpoint\n\nPreview a rule change: the schema report the collection would read under the rules sent, with how many shapes it holds now and how many it would hold. Nothing is changed.\n\n```http\nPOST /v1/projects/:project_id/collections/:collection_id/schema_preview\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Request Body\n\n- **`schema_opaque_paths`** `string[][]`\n Opaque paths to preview, written like the collection's `schema_opaque_paths`. Omitted, the collection's current opaque paths apply. Optional.\n\n- **`schema_tracked_paths`** `string[][]`\n Tracked paths to preview, written like the collection's `schema_tracked_paths`. Omitted, the collection's current tracked paths apply. Optional.\n\n- **`schema_max_depth`** `integer`\n Max depth to preview. Omitted, the collection's current max depth applies. Optional. Minimum: `1`. Maximum: `10`.\n\n### Response\n\nCollection schema rules previewed\n\n```ts\n{\n message: string;\n data: CollectionSchemaReport;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A rule left out of the body keeps the collection's current value.\n- `shapes.before` counts the shapes under the collection's current rules and `shapes.after` under the rules sent; the difference is what updating the collection with those rules would merge.\n\n## List Collection Documents\n\n### Endpoint\n\nRetrieve raw documents from a collection.\n\n```http\nGET /v1/projects/:project_id/collections/:collection_id/docs\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Query Parameters\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: CollectionDoc[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Documents are returned as they were sent, minus Tailglow's own reserved envelope. A value sent in a JSON array or JSONL that is not an object comes back as `{\"value\": ...}`.\n- Paging is forward-only. Pass `pagination.next_cursor` back as `after` to get the next page; no `prev_cursor` is issued, and `before` returns the first page.\n- While the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n- `after` and `before` are mutually exclusive.\n\n",
23
- "api/views": "# Views\n\n## View Model\n\n### Fields\n\n- **`mapping_policy_revision`** `number | null`\n\n- **`mapping_policy_nullable_inputs`** `string[]`\n\n- **`mapping_policy_fallback_groups`** `string[][]`\n\n- **`mapping_policy_allow_drops`** `boolean`\n\n- **`mapping_policy_output_bindings`** `Record<string, string[]>`\n\n- **`mapping_generation`** `number`\n\n- **`object`** `\"view\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `viw_`.\n\n- **`team_id`** `string`\n\n- **`project_id`** `string`\n\n- **`collection_id`** `string | null`\n\n The collection this view reads from; null when it reads from another view.\n\n- **`collection_name`** `string | null`\n\n Display name of that collection.\n\n- **`parent_view_id`** `string | null`\n\n The view this one is derived from; null when it reads from a collection.\n\n- **`parent_view_name`** `string | null`\n\n Display name of that view.\n\n- **`type`** [`ViewType`](/api/views#view-type)\n\n How the view's records are produced: `transform` (one collection or view through a script) or\n `join` (a base view with lookup rows attached). Fixed at creation.\n\n- **`transform_mode`** [`ViewTransformMode`](/api/views#view-transform-mode)\n\n Whether transform scripts are generated for new schema versions or written by you.\n\n- **`name`** `string`\n\n- **`hint`** `string | null`\n\n Plain-language guidance the generator uses when it writes transform scripts.\n\n- **`output_schema`** `ViewOutputField[]`\n\n The fields this view's records contain.\n\n- **`display_fields`** `ViewDisplayFields | null`\n\n Which field labels which in charts: a field of the view mapped to the view field whose value\n names it, such as `{ \"user_id\": \"user_name\" }`. A chart grouped by `user_id` then labels each\n series with the latest `user_name` its records carry. Null when never set.\n\n- **`status`** [`ViewStatus`](/api/views#view-status)\n\n Whether the view is producing records, still being set up, or failed.\n\n- **`error_message`** `string | null`\n\n Why the view failed, when its status is `error`.\n\n- **`waiting_shapes_count`** `number`\n\n How many event shapes arrived that no transform serves yet. Their records are stored and catch\n up automatically once a script covers them; the shapes are listed by the view's deferrals\n endpoint.\n\n- **`deleted_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the view is scheduled to be deleted; null when it is not.\n\n- **`created_at`** [`ISODateString`](/api/views#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/views#iso-date-string)\n\n- **`join_base_view_id`** `string | null`\n\n The base view on a join view; null on other views.\n\n- **`join_base_view_name`** `string | null`\n\n- **`join_status`** [`ViewJoinStatus | null`](/api/views#view-join-status)\n\n- **`join_backfill_cutoff_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n Records after this instant are joined live.\n\n- **`join_live_started_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the join started matching new records.\n\n- **`join_lookup_progress`** `LookupProgress | null`\n\n Lookup progress keyed by clause alias.\n\n- **`join_base_file_count`** `number | null`\n\n Number of base-view files to backfill.\n\n- **`join_base_files_done`** `number | null`\n\n Number of base-view files already processed.\n\n- **`join_error_message`** `string | null`\n\n- **`join_created_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`join_updated_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`joins`** `ViewJoinClause[]`\n\n The views joined onto the base view, on a join view.\n\n- **`metrics_count`** `number`\n\n How many metrics read from this view.\n\n- **`derived_views_count`** `number`\n\n How many views are derived from this one.\n\n- **`join_views_count`** `number`\n\n How many join views use this one as an input.\n\n- **`transform_count`** `number`\n\n How many transforms this view has.\n\n- **`drains_count`** `number`\n\n How many drains forward this view's records.\n\n- **`backfill_file_count`** `number`\n\n How many files every transform on this view has to process between them.\n\n- **`backfill_files_done`** `number`\n\n How many of those files they have processed.\n\n- **`backfill_row_count`** `number`\n\n How many records every transform on this view has to reprocess between them, counted when each\n backfill is planned.\n\n- **`backfill_rows_read`** `number`\n\n How many of those records they have reprocessed.\n\n- **`backfill_started_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the earliest of those backfills began. This is the earliest start across the whole view,\n which is what a throughput estimate has to measure elapsed time from.\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#### ViewType\n\n`\"transform\" | \"join\"`\n\n#### ViewTransformMode\n\n`\"auto\" | \"manual\"`\n\n#### ViewStatus\n\n`\"empty\" | \"building\" | \"needs_transform\" | \"active\" | \"error\" | \"paused\" | \"cancelled\"`\n\n#### ViewJoinStatus\n\n`\"waiting_for_inputs\" | \"building_lookups\" | \"backfilling\" | \"active\" | \"error\"`\n\n#### JoinLookupStatus\n\n`\"building\" | \"ready\" | \"stale\" | \"failed\"`\n\n## View Transform Model\n\n### Fields\n\n- **`object`** `\"view_transform\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `vtr_`.\n\n- **`view_id`** `string`\n\n The view this transform produces records for.\n\n- **`transform_script`** `string | null`\n\n The TGL script that maps incoming records onto the view's fields; null until one is written.\n\n- **`status`** [`ViewTransformStatus`](/api/views#view-transform-status)\n\n Whether the transform is running, waiting for a script, or failed.\n\n- **`error_message`** `string | null`\n\n Why the transform failed, when its status is `error`.\n\n- **`authoring_reason`** `string | null`\n\n What a queued transform's script waits for: `waiting_for_lookups` while a join's lookups are\n being built, `collecting_samples` while no record can show what the script must handle yet (on\n a join, no base record has a match in every lookup), `waiting_for_first_input` while its inputs\n hold no records, `manual_mapping` when it needs a script you write, and `needs_hint` when the\n generator asks you for a hint. Other values name what holds generation back, such as the hourly\n generation limit. Null when it waits on nothing.\n\n- **`tgl_retry_count`** `number`\n\n How many times script generation has been retried for this transform.\n\n- **`max_read_depth`** `number | null`\n\n How many levels deep into a record the script reads.\n\n- **`evidence_notice`** `ViewEvidenceNotice | null`\n\n Set when a path this mapping read as empty at authoring time has since carried a value with a\n type, listing those paths as `L<depth>:<path>` read keys. Rows keep flowing; re-author the\n mapping to use the new column. Cleared when a new script is generated.\n\n- **`backfill_status`** [`ViewTransformBackfillStatus`](/api/views#view-transform-backfill-status)\n\n Whether existing records have been reprocessed through this transform yet.\n\n- **`backfill_cutoff_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n Records after this time are handled live rather than by the backfill.\n\n- **`backfill_started_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the backfill began.\n\n- **`backfill_completed_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the backfill finished.\n\n- **`backfill_file_count`** `number`\n\n How many files the backfill has to process.\n\n- **`backfill_files_done`** `number`\n\n How many of those files it has processed.\n\n- **`backfill_row_count`** `number`\n\n How many records the backfill has to reprocess, counted when it is planned.\n\n- **`backfill_rows_read`** `number`\n\n How many of those records it has reprocessed.\n\n- **`backfill_error`** `string | null`\n\n Why the backfill failed, when it did.\n\n- **`assignments`** `ViewTransformAssignment[]`\n\n Backfill progress for each source schema version this transform covers.\n\n- **`created_at`** [`ISODateString`](/api/views#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/views#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#### ViewTransformStatus\n\n`\"queued\" | \"analyzing\" | \"active\" | \"error\" | \"draft\"`\n\n#### ViewTransformBackfillStatus\n\n`\"pending\" | \"enqueuing\" | \"in_progress\" | \"completed\" | \"failed\" | \"cancelled\"`\n\n## View Version Model\n\n### Fields\n\n- **`object`** `\"view_version\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `vta_`.\n\n- **`view_id`** `string`\n\n- **`version`** `number`\n\n The source schema version this row covers. Versions rise as the source's shape changes.\n\n- **`transform_id`** `string`\n\n The transform whose script serves this version. Several versions can share one transform.\n\n- **`status`** [`ViewTransformStatus`](/api/views#view-transform-status)\n\n Whether the transform is running, waiting for a script, or failed.\n\n- **`error_message`** `string | null`\n\n Why the transform failed, when its status is `error`.\n\n- **`excluded_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When this version was excluded from the view; null while it is included. Its records stay\n stored and never reach the view. `status` still reports the transform, which an exclusion\n leaves as it was.\n\n- **`exclusion_reason`** `string | null`\n\n The reason given when the version was excluded.\n\n- **`evidence_notice`** `ViewEvidenceNotice | null`\n\n Set when a path the serving script read as empty at authoring time has since carried a value\n with a type; lists those paths as `L<depth>:<path>` read keys. Re-author the mapping to use the\n new column. Cleared when a new script is generated.\n\n- **`script_hash`** `string | null`\n\n Identity of the script text. Versions sharing a hash run the identical script.\n\n- **`script`** `string | null`\n\n The TGL script serving this version; null until one is written.\n\n- **`backfill_status`** [`ViewTransformBackfillStatus`](/api/views#view-transform-backfill-status)\n\n Whether the records already on this schema version have been reprocessed yet.\n\n- **`backfill_started_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the backfill began.\n\n- **`backfill_completed_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the backfill finished.\n\n- **`backfill_file_count`** `number`\n\n How many files the backfill has to process for this version.\n\n- **`backfill_files_done`** `number`\n\n How many of those files it has processed.\n\n- **`backfill_row_count`** `number`\n\n How many records the backfill has to reprocess for this version, counted when it is planned.\n\n- **`backfill_rows_read`** `number`\n\n How many of those records it has reprocessed.\n\n- **`backfill_error`** `string | null`\n\n Why the backfill failed, when it did.\n\n- **`created_at`** [`ISODateString`](/api/views#iso-date-string)\n\n When the view started covering this schema version.\n\n- **`updated_at`** [`ISODateString`](/api/views#iso-date-string)\n\n When the transform serving this version 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#### ViewTransformStatus\n\n`\"queued\" | \"analyzing\" | \"active\" | \"error\" | \"draft\"`\n\n#### ViewTransformBackfillStatus\n\n`\"pending\" | \"enqueuing\" | \"in_progress\" | \"completed\" | \"failed\" | \"cancelled\"`\n\n## Waiting Shape Model\n\n### Fields\n\n- **`episode`** `number`\n\n- **`observation_count`** `string | null`\n\n- **`retained_rows`** `number`\n\n- **`first_observed_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`last_observed_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`next_action_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`hint_question`** `string | null`\n\n- **`hint_reason`** `string | null`\n\n- **`hint_paths`** `string[]`\n\n- **`hint_requested_at`** `string | null`\n\n- **`hint_mapping_generation`** `number | null`\n\n- **`exclusion_reason`** `string | null`\n\n- **`excluded_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`transform_id`** `string | null`\n\n- **`object`** `\"view_shape_deferral\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `vsd_`.\n\n- **`view_id`** `string`\n\n- **`full_signature`** `string`\n\n The typed signature identifying the event shape.\n\n- **`event_count`** `number`\n\n Roughly how many events of the shape arrived while it was waiting; retries can count an event\n more than once.\n\n- **`first_event_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n The earliest event time seen for the shape. Completeness reporting pins to this instant.\n\n- **`first_deferred_at`** [`ISODateString`](/api/views#iso-date-string)\n\n- **`last_deferred_at`** [`ISODateString`](/api/views#iso-date-string)\n\n- **`resolved_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the shape became served; null while it is still waiting.\n\n- **`reason`** [`ViewShapeDeferralReason`](/api/views#view-shape-deferral-reason)\n\n Why the shape is waiting: `settling` until its schema version settles (views leave a version\n alone until then), `collecting_samples` until enough evidence is available to author a mapping,\n `awaiting_script` until a transform covers it, `generation_failed` when script generation gave\n up, `catching_up` while its records backfill, `served` once resolved.\n\n- **`schema_version`** `number | null`\n\n The schema version the shape belongs to. Null while no version is linked to it.\n\n- **`schema_version_id`** `string | null`\n\n Id of that schema version, prefixed with `csv_`. Settle a `settling` shape by passing it to\n Update Collection Schema.\n\n- **`created_at`** [`ISODateString`](/api/views#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#### ViewShapeDeferralReason\n\n`\"settling\" | \"collecting_samples\" | \"needs_decision\" | \"needs_hint\" | \"manual_mapping\" | \"generating\" | \"budget_limited\" | \"shape_overflow\" | \"runtime_error\" | \"excluded\" | \"recovery_pending\" | \"awaiting_script\" | \"generation_failed\" | \"catching_up\" | \"served\"`\n\nWhat unblocks a waiting shape.\n\n## Shape Inspection Model\n\n### Fields\n\n- **`view_id`** `string`\n\n- **`deferral_id`** `string`\n\n- **`episode`** `number`\n\n- **`mapping_generation`** `number`\n\n- **`version`** `number | null`\n\n- **`signatures`** `Record<string, string | null>`\n\n- **`decision`** `ViewMappingReuseDecision`\n\n- **`candidates`** `{ transform_id: string; verdict: string; reason: string; exact_paths: string[]; present_null_paths: string[]; absent_paths: string[]; incompatible_paths: string[]; substitution_paths: string[]; unknown_paths: string[]; bindings: Record<...>; }[]`\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## Waiting Shape Hint Model\n\n### Fields\n\n- **`question`** `string`\n\n- **`reason`** `string`\n\n- **`paths`** `string[]`\n\n Verified JSON bracket paths such as $[\"account\"][\"id\"], with literal key characters escaped.\n\n- **`requested_at`** `string`\n\n- **`mapping_generation`** `number`\n\n- **`view_id`** `string`\n\n- **`deferral_id`** `string`\n\n- **`episode`** `number`\n\n- **`transform_id`** `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 Views\n\n### Endpoint\n\nRetrieve a list of views for a project.\n\n```http\nGET /v1/projects/:project_id/views\n```\n\n**Scope:** `views: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\"`, `\"name\"`, `\"updated_at\"`.\n\n- **`project_id`** `string`\n Return only views in this project. Optional.\n\n- **`collection_id`** `string`\n Return only views reading from this collection. Optional.\n\n- **`parent_view_id`** `string`\n Return only views derived from this view. Optional.\n\n- **`deleted_at`** [`NullableDateFilter`](/api/views#nullable-date-filter)\n Filter on deletion state. Soft-deleted views are excluded unless you ask for them. Optional.\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: View[];\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 View\n\n### Endpoint\n\nRetrieve a single view.\n\n```http\nGET /v1/projects/:project_id/views/:view_id\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Response\n\nView retrieved\n\n```ts\n{\n message: string;\n data: View;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create View\n\n### Endpoint\n\nCreate a view: a transform over one collection or view, or a join of a base view with lookups.\n\n```http\nPOST /v1/projects/:project_id/views\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Request Body\n\n- **`type`** [`ViewType`](/api/views#view-type)\n How the view's records are produced: `transform` reads one collection or view through a script; `join` starts from a base view and attaches lookup rows. Fixed at creation. Optional. Defaults to `\"transform\"`.\n\n- **`name`** `string` -- **Required**\n Name for the view. Minimum length: `1`. Maximum length: `128`.\n\n- **`output_schema`** `object[]`\n The fields this view produces. Can be left until transforms define it. Optional.\n\n - **`name`** `string` -- **Required**\n Field name as it appears on the view's records. Minimum length: `1`. Maximum length: `128`.\n\n - **`type`** `string` -- **Required**\n Value type the field holds. Allowed values: `\"string\"`, `\"number\"`, `\"boolean\"`, `\"date\"`, `\"array\"`, `\"null\"`.\n\n - **`description`** `string`\n What the field means. Used as guidance when a transform script is generated. Optional. Maximum length: `512`.\n\n- **`hint`** `string | null`\n Instructions the generator cannot infer from field names alone, such as a formatting rule or a business threshold. Optional.\n\n- **`mapping_policy`** `object`\n Optional.\n\n - **`nullable_inputs`** `string[]`\n Read paths explicitly permitted to be null. Optional.\n\n - **`fallback_groups`** `string[][]`\n Approved ordered fallback read paths. Optional.\n\n - **`output_bindings`** `object`\n Required source path segments for each output binding. Optional.\n\n - **`allow_drops`** `boolean`\n Allow a validated mapping to emit no output for a record. Optional.\n\n- **`display_fields`** `object`\n Which field labels which in charts: each key is a field of the view, its value the view field whose value names it, such as `{ \"user_id\": \"user_name\" }`. A key set to null is removed; on update, keys left out keep their value. Optional.\n\n- **`project_id`** `string`\n Project ID. Optional when using the /projects/:project/views route. Optional.\n\n- **`collection_id`** `string`\n Collection to derive from, when reading raw ingested records. Optional.\n\n- **`parent_view_id`** `string`\n View to derive from, when deriving from a view. Optional.\n\n- **`base_view_id`** `string`\n View every output record of a join starts from. Optional. Minimum length: `1`.\n\n- **`joins`** `object[]`\n Views joined onto the base view, each with its own match fields. Optional.\n\n - **`lookup_view_id`** `string` -- **Required**\n View this clause looks values up in. Minimum length: `1`.\n\n - **`alias`** `string` -- **Required**\n Name this clause's fields are addressed by in the transform script. Minimum length: `1`. Maximum length: `64`.\n\n - **`join_type`** `string`\n How unmatched base records are handled. `left` keeps them with empty lookup fields, `inner` drops them. Optional. Defaults to `\"left\"`. Allowed values: `\"left\"`, `\"inner\"`.\n\n - **`base_field`** `string` -- **Required**\n Field on the base view to match on. Minimum length: `1`. Maximum length: `256`.\n\n - **`lookup_field`** `string` -- **Required**\n Field on the lookup view to match against. Minimum length: `1`. Maximum length: `256`.\n\n - **`match_strategy`** `string`\n Which lookup record wins when several match. `latest` keeps the newest record timestamp and is the only supported value today. Optional. Defaults to `\"latest\"`. Allowed values: `\"latest\"`.\n\n - **`priority`** `integer`\n Order this clause is applied in. Lower runs first. Optional. Defaults to `0`. Minimum: `0`.\n\n- **`transform_script`** `string`\n TGL transform script. When set, the view skips automatic TGL generation. Optional. Minimum length: `1`. Maximum length: `65536`.\n\n- **`transform_mode`** `string`\n Whether a new schema version that no existing transform covers gets a script generated for it, or waits for you to write one. Defaults to auto. Optional. Allowed values: `\"auto\"`, `\"manual\"`.\n\n### Response\n\nView created\n\n```ts\n{\n message: string;\n data: View;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The output schema defines the fields the view produces. A transform is created alongside the view to populate them.\n- Leaving the transform script empty queues it for generation instead of rejecting it, so a view can be created before its TGL is written.\n- Backfilling existing records starts shortly after creation and runs in the background. The view reports its progress while it catches up.\n- A join's base view and lookup views must all be views in this project.\n- A join whose inputs lead back to the join itself, directly or through other views, is rejected.\n- A `transform` view reads from exactly one place: set `collection_id` to derive from raw ingested records, or `parent_view_id` to derive from another view. Setting both, or neither, is rejected, and so are `base_view_id` and `joins`.\n- A `join` view requires `base_view_id` and `joins` (one to ten clauses), and rejects `collection_id` and `parent_view_id`. Join a collection by creating a view over it first.\n- Each join clause's `alias` must be lowercase letters, digits and underscores, and start with a letter. It is how the clause's fields are addressed in the transform script.\n- A join enriches each record with each joined view's latest value, by record timestamp, at the time the record is processed. Records already produced are not revisited.\n- There is no `source_id` on this endpoint. One source can hold many collections, and the collection is what owns the schema and the records a view transforms. List a source's collections with `GET /v1/projects/:project/collections?source_id=...` and pass the one you want.\n- `output_schema` field names must be unique by name, cannot conflict on type, and cannot collide with a reserved query key such as `limit` or `cursor`.\n- `transform_script` cannot be whitespace-only. Supplying one skips automatic TGL generation; omit it entirely to have one generated.\n- Use `hint` for rules the generator cannot read off the field names: a formatting rule (\"display_name should be Name (Type)\"), a business threshold (\"classify as high above 500\"), or a disambiguation when an input field could map more than one way. Skip it for obvious mappings like renames and type conversions, which the generator already handles.\n\n## Update View\n\n### Endpoint\n\nUpdate a view's name or output schema.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Request Body\n\n- **`mapping_policy`** `object`\n Optional.\n\n - **`nullable_inputs`** `string[]`\n Read paths explicitly permitted to be null. Optional.\n\n - **`fallback_groups`** `string[][]`\n Approved ordered fallback read paths. Optional.\n\n - **`output_bindings`** `object`\n Required source path segments for each output binding. Optional.\n\n - **`allow_drops`** `boolean`\n Allow a validated mapping to emit no output for a record. Optional.\n\n- **`name`** `string`\n Name for the view. Optional. Minimum length: `1`. Maximum length: `128`.\n\n- **`output_schema`** `object[]`\n The fields this view produces. Can be left until transforms define it. Optional.\n\n - **`name`** `string` -- **Required**\n Field name as it appears on the view's records. Minimum length: `1`. Maximum length: `128`.\n\n - **`type`** `string` -- **Required**\n Value type the field holds. Allowed values: `\"string\"`, `\"number\"`, `\"boolean\"`, `\"date\"`, `\"array\"`, `\"null\"`.\n\n - **`description`** `string`\n What the field means. Used as guidance when a transform script is generated. Optional. Maximum length: `512`.\n\n- **`hint`** `string | null`\n Instructions the generator cannot infer from field names alone, such as a formatting rule or a business threshold. Optional.\n\n- **`display_fields`** `object`\n Which field labels which in charts: each key is a field of the view, its value the view field whose value names it, such as `{ \"user_id\": \"user_name\" }`. A key set to null is removed; on update, keys left out keep their value. Optional.\n\n- **`transform_mode`** `string`\n Whether a new schema version that no existing transform covers gets a script generated for it, or waits for you to write one. Optional. Allowed values: `\"auto\"`, `\"manual\"`.\n\n- **`view_transform_id`** `string`\n Which transform adopts the schema versions still waiting for a script when switching transform_mode to manual. Required only when the view has more than one distinct transform script. Optional.\n\n- **`deleted_at`** `null`\n Send `null` to restore a view that is scheduled for deletion. The deletion date itself is set by the delete endpoint and cannot be written here. Optional.\n\n### Response\n\nView updated\n\n```ts\n{\n message: string;\n data: View;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Output schema changes are accepted only before a mapping or stored output exists. Changing an existing view's output schema requires a recovery transition that is not yet supported.\n- Mapping policy changes on an existing mapping require reviewed coverage recovery. Hint changes preserve accepted mappings and retry undecided work with the new hint.\n- `display_fields` changes only the keys it names: a key set to a field labels it, a key set to null is removed. Every field it names must be a field of the view; a stored entry whose field the view has since dropped is kept, labels nothing, and never blocks another change.\n- What a view reads from is fixed at creation, so `collection_id` and `parent_view_id` are not accepted here. Create a new view to read from something else.\n- `output_schema` field names must be unique by name, cannot conflict on type, and cannot collide with a reserved query key such as `limit` or `cursor`.\n- Use `hint` for rules the generator cannot read off the field names: a formatting rule (\"display_name should be Name (Type)\"), a business threshold (\"classify as high above 500\"), or a disambiguation when a source field could map more than one way. Skip it for obvious mappings like renames and type conversions, which the generator already handles.\n\n## Delete View\n\n### Endpoint\n\nDelete a view and every view derived from it.\n\n```http\nDELETE /v1/projects/:project_id/views/:view_id\n```\n\n**Scope:** `views:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Response\n\n```ts\n{\n message: string;\n data: View;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Views derived from this one are deleted with it.\n- The delete is refused while an active drain reads from this view or from any view that would cascade with it. Delete those drains first. Draft drains are removed silently.\n- Backfill work for the whole cascade is cancelled immediately, so a deleted view stops consuming capacity before its rows are removed.\n\n## Force Delete View\n\n### Endpoint\n\nDelete a view permanently, without waiting out its restore window.\n\n```http\nDELETE /v1/projects/:project_id/views/:view_id/force\n```\n\n**Scope:** `views:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Response\n\nView queued for permanent deletion.\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- Works on a live view as well as one already scheduled for deletion.\n- Views derived from this one go with it, and the rollups of any metric reading them are destroyed. The records they were built from are not affected, so both can be rebuilt.\n\n## List View Records\n\n### Endpoint\n\nRetrieve the transformed records a view has produced.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/records\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Query Parameters\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: ViewRecord[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Paging is forward-only. Pass `pagination.next_cursor` back as `after` to get the next page; a null `next_cursor` means there is nothing more to read. No `prev_cursor` is issued, and `before` is ignored.\n- A cursor is only valid for the query that produced it. Reusing one after changing the sort direction returns a 400 rather than a page of skipped or repeated records, so start again without a cursor.\n- A cursor stays valid while Tailglow reorganizes stored data in the background, so a paging client is never interrupted by storage maintenance. It is a position in this result set, not a snapshot: records that arrive while you page appear only if they sort after your position.\n- While the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n- `after` and `before` are mutually exclusive.\n\n## List Waiting Shapes\n\n### Endpoint\n\nRetrieve the event shapes a view has deferred because no transform serves them yet.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/deferrals\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Query Parameters\n\n- **`resolved`** `string`\n Filter by resolution. `false` returns only shapes still waiting. Optional. Allowed values: `\"true\"`, `\"false\"`.\n\n- **`order_by`** `string`\n Sort field. Defaults to `last_deferred_at`. Optional. Allowed values: `\"first_deferred_at\"`, `\"last_deferred_at\"`.\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: ViewShapeDeferral[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Deferred records are stored, not lost. They backfill automatically once a script covers the shape, and each entry's `reason` says what unblocks it.\n- `event_count` is approximate: ingest retries can count an event more than once.\n- While the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n- `after` and `before` are mutually exclusive.\n\n## Inspect Waiting Shape\n\n### Endpoint\n\nInspect structural differences and candidate output bindings for a waiting shape.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/deferrals/:deferral_id\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`deferral_id`** `string` -- **Required**\n Unique identifier of the deferral.\n\n### Response\n\nWaiting shape inspected\n\n```ts\n{\n message: string;\n data: ViewShapeInspection;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- While the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n\n## Retrieve Waiting Shape Hint\n\n### Endpoint\n\nRetrieve the model's current binding question for a waiting shape.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/deferrals/:deferral_id/hint\n```\n\n**Scopes:** `views:read` + `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`deferral_id`** `string` -- **Required**\n Unique identifier of the deferral.\n\n### Response 200 (1)\n\nNo pending hint question\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### Response 200 (2)\n\nWaiting shape hint retrieved\n\n```ts\n{\n message: string;\n data: ViewPendingHint;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Requires both views:read and sources:read because model prose may refer to source records. Suggested paths are verified source fields in unambiguous JSON bracket notation.\n- Returns null when no current question remains. Answer using this question's episode, mapping_generation, requested_at, and transform_id.\n\n## Decide Waiting Shape\n\n### Endpoint\n\nResolve the next action for one view and schema version.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/deferrals/:deferral_id/decision\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`deferral_id`** `string` -- **Required**\n Unique identifier of the deferral.\n\n### Request Body\n\n- **`episode`** `integer` -- **Required**\n The deferral episode shown when the decision was inspected. Minimum: `1`.\n\n- **`action`** `string` -- **Required**\n Authorize a bounded authoring attempt, answer a hint, or exclude this exact version in this view. Allowed values: `\"author\"`, `\"hint\"`, `\"exclude\"`.\n\n- **`mapping_generation`** `integer` -- **Required**\n The mapping generation shown when the decision was inspected. Minimum: `1`.\n\n- **`hint`** `string`\n Answer to the model's binding question. Optional. Minimum length: `1`. Maximum length: `4096`.\n\n- **`hint_requested_at`** [`ISODateString`](/api/views#iso-date-string)\n Exact requested_at of the pending hint being answered. Optional.\n\n- **`hint_transform_id`** `string`\n Transform that owns the pending hint being answered. Optional. Minimum length: `1`. Maximum length: `32`.\n\n- **`reason`** `string`\n Required audit reason for an exclusion. Optional. Minimum length: `1`. Maximum length: `2000`.\n\n- **`mapping_policy`** `object`\n Explicit null, fallback, drop, and source-binding rules. Optional.\n\n - **`nullable_inputs`** `string[]`\n Read paths explicitly permitted to be null. Optional.\n\n - **`fallback_groups`** `string[][]`\n Approved ordered fallback read paths. Optional.\n\n - **`output_bindings`** `object`\n Required source path segments for each output binding. Optional.\n\n - **`allow_drops`** `boolean`\n Allow a validated mapping to emit no output for a record. Optional.\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- Authoring and hint answers reserve a bounded model budget. Exclusion covers unprocessed and future records of this exact version in this view.\n- While the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n- Answering a hint requires `hint`; excluding a version requires `reason`. Exclusion covers unprocessed and future records with this exact schema version in this view.\n- Answering a pending model question also requires its exact `hint_requested_at` and `hint_transform_id`. Superseded or answered questions return a conflict; manual guidance without a pending question may omit both fields.\n\n## List View Transforms\n\n### Endpoint\n\nRetrieve a list of a view's transforms.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/transforms\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field the results are sorted by. Creation order matches schema version order. Optional. Defaults to `\"created_at\"`. Allowed values: `\"created_at\"`, `\"updated_at\"`.\n\n- **`status`** [`ViewTransformStatus`](/api/views#view-transform-status)\n Return only transforms in this state. Optional.\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: ViewTransform[];\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 View Transform\n\n### Endpoint\n\nRetrieve a single transform.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/transforms/:transform_id\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`transform_id`** `string` -- **Required**\n Unique identifier of the transform.\n\n### Response\n\nTransform retrieved\n\n```ts\n{\n message: string;\n data: ViewTransform;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## List View Versions\n\n### Endpoint\n\nRetrieve a list of the schema versions a view covers.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/versions\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field the results are sorted by. Optional. Defaults to `\"version\"`. Allowed values: `\"version\"`.\n\n- **`status`** [`ViewTransformStatus`](/api/views#view-transform-status)\n Return only versions whose transform is in this state. Optional.\n\n- **`script_hash`** `string`\n Return only versions served by this script. Take the value from a version's row. Optional. Minimum length: `64`. Maximum length: `64`.\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: ViewVersion[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- One row per source schema version the view covers, newest version first. A view that reads another view has no schema versions, so its list is empty.\n- Versions sharing a `script_hash` run the same transform. Filter by it to see every version one script serves.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve View Version\n\n### Endpoint\n\nRetrieve one schema version a view covers.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/versions/:version_id\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`version_id`** `integer` -- **Required**\n The schema version number: the `version` a view version carries.\n\n### Response\n\nVersion retrieved\n\n```ts\n{\n message: string;\n data: ViewVersion;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `version_id` is the schema version NUMBER, the `version` a row carries, not a `vta_` id.\n\n## Update View Version\n\n### Endpoint\n\nExclude one settled schema version from a view.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/versions/:version_id\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`version_id`** `integer` -- **Required**\n The schema version number: the `version` a view version carries.\n\n### Request Body\n\n- **`is_excluded`** `boolean` -- **Required**\n Set to true to exclude this schema version from the view: its records are left out of the view, now and in the future.\n\n- **`exclusion_reason`** `string`\n Why the version is excluded, kept with the exclusion for whoever reads it later. Optional. Minimum length: `1`. Maximum length: `2000`.\n\n### Response\n\nSchema version excluded from this view\n\n```ts\n{\n message: string;\n data: ViewVersion;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `version_id` is the schema version NUMBER, the `version` a row carries, not a `vta_` id.\n- An excluded version's records are left out of this view, those already ingested and those still to come, and the version no longer holds the view back from being ready for metrics. Other views reading the same collection are not affected.\n- Only a version still waiting on a script can be excluded. A version whose transform holds a script, or is already excluded, answers 409.\n- An exclusion is not undone here: `is_excluded: false` answers 400.\n- Excluding a version requires `exclusion_reason`.\n\n## Create View Transform\n\n### Endpoint\n\nAdd a transform that maps incoming records into the view's output schema.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/transforms\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Request Body\n\n- **`collection_schema_version`** `integer | null`\n Collection schema version this transform covers. Null for view-to-view transforms. Optional.\n\n- **`transform_script`** `string`\n TGL transform script. Omit to have one generated. Optional. Minimum length: `1`. Maximum length: `65536`.\n\n### Response\n\nTransform created\n\n```ts\n{\n message: string;\n data: ViewTransform;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `transform_script` is TGL and must compile. A script that does not parse is rejected rather than stored, because an unparseable script would fail on every record it later touched.\n- The script must write every field the view's output schema declares.\n- Omitting the script queues the transform for generation instead of rejecting it.\n- `collection_schema_version` must be settled. A candidate stays out of views until it settles, so naming one is refused with a 409; settle it with Update Collection Schema first.\n- A transform on a collection-backed view must name the `collection_schema_version` it covers. View-to-view transforms pass null, because they read another view's output rather than a collection schema.\n- `transform_script` cannot be whitespace-only. Omit it to have TGL generated instead.\n\n## Update View Transform\n\n### Endpoint\n\nUpdate a transform's script.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/transforms/:transform_id\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`transform_id`** `string` -- **Required**\n Unique identifier of the transform.\n\n### Request Body\n\n- **`transform_script`** `string`\n TGL transform script. To clear and regenerate, use the regenerate endpoint instead. Optional. Minimum length: `1`. Maximum length: `65536`.\n\n- **`conflict_resolution`** `string`\n How conflicting schema versions are handled when a script edit fans out. Optional. Defaults to `\"strand\"`. Allowed values: `\"strand\"`, `\"keep\"`.\n\n- **`dry_run`** `boolean`\n Preview the edit: returns the per-version verdict report without changing anything. Optional. Defaults to `false`.\n\n- **`preview_token`** `string`\n The `edit_report.preview_token` from this edit's dry run. Required to save: it pins the save to the set of schema versions the preview reported on, so a version assigned or reassigned in between is rejected instead of silently included. Optional. Minimum length: `1`. Maximum length: `128`.\n\n### Response 200 (1)\n\n```ts\n{\n message: string;\n data: ViewTransform & { edit_report: { script_hash: string | null; version_verdicts: { collection_schema_version_id: string; version: number; verdict: \"compatible\" | \"conflict\"; action: \"updated\" | \"kept\" | \"stranded\"; null_paths: string[]; }[]; preview_token?: string | undefined; } };\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n#### Additional Response Fields\n\n- **`edit_report`** `{ script_hash: string | null; version_verdicts: { collection_schema_version_id: string; version: number; verdict: \"compatible\" | \"conflict\"; action: \"updated\" | \"kept\" | \"stranded\"; null_paths: string[]; }[]; preview_token?: string | undefined; }` -- **Required**\n\n### Response 200 (2)\n\nTransform updated\n\n```ts\n{\n message: string;\n data: ViewTransform & { edit_report: { script_hash: string | null; version_verdicts: never[]; } };\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n#### Additional Response Fields\n\n- **`edit_report`** `{ script_hash: string | null; version_verdicts: never[]; }` -- **Required**\n\n### Comments\n\n- `transform_script` is TGL and must compile, and must write every field the view's output schema declares.\n- On collection-backed views, an edit fans out to every schema version using the same prior script and returns a verdict for each version. Additive reads report paths that produce null.\n- Conflicting versions are moved to queued placeholders by default. Set `conflict_resolution` to `keep` to leave those versions on their prior script.\n- Removing an output field is rejected while a metric, facet, child view, or join clause reads it. Consumer checks use the complete nested output path. Records already produced keep their existing values until the transform is re-backfilled.\n- Set `dry_run` to preview the per-version verdict report without saving. A dry run changes nothing and triggers nothing, and returns a `preview_token` alongside the report.\n- Saving requires the `preview_token` from that dry run. It pins the save to the set of schema versions the report described, so a version added or reassigned between the preview and the save cannot be silently included.\n- A save with a missing or outdated token is rejected with 409 and changes nothing. The error carries the report as it stands now, so the next preview is already in hand.\n- `transform_script` cannot be whitespace-only. To clear a script and have it regenerated, use the regenerate endpoint rather than sending an empty one.\n- Saving is a two-step gesture: send `dry_run` to get the report and its `preview_token`, then send the same edit with that token. A save without one, or with a token whose set has since changed, is rejected with 409 and a fresh report.\n\n## Generate View Transform Script\n\n### Endpoint\n\nQueue TGL generation for a transform from the view's input and output schemas.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/transforms/:transform_id/generate\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`transform_id`** `string` -- **Required**\n Unique identifier of the transform.\n\n### Response\n\nTGL generation queued\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- Generation runs in the background. This returns once the work is queued, not once a script exists; poll the transform to see the result.\n- The generated script is written to the transform and replaces whatever was there.\n\n## Re-backfill View Transform\n\n### Endpoint\n\nRe-run a transform over every record the view has already read.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/transforms/:transform_id/rebackfill\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`transform_id`** `string` -- **Required**\n Unique identifier of the transform.\n\n### Response\n\nTransform re-backfill started\n\n```ts\n{\n message: string;\n data: ViewTransform;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Use this after editing a script so existing records pick up the new logic. Without it, an edit only affects records that arrive afterwards.\n- The re-backfill runs in the background and replaces the view's output as it progresses. Anything reading the view sees a mix of old and new values until it finishes.\n- Refused while a metric, derived view or join view depends on this view.\n\n## Regenerate View\n\n### Endpoint\n\nReplace a view's output schema and queue every transform to be regenerated against it.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/regenerate\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Request Body\n\n- **`output_schema`** `object[]`\n The fields the view should produce. Omit to regenerate against the current schema. Optional.\n\n - **`name`** `string` -- **Required**\n Field name as it appears on the view's records. Minimum length: `1`. Maximum length: `128`.\n\n - **`type`** `string` -- **Required**\n Value type the field holds. Allowed values: `\"string\"`, `\"number\"`, `\"boolean\"`, `\"date\"`, `\"array\"`, `\"null\"`.\n\n - **`description`** `string`\n What the field means. Used as guidance when a transform script is generated. Optional. Maximum length: `512`.\n\n- **`hint`** `string | null`\n Instructions the generator cannot infer from field names alone, such as a formatting rule or a business threshold. Optional.\n\n### Response\n\nView schema updated and transforms queued for regeneration\n\n```ts\n{\n message: string;\n data: View;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- This is the schema-change path: it updates the output schema and re-queues the view's transforms in one step, so the scripts are rewritten to produce the new fields.\n- Existing records keep their old shape until the regenerated transforms backfill over them.\n- Refused while a metric, derived view or join view depends on this view.\n- `output_schema` field names must be unique by name, cannot conflict on type, and cannot collide with a reserved query key such as `limit` or `cursor`.\n- Use `hint` for rules the generator cannot read off the field names: a formatting rule (\"display_name should be Name (Type)\"), a business threshold (\"classify as high above 500\"), or a disambiguation when a source field could map more than one way. Skip it for obvious mappings like renames and type conversions, which the generator already handles.\n\n## Delete View Transform\n\n### Endpoint\n\nDelete a transform from a view.\n\n```http\nDELETE /v1/projects/:project_id/views/:view_id/transforms/:transform_id\n```\n\n**Scope:** `views:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`transform_id`** `string` -- **Required**\n Unique identifier of the transform.\n\n### Response\n\nTransform 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 transform already produced are left in place. Re-backfill the view's remaining transforms to rebuild its output without this one.\n\n",
24
- "api/metrics": "# Metrics\n\n## Metric Model\n\n### Fields\n\n- **`object`** `\"metric\"`\n\n- **`id`** `string`\n\n- **`name`** `string`\n\n- **`team_id`** `string`\n\n- **`project_id`** `string`\n\n- **`deleted_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n When the metric is scheduled to be deleted; null when it is not.\n\n- **`view_id`** `string`\n\n The view whose records the metric aggregates. Changing it to a compatible view in the same\n project rebuilds the metric's historical data.\n\n- **`status`** [`MetricStatus`](/api/metrics#metric-status)\n\n Current lifecycle state for live collection and historical backfill.\n\n- **`group_by`** `string[]`\n\n View fields whose value combinations define distinct metric series.\n\n- **`filters`** `string`\n\n Filter expression applied to records before aggregation.\n\n- **`timestamp_field`** `string`\n\n View field used as the aggregation timestamp.\n\n- **`value_field`** `string | null`\n\n View field used as the numeric aggregation value.\n\n- **`unique_field`** `string | null`\n\n View field used for distinct-value counts.\n\n- **`null_handling`** [`NullHandling`](/api/metrics#null-handling)\n\n How null or missing metric values are aggregated.\n\n- **`empty_bucket_handling`** [`EmptyBucketHandling`](/api/metrics#empty-bucket-handling)\n\n How chart buckets without observations are represented.\n\n- **`backfill_strategy`** [`BackfillStrategy`](/api/metrics#backfill-strategy)\n\n Order used when historical data is rebuilt.\n\n- **`ui_chart_family`** [`ChartFamily`](/api/metrics#chart-family)\n\n Read-only: derived from `ui_chart_type`, grouping chart types by layout family.\n\n- **`ui_chart_type`** [`ChartType`](/api/metrics#chart-type)\n\n- **`ui_chart_color_mode`** [`ChartColorMode`](/api/metrics#chart-color-mode)\n\n Strategy used to assign chart colors.\n\n- **`ui_chart_color_base`** `string | null`\n\n Base color used by intensity mode, one of `CHART_COLORS`. In series mode, the color of a metric\n without `group_by`, which draws one series; null draws it blue. Null when the color mode does\n not use one.\n\n- **`ui_chart_color_rules`** `MetricColorRule[] | null`\n\n Ordered value-to-color rules used by value mode.\n\n- **`ui_chart_series_rules`** `MetricSeriesRule[] | null`\n\n Colors for series by value, in order: a series takes the color of the first rule that is not\n dismissed and matches it. Once the metric has such a rule, every series no rule matches is\n gray; without one, each series takes an automatic color that no other series on the chart uses,\n until the palette runs out. Rules color only a metric with `group_by`. When the color step\n runs, errors, warnings and successes it finds on a grouped field get default rules right away,\n marked `is_default`, and the AI's suggestions follow, marked `is_suggested`; a suggestion may\n replace the defaults of its field. Null when no rule was ever saved.\n\n- **`ai_color_fields`** `string[] | null`\n\n Fields whose values carry color meaning: those the AI marked, and grouped fields where\n Tailglow's default list matched a value. Null before either found one.\n\n- **`ai_colors_next_run_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n When the color step runs next for this metric; null when no run is waiting. The step adds\n default rules marked `is_default`, and the AI's suggestions arrive marked `is_suggested`.\n\n- **`ai_colors_started_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n When the run in progress started; null when none is running.\n\n- **`ai_colors_completed_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n When the AI color step last finished for this metric; null before its first run.\n\n- **`ui_chart_curve`** [`ChartCurve`](/api/metrics#chart-curve)\n\n Line interpolation style.\n\n- **`ui_chart_show_alerts`** `boolean`\n\n Whether the chart displays alert markers.\n\n- **`ui_chart_compact_values`** `boolean`\n\n Whether chart tooltips round values to compact notation, such as 321.3M.\n\n- **`ui_chart_y_min`** `number | null`\n\n Fixed lower bound for the y axis. Null lets the axis fit the data.\n\n- **`ui_chart_y_max`** `number | null`\n\n Fixed upper bound for the y axis. Null lets the axis fit the data.\n\n- **`ui_chart_show_trends`** `boolean`\n\n Whether the chart displays regression trends.\n\n- **`ui_chart_show_forecast`** `boolean`\n\n Whether the chart opens with its forecast shown, projecting the fitted trend past now to the\n saved horizon. Viewers in the app can change the forecast for their own view without changing\n this setting.\n\n- **`ui_chart_forecast_horizon`** [`ForecastHorizon`](/api/metrics#forecast-horizon)\n\n How far past now the chart projects when it draws a forecast. Relative values resolve against\n the moment the chart is viewed, not the moment they were saved.\n\n- **`ui_chart_forecast_model`** [`ForecastModel`](/api/metrics#forecast-model)\n\n Curve the forecast is fitted with. `auto` follows whichever model fits the series best at view\n time, so the projected shape can change as data arrives.\n\n- **`ui_value_unit`** `string | null`\n\n Unit label displayed with chart values.\n\n- **`ui_chart_value`** [`MetricChartValue`](/api/metrics#metric-chart-value)\n\n Aggregation value displayed by default.\n\n- **`ui_chart_time_range`** [`AnalyticsTimeRange`](/api/metrics#analytics-time-range)\n\n Window the chart covers by default.\n\n- **`ui_chart_interval`** [`ChartInterval`](/api/metrics#chart-interval)\n\n Bucket size the chart uses by default.\n\n- **`time_dimension`** [`TimeDimension | null`](/api/metrics#time-dimension)\n\n Calendar position the chart folds the metric's hourly data into. With one set, the aggregation\n merges every hour of the range into its hour of the day, weekday or hour of the week, read in\n the request's `timezone`, instead of returning a timeline. Null charts the data over time.\n\n- **`ui_chart_order`** [`ChartOrder | null`](/api/metrics#chart-order)\n\n The order the chart shows its series in: `highest` largest first, `lowest` smallest first,\n `natural` largest first with a time dimension kept in its own order (hour 0 to 23, Monday to\n Sunday). Null is `natural`. An aggregation request's `sort` overrides the direction.\n\n- **`ui_chart_custom_range_start_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Start of the custom chart range.\n\n- **`ui_chart_custom_range_end_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n End of the custom chart range.\n\n- **`ui_chart_max_series`** `number`\n\n Number of series an aggregation returns per page when the request sends no `limit`.\n\n- **`created_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n- **`backfill_percent`** `number`\n\n Percentage of the view's data files the historical backfill has processed.\n\n- **`backfill_total_count`** `number`\n\n How many of the view's data files the historical backfill has to process. Seeded as 1 before\n the run is planned.\n\n- **`backfill_completed_count`** `number`\n\n How many of those files it has processed.\n\n- **`backfill_completed_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Null until the backfill finishes.\n\n- **`backfill_started_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Null until the backfill starts.\n\n- **`compact_percent`** `number`\n\n Percentage of rollup-building steps completed.\n\n- **`compact_total_count`** `number`\n\n How many rollup-building steps the run has planned; 0 until that pass starts.\n\n- **`compact_completed_count`** `number`\n\n How many of those steps are done.\n\n- **`compact_started_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Null until the rollup-building pass starts.\n\n- **`has_triggered_monitors`** `boolean`\n\n Whether at least one alert is currently open. Ended alerts remain in history.\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#### 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\" | \"highest\" | \"lowest\"`\n\nHow a breakdown chart orders its groups. `natural` keeps their own order (hours, weekdays),\n`value` ranks them largest first.\n\n## Timeseries Aggregation Model\n\n### Fields\n\n- **`object`** `\"aggregation\"`\n\n- **`coverage_reasons`** `{ view_id: string; view_name: string; reason: string; count: number | null; }[]`\n\n Count is null when the reason reports a status rather than a measured count.\n\n- **`start_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n- **`end_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n- **`interval`** [`DataInterval`](/api/metrics#data-interval)\n\n Bucket size each entry in a series' `records` covers. For a time dimension it is `hour`, the\n size of the buckets folded into each category.\n\n- **`values`** [`MetricChartValue[]`](/api/metrics#metric-chart-value)\n\n Declares what each number in a series' `records` arrays means; positions align.\n\n- **`series`** [`MetricSeries[]`](/api/metrics#aggregation-series-model)\n\n One page of the series with data in the range, plus a synthetic `__other__` entry that folds in\n every series not on the page. An explicit series filter returns those of its series with data\n in the range and nothing else. Without one, pinned series make up the first page and later\n pages walk the remaining series by value; with no pins every page ranks by value.\n\n- **`complete_through_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Complete through this instant as of this response. A later arrival can reopen an older bucket.\n Null means coverage is unknown. Prefer pending_ranges when present for isolated pending dates.\n\n- **`pending_ranges`** `PendingTimeRange[] | null`\n\n Known pending event-time windows within displayed buckets, end exclusive. Partial boundary\n buckets can extend beyond the requested window. [] means none; null means unknown. Absent when\n the response states no pending windows, as on server telemetry: complete_through_at decides.\n\n- **`input_checkpoint`** `{ processed_position: number; published_position: number; } | null`\n\n Monotonic input progress, independent of the customer-selected event timestamp. Null when the\n response reports none.\n\n- **`first_incomplete_bucket_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Key of the first bucket that is still filling. For isolated pending dates, prefer\n pending_ranges; this older field conservatively marks the suffix from the first pending bucket.\n Null when every bucket in the response is final, and always null on server telemetry, which\n makes no completeness statement. Always null for a time dimension, whose categories merge hours\n from across the range, including the current hour's data so far.\n\n- **`time_dimension`** [`TimeDimension | null`](/api/metrics#time-dimension)\n\n The time dimension the series were folded into, from the metric. When set, each series'\n `records` are keyed by category instead of bucket start: `\"0\"` to `\"23\"` for `hour_of_day`,\n `\"1\"` (Monday) to `\"7\"` for `weekday`, `\"0\"` to `\"167\"` for `hour_of_week`. Null for a\n timeline.\n\n- **`timezone`** `string | null`\n\n The IANA timezone the request's date boundaries, and a time dimension's categories, were read\n in. Null on server telemetry.\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#### DataInterval\n\n`\"minute\" | \"hour\" | \"day\" | \"month\"`\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#### 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## Aggregation Series Model\n\n### Fields\n\n- **`details`** `{ label: string; hash: string; dimensions: Record<string, string | number | boolean | null>; fields: Record<string, string | number | boolean>; }`\n\n The series' identity (`hash`, `dimensions`), its label and the fields it carries. Colors come\n from the metric's `ui_chart_series_rules`: a series that matches a rule takes its color, and\n once the metric has a rule every other series is gray.\n\n- **`records`** `Record<string, (number | null)[]>`\n\n One entry per bucket, keyed by bucket start, or per category when the aggregation has a\n `time_dimension`; values aligned with the aggregation's `values` list. `null` means the bucket\n had no data and the metric's `empty_bucket_handling` is `gaps`. In a time dimension the\n cumulative values are always `null`.\n\n- **`regression`** `LinearRegression | ExponentialRegression | LogarithmicRegression | LogisticRegression | SinusoidalRegression | null`\n\n Trend line fitted over the series; null when none was computed.\n\n- **`forecast`** `RegressionForecast | null`\n\n The trend line's projected next value; null when no trend was computed.\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## Metric Series Model\n\n### Fields\n\n- **`object`** `\"metric_series\"`\n\n- **`id`** `string`\n\n- **`metric_id`** `string`\n\n- **`hash`** `string`\n\n Opaque identifier of the series' dimension combination, stable for the life of the metric.\n\n- **`label`** `string`\n\n Per field in `group_by`, the value of the view's display field for it from `fields`, else the\n raw value; joined with \" | \".\n\n- **`dimensions`** `Record<string, string | number | boolean | null>`\n\n The `group_by` field values that define this series.\n\n- **`fields`** `Record<string, string | number | boolean>`\n\n Values the series keeps from its latest records: the view's display fields for the fields in\n `group_by`, and the fields color rules read that are not in `group_by`. A field missing or null\n in a record leaves its value as it was.\n\n- **`first_seen_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n When the series was registered. Stamped with the processing time, so a backfill registers\n historical combinations at run time.\n\n- **`last_seen_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n Also stamped at registration; not a recency signal, use `last_value_at` for that.\n\n- **`last_value`** `number | null`\n\n The most recent aggregated value observed for the series.\n\n- **`last_value_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n When `last_value` was 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 Metrics\n\n### Endpoint\n\nRetrieve a list of metrics for a project.\n\n```http\nGET /v1/projects/:project_id/metrics\n```\n\n**Scope:** `metrics:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Query Parameters\n\n- **`view_id`** `string`\n Return metrics that read from this view. Optional.\n\n- **`order_by`** `string`\n Field used to order the metrics. Optional. Defaults to `\"name\"`. Allowed values: `\"name\"`.\n\n- **`deleted_at`** [`NullableDateFilter`](/api/metrics#nullable-date-filter)\n Filter on deletion state. Pass `null` for metrics that are not scheduled for deletion. Optional.\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: Metric[];\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 Metric\n\n### Endpoint\n\nRetrieve a single metric.\n\n```http\nGET /v1/projects/:project_id/metrics/:metric_id\n```\n\n**Scope:** `metrics: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### Response\n\nMetric retrieved\n\n```ts\n{\n message: string;\n data: Metric;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Retrieve Metric Aggregation\n\n### Endpoint\n\nRetrieve aggregated time-series data for a metric.\n\n```http\nGET /v1/projects/:project_id/metrics/:metric_id/aggregation\n```\n\n**Scope:** `metrics: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- **`start_at`** [`ISODateString`](/api/metrics#iso-date-string)\n Start of the aggregation range. Send together with `end_at`. Optional.\n\n- **`end_at`** [`ISODateString`](/api/metrics#iso-date-string)\n End of the aggregation range. Send together with `start_at`. Optional.\n\n- **`interval`** [`ChartInterval`](/api/metrics#chart-interval)\n Requested aggregation bucket interval. A metric with a time dimension ignores it and always folds hourly data. Optional.\n\n- **`time_range`** [`AnalyticsTimeRange`](/api/metrics#analytics-time-range)\n Preset used to calculate the aggregation range. Ignored when `start_at` and `end_at` are both sent. Defaults to the metric's saved chart range. Optional.\n\n- **`timezone`** `string`\n IANA timezone used to calculate date boundaries and to place each hour in a time dimension's categories. Optional.\n\n- **`page_id`** `string`\n Page whose saved custom range should be used. Applies only when `time_range` is `custom`. Optional.\n\n- **`normalize`** `boolean`\n Whether the response includes empty time buckets. Optional. Defaults to `true`.\n\n- **`forecast_at`** [`ISODateString`](/api/metrics#iso-date-string)\n Future timestamp requested for a forecast. Optional.\n\n- **`series`** `string`\n Comma-separated series hashes to return. Optional.\n\n- **`limit`** `integer`\n Number of series to return per page. Defaults to the metric's `ui_chart_max_series`. Optional. 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 Ranking direction of the series. `desc` returns the largest first. Defaults to the metric's `ui_chart_order`: `asc` for `lowest`, `desc` otherwise. Optional. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: TimeseriesAggregation;\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `last` returns the latest reading in each time bucket within the requested range.\n- Record keys are UTC bucket starts. Buckets still filling come back with the data counted so far: every key at or after `first_incomplete_bucket_at` is incomplete and can still grow, so report it as \"so far\" and never compare it with a complete bucket. Incomplete buckets with no data yet are left out rather than returned as zero.\n- `complete_through_at` is the instant before which every number is final. Null means completeness is unknown, and then every bucket is incomplete.\n- For complete buckets only, leave out every key at or after `first_incomplete_bucket_at`.\n- `coverage_reasons` names unresolved view scopes; `excluded` is informational.\n- Series are ranked by their total over the range (the count for a `count` metric, the sum of Last readings for `last`, the sum otherwise), ties broken by series hash, and `limit` of them come back per page, the metric's `ui_chart_max_series` by default, in rank order. Walk the pages with `pagination.next_cursor` and `pagination.prev_cursor`; `pagination.total` counts every series with data in the range.\n- Only series with data in the range come back. Without a `series` filter, when none has any, a metric whose `empty_bucket_handling` is `zero` returns one series of zeros labelled with the metric's name (hash `__default__`), unless `normalize` is false.\n- Without `sort`, series rank smallest first when the metric's `ui_chart_order` is `lowest` and largest first otherwise.\n- When the metric colors by series (`ui_chart_color_mode` is `by_series`), series that match one of its `ui_chart_series_rules` rank ahead of every other series, so they take the first page's places first, except on a `table` chart, which ranks all series together. Color a series by the first active rule it matches, and every other series gray once the metric has an active rule. In the other color modes rules color nothing and every series ranks together.\n- A read that shows a series value no color rule covers, on a field in the metric's `ai_color_fields`, asks for new default rules and AI color suggestions (`ai_colors_next_run_at`), at most once a day.\n- `__other__` folds in every series that is not on the current page, so the page plus Other always adds up to the range total and a share is right on every page. It comes last.\n- `series` returns the listed series that have data in the range, on one page, unranked and without `__other__`.\n- A metric with a `time_dimension` returns its range folded instead of as a timeline: every hour merges into its hour of the day, weekday or hour of the week. Record keys are then the category as a decimal string (`\"0\"` to `\"23\"`, `\"1\"` for Monday to `\"7\"` for Sunday, `\"0\"` to `\"167\"`), every category is present whatever `normalize` says, and `time_dimension` echoes it.\n- Data is stored by UTC hour. A fold places each hour in its category in the request `timezone` (echoed as `timezone`), one hour at a time, so a daylight saving change moves only the hours after it.\n- A fold reads hourly data whatever `interval` says, covers at most 400 days (a longer range is refused), and includes the current hour's data so far.\n- In a fold an empty category is `0` when `empty_bucket_handling` is `zero` and `null` when it is `gaps`, cumulative values are `null`, and there is no trend line.\n- The aggregation window is resolved in order: an explicit `start_at` plus `end_at` wins; otherwise `time_range` is used; otherwise the metric's saved chart range is used.\n- `start_at` and `end_at` only take effect when sent together. Sending one without the other has no effect on the window.\n- When both are provided, `start_at` must be earlier than `end_at`.\n- `time_range=custom` without explicit dates falls back to the saved custom range of `page_id`, then the metric, then the project.\n- `timezone` must be a valid IANA timezone name, or `UTC`.\n- `after` and `before` are mutually exclusive.\n\n## Create Metric\n\n### Endpoint\n\nCreate a metric for a project.\n\n```http\nPOST /v1/projects/:project_id/metrics\n```\n\n**Scope:** `metrics: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 The subject only, without the aggregation. Charts title themselves as the aggregation followed by this name, so `API Latency` renders as `P95 of API Latency`. Putting the aggregation here produces `P95 of API P95 Latency`. Minimum length: `1`. Maximum length: `64`.\n\n- **`view_id`** `string` -- **Required**\n View that provides the metric's records. Maximum length: `32`.\n\n- **`group_by`** `string[]`\n View fields used to split records into series. Optional. Defaults to `[]`.\n\n- **`filters`** `string`\n A query string of `?field=operator:value` pairs joined by `&`, applied when records are aggregated. For example `?status=equals:200` or `?level=in:error,warn&path=starts_with:/api`. Comma-separate the values of `in`, `not_in`, and `between`. Every operator must be spelled in full; short forms such as `eq:` are rejected. Optional. Defaults to `\"\"`. Maximum length: `8192`.\n\n- **`timestamp_field`** `string`\n View field used as the aggregation timestamp. Optional. Defaults to `\"timestamp\"`. Minimum length: `1`. Maximum length: `256`.\n\n- **`value_field`** `string | null`\n View field used as the numeric aggregation value. Optional.\n\n- **`unique_field`** `string | null`\n View field used to count distinct values. Optional.\n\n- **`null_handling`** [`NullHandling`](/api/metrics#null-handling)\n How null or missing metric values are aggregated. Optional. Defaults to `\"skip\"`.\n\n- **`empty_bucket_handling`** [`EmptyBucketHandling`](/api/metrics#empty-bucket-handling)\n How chart buckets without observations are represented. Optional.\n\n- **`ui_chart_type`** [`ChartType`](/api/metrics#chart-type)\n Default chart type. Optional.\n\n- **`ui_chart_color_mode`** [`ChartColorMode`](/api/metrics#chart-color-mode)\n Strategy used to assign chart colors. Optional.\n\n- **`ui_chart_color_base`** [`ChartColor`](/api/metrics#chart-color)\n Base color used by intensity mode. In series mode, the color of a metric without `group_by`, which draws one series; null draws it blue. Optional.\n\n- **`ui_chart_color_rules`** `object[] | null`\n Ordered value-to-color rules used by value mode. Optional.\n\n- **`ui_chart_series_rules`** `object[] | null`\n Colors for series by value, in order: a series takes the color of the first rule it matches, and once a rule is active every other series is gray. Rules color only a metric with `group_by`. Rules Tailglow adds from its default list (errors red, warnings amber, successes green) are marked `is_default`, and rules the AI suggests are marked `is_suggested`. Optional.\n\n- **`ui_chart_show_alerts`** `boolean`\n Whether the chart displays alert markers. Optional.\n\n- **`ui_chart_show_trends`** `boolean`\n Whether the chart displays regression trends. Optional.\n\n- **`ui_chart_compact_values`** `boolean`\n Whether chart tooltips round values to compact notation, such as 321.3M instead of the full number. Optional.\n\n- **`ui_chart_y_min`** `number | null`\n Fixed lower bound for the chart's y axis. Null lets the axis fit the data. Optional.\n\n- **`ui_chart_y_max`** `number | null`\n Fixed upper bound for the chart's y axis. Null lets the axis fit the data. Optional.\n\n- **`ui_chart_show_forecast`** `boolean`\n Whether the chart opens with its forecast shown, projecting the fitted trend past now to the saved horizon. Optional.\n\n- **`ui_chart_forecast_horizon`** [`ForecastHorizon`](/api/metrics#forecast-horizon)\n How far past now the chart projects its forecast. The value is relative to when the chart is viewed, so it keeps projecting the same distance ahead as time passes. Optional.\n\n- **`ui_chart_forecast_model`** [`ForecastModel`](/api/metrics#forecast-model)\n Curve the forecast is fitted with. Use `auto` to follow whichever model currently fits the series best. Optional.\n\n- **`ui_value_unit`** `string`\n Unit label displayed with chart values. Optional. Maximum length: `32`.\n\n- **`ui_chart_curve`** [`ChartCurve`](/api/metrics#chart-curve)\n Chart line interpolation style. Optional.\n\n- **`ui_chart_value`** [`MetricChartValue`](/api/metrics#metric-chart-value)\n Aggregation value displayed by default. Optional.\n\n- **`ui_chart_time_range`** [`AnalyticsTimeRange`](/api/metrics#analytics-time-range)\n Time range displayed by default. Optional.\n\n- **`ui_chart_interval`** [`ChartInterval`](/api/metrics#chart-interval)\n Aggregation interval displayed by default. Optional.\n\n- **`time_dimension`** [`TimeDimension`](/api/metrics#time-dimension)\n Calendar position the chart folds the metric's hourly data into, such as the hour of the day. Null charts the data over time. Optional.\n\n- **`ui_chart_order`** `string | null`\n Order the chart shows its series in: `highest` largest first, `lowest` smallest first, `natural` largest first with a time dimension kept in its own order (hour 0 to 23, Monday to Sunday). Null is `natural`, and `value` is read as `highest`. Optional. Allowed values: `\"natural\"`, `\"highest\"`, `\"lowest\"`, `\"value\"`.\n\n- **`ui_chart_custom_range_start_at`** [`ISODateString`](/api/metrics#iso-date-string)\n Start of the custom chart range. Optional.\n\n- **`ui_chart_custom_range_end_at`** [`ISODateString`](/api/metrics#iso-date-string)\n End of the custom chart range. Optional.\n\n- **`ui_chart_max_series`** `integer`\n Number of series an aggregation returns per page when the request sends no `limit`. Optional. Minimum: `1`. Maximum: `50`.\n\n- **`backfill_strategy`** [`BackfillStrategy`](/api/metrics#backfill-strategy)\n Order used to rebuild historical data. Optional.\n\n### Response\n\nYour metric has been created and will begin collecting data immediately.\n\n```ts\n{\n message: string;\n data: Metric;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `view_id` is required when creating a metric.\n- Field references and filters must be compatible with the selected view.\n- Custom chart ranges require both dates, with an interval of at least 30 minutes.\n- `ui_chart_color_base` is required in `by_intensity` mode and refused in `by_value` mode. In `by_series` mode it is optional and colors a metric without `group_by`.\n- `ui_chart_color_rules` needs at least one rule in `by_value` mode and is refused in the other modes.\n- `ui_chart_color_mode` must be a mode the chart type supports. A metric created without a `ui_chart_type` is a `bar` chart and is checked as one.\n- `time_dimension` must be one the chart type supports: `breakdown` takes `hour_of_day` or `weekday`, `heatmap` takes `hour_of_week`, `hour_of_day` or `weekday`, and every other chart type takes none.\n- A `heatmap` chart requires a `time_dimension`.\n- A `time_dimension` cannot be combined with a cumulative `ui_chart_value` (`cumulative_sum` or `cumulative_count`).\n- `filters` must be a valid filter expression.\n- `timestamp_field` cannot reference `__meta__` metadata.\n- `ui_chart_series_rules` holds at most 20 rules that are not dismissed, and each rule's `field` must be a field of the view.\n- A metric created with `group_by` and without `ui_chart_series_rules` gets default rules for the errors, warnings and successes its first series show, marked `is_default`, and then asks the AI for color rules, which arrive marked `is_suggested` and may replace the defaults of their field. A team with Tailglow AI off gets the defaults only.\n\n## Update Metric\n\n### Endpoint\n\nUpdate an existing metric.\n\n```http\nPOST /v1/projects/:project_id/metrics/:metric_id\n```\n\n**Scope:** `metrics:write`\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### Request Body\n\n- **`name`** `string`\n The subject only, without the aggregation. Charts title themselves as the aggregation followed by this name, so `API Latency` renders as `P95 of API Latency`. Putting the aggregation here produces `P95 of API P95 Latency`. Optional. Minimum length: `1`. Maximum length: `64`.\n\n- **`view_id`** `string`\n View that provides the metric's records. Optional. Maximum length: `32`.\n\n- **`group_by`** `string[]`\n View fields used to split records into series. Optional.\n\n- **`filters`** `string`\n A query string of `?field=operator:value` pairs joined by `&`, applied when records are aggregated. For example `?status=equals:200` or `?level=in:error,warn&path=starts_with:/api`. Comma-separate the values of `in`, `not_in`, and `between`. Every operator must be spelled in full; short forms such as `eq:` are rejected. Optional. Maximum length: `8192`.\n\n- **`timestamp_field`** `string`\n View field used as the aggregation timestamp. Optional. Minimum length: `1`. Maximum length: `256`.\n\n- **`value_field`** `string | null`\n View field used as the numeric aggregation value. Optional.\n\n- **`unique_field`** `string | null`\n View field used to count distinct values. Optional.\n\n- **`null_handling`** [`NullHandling`](/api/metrics#null-handling)\n How null or missing metric values are aggregated. Optional.\n\n- **`empty_bucket_handling`** [`EmptyBucketHandling`](/api/metrics#empty-bucket-handling)\n How chart buckets without observations are represented. Optional.\n\n- **`ui_chart_type`** [`ChartType`](/api/metrics#chart-type)\n Default chart type. Optional.\n\n- **`ui_chart_color_mode`** [`ChartColorMode`](/api/metrics#chart-color-mode)\n Strategy used to assign chart colors. Optional.\n\n- **`ui_chart_color_base`** [`ChartColor`](/api/metrics#chart-color)\n Base color used by intensity mode. In series mode, the color of a metric without `group_by`, which draws one series; null draws it blue. Optional.\n\n- **`ui_chart_color_rules`** `object[] | null`\n Ordered value-to-color rules used by value mode. Optional.\n\n- **`ui_chart_series_rules`** `object[] | null`\n Colors for series by value, in order: a series takes the color of the first rule it matches, and once a rule is active every other series is gray. Rules color only a metric with `group_by`. Rules Tailglow adds from its default list (errors red, warnings amber, successes green) are marked `is_default`, and rules the AI suggests are marked `is_suggested`. Optional.\n\n- **`seen_suggestions_at`** [`ISODateString`](/api/metrics#iso-date-string)\n The newest `suggested_at` among the rules you loaded. A suggestion or default left out of `ui_chart_series_rules` is dismissed only when it is no newer than this; without it, nothing is dismissed. Optional.\n\n- **`ui_chart_show_alerts`** `boolean`\n Whether the chart displays alert markers. Optional.\n\n- **`ui_chart_show_trends`** `boolean`\n Whether the chart displays regression trends. Optional.\n\n- **`ui_chart_compact_values`** `boolean`\n Whether chart tooltips round values to compact notation, such as 321.3M instead of the full number. Optional.\n\n- **`ui_chart_y_min`** `number | null`\n Fixed lower bound for the chart's y axis. Null lets the axis fit the data. Optional.\n\n- **`ui_chart_y_max`** `number | null`\n Fixed upper bound for the chart's y axis. Null lets the axis fit the data. Optional.\n\n- **`ui_chart_show_forecast`** `boolean`\n Whether the chart opens with its forecast shown, projecting the fitted trend past now to the saved horizon. Optional.\n\n- **`ui_chart_forecast_horizon`** [`ForecastHorizon`](/api/metrics#forecast-horizon)\n How far past now the chart projects its forecast. The value is relative to when the chart is viewed, so it keeps projecting the same distance ahead as time passes. Optional.\n\n- **`ui_chart_forecast_model`** [`ForecastModel`](/api/metrics#forecast-model)\n Curve the forecast is fitted with. Use `auto` to follow whichever model currently fits the series best. Optional.\n\n- **`ui_value_unit`** `string`\n Unit label displayed with chart values. Optional. Maximum length: `32`.\n\n- **`ui_chart_curve`** [`ChartCurve`](/api/metrics#chart-curve)\n Chart line interpolation style. Optional.\n\n- **`ui_chart_value`** [`MetricChartValue`](/api/metrics#metric-chart-value)\n Aggregation value displayed by default. Optional.\n\n- **`ui_chart_time_range`** [`AnalyticsTimeRange`](/api/metrics#analytics-time-range)\n Time range displayed by default. Optional.\n\n- **`ui_chart_interval`** [`ChartInterval`](/api/metrics#chart-interval)\n Aggregation interval displayed by default. Optional.\n\n- **`time_dimension`** [`TimeDimension`](/api/metrics#time-dimension)\n Calendar position the chart folds the metric's hourly data into, such as the hour of the day. Null charts the data over time. Optional.\n\n- **`ui_chart_order`** `string | null`\n Order the chart shows its series in: `highest` largest first, `lowest` smallest first, `natural` largest first with a time dimension kept in its own order (hour 0 to 23, Monday to Sunday). Null is `natural`, and `value` is read as `highest`. Optional. Allowed values: `\"natural\"`, `\"highest\"`, `\"lowest\"`, `\"value\"`.\n\n- **`ui_chart_custom_range_start_at`** [`ISODateString`](/api/metrics#iso-date-string)\n Start of the custom chart range. Optional.\n\n- **`ui_chart_custom_range_end_at`** [`ISODateString`](/api/metrics#iso-date-string)\n End of the custom chart range. Optional.\n\n- **`ui_chart_max_series`** `integer`\n Number of series an aggregation returns per page when the request sends no `limit`. Optional. Minimum: `1`. Maximum: `50`.\n\n- **`backfill_strategy`** [`BackfillStrategy`](/api/metrics#backfill-strategy)\n Order used to rebuild historical data. Optional.\n\n- **`deleted_at`** `null`\n Send `null` to restore a metric that is scheduled for deletion. The deletion date itself is set by the delete endpoint and cannot be written here. Optional.\n\n### Response\n\nYour metric has been updated and is calculating historical data\n\n```ts\n{\n message: string;\n data: Metric;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Field references and filters must be compatible with the selected view.\n- Changing the metric's data feed rebuilds its historical rollups.\n- Custom chart ranges require both dates, with an interval of at least 30 minutes.\n- `ui_chart_color_base` is required in `by_intensity` mode and refused in `by_value` mode. In `by_series` mode it is optional and colors a metric without `group_by`.\n- `ui_chart_color_rules` needs at least one rule in `by_value` mode and is refused in the other modes.\n- `ui_chart_color_mode` must be a mode the chart type supports. Changing `ui_chart_type` keeps the current mode, so send a supported one with it.\n- `time_dimension` must be one the chart type supports: `breakdown` takes `hour_of_day` or `weekday`, `heatmap` takes `hour_of_week`, `hour_of_day` or `weekday`, and every other chart type takes none. Send `time_dimension: null` when changing to a type that takes none.\n- A `heatmap` chart requires a `time_dimension`.\n- A `time_dimension` cannot be combined with a cumulative `ui_chart_value` (`cumulative_sum` or `cumulative_count`).\n- `filters` must be a valid filter expression.\n- `timestamp_field` cannot reference `__meta__` metadata.\n- `ui_chart_series_rules` holds at most 20 rules that are not dismissed. A rule you add must name a field of the view; a saved rule whose field the view has since dropped is kept and matches nothing.\n- Saving `ui_chart_series_rules` replaces your own rules. A suggestion or default you leave out is dismissed, and never added again, only when you also send `seen_suggestions_at` no older than its `suggested_at`; one added after the rules you loaded stays. A suggestion or default you change becomes your own rule.\n\n## Backfill Metric Data\n\n### Endpoint\n\nBackfill a metric's historical data from its view.\n\n```http\nPOST /v1/projects/:project_id/metrics/:metric_id/backfill\n```\n\n**Scope:** `metrics:write`\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### Response\n\nYour metric data is being rebuilt from its view\n\n```ts\n{\n message: string;\n data: Metric;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Delete Metric\n\n### Endpoint\n\nDelete a metric.\n\n```http\nDELETE /v1/projects/:project_id/metrics/:metric_id\n```\n\n**Scope:** `metrics:delete`\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### Response\n\n```ts\n{\n message: string;\n data: Metric;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The metric is hidden immediately and permanently removed once its restore window elapses. Restore it before then by updating it with `deleted_at` set to null.\n- Collection and backfill keep running for the whole window, so a restored metric resumes with no gap in its history. Its monitors stop evaluating until it is restored.\n\n## Force Delete Metric\n\n### Endpoint\n\nDelete a metric permanently, without waiting out its restore window.\n\n```http\nDELETE /v1/projects/:project_id/metrics/:metric_id/force\n```\n\n**Scope:** `metrics:delete`\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### Response\n\nMetric queued for permanent deletion.\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- Works on a live metric as well as one already scheduled for deletion.\n- Its rollups and monitors are destroyed. The view it reads is not affected, so the metric can be recreated and backfilled.\n\n## List Metric Series\n\n### Endpoint\n\nRetrieve the series discovered for a metric.\n\n```http\nGET /v1/projects/:project_id/metrics/:metric_id/series\n```\n\n**Scope:** `metrics: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- **`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- **`order_by`** `string`\n Field used to order the metric series. Optional. Defaults to `\"first_seen_at\"`. Allowed values: `\"first_seen_at\"`, `\"last_seen_at\"`.\n\n- **`search`** `string`\n Substring matched against hashes, dimension values and carried field values. Optional. Minimum length: `1`. Maximum length: `128`.\n\n### Response\n\n```ts\n{\n message: string;\n data: MetricSeriesRecord[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Each series is labeled by its view's `display_fields`: per field in `group_by`, the value the series carries for its display field, else the raw value.\n- `after` and `before` are mutually exclusive.\n\n",
22
+ "api/collections": "# Collections\n\n## Collection Model\n\n### Fields\n\n- **`object`** `\"collection\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `col_`.\n\n- **`source_id`** `string`\n\n- **`source_name`** `string | null`\n\n- **`project_id`** `string`\n\n- **`slug`** `string`\n\n Permanent identifier used in endpoints. Cannot be changed after creation.\n\n- **`name`** `string`\n\n- **`storage_gb`** `number`\n\n Data stored in this collection's files, in gigabytes of uncompressed data. Recalculated hourly.\n\n- **`schema_count`** `number`\n\n How many schema versions this collection has, in every state.\n\n- **`views_count`** `number`\n\n How many views read from this collection.\n\n- **`drains_count`** `number`\n\n How many drains export from this collection.\n\n- **`schema_opaque_paths`** `string[][]`\n\n Object paths whose contents do not take part in schema identity, one key segment per entry.\n Records that differ only inside an opaque path share one schema version. Changing this re-keys\n the collection.\n\n- **`schema_tracked_paths`** `string[][]`\n\n Children of opaque paths that stay part of schema identity. Changing this re-keys the\n collection.\n\n- **`schema_max_depth`** `number`\n\n How many levels of nesting take part in schema identity. Changing this re-keys the collection.\n\n- **`schema_nulls`** [`SchemaRuleMode`](/api/collections#schema-rule-mode)\n\n `auto` accepts null and absent values where a mapping reads; `manual` waits for a person per\n view.\n\n- **`schema_optional_keys`** [`SchemaRuleMode`](/api/collections#schema-rule-mode)\n\n `auto` lets a record with a subset of a version's keys join it; `manual` treats absence as a\n new shape.\n\n- **`schema_detector`** [`SchemaRuleMode`](/api/collections#schema-rule-mode)\n\n `auto` applies a safe opaque-path proposal on its own; `manual` holds every proposal for a\n person.\n\n- **`schema_recurrence_gap_ms`** `number`\n\n How long after first sight a shape must recur before its version settles, in milliseconds.\n\n- **`schema_bulk_rows`** `number`\n\n A single batch carrying this many rows of a shape settles its version at once; `0` disables it.\n\n- **`schema_stale_after_ms`** `number`\n\n How long a candidate version waits to recur before it goes stale, in milliseconds.\n\n- **`schema_identity_limit`** `number`\n\n How many settled versions the collection may hold; past it, new shapes stay unclassified.\n\n- **`schema_candidate_pool`** `number`\n\n How many candidate and stale versions the collection may hold; past it, new shapes stay\n unclassified.\n\n- **`schema_alias_pool`** `number`\n\n How many exact signature hashes the collection caches for lookup.\n\n- **`schema_writer_limit`** `number`\n\n How many collection files one ingest batch may open for this collection. Shapes past the\n candidate pool get the same number of files per batch, the shapes with the most rows first; the\n rows of shapes past that budget stay unclassified and cannot be linked to a version later.\n\n- **`schema_unclassified_retention_days`** `number | null`\n\n How many days rows without a settled version are kept; null keeps them forever.\n\n- **`deleted_at`** [`ISODateString | null`](/api/collections#iso-date-string)\n\n When the collection is scheduled to be deleted; null when it is not.\n\n- **`created_at`** [`ISODateString`](/api/collections#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/collections#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#### SchemaRuleMode\n\n`\"auto\" | \"manual\"`\n\n## Collection Schema Report\n\n### Fields\n\n- **`object`** `\"collection_schema_report\"`\n\n- **`collection_id`** `string`\n\n- **`rules`** `{ opaque_paths: string[][]; tracked_paths: string[][]; max_depth: number; }`\n\n The rules the report reads the collection's shapes under: the collection's own for the\n retrieve, the rules sent for a preview.\n\n- **`shapes`** `{ before: number; after: number; }`\n\n How many shapes the collection holds under its current rules (`before`) and under the report's\n rules (`after`). Equal unless the report previews a rule change; the difference is what a\n re-key would merge.\n\n- **`proposals`** [`CollectionSchemaProposal[]`](/api/collections#schema-proposal)\n\n Where the detector sees keys being invented, with what applying each proposal would do.\n\n- **`keys_look_like_data`** `boolean`\n\n Whether every shape's top-level keys are its own, such as records keyed by a date. Those keys\n are values, and no opaque path can bring the shapes together.\n\n- **`unclassified`** `{ shapes: number; rows: number; }`\n\n Shapes past the collection's pools, stored with their signature but no version, and the rows\n they carry. Nothing reads them until the pool frees or a rule brings them under a version.\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## Collection Schema Proposal\n\n### Fields\n\n- **`object`** `\"collection_schema_proposal\"`\n\n- **`path`** `string[]`\n\n The path the detector proposes opaque, as key segments.\n\n- **`tracked`** `string[]`\n\n Children of the path every shape carries with one type, which stay in schema identity as\n tracked paths when the proposal is applied.\n\n- **`content_children`** `number`\n\n How many children of the path read as content: keys that keep being invented.\n\n- **`read_by_mapping`** `boolean`\n\n Whether a bound mapping reads inside the path. Applying the proposal would leave that mapping\n unverified, so the detector never applies such a proposal on its own; a person can.\n\n- **`shapes_after`** `number`\n\n How many shapes the collection would hold with this proposal applied.\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 Collections\n\n### Endpoint\n\nRetrieve a list of collections for a project.\n\n```http\nGET /v1/projects/:project_id/collections\n```\n\n**Scope:** `sources: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 collections. Optional. Defaults to `\"name\"`. Allowed values: `\"created_at\"`, `\"name\"`, `\"updated_at\"`.\n\n- **`source_id`** `string`\n Filter to collections under a single source. Optional.\n\n- **`project_id`** `string`\n Filter to collections in a single project. Optional.\n\n- **`deleted_at`** [`NullableDateFilter`](/api/collections#nullable-date-filter)\n Filter by scheduled deletion date. Use `null` for collections that are not scheduled for deletion, `not:null` for collections that are. Optional.\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: Collection[];\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 Collection\n\n### Endpoint\n\nRetrieve a single collection.\n\n```http\nGET /v1/projects/:project_id/collections/:collection_id\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Response\n\nCollection retrieved\n\n```ts\n{\n message: string;\n data: Collection;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create Collection\n\n### Endpoint\n\nCreate a collection under a source.\n\n```http\nPOST /v1/projects/:project_id/collections\n```\n\n**Scope:** `sources: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 collection. Minimum length: `1`. Maximum length: `128`.\n\n- **`slug`** `string`\n Permanent identifier used in ingest URLs. Derived from `name` when omitted. Optional. Minimum length: `1`. Maximum length: `64`.\n\n- **`source_id`** `string` -- **Required**\n Source the collection is created under.\n\n### Response\n\nCollection created\n\n```ts\n{\n message: string;\n data: Collection;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `source_id` is required.\n- `slug` is permanent. It appears in the ingest URL, so pick it deliberately.\n\n## Update Collection\n\n### Endpoint\n\nUpdate a collection.\n\n```http\nPOST /v1/projects/:project_id/collections/:collection_id\n```\n\n**Scope:** `sources:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Request Body\n\n- **`name`** `string`\n Display name for the collection. Optional. Minimum length: `1`. Maximum length: `128`.\n\n- **`expected_updated_at`** [`ISODateString`](/api/collections#iso-date-string)\n Reject this update if the collection changed since this inspected update time. Optional.\n\n- **`schema_opaque_paths`** `string[][]`\n Object paths whose contents do not take part in schema identity, each written as its key segments (`[\"metadata\", \"id\"]`). A path that names an array covers its elements and their children. Records that differ only inside an opaque path share one schema version; the raw values are still stored. Changing this re-keys the collection. Optional.\n\n- **`schema_tracked_paths`** `string[][]`\n Children of opaque paths that stay part of schema identity, written like `schema_opaque_paths`. Changing this re-keys the collection. Optional.\n\n- **`schema_max_depth`** `integer`\n How many levels of nesting take part in schema identity. Changing this re-keys the collection. Optional. Minimum: `1`. Maximum: `10`.\n\n- **`schema_nulls`** [`SchemaRuleMode`](/api/collections#schema-rule-mode)\n `auto` accepts null and absent values at any path a mapping reads; `manual` waits for a person to approve each nullable input per view. Optional.\n\n- **`schema_optional_keys`** [`SchemaRuleMode`](/api/collections#schema-rule-mode)\n `auto` lets a record whose keys are a subset of a version's keys join that version; `manual` treats every absent key as a different shape. Optional.\n\n- **`schema_detector`** [`SchemaRuleMode`](/api/collections#schema-rule-mode)\n `auto` applies a safe opaque-path proposal from the detector on its own; `manual` holds every proposal for a person. Optional.\n\n- **`schema_recurrence_gap_ms`** `integer`\n How long after first sight a shape must be seen again before its version settles, in milliseconds. Optional. Minimum: `60000`. Maximum: `604800000`.\n\n- **`schema_bulk_rows`** `integer`\n A single batch carrying at least this many rows of a shape settles its version immediately. `0` disables the bulk path. Optional. Minimum: `0`. Maximum: `1000000`.\n\n- **`schema_stale_after_ms`** `integer`\n How long a candidate version waits to be seen again before it goes stale, in milliseconds. Optional. Minimum: `3600000`. Maximum: `7776000000`.\n\n- **`schema_identity_limit`** `integer`\n How many settled schema versions the collection may hold. Past it, new shapes stay unclassified with their signatures kept. Optional. Minimum: `1`. Maximum: `4096`.\n\n- **`schema_candidate_pool`** `integer`\n How many candidate and stale versions the collection may hold. Past it, new shapes stay unclassified. Optional. Minimum: `1`. Maximum: `8192`.\n\n- **`schema_alias_pool`** `integer`\n How many exact signature hashes the collection remembers for fast lookup. Past it, a new hash is still resolved but not cached. Optional. Minimum: `16`. Maximum: `100000`.\n\n- **`schema_writer_limit`** `integer`\n How many collection files one ingest batch may open for this collection. Shapes past the candidate pool get the same number of files per batch, the shapes with the most rows first; the rows of shapes past that budget stay unclassified and cannot be linked to a version later. Optional. Minimum: `1`. Maximum: `256`.\n\n- **`schema_unclassified_retention_days`** `integer | null`\n How many days rows without a settled schema version are kept before they are deleted. `null` keeps them forever. Optional.\n\n- **`deleted_at`** `null`\n Send `null` to restore a collection that is scheduled for deletion. The deletion date itself is set by the delete endpoint and cannot be written here. Optional.\n\n### Response\n\nCollection updated\n\n```ts\n{\n message: string;\n data: Collection;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `slug` cannot be changed after creation.\n- Send `deleted_at` as `null` to restore a collection that is scheduled for deletion.\n\n## Delete Collection\n\n### Endpoint\n\nSchedule a collection for deletion.\n\n```http\nDELETE /v1/projects/:project_id/collections/:collection_id\n```\n\n**Scope:** `sources:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Response\n\n```ts\n{\n message: string;\n data: Collection;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The collection is hidden immediately and permanently removed 7 days later. Restore it before then by updating it with `deleted_at` set to null.\n\n## Force Delete Collection\n\n### Endpoint\n\nDelete a collection permanently, without waiting out its restore window.\n\n```http\nDELETE /v1/projects/:project_id/collections/:collection_id/force\n```\n\n**Scope:** `sources:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Response\n\nCollection queued for permanent deletion.\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- Works on a live collection as well as one already scheduled for deletion.\n- Every record, schema version and file in the collection is destroyed immediately.\n\n## List Collection Schemas\n\n### Endpoint\n\nRetrieve the schema versions for a collection.\n\n```http\nGET /v1/projects/:project_id/collections/:collection_id/schemas\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Query Parameters\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- **`state`** `string`\n Return only schema versions in this state. Optional. Allowed values: `\"candidate\"`, `\"settled\"`, `\"stale\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: CollectionSchemaVersion[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `state` is `candidate` from first sight, `settled` once the shape recurs or arrives in bulk, and `stale` when a candidate is not seen again. Only a settled version is offered to views for authoring.\n- `after` and `before` are mutually exclusive.\n\n## Update Collection Schema\n\n### Endpoint\n\nSettle a schema version by hand, so views can author against it before its shape recurs.\n\n```http\nPOST /v1/projects/:project_id/collections/:collection_id/schemas/:schema_version_id\n```\n\n**Scope:** `sources:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n- **`schema_version_id`** `string` -- **Required**\n Unique identifier of the schema.\n\n### Request Body\n\n- **`state`** `string` -- **Required**\n The only state a person sets. `settled` trusts the version for view authoring now, without waiting for its shape to recur; a settled version never goes back. Allowed values: `\"settled\"`.\n\n### Response\n\nSchema version settled\n\n```ts\n{\n message: string;\n data: CollectionSchemaVersion;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `state` accepts only `settled`; a settled version never returns to candidate or stale.\n- A version that is already settled is left as it is and responds with `request_no_update`.\n\n## Retrieve Collection Schema Report\n\n### Endpoint\n\nRetrieve the detector's view of a collection: the opaque paths it proposes, whether the top-level keys are data, and how many shapes sit unclassified past the collection's pools.\n\n```http\nGET /v1/projects/:project_id/collections/:collection_id/schema_report\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Response\n\nCollection schema report retrieved\n\n```ts\n{\n message: string;\n data: CollectionSchemaReport;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A proposal a bound mapping reads inside is reported and never applied on its own, whatever the detector switch; apply it by adding its path to the collection's `schema_opaque_paths` and its tracked children to `schema_tracked_paths`.\n- `shapes.before` and `shapes.after` are equal here; the preview is where they differ.\n\n## Preview Collection Schema Rules\n\n### Endpoint\n\nPreview a rule change: the schema report the collection would read under the rules sent, with how many shapes it holds now and how many it would hold. Nothing is changed.\n\n```http\nPOST /v1/projects/:project_id/collections/:collection_id/schema_preview\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Request Body\n\n- **`schema_opaque_paths`** `string[][]`\n Opaque paths to preview, written like the collection's `schema_opaque_paths`. Omitted, the collection's current opaque paths apply. Optional.\n\n- **`schema_tracked_paths`** `string[][]`\n Tracked paths to preview, written like the collection's `schema_tracked_paths`. Omitted, the collection's current tracked paths apply. Optional.\n\n- **`schema_max_depth`** `integer`\n Max depth to preview. Omitted, the collection's current max depth applies. Optional. Minimum: `1`. Maximum: `10`.\n\n### Response\n\nCollection schema rules previewed\n\n```ts\n{\n message: string;\n data: CollectionSchemaReport;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- A rule left out of the body keeps the collection's current value.\n- `shapes.before` counts the shapes under the collection's current rules and `shapes.after` under the rules sent; the difference is what updating the collection with those rules would merge.\n\n## List Collection Documents\n\n### Endpoint\n\nRetrieve raw documents from a collection.\n\n```http\nGET /v1/projects/:project_id/collections/:collection_id/docs\n```\n\n**Scope:** `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`collection_id`** `string` -- **Required**\n Unique identifier of the collection.\n\n### Query Parameters\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: CollectionDoc[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Documents are returned as they were sent, minus Tailglow's own reserved envelope. A value sent in a JSON array or JSONL that is not an object comes back as `{\"value\": ...}`.\n- Paging is forward-only. Pass `pagination.next_cursor` back as `after` to get the next page; no `prev_cursor` is issued, and `before` returns the first page.\n- While the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n- `after` and `before` are mutually exclusive.\n\n",
23
+ "api/views": "# Views\n\n## View Model\n\n### Fields\n\n- **`mapping_policy_revision`** `number | null`\n\n- **`mapping_policy_nullable_inputs`** `string[]`\n\n- **`mapping_policy_fallback_groups`** `string[][]`\n\n- **`mapping_policy_allow_drops`** `boolean`\n\n- **`mapping_policy_output_bindings`** `Record<string, string[]>`\n\n- **`mapping_generation`** `number`\n\n- **`object`** `\"view\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `viw_`.\n\n- **`team_id`** `string`\n\n- **`project_id`** `string`\n\n- **`collection_id`** `string | null`\n\n The collection this view reads from; null when it reads from another view.\n\n- **`collection_name`** `string | null`\n\n Display name of that collection.\n\n- **`parent_view_id`** `string | null`\n\n The view this one is derived from; null when it reads from a collection.\n\n- **`parent_view_name`** `string | null`\n\n Display name of that view.\n\n- **`type`** [`ViewType`](/api/views#view-type)\n\n How the view's records are produced: `transform` (one collection or view through a script) or\n `join` (a base view with lookup rows attached). Fixed at creation.\n\n- **`transform_mode`** [`ViewTransformMode`](/api/views#view-transform-mode)\n\n How a new schema version gets its transform script: reused from the view or generated (`auto`),\n reused or written by you (`manual`), or only the one you set on that version (`explicit`).\n\n- **`name`** `string`\n\n- **`hint`** `string | null`\n\n Plain-language guidance the generator uses when it writes transform scripts.\n\n- **`output_schema`** `ViewOutputField[]`\n\n The fields this view's records contain.\n\n- **`display_fields`** `ViewDisplayFields | null`\n\n Which field labels which in charts: a field of the view mapped to the view field whose value\n names it, such as `{ \"user_id\": \"user_name\" }`. A chart grouped by `user_id` then labels each\n series with the latest `user_name` its records carry. Null when never set.\n\n- **`status`** [`ViewStatus`](/api/views#view-status)\n\n Whether the view is producing records, still being set up, or failed.\n\n- **`error_message`** `string | null`\n\n Why the view failed, when its status is `error`.\n\n- **`waiting_shapes_count`** `number`\n\n How many event shapes arrived that no transform serves yet. Their records are stored and catch\n up automatically once a script covers them; the shapes are listed by the view's deferrals\n endpoint.\n\n- **`deleted_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the view is scheduled to be deleted; null when it is not.\n\n- **`created_at`** [`ISODateString`](/api/views#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/views#iso-date-string)\n\n- **`join_base_view_id`** `string | null`\n\n The base view on a join view; null on other views.\n\n- **`join_base_view_name`** `string | null`\n\n- **`join_status`** [`ViewJoinStatus | null`](/api/views#view-join-status)\n\n- **`join_backfill_cutoff_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n Records after this instant are joined live.\n\n- **`join_live_started_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the join started matching new records.\n\n- **`join_lookup_progress`** `LookupProgress | null`\n\n Lookup progress keyed by clause alias.\n\n- **`join_base_file_count`** `number | null`\n\n Number of base-view files to backfill.\n\n- **`join_base_files_done`** `number | null`\n\n Number of base-view files already processed.\n\n- **`join_error_message`** `string | null`\n\n- **`join_created_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`join_updated_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`joins`** `ViewJoinClause[]`\n\n The views joined onto the base view, on a join view.\n\n- **`metrics_count`** `number`\n\n How many metrics read from this view.\n\n- **`derived_views_count`** `number`\n\n How many views are derived from this one.\n\n- **`join_views_count`** `number`\n\n How many join views use this one as an input.\n\n- **`transform_count`** `number`\n\n How many transforms this view has.\n\n- **`drains_count`** `number`\n\n How many drains forward this view's records.\n\n- **`backfill_file_count`** `number`\n\n How many files every transform on this view has to process between them.\n\n- **`backfill_files_done`** `number`\n\n How many of those files they have processed.\n\n- **`backfill_row_count`** `number`\n\n How many records every transform on this view has to reprocess between them, counted when each\n backfill is planned.\n\n- **`backfill_rows_read`** `number`\n\n How many of those records they have reprocessed.\n\n- **`backfill_started_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the earliest of those backfills began. This is the earliest start across the whole view,\n which is what a throughput estimate has to measure elapsed time from.\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#### ViewType\n\n`\"transform\" | \"join\"`\n\n#### ViewTransformMode\n\n`\"auto\" | \"manual\" | \"explicit\"`\n\n#### ViewStatus\n\n`\"empty\" | \"building\" | \"needs_transform\" | \"active\" | \"error\" | \"paused\" | \"cancelled\"`\n\n#### ViewJoinStatus\n\n`\"waiting_for_inputs\" | \"building_lookups\" | \"backfilling\" | \"active\" | \"error\"`\n\n#### JoinLookupStatus\n\n`\"building\" | \"ready\" | \"stale\" | \"failed\"`\n\n## View Transform Model\n\n### Fields\n\n- **`object`** `\"view_transform\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `vtr_`.\n\n- **`view_id`** `string`\n\n The view this transform produces records for.\n\n- **`transform_script`** `string | null`\n\n The TGL script that maps incoming records onto the view's fields; null until one is written.\n\n- **`status`** [`ViewTransformStatus`](/api/views#view-transform-status)\n\n Whether the transform is running, waiting for a script, or failed.\n\n- **`error_message`** `string | null`\n\n Why the transform failed, when its status is `error`.\n\n- **`authoring_reason`** `string | null`\n\n What a queued transform's script waits for: `waiting_for_lookups` while a join's lookups are\n being built, `collecting_samples` while no record can show what the script must handle yet (on\n a join, no base record has a match in every lookup), `waiting_for_first_input` while its inputs\n hold no records, `manual_mapping` when it needs a script you write, and `needs_hint` when the\n generator asks you for a hint. Other values name what holds generation back, such as the hourly\n generation limit. Null when it waits on nothing.\n\n- **`tgl_retry_count`** `number`\n\n How many times script generation has been retried for this transform.\n\n- **`max_read_depth`** `number | null`\n\n How many levels deep into a record the script reads.\n\n- **`evidence_notice`** `ViewEvidenceNotice | null`\n\n Set when a path this mapping read as empty at authoring time has since carried a value with a\n type, listing those paths as `L<depth>:<path>` read keys. Rows keep flowing; re-author the\n mapping to use the new column. Cleared when a new script is generated.\n\n- **`backfill_status`** [`ViewTransformBackfillStatus`](/api/views#view-transform-backfill-status)\n\n Whether existing records have been reprocessed through this transform yet.\n\n- **`backfill_cutoff_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n Records after this time are handled live rather than by the backfill.\n\n- **`backfill_started_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the backfill began.\n\n- **`backfill_completed_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the backfill finished.\n\n- **`backfill_file_count`** `number`\n\n How many files the backfill has to process.\n\n- **`backfill_files_done`** `number`\n\n How many of those files it has processed.\n\n- **`backfill_row_count`** `number`\n\n How many records the backfill has to reprocess, counted when it is planned.\n\n- **`backfill_rows_read`** `number`\n\n How many of those records it has reprocessed.\n\n- **`backfill_error`** `string | null`\n\n Why the backfill failed, when it did.\n\n- **`assignments`** `ViewTransformAssignment[]`\n\n Backfill progress for each source schema version this transform covers.\n\n- **`created_at`** [`ISODateString`](/api/views#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/views#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#### ViewTransformStatus\n\n`\"queued\" | \"analyzing\" | \"active\" | \"error\" | \"draft\"`\n\n#### ViewTransformBackfillStatus\n\n`\"pending\" | \"enqueuing\" | \"in_progress\" | \"completed\" | \"failed\" | \"cancelled\"`\n\n## View Version Model\n\n### Fields\n\n- **`object`** `\"view_version\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `vta_`.\n\n- **`view_id`** `string`\n\n- **`version`** `number`\n\n The source schema version this row covers. Versions rise as the source's shape changes.\n\n- **`transform_id`** `string`\n\n The transform whose script serves this version. Several versions can share one transform.\n\n- **`status`** [`ViewTransformStatus`](/api/views#view-transform-status)\n\n Whether the transform is running, waiting for a script, or failed.\n\n- **`error_message`** `string | null`\n\n Why the transform failed, when its status is `error`.\n\n- **`excluded_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When this version was excluded from the view; null while it is included. Its records stay\n stored and never reach the view. `status` still reports the transform, which an exclusion\n leaves as it was.\n\n- **`exclusion_reason`** `string | null`\n\n The reason given when the version was excluded.\n\n- **`evidence_notice`** `ViewEvidenceNotice | null`\n\n Set when a path the serving script read as empty at authoring time has since carried a value\n with a type; lists those paths as `L<depth>:<path>` read keys. Re-author the mapping to use the\n new column. Cleared when a new script is generated.\n\n- **`script_hash`** `string | null`\n\n Identity of the script text. Versions sharing a hash run the identical script.\n\n- **`script`** `string | null`\n\n The TGL script serving this version; null until one is written.\n\n- **`backfill_status`** [`ViewTransformBackfillStatus`](/api/views#view-transform-backfill-status)\n\n Whether the records already on this schema version have been reprocessed yet.\n\n- **`backfill_started_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the backfill began.\n\n- **`backfill_completed_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the backfill finished.\n\n- **`backfill_file_count`** `number`\n\n How many files the backfill has to process for this version.\n\n- **`backfill_files_done`** `number`\n\n How many of those files it has processed.\n\n- **`backfill_row_count`** `number`\n\n How many records the backfill has to reprocess for this version, counted when it is planned.\n\n- **`backfill_rows_read`** `number`\n\n How many of those records it has reprocessed.\n\n- **`backfill_error`** `string | null`\n\n Why the backfill failed, when it did.\n\n- **`created_at`** [`ISODateString`](/api/views#iso-date-string)\n\n When the view started covering this schema version.\n\n- **`updated_at`** [`ISODateString`](/api/views#iso-date-string)\n\n When the transform serving this version 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#### ViewTransformStatus\n\n`\"queued\" | \"analyzing\" | \"active\" | \"error\" | \"draft\"`\n\n#### ViewTransformBackfillStatus\n\n`\"pending\" | \"enqueuing\" | \"in_progress\" | \"completed\" | \"failed\" | \"cancelled\"`\n\n## Waiting Shape Model\n\n### Fields\n\n- **`episode`** `number`\n\n- **`observation_count`** `string | null`\n\n- **`retained_rows`** `number`\n\n- **`first_observed_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`last_observed_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`next_action_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`hint_question`** `string | null`\n\n- **`hint_reason`** `string | null`\n\n- **`hint_paths`** `string[]`\n\n- **`hint_requested_at`** `string | null`\n\n- **`hint_mapping_generation`** `number | null`\n\n- **`exclusion_reason`** `string | null`\n\n- **`excluded_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n- **`transform_id`** `string | null`\n\n- **`object`** `\"view_shape_deferral\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `vsd_`.\n\n- **`view_id`** `string`\n\n- **`full_signature`** `string`\n\n The typed signature identifying the event shape.\n\n- **`event_count`** `number`\n\n Roughly how many events of the shape arrived while it was waiting; retries can count an event\n more than once.\n\n- **`first_event_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n The earliest event time seen for the shape. Completeness reporting pins to this instant.\n\n- **`first_deferred_at`** [`ISODateString`](/api/views#iso-date-string)\n\n- **`last_deferred_at`** [`ISODateString`](/api/views#iso-date-string)\n\n- **`resolved_at`** [`ISODateString | null`](/api/views#iso-date-string)\n\n When the shape became served; null while it is still waiting.\n\n- **`reason`** [`ViewShapeDeferralReason`](/api/views#view-shape-deferral-reason)\n\n Why the shape is waiting: `settling` until its schema version settles (views leave a version\n alone until then), `collecting_samples` until enough evidence is available to author a mapping,\n `awaiting_script` until a transform covers it, `generation_failed` when script generation gave\n up, `catching_up` while its records backfill, `served` once resolved.\n\n- **`schema_version`** `number | null`\n\n The schema version the shape belongs to. Null while no version is linked to it.\n\n- **`schema_version_id`** `string | null`\n\n Id of that schema version, prefixed with `csv_`. Settle a `settling` shape by passing it to\n Update Collection Schema.\n\n- **`created_at`** [`ISODateString`](/api/views#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#### ViewShapeDeferralReason\n\n`\"settling\" | \"collecting_samples\" | \"needs_decision\" | \"needs_hint\" | \"manual_mapping\" | \"generating\" | \"budget_limited\" | \"shape_overflow\" | \"runtime_error\" | \"excluded\" | \"recovery_pending\" | \"awaiting_script\" | \"generation_failed\" | \"catching_up\" | \"served\"`\n\nWhat unblocks a waiting shape.\n\n## Shape Inspection Model\n\n### Fields\n\n- **`view_id`** `string`\n\n- **`deferral_id`** `string`\n\n- **`episode`** `number`\n\n- **`mapping_generation`** `number`\n\n- **`version`** `number | null`\n\n- **`signatures`** `Record<string, string | null>`\n\n- **`decision`** `ViewMappingReuseDecision`\n\n- **`candidates`** `{ transform_id: string; verdict: string; reason: string; exact_paths: string[]; present_null_paths: string[]; absent_paths: string[]; incompatible_paths: string[]; substitution_paths: string[]; unknown_paths: string[]; bindings: Record<...>; }[]`\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## Waiting Shape Hint Model\n\n### Fields\n\n- **`question`** `string`\n\n- **`reason`** `string`\n\n- **`paths`** `string[]`\n\n Verified JSON bracket paths such as $[\"account\"][\"id\"], with literal key characters escaped.\n\n- **`requested_at`** `string`\n\n- **`mapping_generation`** `number`\n\n- **`view_id`** `string`\n\n- **`deferral_id`** `string`\n\n- **`episode`** `number`\n\n- **`transform_id`** `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 Views\n\n### Endpoint\n\nRetrieve a list of views for a project.\n\n```http\nGET /v1/projects/:project_id/views\n```\n\n**Scope:** `views: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\"`, `\"name\"`, `\"updated_at\"`.\n\n- **`project_id`** `string`\n Return only views in this project. Optional.\n\n- **`collection_id`** `string`\n Return only views reading from this collection. Optional.\n\n- **`parent_view_id`** `string`\n Return only views derived from this view. Optional.\n\n- **`deleted_at`** [`NullableDateFilter`](/api/views#nullable-date-filter)\n Filter on deletion state. Soft-deleted views are excluded unless you ask for them. Optional.\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: View[];\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 View\n\n### Endpoint\n\nRetrieve a single view.\n\n```http\nGET /v1/projects/:project_id/views/:view_id\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Response\n\nView retrieved\n\n```ts\n{\n message: string;\n data: View;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Create View\n\n### Endpoint\n\nCreate a view: a transform over one collection or view, or a join of a base view with lookups.\n\n```http\nPOST /v1/projects/:project_id/views\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Request Body\n\n- **`type`** [`ViewType`](/api/views#view-type)\n How the view's records are produced: `transform` reads one collection or view through a script; `join` starts from a base view and attaches lookup rows. Fixed at creation. Optional. Defaults to `\"transform\"`.\n\n- **`name`** `string` -- **Required**\n Name for the view. Minimum length: `1`. Maximum length: `128`.\n\n- **`output_schema`** `object[]`\n The fields this view produces. Can be left until transforms define it. Optional.\n\n - **`name`** `string` -- **Required**\n Field name as it appears on the view's records. Minimum length: `1`. Maximum length: `128`.\n\n - **`type`** `string` -- **Required**\n Value type the field holds. Allowed values: `\"string\"`, `\"number\"`, `\"boolean\"`, `\"date\"`, `\"array\"`, `\"null\"`.\n\n - **`description`** `string`\n What the field means. Used as guidance when a transform script is generated. Optional. Maximum length: `512`.\n\n- **`hint`** `string | null`\n Instructions the generator cannot infer from field names alone, such as a formatting rule or a business threshold. Optional.\n\n- **`mapping_policy`** `object`\n Optional.\n\n - **`nullable_inputs`** `string[]`\n Read paths explicitly permitted to be null. Optional.\n\n - **`fallback_groups`** `string[][]`\n Approved ordered fallback read paths. Optional.\n\n - **`output_bindings`** `object`\n Required source path segments for each output binding. Optional.\n\n - **`allow_drops`** `boolean`\n Allow a validated mapping to emit no output for a record. Optional.\n\n- **`display_fields`** `object`\n Which field labels which in charts: each key is a field of the view, its value the view field whose value names it, such as `{ \"user_id\": \"user_name\" }`. A key set to null is removed; on update, keys left out keep their value. Optional.\n\n- **`project_id`** `string`\n Project ID. Optional when using the /projects/:project/views route. Optional.\n\n- **`collection_id`** `string`\n Collection to derive from, when reading raw ingested records. Optional.\n\n- **`parent_view_id`** `string`\n View to derive from, when deriving from a view. Optional.\n\n- **`base_view_id`** `string`\n View every output record of a join starts from. Optional. Minimum length: `1`.\n\n- **`joins`** `object[]`\n Views joined onto the base view, each with its own match fields. Optional.\n\n - **`lookup_view_id`** `string` -- **Required**\n View this clause looks values up in. Minimum length: `1`.\n\n - **`alias`** `string` -- **Required**\n Name this clause's fields are addressed by in the transform script. Minimum length: `1`. Maximum length: `64`.\n\n - **`join_type`** `string`\n How unmatched base records are handled. `left` keeps them with empty lookup fields, `inner` drops them. Optional. Defaults to `\"left\"`. Allowed values: `\"left\"`, `\"inner\"`.\n\n - **`base_field`** `string` -- **Required**\n Field on the base view to match on. Minimum length: `1`. Maximum length: `256`.\n\n - **`lookup_field`** `string` -- **Required**\n Field on the lookup view to match against. Minimum length: `1`. Maximum length: `256`.\n\n - **`match_strategy`** `string`\n Which lookup record wins when several match. `latest` keeps the newest record timestamp and is the only supported value today. Optional. Defaults to `\"latest\"`. Allowed values: `\"latest\"`.\n\n - **`priority`** `integer`\n Order this clause is applied in. Lower runs first. Optional. Defaults to `0`. Minimum: `0`.\n\n- **`transform_script`** `string`\n TGL transform script. When set, the view skips automatic TGL generation. Optional. Minimum length: `1`. Maximum length: `65536`.\n\n- **`transform_mode`** [`ViewTransformMode`](/api/views#view-transform-mode)\n How a new schema version gets its script. auto reuses a compatible script of the view or generates one; manual reuses a compatible script or waits for you to write one; explicit takes only the script you set on that version, and nothing is reused or generated. Defaults to auto. Optional.\n\n### Response\n\nView created\n\n```ts\n{\n message: string;\n data: View;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The output schema defines the fields the view produces. A transform is created alongside the view to populate them.\n- Leaving the transform script empty queues it for generation instead of rejecting it, so a view can be created before its TGL is written.\n- Backfilling existing records starts shortly after creation and runs in the background. The view reports its progress while it catches up.\n- A join's base view and lookup views must all be views in this project.\n- A join whose inputs lead back to the join itself, directly or through other views, is rejected.\n- A `transform` view reads from exactly one place: set `collection_id` to derive from raw ingested records, or `parent_view_id` to derive from another view. Setting both, or neither, is rejected, and so are `base_view_id` and `joins`.\n- A `join` view requires `base_view_id` and `joins` (one to ten clauses), and rejects `collection_id` and `parent_view_id`. Join a collection by creating a view over it first.\n- Each join clause's `alias` must be lowercase letters, digits and underscores, and start with a letter. It is how the clause's fields are addressed in the transform script.\n- A join enriches each record with each joined view's latest value, by record timestamp, at the time the record is processed. Records already produced are not revisited.\n- There is no `source_id` on this endpoint. One source can hold many collections, and the collection is what owns the schema and the records a view transforms. List a source's collections with `GET /v1/projects/:project/collections?source_id=...` and pass the one you want.\n- `output_schema` field names must be unique by name, cannot conflict on type, and cannot collide with a reserved query key such as `limit` or `cursor`.\n- `transform_script` cannot be whitespace-only. Supplying one skips automatic TGL generation; omit it entirely to have one generated.\n- Use `hint` for rules the generator cannot read off the field names: a formatting rule (\"display_name should be Name (Type)\"), a business threshold (\"classify as high above 500\"), or a disambiguation when an input field could map more than one way. Skip it for obvious mappings like renames and type conversions, which the generator already handles.\n\n## Update View\n\n### Endpoint\n\nUpdate a view's name or output schema.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Request Body\n\n- **`mapping_policy`** `object`\n Optional.\n\n - **`nullable_inputs`** `string[]`\n Read paths explicitly permitted to be null. Optional.\n\n - **`fallback_groups`** `string[][]`\n Approved ordered fallback read paths. Optional.\n\n - **`output_bindings`** `object`\n Required source path segments for each output binding. Optional.\n\n - **`allow_drops`** `boolean`\n Allow a validated mapping to emit no output for a record. Optional.\n\n- **`name`** `string`\n Name for the view. Optional. Minimum length: `1`. Maximum length: `128`.\n\n- **`output_schema`** `object[]`\n The fields this view produces. Can be left until transforms define it. Optional.\n\n - **`name`** `string` -- **Required**\n Field name as it appears on the view's records. Minimum length: `1`. Maximum length: `128`.\n\n - **`type`** `string` -- **Required**\n Value type the field holds. Allowed values: `\"string\"`, `\"number\"`, `\"boolean\"`, `\"date\"`, `\"array\"`, `\"null\"`.\n\n - **`description`** `string`\n What the field means. Used as guidance when a transform script is generated. Optional. Maximum length: `512`.\n\n- **`hint`** `string | null`\n Instructions the generator cannot infer from field names alone, such as a formatting rule or a business threshold. Optional.\n\n- **`display_fields`** `object`\n Which field labels which in charts: each key is a field of the view, its value the view field whose value names it, such as `{ \"user_id\": \"user_name\" }`. A key set to null is removed; on update, keys left out keep their value. Optional.\n\n- **`transform_mode`** [`ViewTransformMode`](/api/views#view-transform-mode)\n How a new schema version gets its script. auto reuses a compatible script of the view or generates one; manual reuses a compatible script or waits for you to write one; explicit takes only the script you set on that version, and nothing is reused or generated. Optional.\n\n- **`view_transform_id`** `string`\n Which transform adopts the schema versions still waiting for a script when switching transform_mode to manual. Required only when the view has more than one distinct transform script. Optional.\n\n- **`deleted_at`** `null`\n Send `null` to restore a view that is scheduled for deletion. The deletion date itself is set by the delete endpoint and cannot be written here. Optional.\n\n### Response\n\nView updated\n\n```ts\n{\n message: string;\n data: View;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Output schema changes are accepted only before a mapping or stored output exists. Changing an existing view's output schema requires a recovery transition that is not yet supported.\n- Mapping policy changes on an existing mapping require reviewed coverage recovery. Hint changes preserve accepted mappings and retry undecided work with the new hint.\n- `display_fields` changes only the keys it names: a key set to a field labels it, a key set to null is removed. Every field it names must be a field of the view; a stored entry whose field the view has since dropped is kept, labels nothing, and never blocks another change.\n- What a view reads from is fixed at creation, so `collection_id` and `parent_view_id` are not accepted here. Create a new view to read from something else.\n- `output_schema` field names must be unique by name, cannot conflict on type, and cannot collide with a reserved query key such as `limit` or `cursor`.\n- Use `hint` for rules the generator cannot read off the field names: a formatting rule (\"display_name should be Name (Type)\"), a business threshold (\"classify as high above 500\"), or a disambiguation when a source field could map more than one way. Skip it for obvious mappings like renames and type conversions, which the generator already handles.\n\n## Delete View\n\n### Endpoint\n\nDelete a view and every view derived from it.\n\n```http\nDELETE /v1/projects/:project_id/views/:view_id\n```\n\n**Scope:** `views:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Response\n\n```ts\n{\n message: string;\n data: View;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Views derived from this one are deleted with it.\n- The delete is refused while an active drain reads from this view or from any view that would cascade with it. Delete those drains first. Draft drains are removed silently.\n- Backfill work for the whole cascade is cancelled immediately, so a deleted view stops consuming capacity before its rows are removed.\n\n## Force Delete View\n\n### Endpoint\n\nDelete a view permanently, without waiting out its restore window.\n\n```http\nDELETE /v1/projects/:project_id/views/:view_id/force\n```\n\n**Scope:** `views:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Response\n\nView queued for permanent deletion.\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- Works on a live view as well as one already scheduled for deletion.\n- Views derived from this one go with it, and the rollups of any metric reading them are destroyed. The records they were built from are not affected, so both can be rebuilt.\n\n## List View Records\n\n### Endpoint\n\nRetrieve the transformed records a view has produced.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/records\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Query Parameters\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: ViewRecord[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Paging is forward-only. Pass `pagination.next_cursor` back as `after` to get the next page; a null `next_cursor` means there is nothing more to read. No `prev_cursor` is issued, and `before` is ignored.\n- A cursor is only valid for the query that produced it. Reusing one after changing the sort direction returns a 400 rather than a page of skipped or repeated records, so start again without a cursor.\n- A cursor stays valid while Tailglow reorganizes stored data in the background, so a paging client is never interrupted by storage maintenance. It is a position in this result set, not a snapshot: records that arrive while you page appear only if they sort after your position.\n- While the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n- `after` and `before` are mutually exclusive.\n\n## List Waiting Shapes\n\n### Endpoint\n\nRetrieve the event shapes a view has deferred because no transform serves them yet.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/deferrals\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Query Parameters\n\n- **`resolved`** `string`\n Filter by resolution. `false` returns only shapes still waiting. Optional. Allowed values: `\"true\"`, `\"false\"`.\n\n- **`order_by`** `string`\n Sort field. Defaults to `last_deferred_at`. Optional. Allowed values: `\"first_deferred_at\"`, `\"last_deferred_at\"`.\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: ViewShapeDeferral[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Deferred records are stored, not lost. They backfill automatically once a script covers the shape, and each entry's `reason` says what unblocks it.\n- `event_count` is approximate: ingest retries can count an event more than once.\n- While the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n- `after` and `before` are mutually exclusive.\n\n## Inspect Waiting Shape\n\n### Endpoint\n\nInspect structural differences and candidate output bindings for a waiting shape.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/deferrals/:deferral_id\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`deferral_id`** `string` -- **Required**\n Unique identifier of the deferral.\n\n### Response\n\nWaiting shape inspected\n\n```ts\n{\n message: string;\n data: ViewShapeInspection;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- While the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n\n## Retrieve Waiting Shape Hint\n\n### Endpoint\n\nRetrieve the model's current binding question for a waiting shape.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/deferrals/:deferral_id/hint\n```\n\n**Scopes:** `views:read` + `sources:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`deferral_id`** `string` -- **Required**\n Unique identifier of the deferral.\n\n### Response 200 (1)\n\nNo pending hint question\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### Response 200 (2)\n\nWaiting shape hint retrieved\n\n```ts\n{\n message: string;\n data: ViewPendingHint;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Requires both views:read and sources:read because model prose may refer to source records. Suggested paths are verified source fields in unambiguous JSON bracket notation.\n- Returns null when no current question remains. Answer using this question's episode, mapping_generation, requested_at, and transform_id.\n\n## Decide Waiting Shape\n\n### Endpoint\n\nResolve the next action for one view and schema version.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/deferrals/:deferral_id/decision\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`deferral_id`** `string` -- **Required**\n Unique identifier of the deferral.\n\n### Request Body\n\n- **`episode`** `integer` -- **Required**\n The deferral episode shown when the decision was inspected. Minimum: `1`.\n\n- **`action`** `string` -- **Required**\n Authorize a bounded authoring attempt, answer a hint, or exclude this exact version in this view. Allowed values: `\"author\"`, `\"hint\"`, `\"exclude\"`.\n\n- **`mapping_generation`** `integer` -- **Required**\n The mapping generation shown when the decision was inspected. Minimum: `1`.\n\n- **`hint`** `string`\n Answer to the model's binding question. Optional. Minimum length: `1`. Maximum length: `4096`.\n\n- **`hint_requested_at`** [`ISODateString`](/api/views#iso-date-string)\n Exact requested_at of the pending hint being answered. Optional.\n\n- **`hint_transform_id`** `string`\n Transform that owns the pending hint being answered. Optional. Minimum length: `1`. Maximum length: `32`.\n\n- **`reason`** `string`\n Required audit reason for an exclusion. Optional. Minimum length: `1`. Maximum length: `2000`.\n\n- **`mapping_policy`** `object`\n Explicit null, fallback, drop, and source-binding rules. Optional.\n\n - **`nullable_inputs`** `string[]`\n Read paths explicitly permitted to be null. Optional.\n\n - **`fallback_groups`** `string[][]`\n Approved ordered fallback read paths. Optional.\n\n - **`output_bindings`** `object`\n Required source path segments for each output binding. Optional.\n\n - **`allow_drops`** `boolean`\n Allow a validated mapping to emit no output for a record. Optional.\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- Authoring and hint answers reserve a bounded model budget. Exclusion covers unprocessed and future records of this exact version in this view.\n- While the project's server is stopped this answers 409 `server_stopped`. Start the server and try again.\n- Answering a hint requires `hint`; excluding a version requires `reason`. Exclusion covers unprocessed and future records with this exact schema version in this view.\n- Answering a pending model question also requires its exact `hint_requested_at` and `hint_transform_id`. Superseded or answered questions return a conflict; manual guidance without a pending question may omit both fields.\n\n## List View Transforms\n\n### Endpoint\n\nRetrieve a list of a view's transforms.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/transforms\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field the results are sorted by. Creation order matches schema version order. Optional. Defaults to `\"created_at\"`. Allowed values: `\"created_at\"`, `\"updated_at\"`.\n\n- **`status`** [`ViewTransformStatus`](/api/views#view-transform-status)\n Return only transforms in this state. Optional.\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: ViewTransform[];\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 View Transform\n\n### Endpoint\n\nRetrieve a single transform.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/transforms/:transform_id\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`transform_id`** `string` -- **Required**\n Unique identifier of the transform.\n\n### Response\n\nTransform retrieved\n\n```ts\n{\n message: string;\n data: ViewTransform;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## List View Versions\n\n### Endpoint\n\nRetrieve a list of the schema versions a view covers.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/versions\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field the results are sorted by. Optional. Defaults to `\"version\"`. Allowed values: `\"version\"`.\n\n- **`status`** [`ViewTransformStatus`](/api/views#view-transform-status)\n Return only versions whose transform is in this state. Optional.\n\n- **`script_hash`** `string`\n Return only versions served by this script. Take the value from a version's row. Optional. Minimum length: `64`. Maximum length: `64`.\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: ViewVersion[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- One row per source schema version the view covers, newest version first. A view that reads another view has no schema versions, so its list is empty.\n- Versions sharing a `script_hash` run the same transform. Filter by it to see every version one script serves.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve View Version\n\n### Endpoint\n\nRetrieve one schema version a view covers.\n\n```http\nGET /v1/projects/:project_id/views/:view_id/versions/:version_id\n```\n\n**Scope:** `views:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`version_id`** `integer` -- **Required**\n The schema version number: the `version` a view version carries.\n\n### Response\n\nVersion retrieved\n\n```ts\n{\n message: string;\n data: ViewVersion;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `version_id` is the schema version NUMBER, the `version` a row carries, not a `vta_` id.\n\n## Update View Version\n\n### Endpoint\n\nExclude one settled schema version from a view.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/versions/:version_id\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`version_id`** `integer` -- **Required**\n The schema version number: the `version` a view version carries.\n\n### Request Body\n\n- **`is_excluded`** `boolean` -- **Required**\n Set to true to exclude this schema version from the view: its records are left out of the view, now and in the future.\n\n- **`exclusion_reason`** `string`\n Why the version is excluded, kept with the exclusion for whoever reads it later. Optional. Minimum length: `1`. Maximum length: `2000`.\n\n### Response\n\nSchema version excluded from this view\n\n```ts\n{\n message: string;\n data: ViewVersion;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `version_id` is the schema version NUMBER, the `version` a row carries, not a `vta_` id.\n- An excluded version's records are left out of this view, those already ingested and those still to come, and the version no longer holds the view back from being ready for metrics. Other views reading the same collection are not affected.\n- Only a version still waiting on a script can be excluded. A version whose transform holds a script, or is already excluded, answers 409.\n- An exclusion is not undone here: `is_excluded: false` answers 400.\n- Excluding a version requires `exclusion_reason`.\n\n## Create View Transform\n\n### Endpoint\n\nAdd a transform that maps incoming records into the view's output schema.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/transforms\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Request Body\n\n- **`collection_schema_version`** `integer | null`\n Collection schema version this transform covers. Null for view-to-view transforms. Optional.\n\n- **`transform_script`** `string`\n TGL transform script. Omit to have one generated. Optional. Minimum length: `1`. Maximum length: `65536`.\n\n### Response\n\nTransform created\n\n```ts\n{\n message: string;\n data: ViewTransform;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `transform_script` is TGL and must compile. A script that does not parse is rejected rather than stored, because an unparseable script would fail on every record it later touched.\n- The script must write every field the view's output schema declares.\n- Omitting the script queues the transform for generation instead of rejecting it.\n- `collection_schema_version` must be settled. A candidate stays out of views until it settles, so naming one is refused with a 409; settle it with Update Collection Schema first.\n- A transform on a collection-backed view must name the `collection_schema_version` it covers. View-to-view transforms pass null, because they read another view's output rather than a collection schema.\n- `transform_script` cannot be whitespace-only. Omit it to have TGL generated instead.\n\n## Update View Transform\n\n### Endpoint\n\nUpdate a transform's script.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/transforms/:transform_id\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`transform_id`** `string` -- **Required**\n Unique identifier of the transform.\n\n### Request Body\n\n- **`transform_script`** `string`\n TGL transform script. To clear and regenerate, use the regenerate endpoint instead. Optional. Minimum length: `1`. Maximum length: `65536`.\n\n- **`conflict_resolution`** `string`\n How conflicting schema versions are handled when a script edit fans out. Optional. Defaults to `\"strand\"`. Allowed values: `\"strand\"`, `\"keep\"`.\n\n- **`dry_run`** `boolean`\n Preview the edit: returns the per-version verdict report without changing anything. Optional. Defaults to `false`.\n\n- **`preview_token`** `string`\n The `edit_report.preview_token` from this edit's dry run. Required to save: it pins the save to the set of schema versions the preview reported on, so a version assigned or reassigned in between is rejected instead of silently included. Optional. Minimum length: `1`. Maximum length: `128`.\n\n### Response 200 (1)\n\n```ts\n{\n message: string;\n data: ViewTransform & { edit_report: { script_hash: string | null; version_verdicts: { collection_schema_version_id: string; version: number; verdict: \"compatible\" | \"conflict\"; action: \"updated\" | \"kept\" | \"stranded\"; null_paths: string[]; }[]; preview_token?: string | undefined; } };\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n#### Additional Response Fields\n\n- **`edit_report`** `{ script_hash: string | null; version_verdicts: { collection_schema_version_id: string; version: number; verdict: \"compatible\" | \"conflict\"; action: \"updated\" | \"kept\" | \"stranded\"; null_paths: string[]; }[]; preview_token?: string | undefined; }` -- **Required**\n\n### Response 200 (2)\n\nTransform updated\n\n```ts\n{\n message: string;\n data: ViewTransform & { edit_report: { script_hash: string | null; version_verdicts: never[]; } };\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n#### Additional Response Fields\n\n- **`edit_report`** `{ script_hash: string | null; version_verdicts: never[]; }` -- **Required**\n\n### Comments\n\n- `transform_script` is TGL and must compile, and must write every field the view's output schema declares.\n- On collection-backed views, an edit fans out to every schema version using the same prior script and returns a verdict for each version. Additive reads report paths that produce null.\n- Conflicting versions are moved to queued placeholders by default. Set `conflict_resolution` to `keep` to leave those versions on their prior script.\n- Removing an output field is rejected while a metric, facet, child view, or join clause reads it. Consumer checks use the complete nested output path. Records already produced keep their existing values until the transform is re-backfilled.\n- Set `dry_run` to preview the per-version verdict report without saving. A dry run changes nothing and triggers nothing, and returns a `preview_token` alongside the report.\n- Saving requires the `preview_token` from that dry run. It pins the save to the set of schema versions the report described, so a version added or reassigned between the preview and the save cannot be silently included.\n- A save with a missing or outdated token is rejected with 409 and changes nothing. The error carries the report as it stands now, so the next preview is already in hand.\n- `transform_script` cannot be whitespace-only. To clear a script and have it regenerated, use the regenerate endpoint rather than sending an empty one.\n- Saving is a two-step gesture: send `dry_run` to get the report and its `preview_token`, then send the same edit with that token. A save without one, or with a token whose set has since changed, is rejected with 409 and a fresh report.\n\n## Generate View Transform Script\n\n### Endpoint\n\nQueue TGL generation for a transform from the view's input and output schemas.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/transforms/:transform_id/generate\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`transform_id`** `string` -- **Required**\n Unique identifier of the transform.\n\n### Response\n\nTGL generation queued\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- Generation runs in the background. This returns once the work is queued, not once a script exists; poll the transform to see the result.\n- The generated script is written to the transform and replaces whatever was there.\n\n## Re-backfill View Transform\n\n### Endpoint\n\nRe-run a transform over every record the view has already read.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/transforms/:transform_id/rebackfill\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`transform_id`** `string` -- **Required**\n Unique identifier of the transform.\n\n### Response\n\nTransform re-backfill started\n\n```ts\n{\n message: string;\n data: ViewTransform;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Use this after editing a script so existing records pick up the new logic. Without it, an edit only affects records that arrive afterwards.\n- The re-backfill runs in the background and replaces the view's output as it progresses. Anything reading the view sees a mix of old and new values until it finishes.\n- Refused while a metric, derived view or join view depends on this view.\n\n## Regenerate View\n\n### Endpoint\n\nReplace a view's output schema and queue every transform to be regenerated against it.\n\n```http\nPOST /v1/projects/:project_id/views/:view_id/regenerate\n```\n\n**Scope:** `views:write`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n### Request Body\n\n- **`output_schema`** `object[]`\n The fields the view should produce. Omit to regenerate against the current schema. Optional.\n\n - **`name`** `string` -- **Required**\n Field name as it appears on the view's records. Minimum length: `1`. Maximum length: `128`.\n\n - **`type`** `string` -- **Required**\n Value type the field holds. Allowed values: `\"string\"`, `\"number\"`, `\"boolean\"`, `\"date\"`, `\"array\"`, `\"null\"`.\n\n - **`description`** `string`\n What the field means. Used as guidance when a transform script is generated. Optional. Maximum length: `512`.\n\n- **`hint`** `string | null`\n Instructions the generator cannot infer from field names alone, such as a formatting rule or a business threshold. Optional.\n\n### Response\n\nView schema updated and transforms queued for regeneration\n\n```ts\n{\n message: string;\n data: View;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- This is the schema-change path: it updates the output schema and re-queues the view's transforms in one step, so the scripts are rewritten to produce the new fields.\n- Existing records keep their old shape until the regenerated transforms backfill over them.\n- Refused while a metric, derived view or join view depends on this view.\n- `output_schema` field names must be unique by name, cannot conflict on type, and cannot collide with a reserved query key such as `limit` or `cursor`.\n- Use `hint` for rules the generator cannot read off the field names: a formatting rule (\"display_name should be Name (Type)\"), a business threshold (\"classify as high above 500\"), or a disambiguation when a source field could map more than one way. Skip it for obvious mappings like renames and type conversions, which the generator already handles.\n\n## Delete View Transform\n\n### Endpoint\n\nDelete a transform from a view.\n\n```http\nDELETE /v1/projects/:project_id/views/:view_id/transforms/:transform_id\n```\n\n**Scope:** `views:delete`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`view_id`** `string` -- **Required**\n Unique identifier of the view.\n\n- **`transform_id`** `string` -- **Required**\n Unique identifier of the transform.\n\n### Response\n\nTransform 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 transform already produced are left in place. Re-backfill the view's remaining transforms to rebuild its output without this one.\n\n",
24
+ "api/metrics": "# Metrics\n\n## Metric Model\n\n### Fields\n\n- **`object`** `\"metric\"`\n\n- **`id`** `string`\n\n- **`name`** `string`\n\n- **`team_id`** `string`\n\n- **`project_id`** `string`\n\n- **`deleted_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n When the metric is scheduled to be deleted; null when it is not.\n\n- **`view_id`** `string`\n\n The view whose records the metric aggregates. Changing it to a compatible view in the same\n project rebuilds the metric's historical data.\n\n- **`status`** [`MetricStatus`](/api/metrics#metric-status)\n\n Current lifecycle state for live collection and historical backfill.\n\n- **`group_by`** `string[]`\n\n View fields whose value combinations define distinct metric series.\n\n- **`filters`** `string`\n\n Filter expression applied to records before aggregation.\n\n- **`timestamp_field`** `string`\n\n View field used as the aggregation timestamp.\n\n- **`value_field`** `string | null`\n\n View field used as the numeric aggregation value.\n\n- **`unique_field`** `string | null`\n\n View field used for distinct-value counts.\n\n- **`null_handling`** [`NullHandling`](/api/metrics#null-handling)\n\n How null or missing metric values are aggregated.\n\n- **`empty_bucket_handling`** [`EmptyBucketHandling`](/api/metrics#empty-bucket-handling)\n\n How chart buckets without observations are represented.\n\n- **`backfill_strategy`** [`BackfillStrategy`](/api/metrics#backfill-strategy)\n\n Order used when historical data is rebuilt.\n\n- **`ui_chart_family`** [`ChartFamily`](/api/metrics#chart-family)\n\n Read-only: derived from `ui_chart_type`, grouping chart types by layout family.\n\n- **`ui_chart_type`** [`ChartType`](/api/metrics#chart-type)\n\n- **`ui_chart_color_mode`** [`ChartColorMode`](/api/metrics#chart-color-mode)\n\n Strategy used to assign chart colors.\n\n- **`ui_chart_color_base`** `string | null`\n\n Base color used by intensity mode, one of `CHART_COLORS`. In series mode, the color of a metric\n without `group_by`, which draws one series; null draws it blue. Null when the color mode does\n not use one.\n\n- **`ui_chart_color_rules`** `MetricColorRule[] | null`\n\n Ordered value-to-color rules used by value mode.\n\n- **`ui_chart_series_rules`** `MetricSeriesRule[] | null`\n\n Colors for series by value, in order: a series takes the color of the first rule that is not\n dismissed and matches it. Once the metric has such a rule, every series no rule matches is\n gray; without one, each series takes an automatic color that no other series on the chart uses,\n until the palette runs out. Rules color only a metric with `group_by`. When the color step\n runs, errors, warnings and successes it finds on a grouped field get default rules right away,\n marked `is_default`, and the AI's suggestions follow, marked `is_suggested`; a suggestion may\n replace the defaults of its field. Null when no rule was ever saved.\n\n- **`ai_color_fields`** `string[] | null`\n\n Fields whose values carry color meaning: those the AI marked, and grouped fields where\n Tailglow's default list matched a value. Null before either found one.\n\n- **`ai_colors_next_run_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n When the color step runs next for this metric; null when no run is waiting. The step adds\n default rules marked `is_default`, and the AI's suggestions arrive marked `is_suggested`.\n\n- **`ai_colors_started_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n When the run in progress started; null when none is running.\n\n- **`ai_colors_completed_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n When the AI color step last finished for this metric; null before its first run.\n\n- **`ui_chart_curve`** [`ChartCurve`](/api/metrics#chart-curve)\n\n Line interpolation style.\n\n- **`ui_chart_show_alerts`** `boolean`\n\n Whether the chart displays alert markers.\n\n- **`ui_chart_compact_values`** `boolean`\n\n Whether chart tooltips round values to compact notation, such as 321.3M.\n\n- **`ui_chart_y_min`** `number | null`\n\n Fixed lower bound for the y axis. Null lets the axis fit the data.\n\n- **`ui_chart_y_max`** `number | null`\n\n Fixed upper bound for the y axis. Null lets the axis fit the data.\n\n- **`ui_chart_show_trends`** `boolean`\n\n Whether the chart displays regression trends.\n\n- **`ui_chart_show_forecast`** `boolean`\n\n Whether the chart opens with its forecast shown, projecting the fitted trend past now to the\n saved horizon. Viewers in the app can change the forecast for their own view without changing\n this setting.\n\n- **`ui_chart_forecast_horizon`** [`ForecastHorizon`](/api/metrics#forecast-horizon)\n\n How far past now the chart projects when it draws a forecast. Relative values resolve against\n the moment the chart is viewed, not the moment they were saved.\n\n- **`ui_chart_forecast_model`** [`ForecastModel`](/api/metrics#forecast-model)\n\n Curve the forecast is fitted with. `auto` follows whichever model fits the series best at view\n time, so the projected shape can change as data arrives.\n\n- **`ui_value_unit`** `string | null`\n\n Unit label displayed with chart values.\n\n- **`ui_chart_value`** [`MetricChartValue`](/api/metrics#metric-chart-value)\n\n Aggregation value displayed by default.\n\n- **`ui_chart_time_range`** [`AnalyticsTimeRange`](/api/metrics#analytics-time-range)\n\n Window the chart covers by default.\n\n- **`ui_chart_interval`** [`ChartInterval`](/api/metrics#chart-interval)\n\n Bucket size the chart uses by default.\n\n- **`time_dimension`** [`TimeDimension | null`](/api/metrics#time-dimension)\n\n Calendar position the chart folds the metric's hourly data into. With one set, the aggregation\n merges every hour of the range into its hour of the day, weekday or hour of the week, read in\n the request's `timezone`, instead of returning a timeline. Null charts the data over time.\n\n- **`ui_chart_order`** [`ChartOrder | null`](/api/metrics#chart-order)\n\n The order the chart shows its series in: `highest` largest first, `lowest` smallest first,\n `natural` largest first with a time dimension kept in its own order (hour 0 to 23, Monday to\n Sunday). Null is `natural`. An aggregation request's `sort` overrides the direction.\n\n- **`ui_chart_custom_range_start_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Start of the custom chart range.\n\n- **`ui_chart_custom_range_end_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n End of the custom chart range.\n\n- **`ui_chart_max_series`** `number`\n\n Number of series an aggregation returns per page when the request sends no `limit`.\n\n- **`created_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n- **`backfill_percent`** `number`\n\n Percentage of the view's data files the historical backfill has processed.\n\n- **`backfill_total_count`** `number`\n\n How many of the view's data files the historical backfill has to process. Seeded as 1 before\n the run is planned.\n\n- **`backfill_completed_count`** `number`\n\n How many of those files it has processed.\n\n- **`backfill_completed_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Null until the backfill finishes.\n\n- **`backfill_started_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Null until the backfill starts.\n\n- **`compact_percent`** `number`\n\n Percentage of rollup-building steps completed.\n\n- **`compact_total_count`** `number`\n\n How many rollup-building steps the run has planned; 0 until that pass starts.\n\n- **`compact_completed_count`** `number`\n\n How many of those steps are done.\n\n- **`compact_started_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Null until the rollup-building pass starts.\n\n- **`has_triggered_monitors`** `boolean`\n\n Whether at least one alert is currently open. Ended alerts remain in history.\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#### 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\" | \"lime\" | \"indigo\" | \"fuchsia\" | \"orange\"`\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\" | \"highest\" | \"lowest\"`\n\nHow a breakdown chart orders its groups. `natural` keeps their own order (hours, weekdays),\n`value` ranks them largest first.\n\n## Timeseries Aggregation Model\n\n### Fields\n\n- **`object`** `\"aggregation\"`\n\n- **`coverage_reasons`** `{ view_id: string; view_name: string; reason: string; count: number | null; }[]`\n\n Count is null when the reason reports a status rather than a measured count.\n\n- **`start_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n- **`end_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n- **`interval`** [`DataInterval`](/api/metrics#data-interval)\n\n Bucket size each entry in a series' `records` covers. For a time dimension it is `hour`, the\n size of the buckets folded into each category.\n\n- **`values`** [`MetricChartValue[]`](/api/metrics#metric-chart-value)\n\n Declares what each number in a series' `records` arrays means; positions align.\n\n- **`series`** [`MetricSeries[]`](/api/metrics#aggregation-series-model)\n\n One page of the series with data in the range, plus a synthetic `__other__` entry that folds in\n every series not on the page. An explicit series filter returns those of its series with data\n in the range and nothing else. Without one, pinned series make up the first page and later\n pages walk the remaining series by value; with no pins every page ranks by value.\n\n- **`complete_through_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Complete through this instant as of this response. A later arrival can reopen an older bucket.\n Null means coverage is unknown. Prefer pending_ranges when present for isolated pending dates.\n\n- **`pending_ranges`** `PendingTimeRange[] | null`\n\n Known pending event-time windows within displayed buckets, end exclusive. Partial boundary\n buckets can extend beyond the requested window. [] means none; null means unknown. Absent when\n the response states no pending windows, as on server telemetry: complete_through_at decides.\n\n- **`input_checkpoint`** `{ processed_position: number; published_position: number; } | null`\n\n Monotonic input progress, independent of the customer-selected event timestamp. Null when the\n response reports none.\n\n- **`first_incomplete_bucket_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n Key of the first bucket that is still filling. For isolated pending dates, prefer\n pending_ranges; this older field conservatively marks the suffix from the first pending bucket.\n Null when every bucket in the response is final, and always null on server telemetry, which\n makes no completeness statement. Always null for a time dimension, whose categories merge hours\n from across the range, including the current hour's data so far.\n\n- **`time_dimension`** [`TimeDimension | null`](/api/metrics#time-dimension)\n\n The time dimension the series were folded into, from the metric. When set, each series'\n `records` are keyed by category instead of bucket start: `\"0\"` to `\"23\"` for `hour_of_day`,\n `\"1\"` (Monday) to `\"7\"` for `weekday`, `\"0\"` to `\"167\"` for `hour_of_week`. Null for a\n timeline.\n\n- **`timezone`** `string | null`\n\n The IANA timezone the request's date boundaries, and a time dimension's categories, were read\n in. Null on server telemetry.\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#### DataInterval\n\n`\"minute\" | \"hour\" | \"day\" | \"month\"`\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#### 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## Aggregation Series Model\n\n### Fields\n\n- **`details`** `{ label: string; hash: string; dimensions: Record<string, string | number | boolean | null>; fields: Record<string, string | number | boolean>; }`\n\n The series' identity (`hash`, `dimensions`), its label and the fields it carries. Colors come\n from the metric's `ui_chart_series_rules`: a series that matches a rule takes its color, and\n once the metric has a rule every other series is gray.\n\n- **`records`** `Record<string, (number | null)[]>`\n\n One entry per bucket, keyed by bucket start, or per category when the aggregation has a\n `time_dimension`; values aligned with the aggregation's `values` list. `null` means the bucket\n had no data and the metric's `empty_bucket_handling` is `gaps`. In a time dimension the\n cumulative values are always `null`.\n\n- **`regression`** `LinearRegression | ExponentialRegression | LogarithmicRegression | LogisticRegression | SinusoidalRegression | null`\n\n Trend line fitted over the series; null when none was computed.\n\n- **`forecast`** `RegressionForecast | null`\n\n The trend line's projected next value; null when no trend was computed.\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## Metric Series Model\n\n### Fields\n\n- **`object`** `\"metric_series\"`\n\n- **`id`** `string`\n\n- **`metric_id`** `string`\n\n- **`hash`** `string`\n\n Opaque identifier of the series' dimension combination, stable for the life of the metric.\n\n- **`label`** `string`\n\n Per field in `group_by`, the value of the view's display field for it from `fields`, else the\n raw value; joined with \" | \".\n\n- **`dimensions`** `Record<string, string | number | boolean | null>`\n\n The `group_by` field values that define this series.\n\n- **`fields`** `Record<string, string | number | boolean>`\n\n Values the series keeps from its latest records: the view's display fields for the fields in\n `group_by`, and the fields color rules read that are not in `group_by`. A field missing or null\n in a record leaves its value as it was.\n\n- **`first_seen_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n When the series was registered. Stamped with the processing time, so a backfill registers\n historical combinations at run time.\n\n- **`last_seen_at`** [`ISODateString`](/api/metrics#iso-date-string)\n\n Also stamped at registration; not a recency signal, use `last_value_at` for that.\n\n- **`last_value`** `number | null`\n\n The most recent aggregated value observed for the series.\n\n- **`last_value_at`** [`ISODateString | null`](/api/metrics#iso-date-string)\n\n When `last_value` was 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 Metrics\n\n### Endpoint\n\nRetrieve a list of metrics for a project.\n\n```http\nGET /v1/projects/:project_id/metrics\n```\n\n**Scope:** `metrics:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n### Query Parameters\n\n- **`view_id`** `string`\n Return metrics that read from this view. Optional.\n\n- **`order_by`** `string`\n Field used to order the metrics. Optional. Defaults to `\"name\"`. Allowed values: `\"name\"`.\n\n- **`deleted_at`** [`NullableDateFilter`](/api/metrics#nullable-date-filter)\n Filter on deletion state. Pass `null` for metrics that are not scheduled for deletion. Optional.\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: Metric[];\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 Metric\n\n### Endpoint\n\nRetrieve a single metric.\n\n```http\nGET /v1/projects/:project_id/metrics/:metric_id\n```\n\n**Scope:** `metrics: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### Response\n\nMetric retrieved\n\n```ts\n{\n message: string;\n data: Metric;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Retrieve Metric Aggregation\n\n### Endpoint\n\nRetrieve aggregated time-series data for a metric.\n\n```http\nGET /v1/projects/:project_id/metrics/:metric_id/aggregation\n```\n\n**Scope:** `metrics: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- **`start_at`** [`ISODateString`](/api/metrics#iso-date-string)\n Start of the aggregation range. Send together with `end_at`. Optional.\n\n- **`end_at`** [`ISODateString`](/api/metrics#iso-date-string)\n End of the aggregation range. Send together with `start_at`. Optional.\n\n- **`interval`** [`ChartInterval`](/api/metrics#chart-interval)\n Requested aggregation bucket interval. A metric with a time dimension ignores it and always folds hourly data. Optional.\n\n- **`time_range`** [`AnalyticsTimeRange`](/api/metrics#analytics-time-range)\n Preset used to calculate the aggregation range. Ignored when `start_at` and `end_at` are both sent. Defaults to the metric's saved chart range. Optional.\n\n- **`timezone`** `string`\n IANA timezone used to calculate date boundaries and to place each hour in a time dimension's categories. Optional.\n\n- **`page_id`** `string`\n Page whose saved custom range should be used. Applies only when `time_range` is `custom`. Optional.\n\n- **`normalize`** `boolean`\n Whether the response includes empty time buckets. Optional. Defaults to `true`.\n\n- **`forecast_at`** [`ISODateString`](/api/metrics#iso-date-string)\n Future timestamp requested for a forecast. Optional.\n\n- **`series`** `string`\n Comma-separated series hashes to return. Optional.\n\n- **`limit`** `integer`\n Number of series to return per page. Defaults to the metric's `ui_chart_max_series`. Optional. 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 Ranking direction of the series. `desc` returns the largest first. Defaults to the metric's `ui_chart_order`: `asc` for `lowest`, `desc` otherwise. Optional. Allowed values: `\"asc\"`, `\"desc\"`.\n\n### Response\n\n```ts\n{\n message: string;\n data: TimeseriesAggregation;\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `last` returns the latest reading in each time bucket within the requested range.\n- Record keys are UTC bucket starts. Buckets still filling come back with the data counted so far: every key at or after `first_incomplete_bucket_at` is incomplete and can still grow, so report it as \"so far\" and never compare it with a complete bucket. Incomplete buckets with no data yet are left out rather than returned as zero.\n- `complete_through_at` is the instant before which every number is final. Null means completeness is unknown, and then every bucket is incomplete.\n- For complete buckets only, leave out every key at or after `first_incomplete_bucket_at`.\n- `coverage_reasons` names unresolved view scopes; `excluded` is informational.\n- Series are ranked by their total over the range (the count for a `count` metric, the sum of Last readings for `last`, the sum otherwise), ties broken by series hash, and `limit` of them come back per page, the metric's `ui_chart_max_series` by default, in rank order. Walk the pages with `pagination.next_cursor` and `pagination.prev_cursor`; `pagination.total` counts every series with data in the range.\n- Only series with data in the range come back. Without a `series` filter, when none has any, a metric whose `empty_bucket_handling` is `zero` returns one series of zeros labelled with the metric's name (hash `__default__`), unless `normalize` is false.\n- Without `sort`, series rank smallest first when the metric's `ui_chart_order` is `lowest` and largest first otherwise.\n- When the metric colors by series (`ui_chart_color_mode` is `by_series`), series that match one of its `ui_chart_series_rules` rank ahead of every other series, so they take the first page's places first, except on a `table` chart, which ranks all series together. Color a series by the first active rule it matches, and every other series gray once the metric has an active rule. In the other color modes rules color nothing and every series ranks together.\n- A read that shows a series value no color rule covers, on a field in the metric's `ai_color_fields`, asks for new default rules and AI color suggestions (`ai_colors_next_run_at`), at most once a day.\n- `__other__` folds in every series that is not on the current page, so the page plus Other always adds up to the range total and a share is right on every page. It comes last.\n- `series` returns the listed series that have data in the range, on one page, unranked and without `__other__`.\n- A metric with a `time_dimension` returns its range folded instead of as a timeline: every hour merges into its hour of the day, weekday or hour of the week. Record keys are then the category as a decimal string (`\"0\"` to `\"23\"`, `\"1\"` for Monday to `\"7\"` for Sunday, `\"0\"` to `\"167\"`), every category is present whatever `normalize` says, and `time_dimension` echoes it.\n- Data is stored by UTC hour. A fold places each hour in its category in the request `timezone` (echoed as `timezone`), one hour at a time, so a daylight saving change moves only the hours after it.\n- A fold reads hourly data whatever `interval` says, covers at most 400 days (a longer range is refused), and includes the current hour's data so far.\n- In a fold an empty category is `0` when `empty_bucket_handling` is `zero` and `null` when it is `gaps`, cumulative values are `null`, and there is no trend line.\n- The aggregation window is resolved in order: an explicit `start_at` plus `end_at` wins; otherwise `time_range` is used; otherwise the metric's saved chart range is used.\n- `start_at` and `end_at` only take effect when sent together. Sending one without the other has no effect on the window.\n- When both are provided, `start_at` must be earlier than `end_at`.\n- `time_range=custom` without explicit dates falls back to the saved custom range of `page_id`, then the metric, then the project.\n- `timezone` must be a valid IANA timezone name, or `UTC`.\n- `after` and `before` are mutually exclusive.\n\n## Create Metric\n\n### Endpoint\n\nCreate a metric for a project.\n\n```http\nPOST /v1/projects/:project_id/metrics\n```\n\n**Scope:** `metrics: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 The subject only, without the aggregation. Charts title themselves as the aggregation followed by this name, so `API Latency` renders as `P95 of API Latency`. Putting the aggregation here produces `P95 of API P95 Latency`. Minimum length: `1`. Maximum length: `64`.\n\n- **`view_id`** `string` -- **Required**\n View that provides the metric's records. Maximum length: `32`.\n\n- **`group_by`** `string[]`\n View fields used to split records into series. Optional. Defaults to `[]`.\n\n- **`filters`** `string`\n A query string of `?field=operator:value` pairs joined by `&`, applied when records are aggregated. For example `?status=equals:200` or `?level=in:error,warn&path=starts_with:/api`. Comma-separate the values of `in`, `not_in`, and `between`. Every operator must be spelled in full; short forms such as `eq:` are rejected. Optional. Defaults to `\"\"`. Maximum length: `8192`.\n\n- **`timestamp_field`** `string`\n View field used as the aggregation timestamp. Optional. Defaults to `\"timestamp\"`. Minimum length: `1`. Maximum length: `256`.\n\n- **`value_field`** `string | null`\n View field used as the numeric aggregation value. Optional.\n\n- **`unique_field`** `string | null`\n View field used to count distinct values. Optional.\n\n- **`null_handling`** [`NullHandling`](/api/metrics#null-handling)\n How null or missing metric values are aggregated. Optional. Defaults to `\"skip\"`.\n\n- **`empty_bucket_handling`** [`EmptyBucketHandling`](/api/metrics#empty-bucket-handling)\n How chart buckets without observations are represented. Optional.\n\n- **`ui_chart_type`** [`ChartType`](/api/metrics#chart-type)\n Default chart type. Optional.\n\n- **`ui_chart_color_mode`** [`ChartColorMode`](/api/metrics#chart-color-mode)\n Strategy used to assign chart colors. Optional.\n\n- **`ui_chart_color_base`** [`ChartColor`](/api/metrics#chart-color)\n Base color used by intensity mode. In series mode, the color of a metric without `group_by`, which draws one series; null draws it blue. Optional.\n\n- **`ui_chart_color_rules`** `object[] | null`\n Ordered value-to-color rules used by value mode. Optional.\n\n- **`ui_chart_series_rules`** `object[] | null`\n Colors for series by value, in order: a series takes the color of the first rule it matches, and once a rule is active every other series is gray. Rules color only a metric with `group_by`. Rules Tailglow adds from its default list (errors red, warnings amber, successes green) are marked `is_default`, and rules the AI suggests are marked `is_suggested`. Optional.\n\n- **`ui_chart_show_alerts`** `boolean`\n Whether the chart displays alert markers. Optional.\n\n- **`ui_chart_show_trends`** `boolean`\n Whether the chart displays regression trends. Optional.\n\n- **`ui_chart_compact_values`** `boolean`\n Whether chart tooltips round values to compact notation, such as 321.3M instead of the full number. Optional.\n\n- **`ui_chart_y_min`** `number | null`\n Fixed lower bound for the chart's y axis. Null lets the axis fit the data. Optional.\n\n- **`ui_chart_y_max`** `number | null`\n Fixed upper bound for the chart's y axis. Null lets the axis fit the data. Optional.\n\n- **`ui_chart_show_forecast`** `boolean`\n Whether the chart opens with its forecast shown, projecting the fitted trend past now to the saved horizon. Optional.\n\n- **`ui_chart_forecast_horizon`** [`ForecastHorizon`](/api/metrics#forecast-horizon)\n How far past now the chart projects its forecast. The value is relative to when the chart is viewed, so it keeps projecting the same distance ahead as time passes. Optional.\n\n- **`ui_chart_forecast_model`** [`ForecastModel`](/api/metrics#forecast-model)\n Curve the forecast is fitted with. Use `auto` to follow whichever model currently fits the series best. Optional.\n\n- **`ui_value_unit`** `string`\n Unit label displayed with chart values. Optional. Maximum length: `32`.\n\n- **`ui_chart_curve`** [`ChartCurve`](/api/metrics#chart-curve)\n Chart line interpolation style. Optional.\n\n- **`ui_chart_value`** [`MetricChartValue`](/api/metrics#metric-chart-value)\n Aggregation value displayed by default. Optional.\n\n- **`ui_chart_time_range`** [`AnalyticsTimeRange`](/api/metrics#analytics-time-range)\n Time range displayed by default. Optional.\n\n- **`ui_chart_interval`** [`ChartInterval`](/api/metrics#chart-interval)\n Aggregation interval displayed by default. Optional.\n\n- **`time_dimension`** [`TimeDimension`](/api/metrics#time-dimension)\n Calendar position the chart folds the metric's hourly data into, such as the hour of the day. Null charts the data over time. Optional.\n\n- **`ui_chart_order`** `string | null`\n Order the chart shows its series in: `highest` largest first, `lowest` smallest first, `natural` largest first with a time dimension kept in its own order (hour 0 to 23, Monday to Sunday). Null is `natural`, and `value` is read as `highest`. Optional. Allowed values: `\"natural\"`, `\"highest\"`, `\"lowest\"`, `\"value\"`.\n\n- **`ui_chart_custom_range_start_at`** [`ISODateString`](/api/metrics#iso-date-string)\n Start of the custom chart range. Optional.\n\n- **`ui_chart_custom_range_end_at`** [`ISODateString`](/api/metrics#iso-date-string)\n End of the custom chart range. Optional.\n\n- **`ui_chart_max_series`** `integer`\n Number of series an aggregation returns per page when the request sends no `limit`. Optional. Minimum: `1`. Maximum: `50`.\n\n- **`backfill_strategy`** [`BackfillStrategy`](/api/metrics#backfill-strategy)\n Order used to rebuild historical data. Optional.\n\n### Response\n\nYour metric has been created and will begin collecting data immediately.\n\n```ts\n{\n message: string;\n data: Metric;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- `view_id` is required when creating a metric.\n- Field references and filters must be compatible with the selected view.\n- Custom chart ranges require both dates, with an interval of at least 30 minutes.\n- `ui_chart_color_base` is required in `by_intensity` mode and refused in `by_value` mode. In `by_series` mode it is optional and colors a metric without `group_by`.\n- `ui_chart_color_rules` needs at least one rule in `by_value` mode and is refused in the other modes.\n- `ui_chart_color_mode` must be a mode the chart type supports. A metric created without a `ui_chart_type` is a `bar` chart and is checked as one.\n- `time_dimension` must be one the chart type supports: `breakdown` takes `hour_of_day` or `weekday`, `heatmap` takes `hour_of_week`, `hour_of_day` or `weekday`, and every other chart type takes none.\n- A `heatmap` chart requires a `time_dimension`.\n- A `time_dimension` cannot be combined with a cumulative `ui_chart_value` (`cumulative_sum` or `cumulative_count`).\n- `filters` must be a valid filter expression.\n- `timestamp_field` cannot reference `__meta__` metadata.\n- `ui_chart_series_rules` holds at most 20 rules that are not dismissed, and each rule's `field` must be a field of the view.\n- A metric created with `group_by` and without `ui_chart_series_rules` gets default rules for the errors, warnings and successes its first series show, marked `is_default`, and then asks the AI for color rules, which arrive marked `is_suggested` and may replace the defaults of their field. A team with Tailglow AI off gets the defaults only.\n\n## Update Metric\n\n### Endpoint\n\nUpdate an existing metric.\n\n```http\nPOST /v1/projects/:project_id/metrics/:metric_id\n```\n\n**Scope:** `metrics:write`\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### Request Body\n\n- **`name`** `string`\n The subject only, without the aggregation. Charts title themselves as the aggregation followed by this name, so `API Latency` renders as `P95 of API Latency`. Putting the aggregation here produces `P95 of API P95 Latency`. Optional. Minimum length: `1`. Maximum length: `64`.\n\n- **`view_id`** `string`\n View that provides the metric's records. Optional. Maximum length: `32`.\n\n- **`group_by`** `string[]`\n View fields used to split records into series. Optional.\n\n- **`filters`** `string`\n A query string of `?field=operator:value` pairs joined by `&`, applied when records are aggregated. For example `?status=equals:200` or `?level=in:error,warn&path=starts_with:/api`. Comma-separate the values of `in`, `not_in`, and `between`. Every operator must be spelled in full; short forms such as `eq:` are rejected. Optional. Maximum length: `8192`.\n\n- **`timestamp_field`** `string`\n View field used as the aggregation timestamp. Optional. Minimum length: `1`. Maximum length: `256`.\n\n- **`value_field`** `string | null`\n View field used as the numeric aggregation value. Optional.\n\n- **`unique_field`** `string | null`\n View field used to count distinct values. Optional.\n\n- **`null_handling`** [`NullHandling`](/api/metrics#null-handling)\n How null or missing metric values are aggregated. Optional.\n\n- **`empty_bucket_handling`** [`EmptyBucketHandling`](/api/metrics#empty-bucket-handling)\n How chart buckets without observations are represented. Optional.\n\n- **`ui_chart_type`** [`ChartType`](/api/metrics#chart-type)\n Default chart type. Optional.\n\n- **`ui_chart_color_mode`** [`ChartColorMode`](/api/metrics#chart-color-mode)\n Strategy used to assign chart colors. Optional.\n\n- **`ui_chart_color_base`** [`ChartColor`](/api/metrics#chart-color)\n Base color used by intensity mode. In series mode, the color of a metric without `group_by`, which draws one series; null draws it blue. Optional.\n\n- **`ui_chart_color_rules`** `object[] | null`\n Ordered value-to-color rules used by value mode. Optional.\n\n- **`ui_chart_series_rules`** `object[] | null`\n Colors for series by value, in order: a series takes the color of the first rule it matches, and once a rule is active every other series is gray. Rules color only a metric with `group_by`. Rules Tailglow adds from its default list (errors red, warnings amber, successes green) are marked `is_default`, and rules the AI suggests are marked `is_suggested`. Optional.\n\n- **`seen_suggestions_at`** [`ISODateString`](/api/metrics#iso-date-string)\n The newest `suggested_at` among the rules you loaded. A suggestion or default left out of `ui_chart_series_rules` is dismissed only when it is no newer than this; without it, nothing is dismissed. Optional.\n\n- **`ui_chart_show_alerts`** `boolean`\n Whether the chart displays alert markers. Optional.\n\n- **`ui_chart_show_trends`** `boolean`\n Whether the chart displays regression trends. Optional.\n\n- **`ui_chart_compact_values`** `boolean`\n Whether chart tooltips round values to compact notation, such as 321.3M instead of the full number. Optional.\n\n- **`ui_chart_y_min`** `number | null`\n Fixed lower bound for the chart's y axis. Null lets the axis fit the data. Optional.\n\n- **`ui_chart_y_max`** `number | null`\n Fixed upper bound for the chart's y axis. Null lets the axis fit the data. Optional.\n\n- **`ui_chart_show_forecast`** `boolean`\n Whether the chart opens with its forecast shown, projecting the fitted trend past now to the saved horizon. Optional.\n\n- **`ui_chart_forecast_horizon`** [`ForecastHorizon`](/api/metrics#forecast-horizon)\n How far past now the chart projects its forecast. The value is relative to when the chart is viewed, so it keeps projecting the same distance ahead as time passes. Optional.\n\n- **`ui_chart_forecast_model`** [`ForecastModel`](/api/metrics#forecast-model)\n Curve the forecast is fitted with. Use `auto` to follow whichever model currently fits the series best. Optional.\n\n- **`ui_value_unit`** `string`\n Unit label displayed with chart values. Optional. Maximum length: `32`.\n\n- **`ui_chart_curve`** [`ChartCurve`](/api/metrics#chart-curve)\n Chart line interpolation style. Optional.\n\n- **`ui_chart_value`** [`MetricChartValue`](/api/metrics#metric-chart-value)\n Aggregation value displayed by default. Optional.\n\n- **`ui_chart_time_range`** [`AnalyticsTimeRange`](/api/metrics#analytics-time-range)\n Time range displayed by default. Optional.\n\n- **`ui_chart_interval`** [`ChartInterval`](/api/metrics#chart-interval)\n Aggregation interval displayed by default. Optional.\n\n- **`time_dimension`** [`TimeDimension`](/api/metrics#time-dimension)\n Calendar position the chart folds the metric's hourly data into, such as the hour of the day. Null charts the data over time. Optional.\n\n- **`ui_chart_order`** `string | null`\n Order the chart shows its series in: `highest` largest first, `lowest` smallest first, `natural` largest first with a time dimension kept in its own order (hour 0 to 23, Monday to Sunday). Null is `natural`, and `value` is read as `highest`. Optional. Allowed values: `\"natural\"`, `\"highest\"`, `\"lowest\"`, `\"value\"`.\n\n- **`ui_chart_custom_range_start_at`** [`ISODateString`](/api/metrics#iso-date-string)\n Start of the custom chart range. Optional.\n\n- **`ui_chart_custom_range_end_at`** [`ISODateString`](/api/metrics#iso-date-string)\n End of the custom chart range. Optional.\n\n- **`ui_chart_max_series`** `integer`\n Number of series an aggregation returns per page when the request sends no `limit`. Optional. Minimum: `1`. Maximum: `50`.\n\n- **`backfill_strategy`** [`BackfillStrategy`](/api/metrics#backfill-strategy)\n Order used to rebuild historical data. Optional.\n\n- **`deleted_at`** `null`\n Send `null` to restore a metric that is scheduled for deletion. The deletion date itself is set by the delete endpoint and cannot be written here. Optional.\n\n### Response\n\nYour metric has been updated and is calculating historical data\n\n```ts\n{\n message: string;\n data: Metric;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Field references and filters must be compatible with the selected view.\n- Changing the metric's data feed rebuilds its historical rollups.\n- Custom chart ranges require both dates, with an interval of at least 30 minutes.\n- `ui_chart_color_base` is required in `by_intensity` mode and refused in `by_value` mode. In `by_series` mode it is optional and colors a metric without `group_by`.\n- `ui_chart_color_rules` needs at least one rule in `by_value` mode and is refused in the other modes.\n- `ui_chart_color_mode` must be a mode the chart type supports. Changing `ui_chart_type` keeps the current mode, so send a supported one with it.\n- `time_dimension` must be one the chart type supports: `breakdown` takes `hour_of_day` or `weekday`, `heatmap` takes `hour_of_week`, `hour_of_day` or `weekday`, and every other chart type takes none. Send `time_dimension: null` when changing to a type that takes none.\n- A `heatmap` chart requires a `time_dimension`.\n- A `time_dimension` cannot be combined with a cumulative `ui_chart_value` (`cumulative_sum` or `cumulative_count`).\n- `filters` must be a valid filter expression.\n- `timestamp_field` cannot reference `__meta__` metadata.\n- `ui_chart_series_rules` holds at most 20 rules that are not dismissed. A rule you add must name a field of the view; a saved rule whose field the view has since dropped is kept and matches nothing.\n- Saving `ui_chart_series_rules` replaces your own rules. A suggestion or default you leave out is dismissed, and never added again, only when you also send `seen_suggestions_at` no older than its `suggested_at`; one added after the rules you loaded stays. A suggestion or default you change becomes your own rule.\n\n## Backfill Metric Data\n\n### Endpoint\n\nBackfill a metric's historical data from its view.\n\n```http\nPOST /v1/projects/:project_id/metrics/:metric_id/backfill\n```\n\n**Scope:** `metrics:write`\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### Response\n\nYour metric data is being rebuilt from its view\n\n```ts\n{\n message: string;\n data: Metric;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n## Delete Metric\n\n### Endpoint\n\nDelete a metric.\n\n```http\nDELETE /v1/projects/:project_id/metrics/:metric_id\n```\n\n**Scope:** `metrics:delete`\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### Response\n\n```ts\n{\n message: string;\n data: Metric;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The metric is hidden immediately and permanently removed once its restore window elapses. Restore it before then by updating it with `deleted_at` set to null.\n- Collection and backfill keep running for the whole window, so a restored metric resumes with no gap in its history. Its monitors stop evaluating until it is restored.\n\n## Force Delete Metric\n\n### Endpoint\n\nDelete a metric permanently, without waiting out its restore window.\n\n```http\nDELETE /v1/projects/:project_id/metrics/:metric_id/force\n```\n\n**Scope:** `metrics:delete`\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### Response\n\nMetric queued for permanent deletion.\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- Works on a live metric as well as one already scheduled for deletion.\n- Its rollups and monitors are destroyed. The view it reads is not affected, so the metric can be recreated and backfilled.\n\n## List Metric Series\n\n### Endpoint\n\nRetrieve the series discovered for a metric.\n\n```http\nGET /v1/projects/:project_id/metrics/:metric_id/series\n```\n\n**Scope:** `metrics: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- **`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- **`order_by`** `string`\n Field used to order the metric series. Optional. Defaults to `\"first_seen_at\"`. Allowed values: `\"first_seen_at\"`, `\"last_seen_at\"`.\n\n- **`search`** `string`\n Substring matched against hashes, dimension values and carried field values. Optional. Minimum length: `1`. Maximum length: `128`.\n\n### Response\n\n```ts\n{\n message: string;\n data: MetricSeriesRecord[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Each series is labeled by its view's `display_fields`: per field in `group_by`, the value the series carries for its display field, else the raw value.\n- `after` and `before` are mutually exclusive.\n\n",
25
25
  "api/monitors": "# Monitors\n\n## Monitor Model\n\n### Fields\n\n- **`object`** `\"monitor\"`\n\n- **`id`** `string`\n\n Unique identifier, prefixed with `mtr_`.\n\n- **`slug`** `string`\n\n URL-safe identifier for the monitor.\n\n- **`team_id`** `string`\n\n- **`project_id`** `string`\n\n- **`metric_id`** `string`\n\n The metric this monitor watches. Fixed for the life of the monitor.\n\n- **`name`** `string`\n\n- **`description`** `string | null`\n\n What this monitor watches for.\n\n- **`status`** [`MonitorStatus`](/api/monitors#monitor-status)\n\n `paused` was switched off by you and `disabled` by its expiry. Change it with pause and resume.\n\n- **`expires_at`** [`ISODateString | null`](/api/monitors#iso-date-string)\n\n When the monitor stops checking; null when it does not expire.\n\n- **`statistic`** [`MonitorStatistic`](/api/monitors#monitor-statistic)\n\n What is computed over each window.\n\n- **`operator`** [`MonitorOperator`](/api/monitors#monitor-operator)\n\n How the statistic is compared with `threshold_value`.\n\n- **`threshold_value`** `number`\n\n- **`empty_window_handling`** [`MonitorEmptyWindowHandling`](/api/monitors#monitor-empty-window-handling)\n\n What the statistic reads when the window holds no data. Always `zero` for `count`, `sum` and\n `count_unique`.\n\n- **`window_type`** [`MonitorWindowType`](/api/monitors#monitor-window-type)\n\n- **`window_minutes`** `number | null`\n\n Length of a rolling window in whole minutes. Null for `all_time`.\n\n- **`scope`** [`MonitorScope`](/api/monitors#monitor-scope)\n\n- **`series_filters`** `MonitorSeriesFilters`\n\n- **`trigger_type`** [`MonitorTriggerType`](/api/monitors#monitor-trigger-type)\n\n- **`sustained_minutes`** `number | null`\n\n How long matches must hold before an alert opens. Set only for `sustained`.\n\n- **`repeat_count`** `number | null`\n\n How many matching checks open an alert. Set only for `repeated`.\n\n- **`repeat_window_minutes`** `number | null`\n\n The window those matching checks must fall within. Set only for `repeated`.\n\n- **`schedule_cron`** `string`\n\n Five-field cron expression, in UTC, setting when the monitor checks. At most once a minute.\n\n- **`alert_mode`** [`MonitorAlertMode`](/api/monitors#monitor-alert-mode)\n\n- **`next_check_at`** [`ISODateString | null`](/api/monitors#iso-date-string)\n\n The next scheduled check. Null while the monitor is paused or disabled.\n\n- **`last_slot_at`** [`ISODateString | null`](/api/monitors#iso-date-string)\n\n The most recent scheduled check that was fully applied.\n\n- **`created_at`** [`ISODateString`](/api/monitors#iso-date-string)\n\n- **`updated_at`** [`ISODateString`](/api/monitors#iso-date-string)\n\n- **`recent_alerts`** [`Alert[]`](/api/alerts#model)\n\n The most recent alerts this monitor raised.\n\n- **`series_states`** [`MonitorSeriesState[]`](/api/monitors#series-state-model)\n\n The current state of each target the monitor checks.\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#### MonitorStatus\n\n`\"active\" | \"paused\" | \"disabled\"`\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#### 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#### MonitorCheckStatus\n\n`\"pending\" | \"ready\" | \"unknown\" | \"error\"`\n\nThe outcome of a target's most recent check. `unknown` means the slot was passed over without a\nverdict, for example while the metric rebuilds; `error` means the read failed and is retried.\n\n## Monitor Preview Model\n\n### Fields\n\n- **`object`** `\"monitor_preview\"`\n\n- **`is_ready`** `boolean`\n\n False while the window is not complete yet. `verdicts` is then empty.\n\n- **`slot_at`** [`ISODateString`](/api/monitors#iso-date-string)\n\n The check being previewed: the most recent slot on the monitor's schedule.\n\n- **`window_start_at`** [`ISODateString`](/api/monitors#iso-date-string)\n\n- **`window_end_at`** [`ISODateString`](/api/monitors#iso-date-string)\n\n- **`complete_through_at`** [`ISODateString | null`](/api/monitors#iso-date-string)\n\n Everything before this instant is final. The window is ready once this reaches `slot_at`. Null\n when no statement can be made.\n\n- **`verdicts`** [`MonitorPreviewVerdict[]`](/api/monitors#verdict-model)\n\n- **`trigger_note`** `string | null`\n\n For `sustained` and `repeated` triggers, a reminder that the preview shows one check, not\n whether the trigger would open an alert. Null otherwise.\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#### MonitorVerdict\n\n`\"match\" | \"no_match\" | \"no_value\"`\n\nOne check's comparison. `no_value` means the window gave nothing to compare, which never opens or\ncloses an alert.\n\n## Monitor Verdict Model\n\n### Fields\n\n- **`series_hash`** `string`\n\n- **`series_label`** `string | null`\n\n The series name for `each_series`. Null for `total` and `any_series`.\n\n- **`value`** `number | null`\n\n The statistic over the window. Null when the window gave nothing to compare.\n\n- **`verdict`** [`MonitorVerdict`](/api/monitors#monitor-verdict)\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#### MonitorVerdict\n\n`\"match\" | \"no_match\" | \"no_value\"`\n\nOne check's comparison. `no_value` means the window gave nothing to compare, which never opens or\ncloses an alert.\n\n## Monitor Series State Model\n\n### Fields\n\n- **`series_hash`** `string`\n\n- **`is_triggered`** `boolean`\n\n For `spanning` monitors, whether an alert is open. For `instant` monitors, whether the\n condition has fired and not yet cleared, which keeps it from firing again.\n\n- **`last_value`** `number | null`\n\n The statistic at the last check. Null when that window gave nothing to compare.\n\n- **`last_check_status`** [`MonitorCheckStatus`](/api/monitors#monitor-check-status)\n\n- **`last_check_reason`** `string | null`\n\n Why the last check had no verdict, when it had none.\n\n- **`last_slot_at`** [`ISODateString | null`](/api/monitors#iso-date-string)\n\n The most recent scheduled check applied to this target.\n\n- **`last_triggered_at`** [`ISODateString | null`](/api/monitors#iso-date-string)\n\n When this target last opened an alert.\n\n- **`sustained_started_at`** [`ISODateString | null`](/api/monitors#iso-date-string)\n\n For `sustained` triggers, when the current run of matches began.\n\n- **`repeat_matches`** `{ matched_at: ISODateString; }[]`\n\n For `repeated` triggers, the matching checks inside the current window.\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#### MonitorCheckStatus\n\n`\"pending\" | \"ready\" | \"unknown\" | \"error\"`\n\nThe outcome of a target's most recent check. `unknown` means the slot was passed over without a\nverdict, for example while the metric rebuilds; `error` means the read failed and is retried.\n\n## Monitor Draft Model\n\n### Fields\n\n- **`outcome`** `\"draft\" | \"clarification\"`\n\n `draft` when the description was enough for a rule, `clarification` when it was not.\n\n- **`draft`** `MonitorDraftRule | Record<string, never>`\n\n The drafted rule. Empty for a clarification. Review it before creating the monitor.\n\n- **`assumptions`** `string[]`\n\n What the draft assumed that the description did not say, one sentence each.\n\n- **`question`** `string | null`\n\n For a clarification, what to add to the description. Null for a draft.\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#### 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## Monitor Draft Rule Model\n\n### Fields\n\n- **`name`** `string`\n\n- **`statistic`** [`MonitorStatistic`](/api/monitors#monitor-statistic)\n\n- **`operator`** [`MonitorOperator`](/api/monitors#monitor-operator)\n\n- **`threshold_value`** `number`\n\n- **`empty_window_handling`** [`MonitorEmptyWindowHandling`](/api/monitors#monitor-empty-window-handling)\n\n- **`window_type`** [`MonitorWindowType`](/api/monitors#monitor-window-type)\n\n- **`window_minutes`** `number | null`\n\n- **`scope`** [`MonitorScope`](/api/monitors#monitor-scope)\n\n- **`series_filters`** `MonitorSeriesFilters`\n\n- **`trigger_type`** [`MonitorTriggerType`](/api/monitors#monitor-trigger-type)\n\n- **`sustained_minutes`** `number | null`\n\n- **`repeat_count`** `number | null`\n\n- **`repeat_window_minutes`** `number | null`\n\n- **`schedule_cron`** `string`\n\n- **`alert_mode`** [`MonitorAlertMode`](/api/monitors#monitor-alert-mode)\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#### 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 Monitors\n\n### Endpoint\n\nList the monitors watching a project's metrics.\n\n```http\nGET /v1/projects/:project_id/monitors\nGET /v1/projects/:project_id/metrics/:metric_id/monitors\n```\n\n**Scope:** `monitors:read`\n\n### Path Parameters\n\n- **`project_id`** `string` -- **Required**\n Unique identifier of the project.\n\n- **`metric_id`** `string`\n Unique identifier of the metric. Only used by `/v1/projects/:project_id/metrics/:metric_id/monitors`.\n\n### Query Parameters\n\n- **`order_by`** `string`\n Field the results are sorted by. Optional. Defaults to `\"name\"`. Allowed values: `\"name\"`, `\"created_at\"`.\n\n- **`metric_id`** `string`\n Return only monitors watching this metric. Optional.\n\n- **`status`** `string`\n Return only monitors in this state. Optional. Allowed values: `\"active\"`, `\"paused\"`, `\"disabled\"`.\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: Monitor[];\n status: 200;\n error: null;\n pagination: Pagination;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Call the nested path to scope the results to one metric, or the flat path with an optional `metric_id` filter.\n- `after` and `before` are mutually exclusive.\n\n## Retrieve Monitor\n\n### Endpoint\n\nRetrieve a single monitor.\n\n```http\nGET /v1/projects/:project_id/metrics/:metric_id/monitors/:monitor_id\n```\n\n**Scope:** `monitors: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- **`monitor_id`** `string` -- **Required**\n Unique identifier of the monitor.\n\n### Response\n\nMonitor retrieved\n\n```ts\n{\n message: string;\n data: Monitor;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Includes the monitor's recent alerts and the current state of every series it watches.\n\n## Create Monitor\n\n### Endpoint\n\nCreate a monitor that raises alerts when a metric meets a condition.\n\n```http\nPOST /v1/projects/:project_id/metrics/:metric_id/monitors\n```\n\n**Scope:** `monitors:write`\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### Request Body\n\n- **`name`** `string`\n Name shown for this monitor. Optional. Minimum length: `2`. Maximum length: `64`.\n\n- **`description`** `string | null`\n What this monitor watches for. Optional.\n\n- **`status`** `string`\n Whether the monitor starts checking immediately or switched off. `disabled` is not accepted: the system uses it to record that it stopped the monitor itself. Optional. Defaults to `\"active\"`. Allowed values: `\"active\"`, `\"paused\"`.\n\n- **`expires_at`** [`ISODateString | null`](/api/monitors#iso-date-string)\n When the monitor stops checking. Leave null for no expiry. Optional.\n\n- **`statistic`** [`MonitorStatistic`](/api/monitors#monitor-statistic) -- **Required**\n What is computed over each window. `count`, `sum` and `count_unique` measure how much happened; the others read a value from the window's observations.\n\n- **`operator`** [`MonitorOperator`](/api/monitors#monitor-operator) -- **Required**\n How the statistic is compared with `threshold_value`.\n\n- **`threshold_value`** `number` -- **Required**\n The value the statistic is compared with. `count` equal to 0 alerts when no data arrives.\n\n- **`empty_window_handling`** [`MonitorEmptyWindowHandling`](/api/monitors#monitor-empty-window-handling)\n What the statistic reads when the window holds no data. `zero` reads 0. `no_value` reads nothing, so that check neither opens nor closes an alert. Optional.\n\n- **`window_type`** [`MonitorWindowType`](/api/monitors#monitor-window-type)\n `rolling` reads the `window_minutes` before each check. `all_time` reads everything from the start of the project. Optional. Defaults to `\"rolling\"`.\n\n- **`window_minutes`** `integer | null`\n Length of a rolling window in whole minutes (1 to 525600). Null for `all_time`. Optional.\n\n- **`scope`** [`MonitorScope`](/api/monitors#monitor-scope)\n `total` checks the whole feed as one value. `any_series` keeps one alert open while any series matches. `each_series` checks every series separately. Optional. Defaults to `\"total\"`.\n\n- **`series_filters`** `object`\n Narrows which of the metric's series this monitor watches. Property values may use `*` wildcards. Empty watches every series. Optional.\n\n - **`include`** `object[]`\n Only series matching one of these property sets are watched. Optional.\n\n - **`exclude`** `object[]`\n Series matching one of these property sets are ignored. Optional.\n\n- **`trigger_type`** [`MonitorTriggerType`](/api/monitors#monitor-trigger-type)\n When a match opens an alert. `immediate` on the first matching check, `sustained` once matches have held for `sustained_minutes`, `repeated` after `repeat_count` matching checks within `repeat_window_minutes`. Optional. Defaults to `\"immediate\"`.\n\n- **`sustained_minutes`** `integer | null`\n How long matches must hold before an alert opens, in whole minutes. Only for `sustained`. Optional.\n\n- **`repeat_count`** `integer | null`\n How many matching checks open an alert. Only for `repeated`. Optional.\n\n- **`repeat_window_minutes`** `integer | null`\n The window, in whole minutes, those matching checks must fall within. Only for `repeated`. Optional.\n\n- **`schedule_cron`** `string`\n Five-field cron expression, in UTC, setting when the monitor checks. Optional. Defaults to `\"* * * * *\"`. Minimum length: `1`. Maximum length: `128`.\n\n- **`alert_mode`** [`MonitorAlertMode`](/api/monitors#monitor-alert-mode)\n `spanning` keeps an alert open until the condition clears. `instant` opens and ends it at once. Optional. Defaults to `\"spanning\"`.\n\n- **`metric_id`** `string`\n Metric to watch. Optional when using the /metrics/:metric/monitors route. Optional.\n\n### Response\n\nYour monitor has been created.\n\n```ts\n{\n message: string;\n data: Monitor;\n status: 201;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- The monitor starts checking on its schedule as soon as it is created, unless it is created with `status` set to `paused`. Its first check is the first scheduled time after it is created.\n- `scope` decides how many alerts a monitor can raise at once: `any_series` raises one for the metric as a whole, `each_series` tracks every series separately.\n- A `rolling` window needs `window_minutes`. An `all_time` window takes `window_minutes: null`.\n- `count`, `sum` and `count_unique` require `empty_window_handling: \"zero\"`. Omitted, it is `zero` for those statistics and `no_value` for the others.\n- An `immediate` trigger takes no trigger fields. `sustained` needs `sustained_minutes` and no repeat fields. `repeated` needs `repeat_count` and `repeat_window_minutes` and no `sustained_minutes`.\n- A trigger the schedule can never satisfy is rejected. A gap between checks longer than `sustained_minutes` restarts the hold, so a `sustained` trigger needs checks that run for `sustained_minutes` without such a gap. A `repeated` trigger needs a `repeat_window_minutes` that can hold `repeat_count` checks. A schedule that only might satisfy the trigger is allowed.\n- `schedule_cron` must be a valid five-field cron expression. It is evaluated in UTC, and the finest granularity is one minute.\n- A monitor cannot be created active with an `expires_at` already in the past.\n\n## Draft Monitor\n\n### Endpoint\n\nDraft a monitor rule from a description in your own words.\n\n```http\nPOST /v1/projects/:project_id/metrics/:metric_id/monitors/draft\n```\n\n**Scope:** `monitors:write`\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### Request Body\n\n- **`description`** `string` -- **Required**\n The alert in your own words, for example `Alert me if no data arrives for 10 minutes.`. Minimum length: `1`. Maximum length: `1000`.\n\n### Response\n\n- The description needs more detail\n- Monitor drafted\n\n```ts\n{\n message: string;\n data: MonitorDraft;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Nothing is saved. Review the draft and its `assumptions`, then create the monitor with the draft as the body.\n- When the description is not enough for a rule (no threshold, no measurable condition), the outcome is `clarification` with a `question` to answer in a new description.\n- Drafts count against a per-team hourly budget (50 on Pro, 500 on Enterprise). Over budget the request is refused with `resource_limit_reached` and says when the budget resets.\n\n## Preview Monitor\n\n### Endpoint\n\nPreview a monitor's rule on its most recent scheduled check, without saving or raising alerts.\n\n```http\nPOST /v1/projects/:project_id/metrics/:metric_id/monitors/preview\n```\n\n**Scope:** `monitors: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### Request Body\n\n- **`name`** `string`\n Name shown for this monitor. Optional. Minimum length: `2`. Maximum length: `64`.\n\n- **`description`** `string | null`\n What this monitor watches for. Optional.\n\n- **`status`** `string`\n Whether the monitor starts checking immediately or switched off. `disabled` is not accepted: the system uses it to record that it stopped the monitor itself. Optional. Defaults to `\"active\"`. Allowed values: `\"active\"`, `\"paused\"`.\n\n- **`expires_at`** [`ISODateString | null`](/api/monitors#iso-date-string)\n When the monitor stops checking. Leave null for no expiry. Optional.\n\n- **`statistic`** [`MonitorStatistic`](/api/monitors#monitor-statistic) -- **Required**\n What is computed over each window. `count`, `sum` and `count_unique` measure how much happened; the others read a value from the window's observations.\n\n- **`operator`** [`MonitorOperator`](/api/monitors#monitor-operator) -- **Required**\n How the statistic is compared with `threshold_value`.\n\n- **`threshold_value`** `number` -- **Required**\n The value the statistic is compared with. `count` equal to 0 alerts when no data arrives.\n\n- **`empty_window_handling`** [`MonitorEmptyWindowHandling`](/api/monitors#monitor-empty-window-handling)\n What the statistic reads when the window holds no data. `zero` reads 0. `no_value` reads nothing, so that check neither opens nor closes an alert. Optional.\n\n- **`window_type`** [`MonitorWindowType`](/api/monitors#monitor-window-type)\n `rolling` reads the `window_minutes` before each check. `all_time` reads everything from the start of the project. Optional. Defaults to `\"rolling\"`.\n\n- **`window_minutes`** `integer | null`\n Length of a rolling window in whole minutes (1 to 525600). Null for `all_time`. Optional.\n\n- **`scope`** [`MonitorScope`](/api/monitors#monitor-scope)\n `total` checks the whole feed as one value. `any_series` keeps one alert open while any series matches. `each_series` checks every series separately. Optional. Defaults to `\"total\"`.\n\n- **`series_filters`** `object`\n Narrows which of the metric's series this monitor watches. Property values may use `*` wildcards. Empty watches every series. Optional.\n\n - **`include`** `object[]`\n Only series matching one of these property sets are watched. Optional.\n\n - **`exclude`** `object[]`\n Series matching one of these property sets are ignored. Optional.\n\n- **`trigger_type`** [`MonitorTriggerType`](/api/monitors#monitor-trigger-type)\n When a match opens an alert. `immediate` on the first matching check, `sustained` once matches have held for `sustained_minutes`, `repeated` after `repeat_count` matching checks within `repeat_window_minutes`. Optional. Defaults to `\"immediate\"`.\n\n- **`sustained_minutes`** `integer | null`\n How long matches must hold before an alert opens, in whole minutes. Only for `sustained`. Optional.\n\n- **`repeat_count`** `integer | null`\n How many matching checks open an alert. Only for `repeated`. Optional.\n\n- **`repeat_window_minutes`** `integer | null`\n The window, in whole minutes, those matching checks must fall within. Only for `repeated`. Optional.\n\n- **`schedule_cron`** `string`\n Five-field cron expression, in UTC, setting when the monitor checks. Optional. Defaults to `\"* * * * *\"`. Minimum length: `1`. Maximum length: `128`.\n\n- **`alert_mode`** [`MonitorAlertMode`](/api/monitors#monitor-alert-mode)\n `spanning` keeps an alert open until the condition clears. `instant` opens and ends it at once. Optional. Defaults to `\"spanning\"`.\n\n- **`metric_id`** `string`\n Metric to watch. Optional when using the /metrics/:metric/monitors route. Optional.\n\n### Response\n\nMonitor preview retrieved\n\n```ts\n{\n message: string;\n data: MonitorPreview;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Reads the same window and computes the same statistic and verdict per target as a scheduled check of the most recent time on the rule's schedule. It shows one check: `trigger_note` explains when a `sustained` or `repeated` trigger would open an alert.\n- A window whose data is not complete yet returns `is_ready: false` with no verdicts.\n- A `rolling` window needs `window_minutes`. An `all_time` window takes `window_minutes: null`.\n- `count`, `sum` and `count_unique` require `empty_window_handling: \"zero\"`. Omitted, it is `zero` for those statistics and `no_value` for the others.\n- An `immediate` trigger takes no trigger fields. `sustained` needs `sustained_minutes` and no repeat fields. `repeated` needs `repeat_count` and `repeat_window_minutes` and no `sustained_minutes`.\n- A trigger the schedule can never satisfy is rejected. A gap between checks longer than `sustained_minutes` restarts the hold, so a `sustained` trigger needs checks that run for `sustained_minutes` without such a gap. A `repeated` trigger needs a `repeat_window_minutes` that can hold `repeat_count` checks. A schedule that only might satisfy the trigger is allowed.\n- `schedule_cron` must be a valid five-field cron expression. It is evaluated in UTC, and the finest granularity is one minute.\n- A monitor cannot be created active with an `expires_at` already in the past.\n\n## Update Monitor\n\n### Endpoint\n\nChange a monitor's rule, schedule, name, description or expiry.\n\n```http\nPOST /v1/projects/:project_id/metrics/:metric_id/monitors/:monitor_id\n```\n\n**Scope:** `monitors:write`\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- **`monitor_id`** `string` -- **Required**\n Unique identifier of the monitor.\n\n### Request Body\n\n- **`name`** `string`\n Name shown for this monitor. Optional. Minimum length: `2`. Maximum length: `64`.\n\n- **`description`** `string | null`\n What this monitor watches for. Optional.\n\n- **`expires_at`** [`ISODateString | null`](/api/monitors#iso-date-string)\n When the monitor stops checking. Leave null for no expiry. Optional.\n\n- **`statistic`** [`MonitorStatistic`](/api/monitors#monitor-statistic)\n What is computed over each window. `count`, `sum` and `count_unique` measure how much happened; the others read a value from the window's observations. Optional.\n\n- **`operator`** [`MonitorOperator`](/api/monitors#monitor-operator)\n How the statistic is compared with `threshold_value`. Optional.\n\n- **`threshold_value`** `number`\n The value the statistic is compared with. `count` equal to 0 alerts when no data arrives. Optional.\n\n- **`empty_window_handling`** [`MonitorEmptyWindowHandling`](/api/monitors#monitor-empty-window-handling)\n What the statistic reads when the window holds no data. `zero` reads 0. `no_value` reads nothing, so that check neither opens nor closes an alert. Optional.\n\n- **`window_type`** [`MonitorWindowType`](/api/monitors#monitor-window-type)\n `rolling` reads the `window_minutes` before each check. `all_time` reads everything from the start of the project. Optional.\n\n- **`window_minutes`** `integer | null`\n Length of a rolling window in whole minutes (1 to 525600). Null for `all_time`. Optional.\n\n- **`scope`** [`MonitorScope`](/api/monitors#monitor-scope)\n `total` checks the whole feed as one value. `any_series` keeps one alert open while any series matches. `each_series` checks every series separately. Optional.\n\n- **`series_filters`** `object`\n Narrows which of the metric's series this monitor watches. Property values may use `*` wildcards. Empty watches every series. Optional.\n\n - **`include`** `object[]`\n Only series matching one of these property sets are watched. Optional.\n\n - **`exclude`** `object[]`\n Series matching one of these property sets are ignored. Optional.\n\n- **`trigger_type`** [`MonitorTriggerType`](/api/monitors#monitor-trigger-type)\n When a match opens an alert. `immediate` on the first matching check, `sustained` once matches have held for `sustained_minutes`, `repeated` after `repeat_count` matching checks within `repeat_window_minutes`. Optional.\n\n- **`sustained_minutes`** `integer | null`\n How long matches must hold before an alert opens, in whole minutes. Only for `sustained`. Optional.\n\n- **`repeat_count`** `integer | null`\n How many matching checks open an alert. Only for `repeated`. Optional.\n\n- **`repeat_window_minutes`** `integer | null`\n The window, in whole minutes, those matching checks must fall within. Only for `repeated`. Optional.\n\n- **`schedule_cron`** `string`\n Five-field cron expression, in UTC, setting when the monitor checks. Optional. Minimum length: `1`. Maximum length: `128`.\n\n- **`alert_mode`** [`MonitorAlertMode`](/api/monitors#monitor-alert-mode)\n `spanning` keeps an alert open until the condition clears. `instant` opens and ends it at once. Optional.\n\n### Response\n\nYour monitor has been updated.\n\n```ts\n{\n message: string;\n data: Monitor;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Send the fields you are changing. A field sent with its current value changes nothing, so sending the whole monitor back is fine.\n- Changing the rule (statistic, comparison, empty window handling, window, scope, series filters, trigger or alert mode) ends the monitor's open alerts with `monitor_changed` and starts checking afresh at the next scheduled time.\n- Changing only `schedule_cron` keeps open alerts. Checks move to the new schedule, and a `sustained` or `repeated` trigger starts counting again.\n- Changing only `name`, `description` or `expires_at` affects nothing else.\n- `metric_id` cannot be changed. Create a new monitor against the other metric instead.\n- The metric a monitor watches cannot be changed. Create a second monitor instead.\n- `status` is not editable here. Use the pause and resume operations, which is also what keeps a customer from writing the `disabled` the system uses for expiry.\n- The fields you send are merged into the stored monitor, and the result must satisfy the create rules for windows, `empty_window_handling`, trigger fields and the schedule, so a change is rejected even when the conflicting value is one you did not send.\n- An omitted `empty_window_handling` becomes `zero` when the new `statistic` is `count`, `sum` or `count_unique`, and `no_value` when the stored statistic was one of those and the new one is not. Otherwise it keeps its stored value.\n- Fields the new window or trigger does not use are cleared when you omit them or send null: switching to `all_time` clears `window_minutes`, and switching `trigger_type` clears the other triggers' fields. Sending a value for a field the new choice does not use is rejected.\n- `schedule_cron` must be a valid five-field cron expression. It is evaluated in UTC, and the finest granularity is one minute.\n- Moving `expires_at` is rejected when that would leave an active monitor with an expiry already in the past. Editing a monitor that has already expired is allowed, including the update that extends its expiry.\n\n## Pause Monitor\n\n### Endpoint\n\nPause a monitor so it stops checking.\n\n```http\nPOST /v1/projects/:project_id/metrics/:metric_id/monitors/:monitor_id/pause\n```\n\n**Scope:** `monitors:write`\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- **`monitor_id`** `string` -- **Required**\n Unique identifier of the monitor.\n\n### Response\n\nMonitor paused\n\n```ts\n{\n message: string;\n data: Monitor;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Pausing ends the monitor's open alerts with `monitor_paused`. A paused monitor is not checked, and resuming starts again at its next scheduled time: the paused period is never checked.\n- Pausing a paused monitor changes nothing. A monitor the system disabled when its expiry passed cannot be paused; resume it instead.\n\n## Resume Monitor\n\n### Endpoint\n\nResume a paused monitor so it checks on its schedule again.\n\n```http\nPOST /v1/projects/:project_id/metrics/:metric_id/monitors/:monitor_id/resume\n```\n\n**Scope:** `monitors:write`\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- **`monitor_id`** `string` -- **Required**\n Unique identifier of the monitor.\n\n### Response\n\nMonitor resumed\n\n```ts\n{\n message: string;\n data: Monitor;\n status: 200;\n error: null;\n pagination: null;\n endpoint: string;\n}\n```\n\n### Comments\n\n- Checking starts again at the next scheduled time; the paused period is never checked.\n- A monitor the system stopped because its expiry passed can be resumed once the expiry is moved into the future. Resuming one whose expiry is still in the past is rejected.\n\n## Delete Monitor\n\n### Endpoint\n\nDelete a monitor.\n\n```http\nDELETE /v1/projects/:project_id/metrics/:metric_id/monitors/:monitor_id\n```\n\n**Scope:** `monitors:delete`\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- **`monitor_id`** `string` -- **Required**\n Unique identifier of the monitor.\n\n### Response\n\nYour monitor 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- Alerts the monitor already raised are deleted with it.\n\n",
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
- "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\" | \"highest\" | \"lowest\"`\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- **`hostname`** `string`\n\n The hostname the page is served on.\n\n- **`pathname`** `string`\n\n Path the page is served at on that hostname, leading slash included. `/` is the 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
- "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\" | \"highest\" | \"lowest\"`\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",
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\" | \"lime\" | \"indigo\" | \"fuchsia\" | \"orange\"`\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\" | \"highest\" | \"lowest\"`\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- **`hostname`** `string`\n\n The hostname the page is served on.\n\n- **`pathname`** `string`\n\n Path the page is served at on that hostname, leading slash included. `/` is the 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
+ "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\" | \"lime\" | \"indigo\" | \"fuchsia\" | \"orange\"`\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\" | \"highest\" | \"lowest\"`\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
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- **`ai_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- **`ai_billing_period_output_tokens`** `number`\n\n AI output tokens consumed through Tailglow-provided model access this billing period.\n\n- **`ai_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- **`ai_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- **`ai_promo_tokens`** `number`\n\n Shared Tailglow AI token credits remaining after the last invoice.\n\n- **`ai_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- **`ai_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- **`ai_chart_colors_count`** `number`\n\n AI chart color runs counted against the plan's hourly limit. The counter rolls over lazily, as\n `ai_tgl_generations_count` does.\n\n- **`ai_chart_colors_reset_at`** [`ISODateString | null`](/api/teams#iso-date-string)\n\n When the counted window ends or ended; a chart waiting on the budget gets its colors after 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#### 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- **`ai_promo_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- **`ai_available_promo_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- **`ai_default_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- **`ai_default_effort`** [`AiEffortLevel`](/api/users#ai-effort-level)\n\n How hard the assistant is asked to work on each turn by default.\n\n- **`timezone`** `string`\n\n IANA time zone the user's browser last reported, such as `America/Los_Angeles`. Updated when\n the dashboard connects, so it follows the user's most recently used device. Until a browser\n reports one it is `America/Los_Angeles`.\n\n- **`timezone_display`** [`TimezoneDisplay`](/api/users#timezone-display)\n\n How the user reads times in the dashboard and in emails: `local` uses `timezone`, `utc` uses\n UTC.\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#### TimezoneDisplay\n\n`\"local\" | \"utc\"`\n\nHow a user reads times: `local` in the time zone their browser reports, `utc` in UTC.\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- **`timezone_display`** [`TimezoneDisplay`](/api/users#timezone-display)\n How the user reads times in the dashboard and in emails: `local` in the time zone their browser reports, `utc` in UTC. Optional.\n\n- **`ai_default_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- **`ai_default_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",
@@ -38,7 +38,7 @@ export const DOCS_PAGES = {
38
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- **`country`** `string | null`\n\n Two-letter ISO 3166 country of the time zone the acting browser reported; null when it reported\n none, as with API keys and entries written before this field existed.\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
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. Keep a readable name next to each id (`user_name` beside `user_id`) and pass `--display-fields '{\"user_id\":\"user_name\"}'`, so a chart grouped by the id shows names.\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
- "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 as Settling under **Pending** on its **Versions** tab 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 the metrics, drains, or views that read it. On narrow screens the diagram runs top to bottom. Past four scripts, older live scripts fold into one node; open it for the full list. A version no script reads yet is a dashed box; open it to see why and how its fields differ. The **Versions** tab lists every version.\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. Settings also hold the view's [Chart labels](/guides/records#chart-labels), which show a chart grouped by an id, such as `user_id`, by a name field instead, such as `user_name`.\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. New and rebuilt metrics automatically update historical buckets when accepted events arrive late; [Late data](/guides/metrics#late-data) explains coverage and the upgrade path for older metrics.\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",
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 as Settling under **Pending** on its **Versions** tab 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. A view whose **Script generation** is set to **Explicit** never reuses a script: each schema version waits for the one you set.\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 the metrics, drains, or views that read it. On narrow screens the diagram runs top to bottom. Past four scripts, older live scripts fold into one node; open it for the full list. A version no script reads yet is a dashed box; open it to see why and how its fields differ. The **Versions** tab lists every version.\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. Settings also hold the view's [Chart labels](/guides/records#chart-labels), which show a chart grouped by an id, such as `user_id`, by a name field instead, such as `user_name`.\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. New and rebuilt metrics automatically update historical buckets when accepted events arrive late; [Late data](/guides/metrics#late-data) explains coverage and the upgrade path for older metrics.\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\nEach device shows the flag of the country its browser's time zone belongs to. The flag comes from\nthe time zone the browser reports, not from its IP address, so a VPN does not change it. A browser\nset to UTC, or one that has not reported a time zone yet, shows a globe. The same flags appear next\nto each device in your team's **Authentication Logs**.\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",