diff --git a/scripts/generate-md-tables.js b/scripts/generate-md-tables.js index 14b6c3f..52a0072 100644 --- a/scripts/generate-md-tables.js +++ b/scripts/generate-md-tables.js @@ -121,7 +121,7 @@ function collectOps(tagName) { * "reference/jobs.md#anchor" (cross-page, used in the README index). */ function summaryRow(p, method, op, hrefBase = '') { - const cost = op['x-token-cost']; + const cost = op['x-average-token-cost']; const restricted = op['x-restricted'] ? ' 🔒' : ''; const href = `${hrefBase}#${anchor(op.summary)}`; return `| \`${method.toUpperCase()}\` | [\`${p}\`](${href})${restricted} | ${esc(cost)} | ${esc(op.summary)} |`; @@ -135,7 +135,7 @@ function buildTagMarkdown(tagName) { const lines = []; // Summary table - lines.push('| Method | Endpoint | Token cost | Description |'); + lines.push('| Method | Endpoint | Average token cost | Description |'); lines.push('| --- | --- | --- | --- |'); for (const { p, method, op } of ops) { lines.push(summaryRow(p, method, op)); @@ -156,7 +156,7 @@ function buildTagMarkdown(tagName) { lines.push(`${method.toUpperCase()} ${BASE_URL}${p}`); lines.push('```'); lines.push(''); - lines.push(`**Token cost:** ${esc(op['x-token-cost'])}`); + lines.push(`**Average token cost:** ${esc(op['x-average-token-cost'])}`); lines.push(''); if (op.description) { lines.push(esc(op.description)); @@ -225,7 +225,7 @@ function buildEndpointIndex() { if (ops.length === 0) continue; lines.push(`### [${tagName}](reference/${slug}.md)`); lines.push(''); - lines.push('| Method | Endpoint | Token cost | Description |'); + lines.push('| Method | Endpoint | Average token cost | Description |'); lines.push('| --- | --- | --- | --- |'); for (const { p, method, op } of ops) { lines.push(summaryRow(p, method, op, `reference/${slug}.md`)); diff --git a/v3/ApiExamples/Nodejs/node-getModel.js b/v3/ApiExamples/Nodejs/node-getModel.js index cf4a111..2a17645 100644 --- a/v3/ApiExamples/Nodejs/node-getModel.js +++ b/v3/ApiExamples/Nodejs/node-getModel.js @@ -13,7 +13,7 @@ const API_KEY = '<>'; // Path placeholder — replace with the model key you want to fetch. Must not contain '/'. const MODEL_KEY = 'your-model-key'; -// Comma-separated list of sub-paths to return (maximum 10), 1000 tokens per path. +// Comma-separated list of sub-paths to return (maximum 10). Cost scales with the data returned per path. // Use `paths` — this is the supported way to read model data. A full model fetch // (empty string here / omit `paths`) requires extended API access and returns a // 403 `extended_access_required` in production, where extended access is off. diff --git a/v3/ApiExamples/Nodejs/node-listModels.js b/v3/ApiExamples/Nodejs/node-listModels.js index f570b9f..ee5330c 100644 --- a/v3/ApiExamples/Nodejs/node-listModels.js +++ b/v3/ApiExamples/Nodejs/node-listModels.js @@ -10,7 +10,7 @@ const axios = require('axios'); const URL_PATH = 'https://katapultpro.com/api/v3'; const API_KEY = '<>'; -// Token cost: 100 tokens per request. +// Token cost scales with the data returned; see the average in the API reference. async function run() { try { const response = await axios({ diff --git a/v3/ApiExamples/Python/py-getModel.py b/v3/ApiExamples/Python/py-getModel.py index b3ba5a4..c783b7e 100644 --- a/v3/ApiExamples/Python/py-getModel.py +++ b/v3/ApiExamples/Python/py-getModel.py @@ -13,7 +13,7 @@ # Path placeholder — replace with the model key you want to fetch. Must not contain '/'. MODEL_KEY = 'your-model-key' -# Comma-separated list of sub-paths to return (maximum 10), 1000 tokens per path. +# Comma-separated list of sub-paths to return (maximum 10). Cost scales with the data returned per path. # Use `paths` — this is the supported way to read model data. A full model fetch # (None here / omit `paths`) requires extended API access and returns a # 403 `extended_access_required` in production, where extended access is off. diff --git a/v3/ApiExamples/Python/py-listModels.py b/v3/ApiExamples/Python/py-listModels.py index d3e39fc..8ea5bd3 100644 --- a/v3/ApiExamples/Python/py-listModels.py +++ b/v3/ApiExamples/Python/py-listModels.py @@ -10,7 +10,7 @@ URL_PATH = 'https://katapultpro.com/api/v3' API_KEY = '<>' -# Token cost: 100 tokens per request. +# Token cost scales with the data returned; see the average in the API reference. try: resp = requests.get(f'{URL_PATH}/models', params={'api_key': API_KEY}) print('Status:', resp.status_code) diff --git a/v3/README.md b/v3/README.md index c630329..561080e 100644 --- a/v3/README.md +++ b/v3/README.md @@ -11,8 +11,8 @@ traces), plus models and users. generated from it — see [Maintaining these docs](#maintaining-these-docs). > **For AI agents:** [`openapi.yaml`](openapi.yaml) is the single source of -> truth for every endpoint, parameter, schema, and token cost (encoded as the -> `x-token-cost` extension on each operation). Prefer it for structured lookups; +> truth for every endpoint, parameter, schema, and average token cost (encoded +> as the `x-average-token-cost` extension on each operation). Prefer it for structured lookups; > use the Markdown pages for prose, concepts, and examples. ## Quick start @@ -49,108 +49,108 @@ in [Quick start](#quick-start) above. ### [Jobs](reference/jobs.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs`](reference/jobs.md#list-all-jobs) | 1 | List all jobs | -| `POST` | [`/jobs`](reference/jobs.md#create-a-job) | 10 | Create a job | -| `GET` | [`/jobs/{job_id}`](reference/jobs.md#get-a-job-partial-data) | 1 | Get a job (partial data) | -| `POST` | [`/jobs/{job_id}`](reference/jobs.md#update-a-job) | 10 | Update a job | -| `POST` | [`/jobs/{job_id}/raw`](reference/jobs.md#raw-job-write) 🔒 | 10 | Raw job write | +| `GET` | [`/jobs`](reference/jobs.md#list-all-jobs) | 2014 | List all jobs | +| `POST` | [`/jobs`](reference/jobs.md#create-a-job) | 17 | Create a job | +| `GET` | [`/jobs/{job_id}`](reference/jobs.md#get-a-job-partial-data) | 3 | Get a job (partial data) | +| `POST` | [`/jobs/{job_id}`](reference/jobs.md#update-a-job) | 49 | Update a job | +| `POST` | [`/jobs/{job_id}/raw`](reference/jobs.md#raw-job-write) 🔒 | 1 | Raw job write | | `GET` | [`/jobs/{job_id}/status`](reference/jobs.md#get-job-status) | 1 | Get job status | -| `POST` | [`/jobs/{job_id}/status`](reference/jobs.md#update-job-status) | 10 | Update job status | +| `POST` | [`/jobs/{job_id}/status`](reference/jobs.md#update-job-status) | 1 | Update job status | ### [Nodes](reference/nodes.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/nodes`](reference/nodes.md#get-all-nodes) | 1 | Get all nodes | -| `POST` | [`/jobs/{job_id}/nodes`](reference/nodes.md#create-a-node) | 10 | Create a node | +| `GET` | [`/jobs/{job_id}/nodes`](reference/nodes.md#get-all-nodes) | 138 | Get all nodes | +| `POST` | [`/jobs/{job_id}/nodes`](reference/nodes.md#create-a-node) | 15 | Create a node | | `GET` | [`/jobs/{job_id}/nodes/{node_id}`](reference/nodes.md#get-a-node) | 1 | Get a node | -| `POST` | [`/jobs/{job_id}/nodes/{node_id}`](reference/nodes.md#update-a-node) | 10 | Update a node | -| `DELETE` | [`/jobs/{job_id}/nodes/{node_id}`](reference/nodes.md#delete-a-node) | 10 | Delete a node | -| `POST` | [`/jobs/{job_id}/nodes/{node_id}/photos`](reference/nodes.md#upload-a-photo-to-a-node) | 10 | Upload a photo to a node | +| `POST` | [`/jobs/{job_id}/nodes/{node_id}`](reference/nodes.md#update-a-node) | 40 | Update a node | +| `DELETE` | [`/jobs/{job_id}/nodes/{node_id}`](reference/nodes.md#delete-a-node) | 4933 | Delete a node | +| `POST` | [`/jobs/{job_id}/nodes/{node_id}/photos`](reference/nodes.md#upload-a-photo-to-a-node) | 5 | Upload a photo to a node | ### [Connections](reference/connections.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/connections`](reference/connections.md#get-all-connections) | 1 | Get all connections | -| `POST` | [`/jobs/{job_id}/connections`](reference/connections.md#create-a-connection) | 10 | Create a connection | +| `GET` | [`/jobs/{job_id}/connections`](reference/connections.md#get-all-connections) | 127 | Get all connections | +| `POST` | [`/jobs/{job_id}/connections`](reference/connections.md#create-a-connection) | 17 | Create a connection | | `GET` | [`/jobs/{job_id}/connections/{connection_id}`](reference/connections.md#get-a-connection) | 1 | Get a connection | -| `POST` | [`/jobs/{job_id}/connections/{connection_id}`](reference/connections.md#update-a-connection) | 10 | Update a connection | -| `DELETE` | [`/jobs/{job_id}/connections/{connection_id}`](reference/connections.md#delete-a-connection) | 10 | Delete a connection | +| `POST` | [`/jobs/{job_id}/connections/{connection_id}`](reference/connections.md#update-a-connection) | 18 | Update a connection | +| `DELETE` | [`/jobs/{job_id}/connections/{connection_id}`](reference/connections.md#delete-a-connection) | 2566 | Delete a connection | ### [Sections](reference/sections.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | | `GET` | [`/jobs/{job_id}/connections/{connection_id}/sections`](reference/sections.md#get-all-sections-on-a-connection) | 1 | Get all sections on a connection | -| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections`](reference/sections.md#create-a-section) | 10 | Create a section | +| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections`](reference/sections.md#create-a-section) | 17 | Create a section | | `GET` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}`](reference/sections.md#get-a-section) | 1 | Get a section | -| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}`](reference/sections.md#update-a-section) | 10 | Update a section | -| `DELETE` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}`](reference/sections.md#delete-a-section) | 10 | Delete a section | -| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}/photos`](reference/sections.md#upload-a-photo-to-a-section) | 10 | Upload a photo to a section | +| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}`](reference/sections.md#update-a-section) | 16 | Update a section | +| `DELETE` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}`](reference/sections.md#delete-a-section) | 5920 | Delete a section | +| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}/photos`](reference/sections.md#upload-a-photo-to-a-section) | 3 | Upload a photo to a section | ### [Photos](reference/photos.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/photos`](reference/photos.md#get-all-photos) | 1 | Get all photos | -| `POST` | [`/jobs/{job_id}/photos`](reference/photos.md#upload-a-photo) | 10 | Upload a photo | -| `GET` | [`/jobs/{job_id}/photos/{photo_id}`](reference/photos.md#get-a-photo) | 1 | Get a photo | -| `GET` | [`/jobs/{job_id}/photos/{photo_id}/url`](reference/photos.md#get-a-photo-download-url) | 1 | Get a photo download URL | -| `POST` | [`/jobs/{job_id}/photos/{photo_id}/associate`](reference/photos.md#associate-a-photo-to-an-item) | 10 | Associate a photo to an item | +| `GET` | [`/jobs/{job_id}/photos`](reference/photos.md#get-all-photos) | 1147 | Get all photos | +| `POST` | [`/jobs/{job_id}/photos`](reference/photos.md#upload-a-photo) | 1 | Upload a photo | +| `GET` | [`/jobs/{job_id}/photos/{photo_id}`](reference/photos.md#get-a-photo) | 2 | Get a photo | +| `GET` | [`/jobs/{job_id}/photos/{photo_id}/url`](reference/photos.md#get-a-photo-download-url) | 4 | Get a photo download URL | +| `POST` | [`/jobs/{job_id}/photos/{photo_id}/associate`](reference/photos.md#associate-a-photo-to-an-item) | 1286 | Associate a photo to an item | ### [Photo Elements](reference/photo-elements.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements`](reference/photo-elements.md#get-all-elements-on-a-photo) | 1 | Get all elements on a photo | -| `POST` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements`](reference/photo-elements.md#create-a-photo-element) | 10 | Create a photo element | -| `GET` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](reference/photo-elements.md#get-a-photo-element) | 1 | Get a photo element | -| `POST` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](reference/photo-elements.md#update-a-photo-element) | 10 | Update a photo element | -| `DELETE` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](reference/photo-elements.md#delete-a-photo-element) | 10 | Delete a photo element | +| `GET` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements`](reference/photo-elements.md#get-all-elements-on-a-photo) | 4 | Get all elements on a photo | +| `POST` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements`](reference/photo-elements.md#create-a-photo-element) | 515 | Create a photo element | +| `GET` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](reference/photo-elements.md#get-a-photo-element) | 7 | Get a photo element | +| `POST` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](reference/photo-elements.md#update-a-photo-element) | 994 | Update a photo element | +| `DELETE` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](reference/photo-elements.md#delete-a-photo-element) | 933 | Delete a photo element | ### [Calibration Anchors](reference/calibration-anchors.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors`](reference/calibration-anchors.md#get-all-calibration-anchors-on-a-photo) | 1 | Get all calibration anchors on a photo | -| `POST` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors`](reference/calibration-anchors.md#create-a-calibration-anchor) | 10 | Create a calibration anchor | +| `GET` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors`](reference/calibration-anchors.md#get-all-calibration-anchors-on-a-photo) | 4 | Get all calibration anchors on a photo | +| `POST` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors`](reference/calibration-anchors.md#create-a-calibration-anchor) | 435 | Create a calibration anchor | | `GET` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id}`](reference/calibration-anchors.md#get-a-calibration-anchor) | 1 | Get a calibration anchor | -| `POST` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id}`](reference/calibration-anchors.md#update-a-calibration-anchor) | 10 | Update a calibration anchor | -| `DELETE` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id}`](reference/calibration-anchors.md#delete-a-calibration-anchor) | 10 | Delete a calibration anchor | +| `POST` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id}`](reference/calibration-anchors.md#update-a-calibration-anchor) | 2691 | Update a calibration anchor | +| `DELETE` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id}`](reference/calibration-anchors.md#delete-a-calibration-anchor) | 685 | Delete a calibration anchor | ### [Traces](reference/traces.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/traces`](reference/traces.md#get-all-traces) | 1 | Get all traces | -| `POST` | [`/jobs/{job_id}/traces`](reference/traces.md#create-a-trace) | 10 | Create a trace | -| `GET` | [`/jobs/{job_id}/traces/{trace_id}`](reference/traces.md#get-a-trace) | 1 | Get a trace | -| `POST` | [`/jobs/{job_id}/traces/{trace_id}`](reference/traces.md#update-a-trace) | 10 | Update a trace | -| `DELETE` | [`/jobs/{job_id}/traces/{trace_id}`](reference/traces.md#delete-a-trace) | 10 | Delete a trace | +| `GET` | [`/jobs/{job_id}/traces`](reference/traces.md#get-all-traces) | 175 | Get all traces | +| `POST` | [`/jobs/{job_id}/traces`](reference/traces.md#create-a-trace) | 5802 | Create a trace | +| `GET` | [`/jobs/{job_id}/traces/{trace_id}`](reference/traces.md#get-a-trace) | 2 | Get a trace | +| `POST` | [`/jobs/{job_id}/traces/{trace_id}`](reference/traces.md#update-a-trace) | 7272 | Update a trace | +| `DELETE` | [`/jobs/{job_id}/traces/{trace_id}`](reference/traces.md#delete-a-trace) | 5902 | Delete a trace | ### [Models](reference/models.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/models`](reference/models.md#get-model-options) | 100 | Get model options | -| `GET` | [`/models/{modelKey}`](reference/models.md#get-model-data) | 9900 (full) \| 1000 per path (max 10) | Get model data | +| `GET` | [`/models`](reference/models.md#get-model-options) | 2 | Get model options | +| `GET` | [`/models/{modelKey}`](reference/models.md#get-model-data) | 394 | Get model data | ### [Users](reference/users.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/users`](reference/users.md#list-all-users) | 1 | List all users | +| `GET` | [`/users`](reference/users.md#list-all-users) | 186 | List all users | | `GET` | [`/users/whoami`](reference/users.md#get-current-user) | 1 | Get current user | | `GET` | [`/users/{user_id}`](reference/users.md#get-a-user) | 1 | Get a user | | `GET` | [`/users/{user_id}/active_state`](reference/users.md#get-user-active-state) 🔒 | 1 | Get user active state | -| `POST` | [`/users/{user_id}/active_state`](reference/users.md#set-user-active-state) 🔒 | 10 | Set user active state | +| `POST` | [`/users/{user_id}/active_state`](reference/users.md#set-user-active-state) 🔒 | 1 | Set user active state | ### [Companies](reference/companies.md) -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | | `GET` | [`/companies/{company_id}/tracked_actions`](reference/companies.md#list-tracked-actions) | 1 | List tracked actions | | `GET` | [`/companies/{company_id}/action_models`](reference/companies.md#list-action-models) | 1 | List action models | @@ -163,7 +163,7 @@ in [Quick start](#quick-start) above. ## Concepts -- [Rate limits & the token bucket](rate-limits.md) — token costs, refills, and +- [Rate limits & the token bucket](rate-limits.md) — average token costs, refills, and best practices for heavy scripts. - [Working with attributes](concepts/attributes.md) - [Complex & codependent parameters](concepts/complex-parameters.md) — worked examples for tricky field combinations (photo association, the attribute trio, `make_midpoint`, `onlyIfExists`, model `paths`). diff --git a/v3/concepts/complex-parameters.md b/v3/concepts/complex-parameters.md index 8c392e9..52646f2 100644 --- a/v3/concepts/complex-parameters.md +++ b/v3/concepts/complex-parameters.md @@ -136,17 +136,18 @@ differently based on `paths`: - **No `paths`** → returns the **full** model. This **requires extended API access** (otherwise `403 extended_access_required`) and, because extended - access is off in production, is **unavailable on `katapultpro.com`**. When - permitted, it costs **9,900 tokens** (nearly the entire default bucket). -- **With `paths`** (comma-separated, **max 10**) → returns only those sub-paths - and costs **1,000 tokens per path**. Open to all callers — the supported route - for most integrations. + access is off in production, is **unavailable on `katapultpro.com`**. A full + model is large, so when permitted it is the most expensive way to read model + data. +- **With `paths`** (comma-separated, **max 10**) → returns only those sub-paths, + so it costs far less than a full fetch. Open to all callers — the supported + route for most integrations. Prefer `paths` whenever you need only part of a model. See -[Rate limits & the token bucket](../rate-limits.md#token-costs). +[Rate limits & the token bucket](../rate-limits.md#average-token-costs). ```sh -# 2 paths -> 2,000 tokens, instead of 9,900 for the whole model: +# Fetch only the sub-paths you need, instead of the whole model: curl "https://katapultpro.com/api/v3/models/MODEL_KEY?api_key=KEY&paths=attributes,button_groups" ``` diff --git a/v3/concepts/responses-and-errors.md b/v3/concepts/responses-and-errors.md index 9c2ffbb..47fe142 100644 --- a/v3/concepts/responses-and-errors.md +++ b/v3/concepts/responses-and-errors.md @@ -17,15 +17,17 @@ type EntityResponse = { // ...other resource fields }, meta: { - token_count: number, // Tokens remaining after the request - last_refill_time: number, // Epoch ms of the last token refill + token_count: number, // Tokens remaining after the request + last_refill_time: number, // Epoch ms of the last token refill + token_warning?: string, // Present only when the bucket is depleted } } ``` -> For callers with [extended API access](../reference/restricted.md#extended-api-access), -> `token_count` is always the fixed placeholder integer `9999999999` instead of -> a real remaining count. +> Callers with [extended API access](../reference/restricted.md#extended-api-access) +> are unthrottled, so their `token_count` is informational only. Token metering +> is currently **advisory**: a depleted bucket is **not** blocked — the request +> is still served and `meta` carries a `token_warning`. For a list, `data` is an array of resources: @@ -33,7 +35,7 @@ For a list, `data` is an array of resources: type ListResponse = { status: "success", data: Array<{ id: string, /* ...fields */ }>, - meta: { token_count: number, last_refill_time: number } + meta: { token_count: number, last_refill_time: number, token_warning?: string } } ``` @@ -49,7 +51,7 @@ type ErrorResponse = { status: "error", message: string, // Human-readable message type: string, // Machine-readable type (see table below) - meta: { token_count: number, last_refill_time: number } + meta: { token_count: number, last_refill_time: number, token_warning?: string } } ``` @@ -66,7 +68,8 @@ type ErrorResponse = { | 403 | `extended_access_required` | The endpoint requires extended API access. | | 403 | `forbidden` | Requested a company that is not your own. | | 404 | `not_found` | The requested resource does not exist. | -| 429 | `token_rate_limit_exceeded` | The token bucket was depleted. See [rate limits](../rate-limits.md). | +| 429 | `server_busy` | The API is shedding load during high database pressure. Retry after the `Retry-After` interval. | +| 429 | `token_rate_limit_exceeded` | The token bucket was depleted. Metering is currently **advisory** — depletion returns a `token_warning` in `meta` (HTTP `200`) instead; this error will be returned once enforcement is enabled. See [rate limits](../rate-limits.md). | | 500 | `internal_error` | Unexpected server error. | ## Authentication & middleware errors diff --git a/v3/guides/building-a-job.md b/v3/guides/building-a-job.md index 9c87535..f1d5098 100644 --- a/v3/guides/building-a-job.md +++ b/v3/guides/building-a-job.md @@ -8,13 +8,14 @@ response is trimmed to the fields that matter. Throughout, substitute your own `YOUR_API_KEY` and copy the ids returned by each step into the next. -> **Token costs.** Writes (`POST`/`DELETE`) cost **10 tokens** each; reads -> (`GET`) cost **1**. This walkthrough spends roughly **44 tokens** (4 writes + -> 4 reads) out of the default 10,000-token bucket. Read `meta.token_count` on -> each response to track your balance, and see +> **Token costs.** Each call deducts tokens from your bucket, and the cost +> **scales with how much data it reads or writes** — writes and broad reads cost +> more than a small single read. Metering is currently advisory (a depleted +> bucket is not blocked; `meta` carries a `token_warning`). Read +> `meta.token_count` on each response to track your balance, and see > [Rate limits & the token bucket](../rate-limits.md) for pacing guidance. -## 1. Create a job — 10 tokens +## 1. Create a job `POST /jobs` requires a `name` and a `model`. You can attach flat `metadata` at creation time. @@ -33,13 +34,13 @@ curl -X POST "https://katapultpro.com/api/v3/jobs?api_key=YOUR_API_KEY" \ { "status": "success", "data": { "id": "-O_jobAbc123", "name": "API Walkthrough Job" }, - "meta": { "token_count": 9990, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99974, "last_refill_time": 1718450000000 } } ``` Save the job id (`-O_jobAbc123`) — every remaining request is scoped to it. -## 2. Add two nodes — 10 tokens each +## 2. Add two nodes `POST /jobs/{job_id}/nodes` requires `latitude` and `longitude`. Use `add_attributes` to set flat attributes such as the node type. We will create @@ -61,7 +62,7 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/nodes?api_key=YOU { "status": "success", "data": { "id": "-O_nodeA1", "latitude": 42.8864, "longitude": -78.8784 }, - "meta": { "token_count": 9980, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99949, "last_refill_time": 1718450000000 } } ``` @@ -81,13 +82,13 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/nodes?api_key=YOU { "status": "success", "data": { "id": "-O_nodeB2", "latitude": 42.8870, "longitude": -78.8770 }, - "meta": { "token_count": 9970, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99926, "last_refill_time": 1718450000000 } } ``` Note the two node ids: `-O_nodeA1` and `-O_nodeB2`. -## 3. Connect the nodes — 10 tokens +## 3. Connect the nodes A connection (span) joins two nodes. `POST /jobs/{job_id}/connections` requires `node_id_1` and `node_id_2`. @@ -110,13 +111,13 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/connections?api_k "node_id_1": "-O_nodeA1", "node_id_2": "-O_nodeB2" }, - "meta": { "token_count": 9960, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99903, "last_refill_time": 1718450000000 } } ``` The connection id is `-O_connXY`. -## 4. Add a midpoint section — 10 tokens +## 4. Add a midpoint section Sections are midspan points on a connection. Pass `make_midpoint: true` to place one at the connection's midpoint without supplying coordinates yourself. @@ -138,13 +139,13 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/connections/-O_co { "status": "success", "data": { "id": "-O_sectionM", "latitude": 42.8867, "longitude": -78.8777 }, - "meta": { "token_count": 9950, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99882, "last_refill_time": 1718450000000 } } ``` The section id is `-O_sectionM`, placed at the computed midpoint. -## 5. Read it all back — 1 token each +## 5. Read it all back ### List the nodes @@ -159,7 +160,7 @@ curl "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/nodes?api_key=YOUR_API_KE { "id": "-O_nodeA1", "latitude": 42.8864, "longitude": -78.8784 }, { "id": "-O_nodeB2", "latitude": 42.8870, "longitude": -78.8770 } ], - "meta": { "token_count": 9949, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99880, "last_refill_time": 1718450000000 } } ``` @@ -183,7 +184,7 @@ curl "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/connections/-O_connXY?api "-O_sectionM": { "latitude": 42.8867, "longitude": -78.8777 } } }, - "meta": { "token_count": 9948, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99878, "last_refill_time": 1718450000000 } } ``` @@ -199,7 +200,7 @@ curl "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/connections/-O_connXY/sec "data": [ { "id": "-O_sectionM", "latitude": 42.8867, "longitude": -78.8777 } ], - "meta": { "token_count": 9947, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99877, "last_refill_time": 1718450000000 } } ``` diff --git a/v3/guides/getting-started.md b/v3/guides/getting-started.md index 98ae149..6727cd6 100644 --- a/v3/guides/getting-started.md +++ b/v3/guides/getting-started.md @@ -20,7 +20,7 @@ key. ## 2. Make the welcome call The welcome endpoint (`GET /`) is the quickest way to confirm a key -authenticates. It costs **0 tokens**, so you can call it freely. +authenticates. It is not charged against your token bucket, so you can call it freely. ```sh curl "https://katapultpro.com/api/v3?api_key=YOUR_API_KEY" @@ -57,7 +57,7 @@ curl "https://katapultpro.com/api/v3/users/whoami?api_key=YOUR_API_KEY" "user_group": "-O_companyXyz", "root_company": "-O_rootCompany" }, - "meta": { "token_count": 9999, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99999, "last_refill_time": 1718450000000 } } ``` @@ -77,7 +77,7 @@ curl "https://katapultpro.com/api/v3/jobs?api_key=YOUR_API_KEY" "data": [ { "id": "-O_jobAbc123", "status": "active", "metadata": { "city": "Buffalo" } } ], - "meta": { "token_count": 9999, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99999, "last_refill_time": 1718450000000 } } ``` @@ -90,7 +90,7 @@ Every response from a route handler — both successes **and** errors — includ `meta` object describing your token-bucket state: ```json -"meta": { "token_count": 9999, "last_refill_time": 1718450000000 } +"meta": { "token_count": 99999, "last_refill_time": 1718450000000 } ``` - `token_count` — tokens remaining **after** this request. @@ -98,9 +98,11 @@ Every response from a route handler — both successes **and** errors — includ bucket refills 60 seconds later, at `last_refill_time + 60000`. Read `meta` on every response and let it drive your client's pacing rather than -hardcoding limits. Token costs and the bucket size are current values and are -subject to change. If you exhaust the bucket you receive HTTP `429` with -`type: "token_rate_limit_exceeded"`; compute how long to wait from `meta`: +hardcoding limits. Per-call costs scale with how much data a call reads or +writes, and both costs and the bucket size are subject to change. Token metering +is currently advisory — if you exhaust the bucket the request is still served +but `meta` carries a `token_warning`; compute how long to wait for a refill from +`meta`: ``` wait_ms = max(0, (last_refill_time + 60000) - now) @@ -110,8 +112,8 @@ The only response shape that omits `meta` is a middleware-level failure (a bad key, or the general rate limit — at most 1 call per 50 ms), because those are rejected before a route handler runs. -See [Rate limits & the token bucket](../rate-limits.md) for the full token-cost -table, refill behavior, and client guidance. +See [Rate limits & the token bucket](../rate-limits.md) for average token costs, +refill behavior, and client guidance. ## Where to go next diff --git a/v3/guides/working-with-photos.md b/v3/guides/working-with-photos.md index 919966a..373d59f 100644 --- a/v3/guides/working-with-photos.md +++ b/v3/guides/working-with-photos.md @@ -10,12 +10,14 @@ Substitute your own `YOUR_API_KEY` and ids throughout. The ids below (`-O_jobAbc123`, `-O_nodeA1`, `-O_connXY`, `-O_sectionM`) carry over from the [Building a job](building-a-job.md) walkthrough. -> **Token costs.** Photo uploads and other writes (`POST`/`DELETE`) cost **10 -> tokens**; reads (`GET`), including the signed-URL endpoint, cost **1**. Read +> **Token costs.** Each call deducts tokens from your bucket, and the cost +> **scales with how much data it reads or writes** — a photo upload moves far +> more data than a small read, so it costs more. Metering is currently advisory +> (a depleted bucket is not blocked; `meta` carries a `token_warning`). Read > `meta.token_count` on each response and see > [Rate limits & the token bucket](../rate-limits.md) for pacing. -## 1. Upload a photo to a node — 10 tokens +## 1. Upload a photo to a node `POST /jobs/{job_id}/nodes/{node_id}/photos` uploads the **raw JPEG bytes** with `Content-Type: image/jpeg` and associates the photo to the node in one call. The @@ -32,14 +34,14 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/nodes/-O_nodeA1/p { "status": "success", "data": { "id": "-O_photo01" }, - "meta": { "token_count": 9990, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99961, "last_refill_time": 1718450000000 } } ``` The upload returns the new photo id (`-O_photo01`). The response is just the id — the image bytes are stored separately and retrieved via the URL endpoint below. -## 2. Get a download URL (7-day signed) — 1 token +## 2. Get a download URL (7-day signed) `GET /jobs/{job_id}/photos/{photo_id}/url` returns a signed URL for downloading the image. **The URL expires 7 days after it is generated**, so fetch it when @@ -56,11 +58,11 @@ curl "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/photos/-O_photo01/url?api "data": { "url": "https://storage.googleapis.com/katapultpro-photos/...&Expires=1719054800&Signature=..." }, - "meta": { "token_count": 9989, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99959, "last_refill_time": 1718450000000 } } ``` -## 3. Add a photo element — 10 tokens +## 3. Add a photo element Photo elements are annotations placed on a photo (an attachment, a measurement, a marked point, …). `POST /jobs/{job_id}/photos/{photo_id}/photo_elements` @@ -82,14 +84,14 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/photos/-O_photo01 { "status": "success", "data": { "id": "-O_elem01" }, - "meta": { "token_count": 9979, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99934, "last_refill_time": 1718450000000 } } ``` > `element_type` can only be set when the element is created. Later updates via > `POST .../photo_elements/{element_id}` cannot change it. -## 4. Add a calibration anchor — 10 tokens +## 4. Add a calibration anchor Calibration anchors are reference points (a pixel location plus a known height) that let Katapult Pro scale measurements on a photo. `POST @@ -109,7 +111,7 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/photos/-O_photo01 { "status": "success", "data": { "id": "-O_anchor01" }, - "meta": { "token_count": 9969, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99911, "last_refill_time": 1718450000000 } } ``` @@ -119,7 +121,7 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/photos/-O_photo01 > Pro** — calibration does not recompute over the API. Plan for photos to read > as uncalibrated between an anchor write and the next in-app view. -## 5. Associate and unassociate a photo — 10 tokens each +## 5. Associate and unassociate a photo A photo can be uploaded to the job on its own (`POST /jobs/{job_id}/photos`) and later attached to an item, or moved between items, with `POST @@ -145,7 +147,7 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/photos/-O_photo01 { "status": "success", "data": {}, - "meta": { "token_count": 9959, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99888, "last_refill_time": 1718450000000 } } ``` @@ -165,7 +167,7 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/photos/-O_photo01 { "status": "success", "data": {}, - "meta": { "token_count": 9949, "last_refill_time": 1718450000000 } + "meta": { "token_count": 99865, "last_refill_time": 1718450000000 } } ``` @@ -184,5 +186,5 @@ curl -X POST "https://katapultpro.com/api/v3/jobs/-O_jobAbc123/photos/-O_photo01 endpoint contracts. - [Building a job](building-a-job.md) — create the job, nodes, connection, and section these photos attach to. -- [Rate limits & the token bucket](../rate-limits.md) — uploads cost 10 tokens - each; pace bulk photo imports accordingly. +- [Rate limits & the token bucket](../rate-limits.md) — photo uploads are among + the most data-heavy calls, so pace bulk photo imports accordingly. diff --git a/v3/openapi.yaml b/v3/openapi.yaml index b2fb2db..362c8c2 100644 --- a/v3/openapi.yaml +++ b/v3/openapi.yaml @@ -6,8 +6,8 @@ # `npm run docs:gen` at the repo root). Edit endpoints here, then regenerate — # do not hand-edit the generated table blocks. # -# Token costs are encoded per operation with the `x-token-cost` extension. -# See `../rate-limits.md` for how token costs and the bucket work. +# Average token costs are encoded per operation with the `x-average-token-cost` +# extension. See `../rate-limits.md` for how token costs and the bucket work. # ============================================================================= openapi: 3.1.0 info: @@ -79,7 +79,7 @@ paths: Returns a welcome message. Useful as a quick check that your API key authenticates successfully. This route is not charged tokens, but it is still subject to authentication and the general rate limit. - x-token-cost: 0 + x-average-token-cost: 0 responses: '200': description: Welcome message. @@ -104,7 +104,7 @@ paths: (most recent first); each entry includes a `last_updated` timestamp when one is available. Use `last_updated` to fetch only jobs changed since a given time, and `limit` to cap the result count. - x-token-cost: 1 + x-average-token-cost: 2014 parameters: - name: includeArchived in: query @@ -140,7 +140,7 @@ paths: tags: [Jobs] operationId: createJob summary: Create a job - x-token-cost: 10 + x-average-token-cost: 17 requestBody: required: true content: @@ -162,7 +162,7 @@ paths: operationId: getJob summary: Get a job (partial data) description: Gets partial job data for the specified job. - x-token-cost: 1 + x-average-token-cost: 3 parameters: - name: paths in: query @@ -183,7 +183,7 @@ paths: tags: [Jobs] operationId: updateJob summary: Update a job - x-token-cost: 10 + x-average-token-cost: 49 requestBody: required: true content: @@ -205,7 +205,7 @@ paths: operationId: writeRawJobData summary: Raw job write x-restricted: true - x-token-cost: 10 + x-average-token-cost: 1 description: > **Restricted — requires extended API access, which is disabled in production.** Writes raw path/value data directly to a job. Body keys @@ -245,7 +245,7 @@ paths: tags: [Jobs] operationId: getJobStatus summary: Get job status - x-token-cost: 1 + x-average-token-cost: 1 responses: '200': description: The job status. @@ -262,7 +262,7 @@ paths: operationId: updateJobStatus summary: Update job status description: Updates a job's status. Set `status` to `archived` to archive the job, or `active` to restore an archived job. - x-token-cost: 10 + x-average-token-cost: 1 requestBody: required: true content: @@ -296,7 +296,7 @@ paths: descending (most recent first); each node includes a `last_updated` timestamp when one is available. Use `last_updated` to fetch only nodes changed since a given time, and `limit` to cap the result count. - x-token-cost: 1 + x-average-token-cost: 138 parameters: - $ref: '#/components/parameters/LastUpdated' - $ref: '#/components/parameters/ResultLimit' @@ -317,7 +317,7 @@ paths: tags: [Nodes] operationId: createNode summary: Create a node - x-token-cost: 10 + x-average-token-cost: 15 requestBody: required: true content: @@ -339,7 +339,7 @@ paths: tags: [Nodes] operationId: getNode summary: Get a node - x-token-cost: 1 + x-average-token-cost: 1 responses: '200': description: The node. @@ -355,7 +355,7 @@ paths: Updates the node. If the node does not exist, it is created with the specified id (must meet the resource ID requirements) unless `onlyIfExists=true`. - x-token-cost: 10 + x-average-token-cost: 40 parameters: - $ref: '#/components/parameters/OnlyIfExists' requestBody: @@ -374,7 +374,7 @@ paths: tags: [Nodes] operationId: deleteNode summary: Delete a node - x-token-cost: 10 + x-average-token-cost: 4933 responses: '200': description: The id of the deleted node. @@ -392,7 +392,7 @@ paths: operationId: uploadPhotoToNode summary: Upload a photo to a node description: Uploads a JPEG and associates it to the specified node. - x-token-cost: 10 + x-average-token-cost: 5 parameters: - $ref: '#/components/parameters/AssociationValueQuery' requestBody: @@ -416,7 +416,7 @@ paths: operationId: listConnections summary: Get all connections description: Gets all connections (and their sections) in the job. - x-token-cost: 1 + x-average-token-cost: 127 responses: '200': description: Array of connections. @@ -434,7 +434,7 @@ paths: tags: [Connections] operationId: createConnection summary: Create a connection - x-token-cost: 10 + x-average-token-cost: 17 requestBody: required: true content: @@ -457,7 +457,7 @@ paths: operationId: getConnection summary: Get a connection description: Gets the connection and its sections. - x-token-cost: 1 + x-average-token-cost: 1 responses: '200': description: The connection. @@ -469,7 +469,7 @@ paths: tags: [Connections] operationId: updateConnection summary: Update a connection - x-token-cost: 10 + x-average-token-cost: 18 parameters: - $ref: '#/components/parameters/OnlyIfExists' requestBody: @@ -489,7 +489,7 @@ paths: operationId: deleteConnection summary: Delete a connection description: Deletes the connection and all of its sections. - x-token-cost: 10 + x-average-token-cost: 2566 responses: '200': description: The id of the deleted connection. @@ -506,7 +506,7 @@ paths: tags: [Sections] operationId: listSections summary: Get all sections on a connection - x-token-cost: 1 + x-average-token-cost: 1 responses: '200': description: Array of sections. @@ -524,7 +524,7 @@ paths: tags: [Sections] operationId: createSection summary: Create a section - x-token-cost: 10 + x-average-token-cost: 17 requestBody: required: true content: @@ -547,7 +547,7 @@ paths: tags: [Sections] operationId: getSection summary: Get a section - x-token-cost: 1 + x-average-token-cost: 1 responses: '200': description: The section. @@ -559,7 +559,7 @@ paths: tags: [Sections] operationId: updateSection summary: Update a section - x-token-cost: 10 + x-average-token-cost: 16 parameters: - $ref: '#/components/parameters/OnlyIfExists' requestBody: @@ -578,7 +578,7 @@ paths: tags: [Sections] operationId: deleteSection summary: Delete a section - x-token-cost: 10 + x-average-token-cost: 5920 responses: '200': description: The id of the deleted section. @@ -597,7 +597,7 @@ paths: operationId: uploadPhotoToSection summary: Upload a photo to a section description: Uploads a JPEG and associates it to the specified section. - x-token-cost: 10 + x-average-token-cost: 3 parameters: - $ref: '#/components/parameters/AssociationValueQuery' requestBody: @@ -620,7 +620,7 @@ paths: tags: [Photos] operationId: listPhotos summary: Get all photos - x-token-cost: 1 + x-average-token-cost: 1147 responses: '200': description: Array of photo records. @@ -639,7 +639,7 @@ paths: operationId: uploadPhoto summary: Upload a photo description: Uploads a JPEG to the job (without associating it to an item). - x-token-cost: 10 + x-average-token-cost: 1 requestBody: required: true content: @@ -662,7 +662,7 @@ paths: operationId: getPhoto summary: Get a photo description: Gets the photo record (metadata, not the image bytes — use the `/url` endpoint for a download link). - x-token-cost: 1 + x-average-token-cost: 2 responses: '200': description: The photo record. @@ -686,7 +686,7 @@ paths: description: > Returns a signed URL for downloading the photo image. The URL expires 7 days after it is generated. - x-token-cost: 1 + x-average-token-cost: 4 parameters: - name: file_size in: query @@ -723,7 +723,7 @@ paths: Associates the photo to (or unassociates it from) a node or a section. Provide `node_id` for a node, or both `connection_id` and `section_id` for a section. Set `association_value` to `null` to unassociate. - x-token-cost: 10 + x-average-token-cost: 1286 requestBody: required: true content: @@ -745,7 +745,7 @@ paths: tags: [Photo Elements] operationId: listPhotoElements summary: Get all elements on a photo - x-token-cost: 1 + x-average-token-cost: 4 responses: '200': description: Array of photo elements. @@ -763,7 +763,7 @@ paths: tags: [Photo Elements] operationId: createPhotoElement summary: Create a photo element - x-token-cost: 10 + x-average-token-cost: 515 requestBody: required: true content: @@ -786,7 +786,7 @@ paths: tags: [Photo Elements] operationId: getPhotoElement summary: Get a photo element - x-token-cost: 1 + x-average-token-cost: 7 responses: '200': description: The photo element. @@ -801,7 +801,7 @@ paths: description: > Updates the element. If no element with the id exists, one is created (unless `onlyIfExists=true`). `element_type` may only be set on create. - x-token-cost: 10 + x-average-token-cost: 994 parameters: - $ref: '#/components/parameters/OnlyIfExists' requestBody: @@ -820,7 +820,7 @@ paths: tags: [Photo Elements] operationId: deletePhotoElement summary: Delete a photo element - x-token-cost: 10 + x-average-token-cost: 933 responses: '200': description: The id of the deleted element. @@ -837,7 +837,7 @@ paths: tags: [Calibration Anchors] operationId: listCalibrationAnchors summary: Get all calibration anchors on a photo - x-token-cost: 1 + x-average-token-cost: 4 responses: '200': description: Array of calibration anchors. @@ -859,7 +859,7 @@ paths: Adding or modifying anchor calibration points always un-calibrates the photo (removes `stick_align`). Viewing the photo in Katapult Pro recalibrates it from the updated points. - x-token-cost: 10 + x-average-token-cost: 435 requestBody: required: true content: @@ -882,7 +882,7 @@ paths: tags: [Calibration Anchors] operationId: getCalibrationAnchor summary: Get a calibration anchor - x-token-cost: 1 + x-average-token-cost: 1 responses: '200': description: The calibration anchor. @@ -894,7 +894,7 @@ paths: tags: [Calibration Anchors] operationId: updateCalibrationAnchor summary: Update a calibration anchor - x-token-cost: 10 + x-average-token-cost: 2691 parameters: - $ref: '#/components/parameters/OnlyIfExists' requestBody: @@ -913,7 +913,7 @@ paths: tags: [Calibration Anchors] operationId: deleteCalibrationAnchor summary: Delete a calibration anchor - x-token-cost: 10 + x-average-token-cost: 685 responses: '200': description: The id of the deleted anchor. @@ -933,7 +933,7 @@ paths: tags: [Traces] operationId: listTraces summary: Get all traces - x-token-cost: 1 + x-average-token-cost: 175 responses: '200': description: Array of traces. @@ -951,7 +951,7 @@ paths: tags: [Traces] operationId: createTrace summary: Create a trace - x-token-cost: 10 + x-average-token-cost: 5802 requestBody: required: true content: @@ -973,7 +973,7 @@ paths: tags: [Traces] operationId: getTrace summary: Get a trace - x-token-cost: 1 + x-average-token-cost: 2 responses: '200': description: The trace. @@ -985,7 +985,7 @@ paths: tags: [Traces] operationId: updateTrace summary: Update a trace - x-token-cost: 10 + x-average-token-cost: 7272 parameters: - $ref: '#/components/parameters/OnlyIfExists' requestBody: @@ -1004,7 +1004,7 @@ paths: tags: [Traces] operationId: deleteTrace summary: Delete a trace - x-token-cost: 10 + x-average-token-cost: 5902 responses: '200': description: The id of the deleted trace. @@ -1019,7 +1019,7 @@ paths: operationId: listModels summary: Get model options description: Returns the caller's available model keys/options. - x-token-cost: 100 + x-average-token-cost: 2 responses: '200': description: Model options. @@ -1041,12 +1041,13 @@ paths: summary: Get model data description: > Returns model data. With `paths`, only the requested sub-paths are - returned and the request costs 1000 tokens per path (maximum 10 paths). - With no `paths`, the full model is returned — this requires extended API - access (otherwise 403 `extended_access_required`) and, since extended - access is off in production, is unavailable on katapultpro.com; request - `paths` instead. See `../rate-limits.md` and `../reference/restricted.md`. - x-token-cost: "9900 (full) | 1000 per path (max 10)" + returned (maximum 10 paths), so the request's cost scales with how much + data those paths contain. With no `paths`, the full model is returned — + this requires extended API access (otherwise 403 + `extended_access_required`) and, since extended access is off in + production, is unavailable on katapultpro.com; request `paths` instead. + See `../rate-limits.md` and `../reference/restricted.md`. + x-average-token-cost: 394 parameters: - name: paths in: query @@ -1066,7 +1067,7 @@ paths: operationId: listUsers summary: List all users description: Lists all users (including external users) in the caller's active company. Requires the caller to be a **company admin**. - x-token-cost: 1 + x-average-token-cost: 186 responses: '200': description: Array of users. @@ -1087,7 +1088,7 @@ paths: operationId: whoAmI summary: Get current user description: Returns the authenticated caller's own identity — the `uid`, `email`, `user_group` (the caller's active company), and `root_company` tied to the API key. Available to any authenticated caller; no admin required. Useful for confirming which account an API key belongs to. - x-token-cost: 1 + x-average-token-cost: 1 responses: '200': description: The caller's identity. @@ -1108,7 +1109,7 @@ paths: operationId: getUser summary: Get a user description: Returns a user's core fields plus their attributes (`metadata`). You can always read your **own** record; reading **another user's** record requires the caller to be a **company admin**. - x-token-cost: 1 + x-average-token-cost: 1 responses: '200': description: The user. @@ -1129,7 +1130,7 @@ paths: operationId: getUserActiveState summary: Get user active state x-restricted: true - x-token-cost: 1 + x-average-token-cost: 1 description: > Returns the active state of a specific user, including their last updated timestamp, the source of activity (`client` or `api`), the @@ -1155,7 +1156,7 @@ paths: operationId: setUserActiveState summary: Set user active state x-restricted: true - x-token-cost: 10 + x-average-token-cost: 1 description: > Sets the active state for a specific user. Sets `source` to `api` and `last_updated` to the current server time. Returns the new active state. @@ -1205,7 +1206,7 @@ paths: newest first (ordered by `timestamp` descending). All filters are optional and combine with AND. `action_id`/`action_name` come from the action models endpoint; `uid` is the User ID shown in the API Key widget. - x-token-cost: 1 + x-average-token-cost: 1 parameters: - name: uid in: query @@ -1264,7 +1265,7 @@ paths: Returns your company's action-tracking configuration: the action groups and the individual actions within each. Use the returned `action_id` / `action_name` values to filter the tracked actions endpoint. - x-token-cost: 1 + x-average-token-cost: 1 responses: '200': description: Array of action groups. @@ -1375,15 +1376,24 @@ components: schemas: Meta: type: object - description: Token-bucket state after the request. + description: >- + Token-bucket state after the request. Token accounting is currently + advisory: a depleted bucket is not blocked (the request is still + served), so treat these values as guidance rather than a hard limit. properties: token_count: type: integer description: >- - Tokens remaining after the request. For callers with extended API - access, this is always the fixed placeholder value `9999999999` - (their bucket is reset to full on every call, so it never - meaningfully depletes) rather than a literal remaining count. + Tokens remaining after the request. When the bucket is depleted + this is `0` and the response also carries `token_warning`, but the + request is still served. Extended-access callers are unthrottled, + so their count is informational only. + token_warning: + type: string + description: >- + Present only when the token bucket is depleted. Advises that usage + should be reduced. Because accounting is currently advisory, the + request was still served rather than rejected. last_refill_time: type: integer description: Epoch milliseconds of the last token-bucket refill. @@ -1437,6 +1447,7 @@ components: - not_found - forbidden - token_rate_limit_exceeded + - server_busy - extended_access_required - internal_error meta: { $ref: '#/components/schemas/Meta' } diff --git a/v3/rate-limits.md b/v3/rate-limits.md index e3af8b3..b88e490 100644 --- a/v3/rate-limits.md +++ b/v3/rate-limits.md @@ -31,39 +31,40 @@ Keep a small delay (≥ 50 ms) between sequential calls. ## Token system Each API key has its own **bucket** of tokens. Every call deducts a token cost -from the bucket. When the bucket cannot cover a call's cost, that call is -blocked with HTTP `429` until the bucket refills. +from the bucket, and the cost **scales with how much data the call consumes** — larger reads and bigger writes cost more (every call costs at least +1 token). How it works: - Each API key has an independent bucket. Limits are **per API key**. -- The bucket holds a default of **10,000 tokens**. +- The bucket holds a default of **100,000 tokens**. - The bucket **refills to full** once **60 seconds** have elapsed since its last refill. (It is a full reset, not a gradual drip.) - Every response from a route handler includes a [`meta`](#reading-your-token-state) object with your current token state. -### Token costs +> **Token metering is currently advisory.** A depleted bucket is **not blocked +> today** — the call still succeeds and the response carries a +> [`token_warning`](#when-your-bucket-is-depleted). This metering will become +> **enforced** in the future, so build clients that respect the balance now +> rather than relying on requests going through once the bucket is empty. -Costs are assigned per call; calls that touch more data cost more. **Costs and -the bucket size are current values and are subject to change** — do not hardcode -them. Read your remaining balance from each response's `meta` instead (see -[below](#reading-your-token-state)). +### Average token costs -| Call | Token cost | -| --- | --- | -| Welcome (`GET /`) | 0 | -| Any `GET` (jobs, nodes, photos, traces, users, …) | 1 | -| Any `POST` or `DELETE` (jobs, nodes, photos, …) | 10 | -| `GET /models` (model options) | 100 | -| `GET /models/{modelKey}` with `paths` | 1,000 per path (max 10 paths) | -| `GET /models/{modelKey}` without `paths` (full model) | 9,900 — extended access only (see note) | +Because a call's cost depends on the volume of data it touches, there is no +single fixed price per endpoint. Instead, each endpoint publishes an **average +token cost** in its [endpoint reference](reference/) (the *Average token cost* +column). -> A full model fetch (no `paths`) **requires [extended API access](reference/restricted.md)**, -> which is off in production — so on `katapultpro.com` it returns `403` -> `extended_access_required`. Request specific `paths` (1,000 each) instead. -> When permitted, a full fetch costs **9,900** tokens — nearly the entire -> default bucket in a single call. +> **Averages are guidance, not guarantees.** Each endpoint's published *Average +> token cost* is a typical figure — a given call can cost more or less than the +> average depending on how much data it moves, and **costs and the bucket size +> are subject to change**. Read your remaining balance from each response's +> `meta` instead of hardcoding numbers (see [below](#reading-your-token-state)). + +As a rule of thumb: reads that return more data, and writes that change more +data, cost more. A broad list or a large write is far more expensive than a +single small read. ### Reading your token state @@ -74,73 +75,84 @@ Every route-handler response (success **and** error) includes a `meta` object: "status": "success", "data": { }, "meta": { - "token_count": 9990, + "token_count": 99990, "last_refill_time": 1718450000000 } } ``` -- `token_count` — tokens remaining **after** this request. For callers with - [extended API access](reference/restricted.md#extended-api-access), this is - always the fixed placeholder integer `9999999999`, since their bucket never - meaningfully depletes. +- `token_count` — tokens remaining **after** this request. - `last_refill_time` — epoch milliseconds of the bucket's last refill. +- `token_warning` — present **only** when the bucket is depleted; see + [below](#when-your-bucket-is-depleted). Your bucket refills 60 seconds after `last_refill_time`, so the next refill is at `last_refill_time + 60000`. -> `token_count` is always a plain integer. Extended API access callers simply -> see the fixed placeholder `9999999999` rather than a real remaining count. +> **Extended API access callers are unthrottled.** Their calls are not metered +> against a bucket, so `token_count` is informational only and their traffic is +> never limited. See [extended API access](reference/restricted.md#extended-api-access). -### When you run out of tokens +### When your bucket is depleted -A blocked call returns HTTP `429` with the standard error envelope: +Token metering is **advisory today**: a depleted bucket does **not** block the +call. The request is still served, `token_count` reports `0`, and the response +`meta` carries a `token_warning`: ```json { - "status": "error", - "message": "Token rate limit exceeded for API key starting with: abcd1234", - "type": "token_rate_limit_exceeded", - "meta": { "token_count": 0, "last_refill_time": 1718450000000 } + "status": "success", + "data": { }, + "meta": { + "token_count": 0, + "last_refill_time": 1718450000000, + "token_warning": "Token bucket is depleted for this API key. This request was not limited, but usage should be reduced." + } } ``` -**There is no `Retry-After` header.** Compute the wait yourself from `meta`: +Treat the warning as a signal to **slow down**: pause until your next refill +(`last_refill_time + 60000`) before continuing heavy work. When metering becomes +enforced, calls made against an empty bucket will instead be rejected with HTTP +`429` (`token_rate_limit_exceeded`), so writing your client to back off on the +warning now keeps it working later. -``` -wait_ms = max(0, (last_refill_time + 60000) - now) -``` +> **`server_busy` (HTTP `429`).** Independently of your own bucket, the API may +> shed load during periods of very high database pressure, returning `429` with +> type `server_busy` and a `Retry-After` header. Back off for the advertised +> interval and retry. -## Managing refills in your client +## Managing your token budget in your client -Because token costs vary and the bucket size may change (see +Because per-call costs vary with data volume and the bucket size may change (see [below](#a-note-on-changing-limits)), build clients that adapt at runtime rather than assuming fixed numbers: 1. **Read `meta` on every response.** Track `token_count` and `last_refill_time`. -2. **Look before you leap on expensive calls.** Before a costly call (e.g. a full - model fetch at 9,900), check that `token_count` covers it; if not, wait until +2. **Look before you leap on expensive calls.** Before a call you expect to be + costly (a broad list, a large write, a full model fetch), check that + `token_count` has comfortable headroom; if not, wait until `last_refill_time + 60000`. -3. **Back off on `429`.** On `token_rate_limit_exceeded`, sleep until the next - refill (`last_refill_time + 60000`), then retry. Since there is no - `Retry-After`, use exponential backoff **with jitter** as a fallback if you - don't have a fresh `meta`. +3. **Heed the warning.** On a `token_warning` (or a future + `token_rate_limit_exceeded`), sleep until the next refill + (`last_refill_time + 60000`), then continue. Use exponential backoff **with + jitter** as a fallback if you don't have a fresh `meta`. 4. **Respect the general limit too.** Keep ≥ 50 ms between calls regardless of token balance. -5. **Spread big jobs across minutes.** With a 10,000-token bucket refilling each - minute, a bulk write of N entities (10 tokens each) needs roughly - `ceil(N / 1000)` minutes. Pace the work accordingly. +5. **Spread big jobs across minutes.** With a bucket that refills each minute, + size each minute's work to the balance you observe in `meta` — do fewer, or + smaller, operations per window rather than draining the bucket in a burst. ### A note on changing limits -The token bucket is being made **dynamic**: the effective bucket size may be -**reduced during periods of high database load**. This system is still being -finalized, so this page intentionally avoids promising fixed capacity over time. +The token bucket is **dynamic**: the effective bucket size may be **reduced +during periods of high database load**, and server owners can configure the +default bucket size. Practical implication: **treat the bucket size, refill cadence, and per-call costs as values you read at runtime, not constants.** A client that keys off the `meta.token_count` it actually receives will keep working as these limits -evolve; one that hardcodes "10,000 per minute" may not. +evolve; one that hardcodes a capacity may not. ### Handling a smaller-than-expected refill @@ -157,16 +169,17 @@ your delays: ```text state: lastRefillSeen = 0, budgetThisWindow = 0 -on each response (success OR 429), read response.meta: +on each response (success OR warning), read response.meta: refill = meta.last_refill_time count = meta.token_count if refill != lastRefillSeen: # a new refill window started lastRefillSeen = refill - budgetThisWindow = count # plan against ACTUAL capacity, not 10,000 + budgetThisWindow = count # plan against ACTUAL capacity observed # scale the work attempted this minute to budgetThisWindow, e.g.: - # maxWritesThisWindow = floor(budgetThisWindow / 10) # POST/DELETE cost 10 + # estimate each operation's cost from recent meta deltas (costs vary by + # data volume), then cap operations so their total stays under budget. # if budgetThisWindow is small -> lower concurrency / raise inter-call delay - # before each call: if budgetThisWindow < callCost, wait until + # before an expensive call: if budgetThisWindow looks too low, wait until # (lastRefillSeen + 60000), then re-read meta from the next response. ``` @@ -174,11 +187,20 @@ Key rules: - **Recompute, don't assume.** Derive each window's budget from the observed `token_count`, never from a hardcoded default. -- **Adapt downward gracefully.** If a refill is smaller, do less this minute - rather than bursting into `429`s. +- **Measure cost from `meta` deltas.** Since costs scale with data volume, infer + a call's real cost from how much `token_count` dropped, not from a fixed table. +- **Adapt downward gracefully.** If a refill is smaller, do less this minute. - **Don't carry size across runs.** A schedule computed yesterday against a larger bucket may overshoot today — recompute from live `meta` every run. +## Monthly token pool + +Beyond the per-minute bucket, sustained API usage also draws on a **monthly pool +tied to your company's subscription** (**1,000,000 tokens per license**). +This is a separate, longer-horizon ceiling on total usage. Company admins are +notified as the pool nears its limit. The bucket state above governs minute-to-minute pacing, while +the monthly pool bounds heavy sustained use over a billing period. + ## Peak hours & overnight batch jobs Run large or token-heavy jobs — bulk creates/updates/deletes, full model @@ -189,15 +211,17 @@ team's timezone). This helps in three ways: responsive for everyone (be a good citizen). - **Avoids contention with interactive use** — your script won't compete with your team actively working in Katapult Pro. -- **Larger effective capacity off-peak** — under the forthcoming dynamic-bucket - system, low-load windows will generally afford more headroom than peak hours. +- **Larger effective capacity off-peak** — under the dynamic-bucket system, + low-load windows will generally afford more headroom than peak hours. Additional tips: -- Use `GET /models/{modelKey}?paths=…` (1,000/path) — a full model fetch - (9,900) requires extended access and is unavailable in production. +- Use `GET /models/{modelKey}?paths=…` (path-scoped, max 10) rather than a full + model fetch — a full fetch returns the entire model (far more data, far more + costly) and requires extended access, so it is unavailable in production. - Use query parameters that narrow results (e.g. `paths` on `GET /jobs/{job_id}`, - `metadataFilter` on `GET /jobs`) to reduce calls and data. + `metadataFilter` on `GET /jobs`) to reduce calls and data — which now also + reduces token cost. - Cache rarely-changing data (like model definitions) instead of re-fetching. - Batch and pace writes to stay within the per-minute budget rather than - bursting into `429`s. + bursting into warnings. diff --git a/v3/reference/calibration-anchors.md b/v3/reference/calibration-anchors.md index 7fe6352..5b5ff97 100644 --- a/v3/reference/calibration-anchors.md +++ b/v3/reference/calibration-anchors.md @@ -9,13 +9,13 @@ Photo calibration anchor points. -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors`](#get-all-calibration-anchors-on-a-photo) | 1 | Get all calibration anchors on a photo | -| `POST` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors`](#create-a-calibration-anchor) | 10 | Create a calibration anchor | +| `GET` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors`](#get-all-calibration-anchors-on-a-photo) | 4 | Get all calibration anchors on a photo | +| `POST` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors`](#create-a-calibration-anchor) | 435 | Create a calibration anchor | | `GET` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id}`](#get-a-calibration-anchor) | 1 | Get a calibration anchor | -| `POST` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id}`](#update-a-calibration-anchor) | 10 | Update a calibration anchor | -| `DELETE` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id}`](#delete-a-calibration-anchor) | 10 | Delete a calibration anchor | +| `POST` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id}`](#update-a-calibration-anchor) | 2691 | Update a calibration anchor | +| `DELETE` | [`/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id}`](#delete-a-calibration-anchor) | 685 | Delete a calibration anchor | ### Get all calibration anchors on a photo @@ -23,7 +23,7 @@ Photo calibration anchor points. GET https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/calibration_anchors ``` -**Token cost:** 1 +**Average token cost:** 4 Path parameters: @@ -38,7 +38,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/calibration_anchors ``` -**Token cost:** 10 +**Average token cost:** 435 Adding or modifying anchor calibration points always un-calibrates the photo (removes `stick_align`). Viewing the photo in Katapult Pro recalibrates it from the updated points. @@ -62,7 +62,7 @@ Body fields: GET https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id} ``` -**Token cost:** 1 +**Average token cost:** 1 Path parameters: @@ -78,7 +78,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id} ``` -**Token cost:** 10 +**Average token cost:** 2691 Path parameters: @@ -107,7 +107,7 @@ Body fields: DELETE https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/calibration_anchors/{anchor_id} ``` -**Token cost:** 10 +**Average token cost:** 685 Path parameters: diff --git a/v3/reference/companies.md b/v3/reference/companies.md index 079009e..15aa4d4 100644 --- a/v3/reference/companies.md +++ b/v3/reference/companies.md @@ -31,7 +31,7 @@ records that predate source tracking. -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | | `GET` | [`/companies/{company_id}/tracked_actions`](#list-tracked-actions) | 1 | List tracked actions | | `GET` | [`/companies/{company_id}/action_models`](#list-action-models) | 1 | List action models | @@ -42,7 +42,7 @@ records that predate source tracking. GET https://katapultpro.com/api/v3/companies/{company_id}/tracked_actions ``` -**Token cost:** 1 +**Average token cost:** 1 Returns a paginated list of tracked-action records for your company, newest first (ordered by `timestamp` descending). All filters are optional and combine with AND. `action_id`/`action_name` come from the action models endpoint; `uid` is the User ID shown in the API Key widget. @@ -71,7 +71,7 @@ Query parameters: GET https://katapultpro.com/api/v3/companies/{company_id}/action_models ``` -**Token cost:** 1 +**Average token cost:** 1 Returns your company's action-tracking configuration: the action groups and the individual actions within each. Use the returned `action_id` / `action_name` values to filter the tracked actions endpoint. diff --git a/v3/reference/connections.md b/v3/reference/connections.md index 3f823df..98cd882 100644 --- a/v3/reference/connections.md +++ b/v3/reference/connections.md @@ -5,13 +5,13 @@ Connections (spans) between nodes. -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/connections`](#get-all-connections) | 1 | Get all connections | -| `POST` | [`/jobs/{job_id}/connections`](#create-a-connection) | 10 | Create a connection | +| `GET` | [`/jobs/{job_id}/connections`](#get-all-connections) | 127 | Get all connections | +| `POST` | [`/jobs/{job_id}/connections`](#create-a-connection) | 17 | Create a connection | | `GET` | [`/jobs/{job_id}/connections/{connection_id}`](#get-a-connection) | 1 | Get a connection | -| `POST` | [`/jobs/{job_id}/connections/{connection_id}`](#update-a-connection) | 10 | Update a connection | -| `DELETE` | [`/jobs/{job_id}/connections/{connection_id}`](#delete-a-connection) | 10 | Delete a connection | +| `POST` | [`/jobs/{job_id}/connections/{connection_id}`](#update-a-connection) | 18 | Update a connection | +| `DELETE` | [`/jobs/{job_id}/connections/{connection_id}`](#delete-a-connection) | 2566 | Delete a connection | ### Get all connections @@ -19,7 +19,7 @@ Connections (spans) between nodes. GET https://katapultpro.com/api/v3/jobs/{job_id}/connections ``` -**Token cost:** 1 +**Average token cost:** 127 Gets all connections (and their sections) in the job. @@ -35,7 +35,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/connections ``` -**Token cost:** 10 +**Average token cost:** 17 Path parameters: @@ -59,7 +59,7 @@ Body fields: GET https://katapultpro.com/api/v3/jobs/{job_id}/connections/{connection_id} ``` -**Token cost:** 1 +**Average token cost:** 1 Gets the connection and its sections. @@ -76,7 +76,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/connections/{connection_id} ``` -**Token cost:** 10 +**Average token cost:** 18 Path parameters: @@ -108,7 +108,7 @@ Body fields: DELETE https://katapultpro.com/api/v3/jobs/{job_id}/connections/{connection_id} ``` -**Token cost:** 10 +**Average token cost:** 2566 Deletes the connection and all of its sections. diff --git a/v3/reference/jobs.md b/v3/reference/jobs.md index c81b269..8f6500d 100644 --- a/v3/reference/jobs.md +++ b/v3/reference/jobs.md @@ -5,15 +5,15 @@ Create, read, update, and archive jobs. -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs`](#list-all-jobs) | 1 | List all jobs | -| `POST` | [`/jobs`](#create-a-job) | 10 | Create a job | -| `GET` | [`/jobs/{job_id}`](#get-a-job-partial-data) | 1 | Get a job (partial data) | -| `POST` | [`/jobs/{job_id}`](#update-a-job) | 10 | Update a job | -| `POST` | [`/jobs/{job_id}/raw`](#raw-job-write) 🔒 | 10 | Raw job write | +| `GET` | [`/jobs`](#list-all-jobs) | 2014 | List all jobs | +| `POST` | [`/jobs`](#create-a-job) | 17 | Create a job | +| `GET` | [`/jobs/{job_id}`](#get-a-job-partial-data) | 3 | Get a job (partial data) | +| `POST` | [`/jobs/{job_id}`](#update-a-job) | 49 | Update a job | +| `POST` | [`/jobs/{job_id}/raw`](#raw-job-write) 🔒 | 1 | Raw job write | | `GET` | [`/jobs/{job_id}/status`](#get-job-status) | 1 | Get job status | -| `POST` | [`/jobs/{job_id}/status`](#update-job-status) | 10 | Update job status | +| `POST` | [`/jobs/{job_id}/status`](#update-job-status) | 1 | Update job status | ### List all jobs @@ -21,7 +21,7 @@ Create, read, update, and archive jobs. GET https://katapultpro.com/api/v3/jobs ``` -**Token cost:** 1 +**Average token cost:** 2014 Gets a list of jobs accessible to the requester. List entries do not contain full job data. Results are ordered by `last_updated` descending (most recent first); each entry includes a `last_updated` timestamp when one is available. Use `last_updated` to fetch only jobs changed since a given time, and `limit` to cap the result count. @@ -41,7 +41,7 @@ Query parameters: POST https://katapultpro.com/api/v3/jobs ``` -**Token cost:** 10 +**Average token cost:** 17 Body fields: @@ -59,7 +59,7 @@ Body fields: GET https://katapultpro.com/api/v3/jobs/{job_id} ``` -**Token cost:** 1 +**Average token cost:** 3 Gets partial job data for the specified job. @@ -81,7 +81,7 @@ Query parameters: POST https://katapultpro.com/api/v3/jobs/{job_id} ``` -**Token cost:** 10 +**Average token cost:** 49 Path parameters: @@ -107,7 +107,7 @@ Body fields: POST https://katapultpro.com/api/v3/jobs/{job_id}/raw ``` -**Token cost:** 10 +**Average token cost:** 1 **Restricted — requires extended API access, which is disabled in production.** Writes raw path/value data directly to a job. Body keys are job-relative paths (e.g. `nodes/{node_id}` or `nodes` for a full replace); values are written as-is, with `null` deleting a path. Allowed top-level keys: `nodes`, `connections`, `photos`, `photo_summary`, `files`, `traces`, `compatible_units`, `warning_reports`. Reserved keys (`metadata`, `name`, `model`, `map_styles`, `sharing`) must use the non-raw update endpoint. See `../reference/restricted.md`. @@ -125,7 +125,7 @@ Request body: free-form JSON object (see endpoint description). GET https://katapultpro.com/api/v3/jobs/{job_id}/status ``` -**Token cost:** 1 +**Average token cost:** 1 Path parameters: @@ -139,7 +139,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/status ``` -**Token cost:** 10 +**Average token cost:** 1 Updates a job's status. Set `status` to `archived` to archive the job, or `active` to restore an archived job. diff --git a/v3/reference/models.md b/v3/reference/models.md index 64676aa..64471e7 100644 --- a/v3/reference/models.md +++ b/v3/reference/models.md @@ -7,9 +7,9 @@ Read model options and model data. > [`extended_access_required`](../concepts/responses-and-errors.md) without it, > and since [extended access](restricted.md) is **off in production**, a full > fetch is **unavailable on `katapultpro.com`**. Request specific `paths` -> (1,000 tokens each, max 10) instead — this is how most integrations read model -> data. A full fetch (when permitted) costs **9,900 tokens**. See -> [Rate limits & the token bucket](../rate-limits.md#token-costs). +> (max 10) instead — this is how most integrations read model data. A full fetch +> returns the entire model, so it costs far more than a path-scoped fetch. See +> [Rate limits & the token bucket](../rate-limits.md#average-token-costs). Full-model vs `paths` fetches differ in both behavior and cost — see [Complex & codependent parameters](../concepts/complex-parameters.md#fetching-model-data-full-vs-paths). @@ -17,10 +17,10 @@ Full-model vs `paths` fetches differ in both behavior and cost — see -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/models`](#get-model-options) | 100 | Get model options | -| `GET` | [`/models/{modelKey}`](#get-model-data) | 9900 (full) \| 1000 per path (max 10) | Get model data | +| `GET` | [`/models`](#get-model-options) | 2 | Get model options | +| `GET` | [`/models/{modelKey}`](#get-model-data) | 394 | Get model data | ### Get model options @@ -28,7 +28,7 @@ Full-model vs `paths` fetches differ in both behavior and cost — see GET https://katapultpro.com/api/v3/models ``` -**Token cost:** 100 +**Average token cost:** 2 Returns the caller's available model keys/options. @@ -38,9 +38,9 @@ Returns the caller's available model keys/options. GET https://katapultpro.com/api/v3/models/{modelKey} ``` -**Token cost:** 9900 (full) \| 1000 per path (max 10) +**Average token cost:** 394 -Returns model data. With `paths`, only the requested sub-paths are returned and the request costs 1000 tokens per path (maximum 10 paths). With no `paths`, the full model is returned — this requires extended API access (otherwise 403 `extended_access_required`) and, since extended access is off in production, is unavailable on katapultpro.com; request `paths` instead. See `../rate-limits.md` and `../reference/restricted.md`. +Returns model data. With `paths`, only the requested sub-paths are returned (maximum 10 paths), so the request's cost scales with how much data those paths contain. With no `paths`, the full model is returned — this requires extended API access (otherwise 403 `extended_access_required`) and, since extended access is off in production, is unavailable on katapultpro.com; request `paths` instead. See `../rate-limits.md` and `../reference/restricted.md`. Path parameters: diff --git a/v3/reference/nodes.md b/v3/reference/nodes.md index 49a3e43..9cc46e1 100644 --- a/v3/reference/nodes.md +++ b/v3/reference/nodes.md @@ -5,14 +5,14 @@ Nodes within a job. -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/nodes`](#get-all-nodes) | 1 | Get all nodes | -| `POST` | [`/jobs/{job_id}/nodes`](#create-a-node) | 10 | Create a node | +| `GET` | [`/jobs/{job_id}/nodes`](#get-all-nodes) | 138 | Get all nodes | +| `POST` | [`/jobs/{job_id}/nodes`](#create-a-node) | 15 | Create a node | | `GET` | [`/jobs/{job_id}/nodes/{node_id}`](#get-a-node) | 1 | Get a node | -| `POST` | [`/jobs/{job_id}/nodes/{node_id}`](#update-a-node) | 10 | Update a node | -| `DELETE` | [`/jobs/{job_id}/nodes/{node_id}`](#delete-a-node) | 10 | Delete a node | -| `POST` | [`/jobs/{job_id}/nodes/{node_id}/photos`](#upload-a-photo-to-a-node) | 10 | Upload a photo to a node | +| `POST` | [`/jobs/{job_id}/nodes/{node_id}`](#update-a-node) | 40 | Update a node | +| `DELETE` | [`/jobs/{job_id}/nodes/{node_id}`](#delete-a-node) | 4933 | Delete a node | +| `POST` | [`/jobs/{job_id}/nodes/{node_id}/photos`](#upload-a-photo-to-a-node) | 5 | Upload a photo to a node | ### Get all nodes @@ -20,7 +20,7 @@ Nodes within a job. GET https://katapultpro.com/api/v3/jobs/{job_id}/nodes ``` -**Token cost:** 1 +**Average token cost:** 138 Gets all nodes in the job. Results are ordered by `last_updated` descending (most recent first); each node includes a `last_updated` timestamp when one is available. Use `last_updated` to fetch only nodes changed since a given time, and `limit` to cap the result count. @@ -43,7 +43,7 @@ Query parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/nodes ``` -**Token cost:** 10 +**Average token cost:** 15 Path parameters: @@ -66,7 +66,7 @@ Body fields: GET https://katapultpro.com/api/v3/jobs/{job_id}/nodes/{node_id} ``` -**Token cost:** 1 +**Average token cost:** 1 Path parameters: @@ -81,7 +81,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/nodes/{node_id} ``` -**Token cost:** 10 +**Average token cost:** 40 Updates the node. If the node does not exist, it is created with the specified id (must meet the resource ID requirements) unless `onlyIfExists=true`. @@ -114,7 +114,7 @@ Body fields: DELETE https://katapultpro.com/api/v3/jobs/{job_id}/nodes/{node_id} ``` -**Token cost:** 10 +**Average token cost:** 4933 Path parameters: @@ -129,7 +129,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/nodes/{node_id}/photos ``` -**Token cost:** 10 +**Average token cost:** 5 Uploads a JPEG and associates it to the specified node. diff --git a/v3/reference/photo-elements.md b/v3/reference/photo-elements.md index 088a8e1..33d13cc 100644 --- a/v3/reference/photo-elements.md +++ b/v3/reference/photo-elements.md @@ -11,13 +11,13 @@ Element attributes are stored as a flat map directly on the element — see -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements`](#get-all-elements-on-a-photo) | 1 | Get all elements on a photo | -| `POST` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements`](#create-a-photo-element) | 10 | Create a photo element | -| `GET` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](#get-a-photo-element) | 1 | Get a photo element | -| `POST` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](#update-a-photo-element) | 10 | Update a photo element | -| `DELETE` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](#delete-a-photo-element) | 10 | Delete a photo element | +| `GET` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements`](#get-all-elements-on-a-photo) | 4 | Get all elements on a photo | +| `POST` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements`](#create-a-photo-element) | 515 | Create a photo element | +| `GET` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](#get-a-photo-element) | 7 | Get a photo element | +| `POST` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](#update-a-photo-element) | 994 | Update a photo element | +| `DELETE` | [`/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id}`](#delete-a-photo-element) | 933 | Delete a photo element | ### Get all elements on a photo @@ -25,7 +25,7 @@ Element attributes are stored as a flat map directly on the element — see GET https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/photo_elements ``` -**Token cost:** 1 +**Average token cost:** 4 Path parameters: @@ -40,7 +40,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/photo_elements ``` -**Token cost:** 10 +**Average token cost:** 515 Path parameters: @@ -66,7 +66,7 @@ Body fields: GET https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id} ``` -**Token cost:** 1 +**Average token cost:** 7 Path parameters: @@ -82,7 +82,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id} ``` -**Token cost:** 10 +**Average token cost:** 994 Updates the element. If no element with the id exists, one is created (unless `onlyIfExists=true`). `element_type` may only be set on create. @@ -117,7 +117,7 @@ Body fields: DELETE https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/photo_elements/{element_id} ``` -**Token cost:** 10 +**Average token cost:** 933 Path parameters: diff --git a/v3/reference/photos.md b/v3/reference/photos.md index ab54797..d686b07 100644 --- a/v3/reference/photos.md +++ b/v3/reference/photos.md @@ -13,13 +13,13 @@ section; accepted `association_value`s) — see -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/photos`](#get-all-photos) | 1 | Get all photos | -| `POST` | [`/jobs/{job_id}/photos`](#upload-a-photo) | 10 | Upload a photo | -| `GET` | [`/jobs/{job_id}/photos/{photo_id}`](#get-a-photo) | 1 | Get a photo | -| `GET` | [`/jobs/{job_id}/photos/{photo_id}/url`](#get-a-photo-download-url) | 1 | Get a photo download URL | -| `POST` | [`/jobs/{job_id}/photos/{photo_id}/associate`](#associate-a-photo-to-an-item) | 10 | Associate a photo to an item | +| `GET` | [`/jobs/{job_id}/photos`](#get-all-photos) | 1147 | Get all photos | +| `POST` | [`/jobs/{job_id}/photos`](#upload-a-photo) | 1 | Upload a photo | +| `GET` | [`/jobs/{job_id}/photos/{photo_id}`](#get-a-photo) | 2 | Get a photo | +| `GET` | [`/jobs/{job_id}/photos/{photo_id}/url`](#get-a-photo-download-url) | 4 | Get a photo download URL | +| `POST` | [`/jobs/{job_id}/photos/{photo_id}/associate`](#associate-a-photo-to-an-item) | 1286 | Associate a photo to an item | ### Get all photos @@ -27,7 +27,7 @@ section; accepted `association_value`s) — see GET https://katapultpro.com/api/v3/jobs/{job_id}/photos ``` -**Token cost:** 1 +**Average token cost:** 1147 Path parameters: @@ -41,7 +41,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/photos ``` -**Token cost:** 10 +**Average token cost:** 1 Uploads a JPEG to the job (without associating it to an item). @@ -59,7 +59,7 @@ Request body: raw `image/jpeg` bytes. GET https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id} ``` -**Token cost:** 1 +**Average token cost:** 2 Gets the photo record (metadata, not the image bytes — use the `/url` endpoint for a download link). @@ -76,7 +76,7 @@ Path parameters: GET https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/url ``` -**Token cost:** 1 +**Average token cost:** 4 Returns a signed URL for downloading the photo image. The URL expires 7 days after it is generated. @@ -100,7 +100,7 @@ Query parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/photos/{photo_id}/associate ``` -**Token cost:** 10 +**Average token cost:** 1286 Associates the photo to (or unassociates it from) a node or a section. Provide `node_id` for a node, or both `connection_id` and `section_id` for a section. Set `association_value` to `null` to unassociate. diff --git a/v3/reference/restricted.md b/v3/reference/restricted.md index 43fddfc..0d53989 100644 --- a/v3/reference/restricted.md +++ b/v3/reference/restricted.md @@ -7,12 +7,10 @@ call them. Attempting to without extended access returns HTTP `403` with type ## Extended API access -Extended API access exempts calls from token costs — instead of deducting the -call's cost, the bucket is reset to full on every call, so it never depletes — -and unlocks the raw write endpoint and full model fetches (see below). Every -response's `meta.token_count` reports the fixed placeholder integer -`9999999999` instead of a real remaining count for these callers. It is -granted only when **both** are true: +Extended API access exempts calls from token metering — these callers are +**unthrottled** and their traffic is never limited — and unlocks the raw write +endpoint and full model fetches (see below). Their `meta.token_count` is +informational only. It is granted only when **both** are true: - The caller's company has the `can_grant_extended_api_access` feature enabled. - The caller's user has the `extended_api_access` role. @@ -67,7 +65,7 @@ supported way to read model data. Because extended access is off in production, request `?paths=...` on `katapultpro.com`. See [Models](models.md) and -[Rate limits & the token bucket](../rate-limits.md#token-costs). +[Rate limits & the token bucket](../rate-limits.md#average-token-costs). ## User active state @@ -80,7 +78,7 @@ POST https://katapultpro.com/api/v3/users/{user_id}/active_state ### GET — Get user active state -**Token cost:** 1 +**Average token cost:** 1 Returns the active state of a specific user. The response `data` is `null` if no active state has been recorded, otherwise an object with: @@ -96,7 +94,7 @@ Returns `404` (`not_found`) if `user_id` does not exist. ### POST — Set user active state -**Token cost:** 10 +**Average token cost:** 1 Sets the active state for a specific user. `source` is always set to `"api"` and `last_updated` is set to the current server time. diff --git a/v3/reference/sections.md b/v3/reference/sections.md index 4995ea8..45ed50d 100644 --- a/v3/reference/sections.md +++ b/v3/reference/sections.md @@ -11,14 +11,14 @@ a section — see [Complex & codependent parameters](../concepts/complex-paramet -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | | `GET` | [`/jobs/{job_id}/connections/{connection_id}/sections`](#get-all-sections-on-a-connection) | 1 | Get all sections on a connection | -| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections`](#create-a-section) | 10 | Create a section | +| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections`](#create-a-section) | 17 | Create a section | | `GET` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}`](#get-a-section) | 1 | Get a section | -| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}`](#update-a-section) | 10 | Update a section | -| `DELETE` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}`](#delete-a-section) | 10 | Delete a section | -| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}/photos`](#upload-a-photo-to-a-section) | 10 | Upload a photo to a section | +| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}`](#update-a-section) | 16 | Update a section | +| `DELETE` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}`](#delete-a-section) | 5920 | Delete a section | +| `POST` | [`/jobs/{job_id}/connections/{connection_id}/sections/{section_id}/photos`](#upload-a-photo-to-a-section) | 3 | Upload a photo to a section | ### Get all sections on a connection @@ -26,7 +26,7 @@ a section — see [Complex & codependent parameters](../concepts/complex-paramet GET https://katapultpro.com/api/v3/jobs/{job_id}/connections/{connection_id}/sections ``` -**Token cost:** 1 +**Average token cost:** 1 Path parameters: @@ -41,7 +41,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/connections/{connection_id}/sections ``` -**Token cost:** 10 +**Average token cost:** 17 Path parameters: @@ -66,7 +66,7 @@ Body fields: GET https://katapultpro.com/api/v3/jobs/{job_id}/connections/{connection_id}/sections/{section_id} ``` -**Token cost:** 1 +**Average token cost:** 1 Path parameters: @@ -82,7 +82,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/connections/{connection_id}/sections/{section_id} ``` -**Token cost:** 10 +**Average token cost:** 16 Path parameters: @@ -114,7 +114,7 @@ Body fields: DELETE https://katapultpro.com/api/v3/jobs/{job_id}/connections/{connection_id}/sections/{section_id} ``` -**Token cost:** 10 +**Average token cost:** 5920 Path parameters: @@ -130,7 +130,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/connections/{connection_id}/sections/{section_id}/photos ``` -**Token cost:** 10 +**Average token cost:** 3 Uploads a JPEG and associates it to the specified section. diff --git a/v3/reference/traces.md b/v3/reference/traces.md index 8702263..8286536 100644 --- a/v3/reference/traces.md +++ b/v3/reference/traces.md @@ -8,13 +8,13 @@ Trace attributes are stored as a flat map directly on the trace — see -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/jobs/{job_id}/traces`](#get-all-traces) | 1 | Get all traces | -| `POST` | [`/jobs/{job_id}/traces`](#create-a-trace) | 10 | Create a trace | -| `GET` | [`/jobs/{job_id}/traces/{trace_id}`](#get-a-trace) | 1 | Get a trace | -| `POST` | [`/jobs/{job_id}/traces/{trace_id}`](#update-a-trace) | 10 | Update a trace | -| `DELETE` | [`/jobs/{job_id}/traces/{trace_id}`](#delete-a-trace) | 10 | Delete a trace | +| `GET` | [`/jobs/{job_id}/traces`](#get-all-traces) | 175 | Get all traces | +| `POST` | [`/jobs/{job_id}/traces`](#create-a-trace) | 5802 | Create a trace | +| `GET` | [`/jobs/{job_id}/traces/{trace_id}`](#get-a-trace) | 2 | Get a trace | +| `POST` | [`/jobs/{job_id}/traces/{trace_id}`](#update-a-trace) | 7272 | Update a trace | +| `DELETE` | [`/jobs/{job_id}/traces/{trace_id}`](#delete-a-trace) | 5902 | Delete a trace | ### Get all traces @@ -22,7 +22,7 @@ Trace attributes are stored as a flat map directly on the trace — see GET https://katapultpro.com/api/v3/jobs/{job_id}/traces ``` -**Token cost:** 1 +**Average token cost:** 175 Path parameters: @@ -36,7 +36,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/traces ``` -**Token cost:** 10 +**Average token cost:** 5802 Path parameters: @@ -57,7 +57,7 @@ Body fields: GET https://katapultpro.com/api/v3/jobs/{job_id}/traces/{trace_id} ``` -**Token cost:** 1 +**Average token cost:** 2 Path parameters: @@ -72,7 +72,7 @@ Path parameters: POST https://katapultpro.com/api/v3/jobs/{job_id}/traces/{trace_id} ``` -**Token cost:** 10 +**Average token cost:** 7272 Path parameters: @@ -100,7 +100,7 @@ Body fields: DELETE https://katapultpro.com/api/v3/jobs/{job_id}/traces/{trace_id} ``` -**Token cost:** 10 +**Average token cost:** 5902 Path parameters: diff --git a/v3/reference/users.md b/v3/reference/users.md index 9c70b2c..125d1f6 100644 --- a/v3/reference/users.md +++ b/v3/reference/users.md @@ -6,13 +6,13 @@ record require **company admin**; you can always read your own record. -| Method | Endpoint | Token cost | Description | +| Method | Endpoint | Average token cost | Description | | --- | --- | --- | --- | -| `GET` | [`/users`](#list-all-users) | 1 | List all users | +| `GET` | [`/users`](#list-all-users) | 186 | List all users | | `GET` | [`/users/whoami`](#get-current-user) | 1 | Get current user | | `GET` | [`/users/{user_id}`](#get-a-user) | 1 | Get a user | | `GET` | [`/users/{user_id}/active_state`](#get-user-active-state) 🔒 | 1 | Get user active state | -| `POST` | [`/users/{user_id}/active_state`](#set-user-active-state) 🔒 | 10 | Set user active state | +| `POST` | [`/users/{user_id}/active_state`](#set-user-active-state) 🔒 | 1 | Set user active state | ### List all users @@ -20,7 +20,7 @@ record require **company admin**; you can always read your own record. GET https://katapultpro.com/api/v3/users ``` -**Token cost:** 1 +**Average token cost:** 186 Lists all users (including external users) in the caller's active company. Requires the caller to be a **company admin**. @@ -30,7 +30,7 @@ Lists all users (including external users) in the caller's active company. Requi GET https://katapultpro.com/api/v3/users/whoami ``` -**Token cost:** 1 +**Average token cost:** 1 Returns the authenticated caller's own identity — the `uid`, `email`, `user_group` (the caller's active company), and `root_company` tied to the API key. Available to any authenticated caller; no admin required. Useful for confirming which account an API key belongs to. @@ -40,7 +40,7 @@ Returns the authenticated caller's own identity — the `uid`, `email`, `user_gr GET https://katapultpro.com/api/v3/users/{user_id} ``` -**Token cost:** 1 +**Average token cost:** 1 Returns a user's core fields plus their attributes (`metadata`). You can always read your **own** record; reading **another user's** record requires the caller to be a **company admin**. @@ -58,7 +58,7 @@ Path parameters: GET https://katapultpro.com/api/v3/users/{user_id}/active_state ``` -**Token cost:** 1 +**Average token cost:** 1 Returns the active state of a specific user, including their last updated timestamp, the source of activity (`client` or `api`), the path of the last activity, and the page (`map` or `photos`). Returns `null` if no active state has been recorded. Requires extended API access and the `enable_api_user_state_calls` feature flag. @@ -76,7 +76,7 @@ Path parameters: POST https://katapultpro.com/api/v3/users/{user_id}/active_state ``` -**Token cost:** 10 +**Average token cost:** 1 Sets the active state for a specific user. Sets `source` to `api` and `last_updated` to the current server time. Returns the new active state. Requires extended API access and the `enable_api_user_state_calls` feature flag. The caller must either be the user being updated or a company admin (otherwise 403 `forbidden`); the target user must exist (otherwise 404 `not_found`). Note that the target user's client only visually applies the new state if they have opted in to `allow_api_active_state` in their account settings — the first API-driven change prompts them to accept or deny.