{"openapi":"3.1.0","info":{"title":"Roar AI API","version":"1.0.0","description":"One key, two surfaces. The **gateway** (`/v1/*`) is OpenAI-compatible — point any OpenAI SDK at it. The **management API** (`/client/*`) lets an agent provision keys, read spend, and deploy apps. Both authenticate with a `roar_live_…` key sent as `Authorization: Bearer <key>`. A key carries scopes (inference, keys:read, keys:write, usage:read, requests:read, apps:read, apps:deploy, data:read, projects:write, webhooks:read, webhooks:write, mcp:read, mcp:write); each `/client` route requires a specific one. There is also a hosted MCP server at `POST /mcp` exposing the same operations as tools."},"externalDocs":{"description":"Guides and the interactive reference","url":"https://roar-ai.com/docs"},"servers":[{"url":"https://api.roar-ai.com"}],"security":[{"BearerAuth":[]}],"tags":[{"name":"Gateway","description":"OpenAI-compatible inference"},{"name":"Keys","description":"API key management"},{"name":"Usage","description":"Usage, cost, and budgets"},{"name":"Apps","description":"Hosted app deploys"},{"name":"Projects","description":"Grouping + budgets"},{"name":"App data","description":"An app’s managed database and its restore points"},{"name":"Account","description":"Profile, onboarding and account-wide reporting"},{"name":"Webhooks","description":"Outbound event notifications"},{"name":"Tool gateway","description":"Your own tool servers (MCP), which of their tools your agents may use, and who decides. Agents call the tools at `POST /mcp/gateway` with a key a bundle was given to"}],"paths":{"/v1/models":{"get":{"tags":["Gateway"],"summary":"List available models","description":"Models reachable through the gateway, with their modalities and capabilities. Requires the `inference` scope.\n\n**Calling from a browser.** Requests carrying an `Origin` header are refused with a 403 unless they also send `x-roar-dangerous-browser-access: true`. Your API key belongs on your server: put it in a web page and every visitor can read it, and there is no way to un-leak it afterwards. Send the header only when that is genuinely what you want — an internal tool, or an app where each person supplies their own key. Server-side calls send no `Origin` and are unaffected.","responses":{"200":{"description":"A list of models","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelList"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Called from a browser without `x-roar-dangerous-browser-access: true`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/models/{model}":{"get":{"tags":["Gateway"],"summary":"Retrieve one model","description":"One entry of `GET /v1/models`, by id — what OpenAI SDKs call for `models.retrieve(id)`. A model this key cannot use is `404 model_not_found`, exactly as the list omits it. Requires the `inference` scope.\n\n**Calling from a browser.** Requests carrying an `Origin` header are refused with a 403 unless they also send `x-roar-dangerous-browser-access: true`. Your API key belongs on your server: put it in a web page and every visitor can read it, and there is no way to un-leak it afterwards. Send the header only when that is genuinely what you want — an internal tool, or an app where each person supplies their own key. Server-side calls send no `Origin` and are unaffected.","parameters":[{"name":"model","in":"path","required":true,"schema":{"type":"string"},"description":"The model id, as `GET /v1/models` lists it"}],"responses":{"200":{"description":"The model"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Called from a browser without `x-roar-dangerous-browser-access: true`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such model, or not available to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/chat/completions":{"post":{"tags":["Gateway"],"summary":"Create a chat completion","description":"OpenAI-compatible. Streaming (`\"stream\": true`) returns SSE. Routed, metered, and white-labelled. Name any model from `GET /v1/models`. Requires `inference`.\n\n**Calling from a browser.** Requests carrying an `Origin` header are refused with a 403 unless they also send `x-roar-dangerous-browser-access: true`. Your API key belongs on your server: put it in a web page and every visitor can read it, and there is no way to un-leak it afterwards. Send the header only when that is genuinely what you want — an internal tool, or an app where each person supplies their own key. Server-side calls send no `Origin` and are unaffected.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatCompletionRequest"}}}},"responses":{"200":{"description":"A chat completion (or SSE stream)"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Budget exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Called from a browser without `x-roar-dangerous-browser-access: true`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/audio/speech":{"post":{"tags":["Gateway"],"summary":"Text to speech","description":"OpenAI-compatible. Returns audio bytes with the provider's content type, not JSON. Requires `inference`.\n\n**Calling from a browser.** Requests carrying an `Origin` header are refused with a 403 unless they also send `x-roar-dangerous-browser-access: true`. Your API key belongs on your server: put it in a web page and every visitor can read it, and there is no way to un-leak it afterwards. Send the header only when that is genuinely what you want — an internal tool, or an app where each person supplies their own key. Server-side calls send no `Origin` and are unaffected.","responses":{"200":{"description":"Audio bytes","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Bad voice, format or input length","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/audio/transcriptions":{"post":{"tags":["Gateway"],"summary":"Speech to text","description":"OpenAI-compatible. `multipart/form-data` with a `file` and a `model`. Requires `inference`.","responses":{"200":{"description":"The transcript"},"400":{"description":"Bad file or model","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/responses":{"post":{"tags":["Gateway"],"summary":"Create a response","description":"OpenAI-compatible Responses API, passed through to the provider. Only models served directly by a provider that offers this endpoint can answer it — everything else returns 400 naming `/v1/chat/completions` instead. `stream: true` is supported. Of the built-in tools, `web_search` runs and is billed per search on top of tokens — the rate is on each model in `GET /client/models` (`builtinTools`), and each call shows its searches in the usage log. The other built-in tools (`file_search`, `code_interpreter`, `hosted_shell`, `image_generation`, `computer_use`) are refused. Function and custom tools work normally. Requires `inference`.","responses":{"200":{"description":"The response object"},"400":{"description":"Unsupported model, streaming, or a fee-bearing built-in tool","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/images/generations":{"post":{"tags":["Gateway"],"summary":"Generate an image","description":"OpenAI-compatible. Name an image model from `GET /v1/models` — the ones whose `roar_modalities` include image output. Requires `inference`.","responses":{"200":{"description":"The generated image(s)"},"400":{"description":"Bad prompt, size or model","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/files":{"post":{"tags":["Gateway"],"summary":"Upload a file","description":"OpenAI-compatible. `multipart/form-data`. Files are what a batch reads its requests from — upload with `purpose: batch`, then name the returned id in `POST /v1/batches`. Requires `inference`.","responses":{"200":{"description":"The stored file"},"400":{"description":"Bad upload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/files/{id}":{"get":{"tags":["Gateway"],"summary":"One file","description":"Metadata for a file you uploaded. Requires `inference`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The file"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such file","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/batches":{"get":{"tags":["Gateway"],"summary":"List batches","description":"Your batch jobs, newest first. Requires `inference`.","responses":{"200":{"description":"Batches"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Gateway"],"summary":"Create a batch","description":"OpenAI-compatible. Runs many completions offline from an uploaded file, at a lower rate than live traffic. Only models whose `roar_batch` is true are accepted — check `GET /v1/models`. Requires `inference`.","responses":{"200":{"description":"The queued batch"},"400":{"description":"Bad file, or a model that does not accept batches","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/batches/{id}":{"get":{"tags":["Gateway"],"summary":"One batch","description":"Status and counts for a batch. THIS is the polling endpoint, and it spends the same request-rate allowance as inference — a client that polls hard has less of its minute left for model calls, which is the trade-off to make knowingly. Requires `inference`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The batch"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such batch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/batches/{id}/cancel":{"post":{"tags":["Gateway"],"summary":"Cancel a batch","description":"Stops a batch that has not finished. Work already done is still billed. Requires `inference`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The cancelled batch"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such batch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/batches/{id}/content":{"get":{"tags":["Gateway"],"summary":"A finished batch's results","description":"The output file, once the batch has completed. Requires `inference`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The results"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such batch, or it has not finished","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/embeddings":{"post":{"tags":["Gateway"],"summary":"Create embeddings","description":"OpenAI-compatible vector embeddings. Requires `inference`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmbeddingsRequest"}}}},"responses":{"200":{"description":"Embedding vectors"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/evaluate":{"post":{"tags":["Gateway"],"summary":"Evaluate state against typed questions","description":"Evaluation models: shared `state` plus typed `questions` in, calibrated probabilities out. A `boolean` answers with a probability, a `choice` with the selected option and a probability per option, a `score` with an interpolated value and a probability per rung. Several questions are answered in one round trip against the same state. NOT an OpenAI-compatible shape — there is no `messages` and no completion. Billed on input tokens. Requires `inference`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EvaluateRequest"}}}},"responses":{"200":{"description":"An answer per question key"},"400":{"description":"Malformed question, or a model that is not an evaluation model","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/keys":{"get":{"tags":["Keys"],"summary":"List API keys","description":"Never returns key plaintext. Requires `keys:read`.","responses":{"200":{"description":"The account's keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyList"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks keys:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Keys"],"summary":"Create an API key","description":"Returns the plaintext key ONCE. Granting management scopes requires an owner session or a key that already holds them. A key cannot create a key in a project whose tool bundles run with full access (403) — its tools would run without approval; an owner or admin in the console can. Requires `keys:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}}},"responses":{"201":{"description":"The created key (plaintext shown once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedKey"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks keys:write / cannot escalate scopes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get one key","description":"Full key detail: settings, spend windows (total/today/week/month), and budget status. Never returns key plaintext. Requires `keys:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Key detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyDetail"}}}},"404":{"description":"No such key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Keys"],"summary":"Update a key","description":"Rename, move project, change scopes, pause/resume (`disabled`), and — owner only — set the spend cap or expiry (`expiresAt`). A revoked key rejects `disabled`. Moving a key to another project is a person’s act in the console (403 for a key): the tools a key may use without approval depend on its project. An app’s own key moves only with its app (409 — move the app instead). Read the key back with `GET` to see the result.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateKeyRequest"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ack"}}}},"403":{"description":"A key cannot move a key to another project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The key belongs to an app — move the app instead","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Keys"],"summary":"Revoke or delete a key","description":"Revokes by default (key stops working, stays listed). With `?permanent=true`, deletes the key permanently; usage history is kept.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"permanent","in":"query","schema":{"type":"string","enum":["true"]}}],"responses":{"200":{"description":"Revoked or deleted"},"404":{"description":"No such key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/usage":{"get":{"tags":["Usage"],"summary":"Usage summary","description":"Token counts, request volume, latency, and cost — headline totals, a per-model breakdown, and the recent requests themselves. Model calls only: tool gateway calls are reported by `GET /client/usage/tools`. Requires `usage:read`.","parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}},{"name":"limit","in":"query","schema":{"type":"integer","default":100},"description":"How many recent requests to return (max 1000)"},{"name":"keyId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Limit to one API key"}],"responses":{"200":{"description":"Usage report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageReport"}}}},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/budget":{"get":{"tags":["Usage"],"summary":"Budget status","description":"Real-time spend vs. cap per budgeted scope, and the company credit balance (`balanceUsd`). Requires `usage:read`.","responses":{"200":{"description":"Budget report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetReport"}}}},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps":{"get":{"tags":["Apps"],"summary":"List apps","description":"Hosted apps with their live URL and status. Requires `apps:read`.","responses":{"200":{"description":"Apps","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppList"}}}},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Apps"],"summary":"Deploy an app","description":"Provision + deploy from a git repo. Returns 202 with the app, its web service and the queued deployment; poll `/client/apps/{id}/progress` for the rest. Requires `apps:deploy`.\n\nThis route takes no `databaseEnabled` — a database Roar provides is `POST /client/apps/github`. To use your own, send its `DATABASE_URL` in `env`.\n\nA repository we can read and already know cannot build — several apps and no `rootDir`, or nothing we know how to build — is refused with 400 before anything is created, and the message says what to change. One we cannot tell how to START passes once `startCommand` says how (one line, at most 300 characters; an obviously unfinished command such as a bare `node` or one ending in a folder is refused). The body is strict: a field this route does not take is a 400, never silently ignored. `GET /client/github/build-check` asks the same question first.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployAppRequest"}}}},"responses":{"202":{"description":"Deploy started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppDeployAccepted"}}}},"400":{"description":"A DATABASE_URL of your own alongside a Roar database, or a repository that cannot build as it stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/templates/{template}":{"post":{"tags":["Apps"],"summary":"Launch a ready-made app","description":"Launches a ready-made app with no repository of your own — `n8n` or `open-webui`. The template fixes what is deployed and turns on a Roar database (where each keeps its data; `open-webui` also gets file storage, and your Roar models connected); you choose only the name, region, project and protection. Returns 202 exactly like `POST /client/apps`; poll `/client/apps/{id}/progress`. Requires `apps:deploy`.","parameters":[{"name":"template","in":"path","required":true,"schema":{"type":"string","enum":["n8n","open-webui"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LaunchTemplateRequest"}}}},"responses":{"202":{"description":"Deploy started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppDeployAccepted"}}}},"400":{"description":"Unknown template, or a field the route does not take","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/deployments":{"get":{"tags":["Apps"],"summary":"Deploy history","description":"The last 20 deploys of one app service, newest first. Each carries `timings` — where that deploy spent its time, phase by phase, and how big the image it produced is. `timings` is null for a deploy nothing measured (an older host), which is not the same as one whose phases were all instant.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Recent deploys, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppDeploymentList"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}":{"get":{"tags":["Apps"],"summary":"One app","description":"The app, its services, addresses and current state. `services[].id` is the SERVICE id every `/client/services/{id}/...` path below needs.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The app"},"404":{"description":"No such app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Apps"],"summary":"Delete an app","description":"Tears the app down: container, proxy site, domains and its gateway key. Its DATABASE is not deleted with it. `id` is the APP id, not a service id. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/github":{"post":{"tags":["Apps"],"summary":"Deploy from a connected GitHub repo","description":"The private-repo counterpart to `POST /client/apps`: deploys a repo reachable through your installed GitHub App. Returns 202 with the app, its web service and the queued deployment. Requires `apps:deploy`.\n\n`databaseEnabled` and a `DATABASE_URL` of your own in `env` are mutually exclusive: when Roar provides the database it sets `DATABASE_URL` itself.\n\n`deployOnPush` (boolean, default `true`) sets whether a push to `branch` deploys the app automatically.\n\nA repository we can read and already know cannot build is refused with 400 before anything is created: with several apps and no `rootDir`, the message lists the folders `rootDir` can take. A repo we cannot tell how to START is refused the same way unless `startCommand` (one line, at most 300 characters — the same rule as the Build settings start command) says how; it is stored as that build setting, and an obviously unfinished command (a bare `node`, one ending in a folder, an unclosed quote) is refused. The body is strict: a field this route does not take is a 400, never silently ignored. `GET /client/github/build-check` asks the same question first.","responses":{"202":{"description":"Deploy started"},"400":{"description":"GitHub not connected, a DATABASE_URL of your own alongside a Roar database, or a repository that cannot build as it stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/regions":{"get":{"tags":["Apps"],"summary":"Regions you can deploy into","description":"The regions available to this account, for the `cell` field on a deploy. Each row carries `available` (can it take this app now, given `database`) and `databases` (does the region offer a Roar-provisioned database at all). A region with `databases: false` runs apps with no database or with a `DATABASE_URL` of your own, and refuses `databaseEnabled` — that does not change by waiting.\n\n`database` is REQUIRED: pass `1` if the app will use a Roar-provisioned database and `0` if it will not. The answer differs — a region can host apps without being able to host their data.","parameters":[{"name":"database","in":"query","required":true,"schema":{"type":"string","enum":["0","1"]},"description":"Whether the app needs a Roar database. `1` narrows the list to regions that can host one"}],"responses":{"200":{"description":"Available regions"},"400":{"description":"`database` missing or not `0`/`1`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/usage":{"get":{"tags":["Apps"],"summary":"One app's hosting usage","description":"Memory, CPU and traffic for this app, with what it has cost. `id` is the APP id.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Usage for the app"},"404":{"description":"No such app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/env":{"get":{"tags":["Apps"],"summary":"An app's environment variables","description":"The variables your app receives. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself.\n\nValues come back only if your project role may read them — otherwise the names are returned with null values and `canReveal` is false. Pass `?reveal=0` to ask for names only whatever your role, which is what an assistant should send: a value it receives is a value in a transcript.\n\n`managed` lists the variables Roar sets itself (`ROAR_API_KEY`, `ROAR_BASE_URL`, `ROAR_MCP_URL`) — read-only, and already present without you adding them.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Variables","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppEnvView"}}}},"403":{"description":"Role may not list secrets","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Apps"],"summary":"Set environment variables","description":"Upserts one or more variables, and removes others, in ONE change: `{ \"vars\": { \"NAME\": \"value\" }, \"remove\": [\"OLD_NAME\"] }` (either may be omitted, not both). Saving restarts the app once, on the version already running, so the change is live within about a minute — send every change together rather than one request each, or the app restarts once per request. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.\n\nNames Roar manages are refused, and the refusal says what your app already has.\n\nA 409 means somebody else was saving at the same moment and NOTHING of yours was written — send the same request again. It is never a partial save.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Saved"},"400":{"description":"Reserved name, or a value the env file cannot carry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A concurrent change won; nothing was saved, retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/env/{name}":{"delete":{"tags":["Apps"],"summary":"Remove one environment variable","description":"Removed, and the app restarts once without it — not recoverable, so if the value was a key you need the original to put it back. To remove several, or remove and set together, use `remove` on the POST above: one restart instead of one each. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.\n\nA 409 means a concurrent change won and nothing was removed — send it again.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"The variable name"}],"responses":{"200":{"description":"Removed"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A concurrent change won; nothing was removed, retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/stop":{"post":{"tags":["Apps"],"summary":"Pause an app","description":"Stops the container; the app stops serving and stops costing compute. Reversible with `/start`. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Stopping"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/start":{"post":{"tags":["Apps"],"summary":"Bring a paused app back","description":"The counterpart to `/stop`. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Starting"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/rollback":{"post":{"tags":["Apps"],"summary":"Roll back to an earlier deploy","description":"Redeploys a previous build rather than rebuilding — the fast way out of a bad ship. Name the deployment from `GET /client/services/{id}/deployments`. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The queued deployment"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/build":{"get":{"tags":["Apps"],"summary":"What the builder decided about this repo","description":"The framework it detected, the commands it will run, and the root directory it resolved. **Read this before theorising about a failed deploy** — it says which branch the builder took.\n\n`envKeys` is what your repo's committed `.env.example` asks for, read from your app's ROOT DIRECTORY (so a monorepo app in `app/` is read from `app/.env.example`). `missingEnv` is the subset nothing has set — the actionable half, and the usual reason a build succeeds and the container then fails. Both are null until a deploy has recorded one; `[]` means the repo committed no example file, which is a different answer. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Build settings and detection"},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Apps"],"summary":"What the builder decided about this repository","description":"Framework, build command, start command and why each was chosen. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself.\n\nWorth calling before diagnosing a failed deploy: a styled 404 from a running app and a misdetected framework look identical from outside, and this endpoint is what tells them apart. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The build decision. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/deployments/{id}/logs":{"get":{"tags":["Apps"],"summary":"One deployment's build output","description":"The build log for a single deploy — what the image build printed, and why it failed if it did. NOT the running app's output: that is `GET /client/services/{id}/logs`. `id` is a DEPLOYMENT id, from `GET /client/services/{id}/deployments`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The build log"},"404":{"description":"No such deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/domains":{"get":{"tags":["Apps"],"summary":"An app's domains","description":"Every hostname this app answers on, with its certificate state, as `data`. `mainDomainId` is the app’s main address — the link people are given, and where its Roar address redirects — and `chosenDomainId` is the one you picked with `POST /client/domains/{id}/make-primary` (null if you never have). They differ while a chosen domain is not answering: the next live address stands in until it does. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Domains"},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Apps"],"summary":"Attach a custom domain","description":"Adds one of your own hostnames. You point DNS at us; the certificate is issued automatically once it resolves. Every record we ask for is a **CNAME** onto the app’s own Roar address, including at a zone apex — so the domain follows the app if it ever moves and you never have to edit it again. Your DNS provider must be able to put a CNAME at the root of a domain (Cloudflare, DNSimple, Namecheap and bunny.net can); if yours cannot, use `www.` as your address and forward the bare root to it at your registrar.\n\nBody: `{ \"domain\": \"shop.example.com\" }`. For a root domain you may also send `preferWww: true`, which makes `www.example.com` the address we present as yours — the other name redirects to it. Sending `www.example.com` itself does the same: it attaches `example.com` with `www` as the address. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Attached"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/domains/{id}/dns":{"get":{"tags":["Apps"],"summary":"What is left to set up for this domain","description":"The DNS records to create at your registrar, and a LIVE lookup of where the name points right now. `id` is a DOMAIN id, from `GET /client/services/{id}/domains`. Requires `apps:read`.\n\nEach record carries `type`, `name` (what a registrar’s Name/Host box wants — `@` at a zone apex), `fullName` (the complete hostname) and `value`. `check.verdict` is one of `pointed` (resolves here — nothing left to do), `missing` (nothing published at that name yet), `elsewhere` (a record exists and sends traffic somewhere else — `check.observed` says where) or `unknown` (WE could not check; not a verdict about your DNS). A Roar subdomain has no records and always answers `unknown`, because there is nothing of yours to measure.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Records to create, and what DNS says now"},"404":{"description":"No such domain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/domains/{id}/primary":{"patch":{"tags":["Apps"],"summary":"Choose which address a root domain leads with","description":"For a root domain (`example.com`), which of its two names people should use — `example.com` itself or `www.example.com`. Body: `{ \"primary\": \"www.example.com\" }`, and it must be one of those two.\n\nThe other name redirects to the one you pick, and this decides which DNS record is presented as required and which as optional. Nothing is redeployed. A subdomain has no second name and is refused. To choose which of your app’s DOMAINS is its main address, use `POST /client/domains/{id}/make-primary`. `id` is a DOMAIN id, from `GET /client/services/{id}/domains`. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The updated domain"},"400":{"description":"Not a root domain, or not one of its two names","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such domain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/domains/{id}/make-primary":{"post":{"tags":["Apps"],"summary":"Make this domain your app’s main address","description":"The main address is the link people are given for your app, and where its Roar address redirects. Your other domains keep serving the app as they are — nothing else is redirected — and your app’s `APP_URL` variable does not change. Only a live domain can be chosen. If it later stops answering, the next live address stands in until it does. No body.\n\n`id` is a DOMAIN id, from `GET /client/services/{id}/domains`. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"`{ primaryDomainId }`"},"400":{"description":"The domain is not live yet, or is not answering","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such domain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/domains/{id}/proxied":{"post":{"tags":["Apps"],"summary":"Put your app behind Cloudflare, or take it back out","description":"Serve your app’s Roar address through Cloudflare instead of resolving straight to the server. Body: `{ \"proxied\": true }` to turn it on, `false` to turn it off.\n\nIt filters bad traffic and is faster for visitors far from where your app is hosted — and slower for visitors in that same region, because their requests take a detour. Colombo pays the most for this; Singapore and Bangalore pay very little.\n\nOnly your free `*.roarai.app` address can be proxied. **A domain you own cannot**, and neither can a Roar address that one of your own domains points at — turning it on there would send your visitors to an edge that has never heard of your name. Put your own domain behind your own Cloudflare instead, which gives you your own rules. `id` is a DOMAIN id, from `GET /client/services/{id}/domains`. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The updated domain"},"400":{"description":"A domain you own, one your own domain points at, or one with no DNS target yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such domain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/domains/{id}":{"delete":{"tags":["Apps"],"summary":"Detach a domain","description":"Stops serving this hostname. `id` is a DOMAIN id, from `GET /client/services/{id}/domains`. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Detached"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such domain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/jobs":{"get":{"tags":["Apps"],"summary":"Scheduled runs for this app","description":"Jobs we call on a schedule by requesting a path on your app. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.\n\nThe response carries `secret` for a caller entitled to read it — the value your app should check the `x-roar-cron-secret` header against, so it can tell our call from anybody else's. `canReveal` says whether you were given it.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Jobs, and the shared secret if you may see it"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Apps"],"summary":"Add a scheduled run","description":"A path on your app and a schedule to call it on. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.\n\nWe wait 30 seconds for your app to answer; set `timeoutSeconds` (5 to 300; at most 110 for an app not hosted on our servers — the jobs list returns the app's own `maxTimeoutSeconds`) for a job that genuinely takes longer. Past that, answer at once and finish the work in the background.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The created job"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/jobs/{jobId}":{"patch":{"tags":["Apps"],"summary":"Change a scheduled run","description":"Edit the path, the schedule, how long we wait (`timeoutSeconds`, 5 up to the app's `maxTimeoutSeconds`; `null` for the standard 30), or turn it off. Requires `apps:deploy`.","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The updated job"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Apps"],"summary":"Remove a scheduled run","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Removed"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/jobs/{jobId}/run":{"post":{"tags":["Apps"],"summary":"Run a scheduled job now","description":"Fires the job immediately without waiting for its schedule — the way to test one. Answers with the result if your app answers within about 25 seconds; otherwise with the run as `running`, which finishes in the job's history. Requires `apps:deploy`.","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The run"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/jobs/{jobId}/runs":{"get":{"tags":["Apps"],"summary":"History of one scheduled job","description":"What each run did: when, how long, and the status your app answered with.","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Runs, newest first"},"404":{"description":"No such job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/whoami":{"get":{"tags":["Account"],"summary":"Who this key belongs to","description":"The account and project this credential acts for, and the scopes it carries. The cheapest way to check a key works.","responses":{"200":{"description":"The caller"}}}},"/client/github/member-connect":{"post":{"tags":["Account"],"summary":"Allow or stop members connecting their own GitHub","description":"Owner-only. When allowed (the default), any member who can deploy may connect a GitHub account of their own, and it ADDS to what the company can reach rather than replacing anything. Turning it off leaves existing connections in place — disconnect one explicitly to remove it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["allowed"],"properties":{"allowed":{"type":"boolean"}}}}}},"responses":{"200":{"description":"The policy now in force"},"403":{"description":"Only an owner can change this","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/repo-allow-list":{"get":{"tags":["Account"],"summary":"Which repositories this workspace may deploy from","description":"Owner-only. `restricted: false` (the default) means every repository the workspace’s GitHub connections grant; `true` means only what `mode` names — `repos` (`allowed`) or `orgs` (`allowedOrgs`, GitHub accounts, matched against the owner half of `owner/name`). Both lists are kept while the other mode is active. `candidateOrgs` is the accounts this workspace is connected to. `candidates` is what the caller can choose from — the repositories the connections grant that THIS PERSON can also see on GitHub — and is never narrowed by the list itself, so a removed repository can be added back.","responses":{"200":{"description":"The setting, what is allowed, and what can be added"},"403":{"description":"Only an owner can see this","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Account"],"summary":"Limit which repositories this workspace may deploy from","description":"Owner-only, and a replace: send the whole list. Exists because GitHub holds one repository selection per installation and several workspaces can share one, so narrowing it there narrows it for all of them. Holding the list here leaves GitHub’s selection alone. While `restricted`, a repository outside the list cannot be chosen, browsed or deployed — by a person, an API key or an operator — and a repository that is not on it is refused with a 404. `repos`/`orgs` omitted keeps the stored list. Refused (409) when an app here deploys from a repository or organisation the list omits, because that app would stop deploying; and (400) for a repository the caller cannot see on GitHub, or an organisation this workspace is not connected to.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["restricted"],"properties":{"restricted":{"type":"boolean"},"mode":{"type":"string","enum":["repos","orgs"],"description":"What the limit counts. Omitted keeps the stored mode."},"repos":{"type":"array","items":{"type":"string","description":"\"owner/name\""},"maxItems":500},"orgs":{"type":"array","items":{"type":"string","description":"A GitHub account login"},"maxItems":100}}}}}},"responses":{"200":{"description":"The setting now in force"},"400":{"description":"A name that is not owner/name, or one the caller cannot see","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Only an owner can change this","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"An app here deploys from a repository the list omits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/organizations":{"get":{"tags":["Account"],"summary":"Organisations you belong to","description":"The organisations this person is a member of. `current` is where calls act right now and is always set; `defaultOrganizationId` is what has actually been STORED and is null when nothing has been chosen — the two differ whenever the caller has not picked yet. An API key is issued inside one organisation and always acts there, so it returns an empty list with `fixed: true`.","responses":{"200":{"description":"Memberships, and the organisation in use"}}}},"/client/organizations/default":{"post":{"tags":["Account"],"summary":"Set the organisation this connection acts in","description":"Point an MCP connection at one of your own organisations, for good. Refuses an organisation you do not belong to (404) and one that is not active (400). An API key cannot be repointed — mint a key in the organisation you want instead.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["organizationId"],"properties":{"organizationId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"The organisation now in use"},"400":{"description":"Not an active organisation, or the caller is an API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/models":{"get":{"tags":["Account"],"summary":"Models available to you, with prices","description":"The catalogue as YOUR account sees it — the models you may call and what each costs. `GET /v1/models` is the OpenAI-compatible version and carries no prices.","responses":{"200":{"description":"Models with pricing"}}}},"/client/credits":{"get":{"tags":["Account"],"summary":"Credit balance","description":"What is left to spend. Requests are refused once it reaches zero, so this is the number to watch.","responses":{"200":{"description":"Balance and recent movements"}}}},"/client/credits/statement":{"get":{"tags":["Account"],"summary":"Credit statement","description":"Every movement of your balance, newest first, with the balance after each one.\n\nNarrow it with `lens`: `payments` (card top-ups, refunds and manual adjustments), `usage` (AI gateway spend) or `hosting` (app hosting). Omit it for all three interleaved.\n\n**Use `lens=payments` to find a card payment.** Gateway usage is written about once a minute while traffic flows, so an unfiltered statement on a busy account reaches back roughly an hour. `limit` defaults to 100 and caps at 500. `from` and `to` (ISO timestamps, `to` exclusive) bound it to a date range.\n\nA day of metered app-hosting charges arrives as ONE line carrying `coversUtcDay` and `coversEntries`; the per-day hosting base fee keeps its own line with a note saying what it bought.","responses":{"200":{"description":"Statement lines, newest first"}}}},"/client/credits/summary":{"get":{"tags":["Account"],"summary":"Credit statement as a ledger","description":"One period of your balance: `openingBalanceUsd`, then `paymentsUsd` (top-ups), `adjustmentsUsd` (refunds and corrections, signed), `hostingUsd` and `aiUsd` (what was spent, positive), and `closingBalanceUsd`. Opening + payments + adjustments − hosting − AI = closing, exactly.\n\n`from` and `to` are ISO timestamps, `to` exclusive; with no `from` the opening balance is zero and the period is your whole history.","responses":{"200":{"description":"The period as a ledger"}}}},"/client/requests":{"get":{"tags":["Usage"],"summary":"Gateway request log","description":"One row per model call through `/v1` (tool gateway calls are `GET /client/usage/tools/calls`): model, tokens, cost, latency, time to first token (`ttftMs`, streamed calls) and status. Narrow with `from`/`to`, `keyId`, `model` (one model, exactly), `q` (request id, or text in the model or endpoint) and `band` — `errors`, or `slow`: a streamed call whose first token took over 5s, or an unstreamed one over 30s. Keyset-paginated — pass the previous response's `nextCursor` as `cursor`. Requires `usage:read`.","responses":{"200":{"description":"Requests, newest first"},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/requests/{id}":{"get":{"tags":["Usage"],"summary":"One gateway request","description":"A single call in full. Prompt and response bodies are included only if your project role may read them and the account has request logging switched on. Requires `usage:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The request"},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/requests":{"get":{"tags":["Apps"],"summary":"HTTP requests your app served","description":"One row per request the app answered — time, method, path, status, duration. NOT the same as `/logs`, which is what the app PRINTED.\n\n`id` is a SERVICE id: take it from `services[].id` on `GET /client/apps/{appId}`.\n\nKeyset-paginated: pass the previous response's `nextCursor` as `cursor`. Filter with `since`/`until` (ISO timestamps), `band` (a status band), or `requestId` for one exact request.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Requests, newest first"},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/requests":{"get":{"tags":["Apps"],"summary":"HTTP requests across all your apps","description":"The per-app request feed (`GET /client/services/{id}/requests`) for every app you can see at once, each row naming its app. The first page also carries `summary` (the window's totals), `facets` (counts per app, status class, method and busiest path — over the window, not narrowed by the filters) and `series` (requests per hour, or per day past two days — narrowed by the filters). Filter with `since`/`until` (ISO timestamps), `band` (`errors`, `2xx`, `3xx`, `4xx`, `5xx`), `appId`, `method`, `path` (exact, as listed) or `requestId` (which searches everything held and ignores the rest). Keyset-paginated: pass `nextCursor` back as `cursor`. Requires `apps:read`.","parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"band","in":"query","schema":{"type":"string","enum":["all","errors","2xx","3xx","4xx","5xx"]}},{"name":"appId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"method","in":"query","schema":{"type":"string"},"description":"One HTTP method, e.g. POST"},{"name":"path","in":"query","schema":{"type":"string"},"description":"One path, exactly as a row shows it"},{"name":"requestId","in":"query","schema":{"type":"string"},"description":"A request id, whole or its first half"},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":500}},{"name":"cursor","in":"query","schema":{"type":"string"}},{"name":"summaryOnly","in":"query","schema":{"type":"string","enum":["1"]},"description":"The window's summary alone: no rows, facets or timeline"}],"responses":{"200":{"description":"Requests, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientAppRequestPage"}}}},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/redeploy":{"post":{"tags":["Apps"],"summary":"Redeploy an app","description":"Builds and ships the current head of the tracked branch again — the way to retry a failed deploy or pick up a change without pushing. Returns the queued deployment; poll `GET /client/apps/{appId}/progress`. Requires `apps:deploy`. `id` is a SERVICE id.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The queued deployment"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/gateway-key/replace":{"post":{"tags":["Apps"],"summary":"Replace an app's AI key","description":"Revokes the key the app runs on, gives it a new one with the same settings and tool bundles (active, even if the old one was paused), and redeploys the app so it starts using it. The old key stops working immediately. `redeployed: false` means the key was replaced but the redeploy was not started — redeploy the app to start using the new key. The new key is never returned. **A person signed in to the console only**: an API key or agent connection gets 403. Needs deploy rights on the app and key rights in its project. `id` is a SERVICE id.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The new key's id and prefix, and whether the redeploy started. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Not a signed-in person, or no rights to deploy this app or manage its keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A deploy is in progress, or the app has no key to replace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/logs":{"get":{"tags":["Apps"],"summary":"An app's own output","description":"The last tail we hold of what the app printed. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself.\n\nA SNAPSHOT, not a stream: `collectedAt` says when the node last sent it, and nothing collects on a schedule. To get current output, `POST` to the sibling below and read this again a second or two later.\n\nFalls back to the last deploy's BUILD output when the app has never sent a runtime tail — `source` says which you got, and the two are different questions.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The tail we hold","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceLogs"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/logs/refresh":{"post":{"tags":["Apps"],"summary":"Fetch a fresh tail","description":"Asks the server the app runs on for current output, then answers. It does NOT return the lines — read `GET /client/services/{id}/logs` a second or two afterwards, once the node has replied.\n\nRequires `apps:deploy`, not `apps:read`: it makes the platform reach out to a machine, which is a write however much it reads like one.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The request was queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app service","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/progress":{"get":{"tags":["Apps"],"summary":"Deploy progress","description":"Where the last deploy got to (`phase`), whether the app is running right now (`runState`), and its address. The two are separate questions: a paused app reports `phase: live` with `runState: paused`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Build/deploy progress + URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppProgress"}}}},"404":{"description":"No such app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/projects":{"get":{"tags":["Projects"],"summary":"List projects","responses":{"200":{"description":"Projects","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectList"}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","description":"Group keys and apps under a shared budget. Requires `projects:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectRequest"}}}},"responses":{"201":{"description":"The created project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedProject"}}}},"403":{"description":"Key lacks projects:write","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/webhooks":{"get":{"tags":["Webhooks"],"summary":"List webhooks","description":"Registered webhooks (never the signing secret), plus every deliverable event. Requires `webhooks:read`.","responses":{"200":{"description":"Webhooks","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookList"}}}},"403":{"description":"Key lacks webhooks:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Webhooks"],"summary":"Register a webhook","description":"Returns an HMAC signing secret ONCE. Subscribe to named events or to `[\"*\"]`. Requires `webhooks:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"}}}},"responses":{"201":{"description":"The created webhook (secret shown once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedWebhook"}}}},"400":{"description":"Invalid or unsafe URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks webhooks:write","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/webhooks/{id}":{"patch":{"tags":["Webhooks"],"summary":"Update a webhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookRequest"}}}},"responses":{"200":{"description":"The updated webhook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"}}}},"404":{"description":"No such webhook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"No such webhook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Send a test event","description":"Delivers a `ping` to this endpoint, so a receiver can be verified before a real event arrives.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Test queued"},"404":{"description":"No such webhook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/profile":{"get":{"tags":["Account"],"summary":"Read the account profile","description":"Name, status, region policy and the account-wide defaults a key inherits. Requires `usage:read`.","responses":{"200":{"description":"The account profile. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Account"],"summary":"Update the account profile","description":"Account-wide settings an owner controls, including the organization `name`. **A routing policy cannot be set here** — the field is refused by name: each model is served one chosen way and that is not a per-account choice (see `GET /v1/models`). Requires `projects:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"The updated profile. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"A field that is not settable, or an invalid value","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Key lacks projects:write","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/overview":{"get":{"tags":["Account"],"summary":"Account overview","description":"The headline figures the console home page is built from — spend, requests, apps and credit, over the current window. Requires `usage:read`.","responses":{"200":{"description":"Overview figures. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/usage/series":{"get":{"tags":["Usage"],"summary":"Usage over time","description":"A time series of spend and request volume for charting, rather than the totals `GET /client/usage` returns. Narrow with `from`/`to`, `keyId` and `model` (one model, exactly). Requires `usage:read`.","responses":{"200":{"description":"One point per bucket. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/usage/comparison":{"get":{"tags":["Usage"],"summary":"This period against the last","description":"The same usage figures for the current and previous period, so a caller can show a change without computing two windows. Requires `usage:read`.","responses":{"200":{"description":"Both periods. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/usage/tools":{"get":{"tags":["Usage"],"summary":"Tool gateway calls","description":"Calls your agents made through the tool gateway: per tool, failures (`errors`) apart from calls a rule stopped (`stopped` — approvals, limits, the guardrail), every code seen, what the guardrail masked or stopped, latency, which key calls which tool, and `series` — calls per hour (per day past two days) for a timeline. These are NOT in `GET /client/usage`, whose figures are model calls only. No cost — gateway tool calls are free. (A model’s own built-in web search is billed per search and appears in `GET /client/usage` instead.) Optional `from`/`to` and `keyId`. Requires `usage:read`.","parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"keyId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Limit to one API key"}],"responses":{"200":{"description":"Tool call figures","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolUsageReport"}}}},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/usage/tools/calls":{"get":{"tags":["Usage"],"summary":"Tool gateway call log","description":"One row per tool call, newest first: which tool, which key, status, latency, guardrail outcome and error code. **Never the arguments or the result** — they are not stored. `band` is `errors` (failed calls — since 2026-09-27 NOT the calls a rule stopped or whose answer the guardrail withheld), `stopped` (calls a rule stopped before the tool: approval, a limit, the guardrail on the arguments) or `guardrail`; `endpoint` narrows to one tool (`mcp:<server>/<tool>`). Keyset-paginated: pass the previous response's `nextCursor` as `cursor`. Requires `usage:read`.","parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"keyId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"band","in":"query","schema":{"type":"string","enum":["all","errors","stopped","guardrail"]}},{"name":"endpoint","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Tool calls, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolCallPage"}}}},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/requests/recent":{"get":{"tags":["Usage"],"summary":"The most recent calls","description":"Metadata only — model, status, latency, tokens and cost. **Never prompt or response content**, which needs `requests:read` and `GET /client/requests/{id}`. Requires `usage:read`.","responses":{"200":{"description":"Recent request metadata. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/requests/window":{"get":{"tags":["Usage"],"summary":"Calls in a time window","description":"The same metadata as `/requests/recent`, bounded by an explicit window rather than a count. Requires `usage:read`.","responses":{"200":{"description":"Request metadata for the window. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/org-map":{"get":{"tags":["Projects"],"summary":"People, projects and who can reach what","description":"The whole workspace in one shape: members, projects, and the membership between them. Requires `projects:write`.","responses":{"200":{"description":"The org map. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks projects:write","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get one project","description":"Settings, budget and spend for a single project. Requires `projects:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Project detail. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such project for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Projects"],"summary":"Update a project","description":"Rename, or change its budget and alerting. Requires `projects:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"The updated project. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such project for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Projects"],"summary":"Delete a project","description":"Refused while anything still belongs to it. Requires `projects:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"},"400":{"description":"The project still has keys or apps","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such project for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/projects/{id}/members":{"get":{"tags":["Projects"],"summary":"List project members","description":"Who can reach this project, and with what role. Requires `projects:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The members. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such project for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Projects"],"summary":"Add someone to a project","description":"The person must already be a member of the workspace — this grants project access, it does not invite. Requires `projects:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"201":{"description":"The membership. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such project or person for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/projects/{id}/members/{userId}":{"patch":{"tags":["Projects"],"summary":"Change someone’s role on a project","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The member’s user id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"description":"Requires `projects:write`.","responses":{"200":{"description":"The updated membership. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such project or member for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Projects"],"summary":"Remove someone from a project","description":"Removes project access only; the person stays in the workspace. Requires `projects:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The member’s user id"}],"responses":{"200":{"description":"Removed"},"404":{"description":"No such project or member for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/log-requests":{"post":{"tags":["Account"],"summary":"Turn prompt and response logging on or off, and choose how long it is kept","description":"Account-wide. `enabled: false` stops storing content and deletes what is stored; we keep metadata only, so `GET /client/requests/{id}` has nothing to return for those calls. `retention` is how long stored content is kept: `7d`, `30d` (the default) or `forever` (until you delete it or turn logging off). A new retention applies to what is already stored, so a shorter one deletes older content at once. Requires `projects:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"retention":{"type":"string","enum":["7d","30d","forever"]}}}}}},"responses":{"200":{"description":"Applied"},"403":{"description":"Key lacks projects:write","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/database":{"get":{"tags":["App data"],"summary":"Is a database attached, and is it ready","description":"Status only — never the connection string. Requires `apps:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Database status. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["App data"],"summary":"Attach a managed database","description":"Provisions Postgres for the app and injects `DATABASE_URL` at its next deploy. Idempotent. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The database, provisioning or ready. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/storage":{"get":{"tags":["App data"],"summary":"Does the app have file storage, and how does it use it","description":"Status (`none`, `creating`, `ready`, `error`, `suspended`) and the NAMES of the variables the app reads — `S3_ENDPOINT`, `S3_BUCKET`, `S3_REGION`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, plus `S3_PUBLIC_URL` while files are public and the `AWS_*` aliases. Never the values. `usage` is the last 30 days once the daily meter has read it; a null size means not reported yet, never zero. Requires `apps:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"File storage status. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["App data"],"summary":"Give the app a private, S3-compatible bucket","description":"Creates a bucket and a key that opens only that bucket, and redeploys the app so the S3 variables reach it. Idempotent. Files are private: share one with a presigned URL from the app; an S3 ACL such as `public-read` has no effect. Removing storage and making files public are console actions. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"File storage status. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Not available for this app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/database/health":{"get":{"tags":["App data"],"summary":"How the database is doing","description":"Charts, notices and tables for the app’s managed database: connection wait, connections in use against the limit, errors, response time and queries per second, as hour buckets (`?hours=`, up to 168) or day buckets (`?days=`, up to 730) — send one, not both. Also errors by SQLSTATE code over the same range (summing to the buckets’ error lines), the latest stuck-query reading, this month’s uptime, notices for anything wrong right now, and the incident log. Null means not measured, never zero. No query text or row data. Requires `apps:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Database health report. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Both ranges, or one past what is kept","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/database/catalogue":{"get":{"tags":["App data"],"summary":"The tables and columns we know about","description":"The last introspection result. `POST` to refresh it. Requires `data:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Tables and columns. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app, or no database","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["App data"],"summary":"Re-read the schema","description":"Introspects the database now and stores the result. Requires `data:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The refreshed catalogue. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app, or no database","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/database/queries":{"get":{"tags":["App data"],"summary":"Past query runs","description":"What has been run against this database and by whom. Requires `data:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Query runs. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app, or no database","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["App data"],"summary":"Run a read query","description":"Read-only. Returns a run id; poll `GET /database/queries/{runId}` for the result. Requires `data:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"202":{"description":"The run id. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"The statement is not read-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app, or no database","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/database/queries/{runId}":{"get":{"tags":["App data"],"summary":"One query run and its rows","description":"Requires `data:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"runId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"A run id from `POST /database/queries`"}],"responses":{"200":{"description":"The run, with rows once it has finished. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such run for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/database/edits":{"get":{"tags":["App data"],"summary":"The record of row edits","description":"Every change made through the console, kept because an edit to live customer data should be answerable afterwards. Requires `data:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Edits, newest first. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app, or no database","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/database/edits/{editId}":{"get":{"tags":["App data"],"summary":"One edit, before and after","description":"Requires `data:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"editId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"An edit id from `GET /database/edits`"}],"responses":{"200":{"description":"The edit. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such edit for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/restore-points":{"get":{"tags":["App data"],"summary":"The points a database can be restored to","description":"Requires `data:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Available restore points. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app, or no database","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/restores":{"get":{"tags":["App data"],"summary":"Restores that have been run","description":"Reading is `data:read`. STARTING one is not available to an API key at all — a restore overwrites live customer data, so it is a console action.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Restore history. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app, or no database","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/analytics":{"get":{"tags":["Apps"],"summary":"Traffic to a hosted app","description":"Requests, bytes and status mix for the app itself — not the AI calls it makes, which are in `/client/usage`. Requires `apps:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"App traffic. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/usage/series":{"get":{"tags":["Apps"],"summary":"What an app has consumed, over time","description":"Memory, processor and storage per bucket — what hosting is billed on. Requires `apps:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"One point per bucket. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/cdn":{"get":{"tags":["Apps"],"summary":"How an app was served by our CDN","description":"Requests and data answered from the CDN's cache versus fetched from the app, response time at the edge, threats blocked, error responses and the busiest paths — counted at the edge, so it includes the visits the app itself never saw. `hours` is 24, 168 or 720; `coveredHours` says how much of that the CDN still holds. `state: \"direct\"` means the app is not served through the CDN, so there is nothing to count. Requires `apps:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Edge delivery figures. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/referenced-by":{"get":{"tags":["Apps"],"summary":"Which of your other apps point at this one","description":"Every app of yours whose settings contain a `${app:<name>.url}` reference to THIS app, and the variables carrying it. Read before deleting: a reference is turned into a URL when a deploy bundle is built, so removing this app does nothing immediately and then refuses the other app's next deploy. An entry with `unreadable: true` is an app whose settings could not be read — not an app that does not reference this one. Requires `apps:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Apps that reference this one. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/preview":{"get":{"tags":["Apps"],"summary":"A screenshot of the running app","description":"The most recent capture, or a reason there is none. Requires `apps:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The preview, or why there is not one. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such app for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/apps/{id}/group":{"post":{"tags":["Apps"],"summary":"Move an app into a project","description":"Changes which project the app is grouped and budgeted under; its gateway key moves with it. A key cannot move an app into a project whose tool bundles run with full access (403) — an owner or admin in the console can. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Moved"},"404":{"description":"No such app or project for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/status":{"get":{"tags":["Apps"],"summary":"Is GitHub connected","description":"Whether this workspace has a GitHub App installation, and what it can reach. Requires `apps:read`.","responses":{"200":{"description":"Connection status. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/repos":{"get":{"tags":["Apps"],"summary":"Repositories we can deploy from","description":"Only what the installation grants — granting access to more is done on GitHub. Requires `apps:read`.","responses":{"200":{"description":"Repositories. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/authorize":{"get":{"tags":["Apps"],"summary":"Begin connecting GitHub","description":"Returns the signed URL to send a person to. **The install must start here** — one begun from GitHub's own UI lands unbound and cannot be adopted; it has to be uninstalled and redone. Requires `apps:read`.","responses":{"200":{"description":"The URL to open. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/authorization":{"get":{"tags":["Apps"],"summary":"The signed-in person’s GitHub authorization","description":"Requires `apps:read`.","responses":{"200":{"description":"The authorization, if there is one. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Apps"],"summary":"Disconnect GitHub for this person","description":"Leaves the workspace installation in place. Requires `apps:deploy`.","responses":{"200":{"description":"Disconnected"},"403":{"description":"Key lacks apps:deploy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/claim":{"post":{"tags":["Apps"],"summary":"Bind a fresh installation to this workspace","description":"The second half of the connect flow, carrying the state from `/github/authorize`. Requires `apps:deploy`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"The bound installation. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"State missing, stale or already claimed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/share":{"post":{"tags":["Apps"],"summary":"Use a GitHub connection from another org you manage","description":"Body `{ installationId }`, taken from `shareable` on `/github/status`. For a GitHub organisation already connected to another org, which the normal connect flow cannot finish. Needs a signed-in person who is owner or admin both here and in an org the installation is already connected to. An API key is refused.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["installationId"],"properties":{"installationId":{"type":"string"}}}}}},"responses":{"200":{"description":"The account now connected here. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"This workspace is already connected, or GitHub could not be asked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not an owner or admin here, or called with an API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No GitHub connection you can share here","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/reachable":{"get":{"tags":["Apps"],"summary":"GitHub organisations this person can add to the workspace","description":"The installations of the Roar app that THIS PERSON’S OWN GitHub login reaches, minus the ones the workspace already holds — including an organisation other workspaces already use. `connected: false` means they have not linked their own GitHub yet (nothing else is offered); `failed: true` means GitHub could not be asked, which is not the same as nothing to add; `canAdd: false` means the workspace’s member-connect policy, or an API key or operator, which have no GitHub login. Says nothing about which other workspaces use an installation. `installUrl` is GitHub’s install flow for an organisation the app is not approved on yet — a non-owner’s click there becomes a request to that organisation’s owners. Requires `apps:read`.","responses":{"200":{"description":"What can be added, and why the list may be empty. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/add-installation":{"post":{"tags":["Apps"],"summary":"Add a GitHub organisation through your own GitHub login","description":"Body `{ installationId }`, taken from `/github/reachable`. Allowed while other workspaces use the installation and while this workspace already has connections. Re-checks with GitHub, using the caller’s own token, that they can reach it; one they cannot gets the same 404 as one that does not exist. Recorded as a person’s reach and never as company property — in particular an API key does not read through it, and every deploy is still checked against the person’s own GitHub access — even when the caller is an owner. Idempotent. Same policy as members connecting an account: an owner always, a member while the workspace allows it. A signed-in person; an API key is refused.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["installationId"],"properties":{"installationId":{"type":"string"}}}}}},"responses":{"200":{"description":"The account now on this workspace, whether it was already, and whether the workspace’s organisation limit allows it. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"The caller has not linked their own GitHub, or GitHub could not be asked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not allowed to connect here, or called with an API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No GitHub organisation to add","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/app-roots":{"get":{"tags":["Apps"],"summary":"Deployable directories in a repository","description":"For a monorepo: where an app could be rooted. Requires `apps:read`.","responses":{"200":{"description":"Candidate roots. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/branches":{"get":{"tags":["Apps"],"summary":"Branches in a repository","description":"Query `repo`. `{ data: { defaultBranch, branches } }` with the default first. `data: null` when GitHub could not be read — offer the default only. Pass a chosen branch as `branch` when creating the app. Requires `apps:read`.","responses":{"200":{"description":"The branches"},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"That repository was not found in your GitHub account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/app-analysis":{"get":{"tags":["Apps"],"summary":"What we think this repository is","description":"The framework, build and start commands we would use. **Read this before theorising about a build** — it says which path the builder took, which a symptom cannot. Requires `apps:read`.","responses":{"200":{"description":"The detection result. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/build-check":{"get":{"tags":["Apps"],"summary":"Will this repository build","description":"Query `repo`, optional `branch`, `rootDir` and `startCommand`. `{ data: { verdict, reason, fix, needsFolder, candidates, folders, foldersCapped, needsStart, offersStartCommand } }`. When `needsStart` is true, a `startCommand` is the fix; `offersStartCommand` is false for an app that starts itself (a repo Dockerfile, an image), where one is refused. `verdict` is `ok`, `blocked` (we read it and its first deploy would fail — creating the app is refused with the same reason) or `unread` (we could not read it; never a refusal). When `needsFolder` is true, pass one of `candidates` as `rootDir`. `data: null` when GitHub is not connected. Requires `apps:read`.","responses":{"200":{"description":"The verdict"},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"That repository was not found in your GitHub account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/app-insight/estimate":{"get":{"tags":["Apps"],"summary":"What one setup-advice read usually costs","description":"`{ estimateUsd }`: the median of what recent setup-advice reads billed, or `null` when there are too few to call one typical. An estimate, shown before `app-insight` spends the customer's AI credit. Requires `apps:read`.","responses":{"200":{"description":"The estimate"},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/github/app-insight":{"get":{"tags":["Apps"],"summary":"A plain-language read of a repository","description":"What the app appears to do and what it will need — written for somebody who is not a developer. Answers **202** `{ operationId, status: \"running\" }` at once and runs the work in the background — it can take minutes. Poll `GET /client/operations/{operationId}` until it is `done` (the body is in `result`) or `failed` (`error` carries what the call would have refused with). Requires `apps:read`.","responses":{"200":{"description":"`{ data: null }`: no connected installation reaches this repository, so nothing will run"},"202":{"description":"The read has started — follow it on GET /client/operations/{id}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperationStarted"}}}},"403":{"description":"Key lacks apps:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/operations/{id}":{"get":{"tags":["Apps"],"summary":"Follow work that answered 202","description":"The read-back for every route here that answers 202 (`app-insight`, `autofix`). Poll it every couple of seconds until `status` is `done` — `result` is then the body the route would have answered with — or `failed`, where `error` carries the status and message it would have refused with. A `running` operation older than the platform restart window reads as `failed` with a message saying so. Rows are kept seven days. Readable by a session, or by a key holding the scope the starting route required; any other company's id is a 404.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperationView"}}}},"403":{"description":"Key lacks the scope the starting route required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such operation for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/deploy-on-push":{"post":{"tags":["Apps"],"summary":"Turn automatic deploys on or off","description":"Whether a push to the connected branch redeploys. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Applied"},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/branch":{"post":{"tags":["Apps"],"summary":"Change the branch an app deploys from","description":"Send `{ \"branch\": \"staging\" }`. Refused unless GitHub confirms the branch exists. Deploys nothing: the next push to that branch, or a redeploy, ships it. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Changed"},"400":{"description":"No such branch, or GitHub could not be asked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/branches":{"get":{"tags":["Apps"],"summary":"Branches an app could deploy from","description":"`{ data: { defaultBranch, branches } }`, default first; `data: null` when the repo could not be read. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:read`, and a project role that may deploy the app.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The branches"},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/deploy/cancel":{"post":{"tags":["Apps"],"summary":"Cancel a deploy in progress","description":"The version already serving stays up. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancelled"},"400":{"description":"Nothing in progress to cancel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/roar-domain":{"post":{"tags":["Apps"],"summary":"Give the service a Roar address","description":"A hostname we own and manage, ready as soon as the app runs — no DNS for the customer. For their own domain, use `POST /client/services/{id}/domains`. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The address. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/migrate":{"get":{"tags":["Apps"],"summary":"The database migration we would run","description":"The command found in the repository, and whether it is safe to run. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The migration plan. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Apps"],"summary":"Run the database migration","description":"Runs the command above against the app's managed database. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"The queued run. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Nothing to run, or not permitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/migrations":{"post":{"tags":["Apps"],"summary":"Set whether migrations run on every deploy","description":"On for an app created with a Roar database (its database starts empty, so its schema has to be applied on the first deploy); off for every other app, because applying a schema change to existing data is a decision the owner makes once, explicitly. Either way the owner can change it here. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Applied"},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/autofix":{"post":{"tags":["Apps"],"summary":"Ask us to fix a failed build","description":"USES THE COMPANY'S AI CREDITS. Reads the build log, has our model write a minimal fix, stores it as a reversible overlay on the app's build (the repository itself is not changed — `DELETE .../ai-fixes` removes it) and redeploys with it; the settled result is `{ ok, summary }`, where `ok: false` means there was nothing to offer. Only a failed deploy that is marked fixable; repeating it on the same failure spends credits again. Answers **202** `{ operationId, status: \"running\" }` at once and runs the work in the background — it can take minutes. Poll `GET /client/operations/{operationId}` until it is `done` (the body is in `result`) or `failed` (`error` carries what the call would have refused with). `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"The fix has started — follow it on GET /client/operations/{id}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperationStarted"}}}},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/ai-fixes":{"delete":{"tags":["Apps"],"summary":"Discard the suggested fixes","description":"Clears what `autofix` proposed, without applying any of it. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cleared"},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/email":{"post":{"tags":["Apps"],"summary":"Set up sending email from the app","description":"**Not available yet** — this returns `400` while the feature is on hold. Connect your own email provider with ordinary environment variables in the meantime. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:deploy`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"400":{"description":"Email sending is not available yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/services/{id}/egress-alert":{"get":{"tags":["Apps"],"summary":"The outbound-traffic alert for a service","description":"Whether an unusual-traffic alert is standing against this service, and what tripped it. `id` is a SERVICE id — take it from `services[].id` on `GET /client/apps/{appId}`, not the app id itself. Requires `apps:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The alert, if any. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"No such service for you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/webhooks/{id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"Recent delivery attempts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Delivery log","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryList"}}}},"404":{"description":"No such webhook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/overview":{"get":{"tags":["Tool gateway"],"summary":"The tool gateway as you see it","description":"Where agents connect, whether the gateway is on for your organisation, what you may change, and which keys you can see would be served tools right now (tool names only). Requires `mcp:read`.","responses":{"200":{"description":"Overview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpOverview"}}}},"403":{"description":"Key lacks mcp:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/upstreams":{"get":{"tags":["Tool gateway"],"summary":"List tool servers","description":"Your MCP servers. Addresses go to an owner or admin only; a credential is reported as set or not, never returned. Requires `mcp:read`.","responses":{"200":{"description":"Tool servers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpUpstreamList"}}}},"403":{"description":"Key lacks mcp:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Tool gateway"],"summary":"Add a tool server","description":"Registers an MCP server the gateway will forward tool calls to. Its tools arrive switched off until approved — refresh it next. Up to 20 per organisation. Owner or admin, or a key with `mcp:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMcpUpstreamRequest"}}}},"responses":{"201":{"description":"The added tool server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpUpstream"}}}},"400":{"description":"Invalid address or credential, or the organisation is at its limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not an owner or admin, or key lacks mcp:write","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A tool server with that name exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/upstreams/test":{"post":{"tags":["Tool gateway"],"summary":"Test a connection before saving it","description":"Connects to the address with the credential given — or, with `upstreamId` and no `auth`, that server’s saved one, and then only at the exact address it was saved for — and answers with the tools it lists. Nothing is stored. At most 10 tests a minute per organisation. Owner or admin, or a key with `mcp:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestMcpUpstreamRequest"}}}},"responses":{"200":{"description":"The tools the server lists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpTestResult"}}}},"400":{"description":"Invalid address or credential, an address that is not allowed, or a saved credential asked to go to another address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such tool server (`upstreamId`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tests this minute","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The server could not be reached, refused the credential, or did not speak MCP","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"The server did not answer in time","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/upstreams/{id}":{"patch":{"tags":["Tool gateway"],"summary":"Change a tool server","description":"Address, credential, on/off, or the argument policy. Changing the ADDRESS takes an owner or admin signed in to the console (403 for a key) — re-pointing a tool server decides what agents run. Owner or admin, or a key with `mcp:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpUpstreamRequest"}}}},"responses":{"200":{"description":"The updated tool server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpUpstream"}}}},"400":{"description":"Invalid change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such tool server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Tool gateway"],"summary":"Remove a tool server","description":"Its tools leave every bundle. Owner or admin, or a key with `mcp:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Removed"},"404":{"description":"No such tool server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/oauth/status":{"get":{"tags":["Tool gateway"],"summary":"Sign-in status of every signed-in tool server","description":"For each tool server that signs in: connected or needs signing in again, who signed in and when. No token is ever returned. Requires `mcp:read`.","responses":{"200":{"description":"Sign-in status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpSignInStatusList"}}}},"403":{"description":"Key lacks mcp:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/upstreams/{id}/oauth":{"get":{"tags":["Tool gateway"],"summary":"One tool server’s sign-in status","description":"Requires `mcp:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Sign-in status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpSignInStatus"}}}},"404":{"description":"No such tool server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Tool gateway"],"summary":"Sign a tool server out","description":"Revokes the tokens at the vendor where it can, and deletes them whether or not it could. Its tools then answer \"needs sign-in\" until someone signs in again. Owner or admin, or a key with `mcp:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"What happened","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpSignOutResult"}}}},"404":{"description":"No such tool server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/upstreams/{id}/refresh":{"post":{"tags":["Tool gateway"],"summary":"Read a tool server’s tools","description":"Connects to the server and records what it lists. New and changed tools wait for approval. At most 10 refreshes a minute per organisation. Owner or admin, or a key with `mcp:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"What the refresh found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpRefreshResult"}}}},"404":{"description":"No such tool server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many refreshes this minute","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The server could not be reached or did not speak MCP","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/tools":{"get":{"tags":["Tool gateway"],"summary":"List tools","description":"Every tool your servers list, with its state. Only `served` tools reach an agent. Requires `mcp:read`.","parameters":[{"name":"upstreamId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Only this tool server’s tools"}],"responses":{"200":{"description":"Tools","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpToolList"}}}},"404":{"description":"No such tool server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/tools/approve":{"post":{"tags":["Tool gateway"],"summary":"Approve tools","description":"Approves each tool AT THE DEFINITION YOU WERE SHOWN: send the `definitionHash` from `GET /client/mcp/tools`. If any changed since, nothing is approved (409). Re-approving a changed tool that now says it only reads takes an owner or admin signed in to the console (403 for a key). Owner or admin, or a key with `mcp:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApproveMcpToolsRequest"}}}},"responses":{"200":{"description":"How many were approved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpApproveResult"}}}},"404":{"description":"No such tool","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A tool changed since you read it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/tools/{id}":{"patch":{"tags":["Tool gateway"],"summary":"Turn a tool on or off, say what it does, lock inputs, or set what agents read","description":"Body `{\"status\":\"active\"|\"disabled\"}`, `{\"effect\":\"reads\"|\"changes\"|null}`, `{\"fixedArgs\":{…}|null}` and/or `{\"descriptionOverride\":\"…\"|null}`. Off hides it from every agent on their next call. `effect` overrides the server’s own `readOnlyHint` (null goes back to it): a tool that changes things is hidden by a read-only grant and waits for approval under “ask”. `fixedArgs` replaces the tool’s locked inputs: each name must be one of its inputs and each value must fit that input’s rules (400 otherwise); agents stop seeing those inputs and the gateway always sends these values. `descriptionOverride` (at most 1,000 characters, plain text) is what agents read instead of the server’s description. A key may only narrow: it may set `\"changes\"`; `\"reads\"` and null effect take a signed-in owner or admin, and so does ANY change to `fixedArgs` or `descriptionOverride` — a lock rewrites every agent’s arguments and an override is text every agent follows. Only a signed-in owner or admin is shown locked values; everyone else reads `\"[locked]\"`. Owner or admin, or a key with `mcp:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["active","disabled"]},"effect":{"type":"string","enum":["reads","changes"],"nullable":true},"fixedArgs":{"type":"object","nullable":true,"additionalProperties":{"type":["string","number","boolean"]}},"descriptionOverride":{"type":"string","nullable":true,"maxLength":1000}}}}}},"responses":{"204":{"description":"Changed"},"400":{"description":"A lock that does not fit the tool, or a description that is empty or too long","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"A key tried to change a lock or a description","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such tool","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/groups":{"get":{"tags":["Tool gateway"],"summary":"List bundles","description":"Named sets of tools — the only thing that can be given to a project or a key. Requires `mcp:read`.","responses":{"200":{"description":"Bundles","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpToolGroupList"}}}}}},"post":{"tags":["Tool gateway"],"summary":"Create a bundle","description":"Body `{\"name\", \"description\"?}`; then set its tools. Owner or admin, or a key with `mcp:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":"string"}}}}}},"responses":{"201":{"description":"The created bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpToolGroup"}}}},"409":{"description":"A bundle with that name exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/groups/{id}":{"patch":{"tags":["Tool gateway"],"summary":"Rename a bundle","description":"Owner or admin, or a key with `mcp:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"The updated bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpToolGroup"}}}},"404":{"description":"No such bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Tool gateway"],"summary":"Delete a bundle","description":"Everything it was given to loses its tools. Owner or admin, or a key with `mcp:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"No such bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/groups/{id}/tools":{"put":{"tags":["Tool gateway"],"summary":"Set a bundle’s tools","description":"Replaces the bundle’s tools with `{\"toolIds\":[…]}`, which may span tool servers. Owner or admin, or a key with `mcp:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["toolIds"],"properties":{"toolIds":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}},"responses":{"204":{"description":"Saved"},"404":{"description":"No such bundle or tool","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/grants":{"get":{"tags":["Tool gateway"],"summary":"List grants","description":"Which bundle is given to which project or key. An agent’s tools are its key’s bundles plus its project’s. Requires `mcp:read`.","responses":{"200":{"description":"Grants","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpGrantList"}}}}}},"post":{"tags":["Tool gateway"],"summary":"Give a bundle","description":"Body `{\"groupId\", \"projectId\"}` or `{\"groupId\", \"apiKeyId\"}`, with `access` (default `ask`) and optionally `callLimit`. An owner or admin (or a key with `mcp:write`) may give to any project or key; a project manager to the projects they manage. A key can only narrow: `full` takes a signed-in owner or admin; so does giving tools that already reach the same agents through a bundle with a call limit, and giving tools that carry a limit anywhere without a `callLimit` at least as tight (403 for a key).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["groupId"],"properties":{"groupId":{"type":"string","format":"uuid"},"projectId":{"type":"string","format":"uuid"},"apiKeyId":{"type":"string","format":"uuid"},"access":{"$ref":"#/components/schemas/McpGrantAccess"},"callLimit":{"$ref":"#/components/schemas/McpCallLimit"}}}}}},"responses":{"201":{"description":"The grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpGrant"}}}},"403":{"description":"Not allowed to give bundles there","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such bundle, project or key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already given","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/grants/{id}":{"patch":{"tags":["Tool gateway"],"summary":"Change what a grant allows","description":"Body `{\"access\":\"read_only\"|\"ask\"|\"full\"}` and/or `{\"callLimit\":{\"count\":100,\"per\":\"day\"}|null}`. Its agents get the new setting on their next call; a call over the limit is refused with a tool error saying when it resets. Same rule as giving a bundle. A key can only narrow: `full`, and removing or raising a limit, take a signed-in owner or admin (403 for a key).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"access":{"$ref":"#/components/schemas/McpGrantAccess"},"callLimit":{"$ref":"#/components/schemas/McpCallLimit"}}}}}},"responses":{"200":{"description":"The grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpGrant"}}}},"403":{"description":"Not allowed to change that grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Tool gateway"],"summary":"Take a bundle away","description":"Its agents lose those tools on their next call. Same rule as giving one.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Taken away"},"403":{"description":"Not allowed to change that grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/approvals":{"get":{"tags":["Tool gateway"],"summary":"Tool calls waiting for approval","description":"Calls to tools that change things, from agents whose grant says “ask”: the waiting ones first, then recent history. You see requests from the keys you can see; the masked arguments only if you may decide. Requires `mcp:read`.","responses":{"200":{"description":"Requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpApprovalList"}}}}}}},"/client/mcp/approvals/waiting":{"get":{"tags":["Tool gateway"],"summary":"Only the tool calls waiting now","description":"The waiting requests alone — id, reference, tool, key and times — with no history and no arguments, for a cheap poll (a count, a notification). The same scoping as `GET /client/mcp/approvals`. Takes no parameters. Requires `mcp:read`.","responses":{"200":{"description":"Waiting requests and `canDecide`. (Response shape not yet modelled in this spec.)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"A parameter was sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/approvals/{id}/approve":{"post":{"tags":["Tool gateway"],"summary":"Approve a tool call","description":"The agent’s same call — same key, tool and arguments — then runs once within 15 minutes. A signed-in owner or admin only: no API key or agent connection can approve, so an agent cannot approve its own request.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Approved"},"403":{"description":"Not a signed-in owner or admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already decided, or expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/approvals/{id}/deny":{"post":{"tags":["Tool gateway"],"summary":"Decline a tool call","description":"The agent is told an admin declined it. A signed-in owner or admin only, as for approving.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Declined"},"403":{"description":"Not a signed-in owner or admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already decided, or expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/switches":{"get":{"tags":["Tool gateway"],"summary":"Off switches","description":"Whether the gateway is on for your organisation (Roar’s switch — not settable here) and for each project you can see. Requires `mcp:read`.","responses":{"200":{"description":"Switches","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpSwitches"}}}}}}},"/client/mcp/projects/{id}/switch":{"put":{"tags":["Tool gateway"],"summary":"Turn the gateway off or on for a project","description":"Body `{\"enabled\": false}` stops every key in the project from seeing or calling a tool, from its next call. Model calls are unaffected. Owner or admin, or a key with `mcp:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Switches after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpSwitches"}}}},"404":{"description":"No such project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/apps/{id}/ai-tools":{"put":{"tags":["Tool gateway"],"summary":"Let an app's AI use its tools","description":"Body `{\"enabled\": true}` makes Roar add your company's tools to this app's own `/v1/chat/completions` requests that send no tools of their own, so the app gets the tools its key is given without a code change. Each such request then carries the tools' descriptions, which it is billed for. `{id}` is the app's web service. The same people who may give the app tools may change it; turning it ON takes a person signed in to the console (403 for a key), and a key may only turn it off.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"The setting","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppAiTools"}}}},"403":{"description":"Not allowed to turn it on","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/client/mcp/keys/{id}/tools":{"get":{"tags":["Tool gateway"],"summary":"Tools a key can use","description":"What this key would be served through the gateway right now — names only. Requires `mcp:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Tools","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpKeyToolList"}}}},"404":{"description":"No such key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"roar_live_…","description":"A scoped Roar API key."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["message","type","code","param"],"properties":{"message":{"type":"string"},"type":{"type":"string"},"code":{"type":"string","nullable":true},"param":{"type":"string","nullable":true}}}}},"ModelList":{"type":"object","required":["object","data"],"properties":{"object":{"type":"string"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Model"}}}},"Model":{"type":"object","required":["id","object","created","owned_by","roar_modalities","roar_batch"],"properties":{"id":{"type":"string","description":"The model name to send as `model`"},"object":{"type":"string"},"created":{"type":"integer","description":"Unix seconds"},"owned_by":{"type":"string"},"roar_modalities":{"type":"array","items":{"type":"string"}},"roar_batch":{"type":"boolean","description":"Accepted by `POST /v1/batches`"},"roar_image_input":{"$ref":"#/components/schemas/FeatureSupport","description":"Chat/completion models only"},"roar_tool_calling":{"$ref":"#/components/schemas/FeatureSupport","description":"Chat/completion models only"},"roar_json_output":{"$ref":"#/components/schemas/FeatureSupport","description":"Chat/completion models only"},"roar_structured_output":{"$ref":"#/components/schemas/FeatureSupport","description":"Chat/completion models only"},"roar_reasoning":{"$ref":"#/components/schemas/FeatureSupport","description":"Chat/completion models only"},"roar_pricing":{"$ref":"#/components/schemas/ModelPricing","description":"Absent when no rate beyond base in/out is published"},"roar_notes":{"type":"string","description":"Absent when there is no note"}}},"FeatureSupport":{"type":"string","enum":["yes","no","unknown"]},"ModelPricing":{"type":"object","properties":{"inputPer1M":{"type":"number"},"outputPer1M":{"type":"number"},"cachedInputPer1M":{"type":"number"},"cacheWriteInputPer1M":{"type":"number"},"cacheWrite1hInputPer1M":{"type":"number"},"priorityInputPer1M":{"type":"number"},"priorityOutputPer1M":{"type":"number"},"batchInputPer1M":{"type":"number"},"batchOutputPer1M":{"type":"number"},"flexInputPer1M":{"type":"number"},"flexOutputPer1M":{"type":"number"},"longContextThresholdTokens":{"type":"integer","description":"Above this many input tokens the whole request reprices. Absent means the model has no long-context tier."},"longContextInputMultiplier":{"type":"number","description":"Applied to input, cached input and cache writes past the threshold."},"longContextOutputMultiplier":{"type":"number","description":"Applied to output past the threshold."}}},"ChatCompletionRequest":{"type":"object","required":["model","messages"],"properties":{"model":{"type":"string","description":"A model id from /v1/models"},"messages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string"},"content":{"type":"string"}}}},"stream":{"type":"boolean","default":false},"temperature":{"type":"number"},"tools":{"type":"array","items":{"type":"object"}}}},"EmbeddingsRequest":{"type":"object","required":["model","input"],"properties":{"model":{"type":"string"},"input":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}}},"EvaluateRequest":{"type":"object","required":["model","state","questions"],"properties":{"model":{"type":"string"},"state":{"description":"What is being judged. A string, an object or an array — passed through as given.","oneOf":[{"type":"string"},{"type":"object"},{"type":"array","items":{}}]},"questions":{"type":"object","description":"Each key names an answer in the response. `criteria` differs per type: an optional true/false gloss for `boolean`, a record of option name to description for `choice` (two or more), an ordered array lowest-to-highest for `score` (two or more).","additionalProperties":{"type":"object","required":["type","instructions"],"properties":{"type":{"type":"string","enum":["boolean","choice","score"]},"instructions":{"type":"string"},"criteria":{"oneOf":[{"type":"object"},{"type":"array","items":{"type":"string"}}]}}}}}},"ApiKey":{"type":"object","required":["id","name","prefix","projectId","scopes","expiresAt","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"prefix":{"type":"string","description":"The visible head of the key — enough to identify it, never enough to use it"},"projectId":{"type":"string","format":"uuid","nullable":true},"scopes":{"type":"array","items":{"type":"string","enum":["inference","keys:read","keys:write","usage:read","requests:read","apps:read","apps:deploy","data:read","projects:write","webhooks:read","webhooks:write","mcp:read","mcp:write"]}},"expiresAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ApiKeySettings":{"type":"object","required":["spendLimitUsd","budgetPeriod","hardCap","revoked","disabledAt","lastUsedAt"],"properties":{"spendLimitUsd":{"type":"string","description":"null = no cap on this key","nullable":true},"budgetPeriod":{"type":"string","enum":["day","week","month","total"]},"hardCap":{"type":"boolean","description":"A PREFERENCE, and on its own it protects nothing: true = refuse at the cap, false = only alert — but either way it does nothing until `spendLimitUsd` is set. Read `capped` for the question \"will this key actually be stopped\"."},"revoked":{"type":"string","format":"date-time","nullable":true},"disabledAt":{"type":"string","format":"date-time","nullable":true,"description":"Paused by its owner — reversible, unlike `revoked`"},"lastUsedAt":{"type":"string","format":"date-time","nullable":true}}},"ApiKeyList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyListItem"}}}},"ApiKeyListItem":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/ApiKeySettings"},{"type":"object","required":["requests","costUsd","appName","appId","capped"],"properties":{"requests":{"type":"integer","description":"Requests in the last 30 days"},"costUsd":{"type":"string","description":"Spend in the last 30 days"},"appName":{"type":"string","nullable":true,"description":"The hosted app this key was created for, or null for a key you made yourself. A key with a name here is the credential its app runs on: it cannot be revoked or deleted while the app exists (pause it instead, or delete the app)."},"appId":{"type":"string","format":"uuid","nullable":true,"description":"The id of that hosted app (as in `/client/apps/{id}`), or null for a key you made yourself. App names are not unique; this is."},"capped":{"type":"boolean","description":"Whether spend on this key is actually REFUSED at a limit — `spendLimitUsd` set AND `hardCap` true. Neither field answers that alone, so read this one: a key with `hardCap: true` and no limit is uncapped. The same predicate the gateway itself filters budgets through."},"mcp":{"$ref":"#/components/schemas/McpKeyLabel","nullable":true,"description":"Set on an MCP key — one an assistant connected over MCP spends through, created when the person connected it — and null on every other key. Pause it to stop that assistant making AI calls."},"viaSetup":{"type":"boolean","description":"True when a coding agent fetched this key with a one-time setup code — requests on it are labelled \"AI assistant · API key\" in the request log."}}}]},"ApiKeyRecord":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/ApiKeySettings"},{"type":"object","required":["alertAtPct","rateLimitPerMin","mcp","setupCode"],"properties":{"alertAtPct":{"type":"integer","description":"Percent of the cap at which the budget starts alerting"},"rateLimitPerMin":{"type":"integer","nullable":true},"mcp":{"$ref":"#/components/schemas/McpKeyLabel","nullable":true,"description":"Set on an MCP key — which assistant and whose connection it is — and null on every other key"},"setupCode":{"type":"string","nullable":true,"enum":["pending","used","expired",null],"description":"For a key delivered by a one-time setup code (`delivery: setup_code`): whether an agent has spent the code yet, or it expired unspent. Null for a key that was revealed instead."}}}]},"McpKeyLabel":{"type":"object","required":["app","person","personId","revokedAutomatically"],"properties":{"app":{"type":"string","description":"The assistant app, e.g. Claude"},"person":{"type":"string","description":"Whose connection it is"},"personId":{"type":"string","description":"That person’s user id, to tell your own connections from a teammate’s"},"revokedAutomatically":{"type":"string","nullable":true,"enum":["unused","connection_gone",null],"description":"Why it was revoked automatically, when it was: unused for 90 days, or the connection is gone. Null when it is live or an admin revoked it."}}},"ApiKeyDetail":{"type":"object","required":["key","usage","budget"],"properties":{"key":{"$ref":"#/components/schemas/ApiKeyRecord"},"usage":{"$ref":"#/components/schemas/KeySpend"},"budget":{"$ref":"#/components/schemas/BudgetStatus","nullable":true}}},"KeySpend":{"type":"object","required":["total","requests","today","week","month"],"properties":{"total":{"type":"string","description":"Decimal USD as a string"},"requests":{"type":"integer"},"today":{"type":"string","description":"Decimal USD as a string"},"week":{"type":"string","description":"Decimal USD as a string"},"month":{"type":"string","description":"Decimal USD as a string"}}},"CreateKeyRequest":{"type":"object","properties":{"name":{"type":"string"},"projectId":{"type":"string","format":"uuid","nullable":true},"scopes":{"type":"array","items":{"type":"string","enum":["inference","keys:read","keys:write","usage:read","requests:read","apps:read","apps:deploy","data:read","projects:write","webhooks:read","webhooks:write","mcp:read","mcp:write"]},"default":["inference"]},"expiresInDays":{"type":"integer"},"spendLimitUsd":{"type":"number","nullable":true},"budgetPeriod":{"type":"string","enum":["day","week","month","total"]}}},"UpdateKeyRequest":{"type":"object","properties":{"name":{"type":"string","nullable":true},"projectId":{"type":"string","format":"uuid","nullable":true},"scopes":{"type":"array","items":{"type":"string","enum":["inference","keys:read","keys:write","usage:read","requests:read","apps:read","apps:deploy","data:read","projects:write","webhooks:read","webhooks:write","mcp:read","mcp:write"]}},"expiresAt":{"type":"string","format":"date-time","nullable":true},"disabled":{"type":"boolean","description":"Pause or resume the key"},"spendLimitUsd":{"type":"number","nullable":true},"budgetPeriod":{"type":"string","enum":["day","week","month","total"]}}},"CreatedKey":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"object","required":["key","baseUrl","warning"],"properties":{"key":{"type":"string","description":"Plaintext — shown once"},"baseUrl":{"type":"string","description":"The OpenAI-compatible endpoint this key is used against"},"warning":{"type":"string"}}}]},"Ack":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"UsageReport":{"type":"object","required":["summary","byModel","data"],"properties":{"summary":{"$ref":"#/components/schemas/UsageSummary"},"byModel":{"type":"array","items":{"$ref":"#/components/schemas/ModelUsage"}},"data":{"type":"array","items":{"$ref":"#/components/schemas/UsageRecord"},"description":"Recent requests, newest first (`limit`, default 100)"}}},"UsageSummary":{"type":"object","required":["requests","inputTokens","outputTokens","costUsd","cacheSavingsUsd","promptCacheRequests","avgLatencyMs","p95LatencyMs","avgTtftMs","cacheHits","failovers","errors","clientAborted","builtinToolUsd","builtinToolCalls"],"properties":{"requests":{"type":"integer"},"inputTokens":{"type":"integer"},"outputTokens":{"type":"integer"},"costUsd":{"type":"string","description":"Decimal USD as a string"},"cacheSavingsUsd":{"type":"string","description":"What the provider's prompt cache saved over the base input rate. A different mechanism from `cacheHits`, over a disjoint set of requests."},"promptCacheRequests":{"type":"integer","description":"Requests that contributed to `cacheSavingsUsd`"},"avgLatencyMs":{"type":"integer"},"p95LatencyMs":{"type":"integer"},"avgTtftMs":{"type":"integer","description":"Average time to first token, streaming requests only"},"cacheHits":{"type":"integer","description":"Requests answered from the response cache — no upstream call, billed $0"},"failovers":{"type":"integer","description":"Requests that needed more than one attempt"},"errors":{"type":"integer","description":"Requests that failed. Excludes cancellations — see `clientAborted`."},"clientAborted":{"type":"integer","description":"Requests the caller hung up on before they finished"},"builtinToolUsd":{"type":"string","description":"The part of `costUsd` charged for built-in tools such as web search — included in `costUsd`, not added to it"},"builtinToolCalls":{"type":"object","additionalProperties":{"type":"integer"},"description":"How many of each priced built-in tool ran, e.g. `{ \"web_search\": 12 }`"}}},"ToolUsageSummary":{"type":"object","required":["calls","errors","stopped","withheld","redacted","blocked","avgLatencyMs","p95LatencyMs"],"properties":{"calls":{"type":"integer"},"errors":{"type":"integer","description":"Calls that FAILED — the tool, its server or we broke (including `tool_error`, an HTTP 200), or no such tool. Since 2026-09-27 this no longer includes calls a rule stopped (`stopped`) or whose answer the guardrail withheld (`withheld`); a monitor on `errors` that expected those should add them"},"stopped":{"type":"integer","description":"Calls a rule stopped before they reached the tool — waiting for or declined an approval, over a limit, too many at once, or the guardrail refusing the arguments. Not failures"},"withheld":{"type":"integer","description":"Calls whose tool ran and whose answer the guardrail withheld — a change may have happened. Not failures"},"redacted":{"type":"integer","description":"Calls whose arguments or result the guardrail masked"},"blocked":{"type":"integer","description":"Calls the guardrail blocked: arguments refused (in `stopped`) or answer withheld (`withheld`)"},"avgLatencyMs":{"type":"integer","nullable":true,"description":"Over calls that reached a tool; null when none did"},"p95LatencyMs":{"type":"integer","nullable":true,"description":"Over calls that reached a tool; null when none did"}}},"ToolUsageReport":{"type":"object","required":["summary","byTool","byError","byKey","series"],"properties":{"summary":{"$ref":"#/components/schemas/ToolUsageSummary"},"series":{"type":"array","description":"Calls per hour (per day past two days), oldest first; an hour with no calls is absent","items":{"type":"object","required":["bucket","calls","errors","stopped"],"properties":{"bucket":{"type":"string","format":"date-time"},"calls":{"type":"integer"},"errors":{"type":"integer"},"stopped":{"type":"integer"}}}},"byTool":{"type":"array","items":{"type":"object","required":["endpoint","tool","calls","errors","stopped","withheld","redacted","blocked","avgLatencyMs","p95LatencyMs","lastCalledAt"],"properties":{"endpoint":{"type":"string","description":"`mcp:<server>/<tool>`, or `mcp:gateway` for a name that matched no tool"},"tool":{"type":"string","nullable":true,"description":"`<server>/<tool>`; null for the `mcp:gateway` bucket"},"calls":{"type":"integer"},"errors":{"type":"integer"},"stopped":{"type":"integer"},"withheld":{"type":"integer"},"redacted":{"type":"integer"},"blocked":{"type":"integer"},"avgLatencyMs":{"type":"integer","nullable":true},"p95LatencyMs":{"type":"integer","nullable":true},"lastCalledAt":{"type":"string","format":"date-time"}}}},"byError":{"type":"array","items":{"type":"object","required":["code","calls"],"properties":{"code":{"type":"string","enum":["unknown_tool","rate_limited","concurrency","guardrail_blocked","guardrail_args_blocked","upstream_error","tool_error","internal_error","approval_pending","approval_denied","grant_limit"]},"calls":{"type":"integer","description":"Rows, not refusals: repeated refusals are logged once per minute per key"}}}},"byKey":{"type":"array","description":"Which key, filed under which project, called which tool","items":{"type":"object","required":["apiKeyId","keyName","projectId","projectName","endpoint","tool","calls","errors","stopped"],"properties":{"apiKeyId":{"type":"string","format":"uuid","nullable":true,"description":"Null once the key is deleted"},"keyName":{"type":"string","nullable":true},"projectId":{"type":"string","format":"uuid","nullable":true},"projectName":{"type":"string","nullable":true},"endpoint":{"type":"string"},"tool":{"type":"string","nullable":true},"calls":{"type":"integer"},"errors":{"type":"integer"},"stopped":{"type":"integer"}}}}}},"ToolCallRow":{"type":"object","required":["id","apiKeyId","endpoint","tool","status","latencyMs","guardrailAction","errorCode","requestId","createdAt"],"properties":{"id":{"type":"integer"},"apiKeyId":{"type":"string","format":"uuid","nullable":true},"endpoint":{"type":"string"},"tool":{"type":"string","nullable":true},"status":{"type":"integer"},"latencyMs":{"type":"integer"},"guardrailAction":{"type":"string","enum":["redact","block"],"nullable":true},"errorCode":{"type":"string","enum":["unknown_tool","rate_limited","concurrency","guardrail_blocked","guardrail_args_blocked","upstream_error","tool_error","internal_error","approval_pending","approval_denied","grant_limit"],"nullable":true},"requestId":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ToolCallPage":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ToolCallRow"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass back as `cursor` for the next page; null on the last"}}},"ClientAppRequestsSummary":{"type":"object","required":["requests","appErrors","badRequests","p95DurationMs","p50DurationMs","blocked"],"properties":{"blocked":{"type":"integer","description":"Stopped by the security rules before reaching the app — scanners and attacks. In none of the other counts."},"requests":{"type":"integer"},"appErrors":{"type":"integer","description":"Status 500–599: the app broke"},"badRequests":{"type":"integer","description":"Status 400–499: mostly crawlers on a public app"},"p95DurationMs":{"type":"integer","nullable":true,"description":"Over requests that finished with a reported duration (incomplete ones are left out); null when none had one"},"p50DurationMs":{"type":"integer","nullable":true,"description":"The median over the same requests as p95DurationMs; null when none had a reported duration"}}},"ClientAppRequestPage":{"type":"object","required":["data","nextCursor","summary","retentionDays"],"properties":{"retentionDays":{"type":"integer","description":"How many days of requests are kept. Older rows are deleted, so a window reaching further back returns no more."},"data":{"type":"array","items":{"type":"object","required":["appId","appName","serviceId","at","method","path","status","durationMs","incomplete","bytes","host","requestId","reachedApp","appStatus","blocked"],"properties":{"appId":{"type":"string","format":"uuid"},"appName":{"type":"string"},"serviceId":{"type":"string","format":"uuid"},"at":{"type":"string","format":"date-time"},"method":{"type":"string"},"path":{"type":"string","description":"Query parameter names kept, every value removed"},"status":{"type":"integer"},"durationMs":{"type":"integer","nullable":true,"description":"Null when not reported, or when the response was incomplete"},"incomplete":{"type":"boolean","description":"The response stopped before it finished (the visitor left, or the app broke off mid-response). The status is the one already sent; there is no duration."},"bytes":{"type":"integer","nullable":true},"host":{"type":"string","nullable":true},"requestId":{"type":"string","nullable":true,"description":"The `cf-ray`; null when the node did not report one"},"blocked":{"type":"boolean","description":"Stopped by the security rules before reaching the app — a scanner or an attack. Listed only with `band=blocked`, and never counted as a request or an error."},"reachedApp":{"type":"boolean","description":"False when the request never reached the app: it did not answer, or the caller gave up first. Such a row has no duration or size."},"appStatus":{"type":"integer","nullable":true,"description":"What the app answered, when the caller got something else (`status`) — the app finished after the caller had already been cut off. Null otherwise."}}}},"nextCursor":{"type":"string","nullable":true,"description":"Pass back as `cursor` for older requests; null on the last page"},"summary":{"$ref":"#/components/schemas/ClientAppRequestsSummary","nullable":true,"description":"Over the whole window and every app — first page only, null after it"},"facets":{"type":"object","nullable":true,"description":"Counts over the window, not narrowed by the filters — first page only","required":["byApp","byStatus","byMethod","byPath"],"properties":{"byApp":{"type":"array","items":{"type":"object","required":["appId","requests","appErrors"],"properties":{"appId":{"type":"string","format":"uuid"},"requests":{"type":"integer"},"appErrors":{"type":"integer"}}}},"byStatus":{"type":"object","required":["2xx","3xx","4xx","5xx","blocked"],"properties":{"2xx":{"type":"integer"},"3xx":{"type":"integer"},"4xx":{"type":"integer"},"5xx":{"type":"integer"},"blocked":{"type":"integer","description":"Stopped by the security rules; beside the classes, never inside them"}}},"byMethod":{"type":"array","items":{"type":"object","required":["method","requests"],"properties":{"method":{"type":"string"},"requests":{"type":"integer"}}}},"byPath":{"type":"array","description":"The ten busiest paths","items":{"type":"object","required":["path","requests","appErrors","badRequests"],"properties":{"path":{"type":"string"},"requests":{"type":"integer"},"appErrors":{"type":"integer"},"badRequests":{"type":"integer"}}}}}},"series":{"type":"array","nullable":true,"description":"Requests per hour (per day past two days) over the FILTERED list — first page only","items":{"type":"object","required":["bucket","requests","appErrors","badRequests"],"properties":{"bucket":{"type":"string","format":"date-time"},"requests":{"type":"integer"},"appErrors":{"type":"integer"},"badRequests":{"type":"integer"}}}}}},"ModelUsage":{"type":"object","required":["model","internalFeature","requests","costUsd","inputTokens","outputTokens","avgLatencyMs","errors"],"properties":{"model":{"type":"string","nullable":true,"description":"The model the caller asked for. NULL when `internalFeature` is set: that row is one of our own features calling a model on your behalf, and which model it chose is ours rather than yours."},"internalFeature":{"type":"string","nullable":true,"description":"Set when this row is a platform feature — the automatic deploy fix, or setup advice — rather than a request you made. Billed to you like any other usage; grouped here by the feature so the spend is accountable without naming a model you never chose."},"requests":{"type":"integer"},"costUsd":{"type":"string","description":"Decimal USD as a string"},"inputTokens":{"type":"integer"},"outputTokens":{"type":"integer"},"avgLatencyMs":{"type":"integer"},"errors":{"type":"integer"}}},"UsageRecord":{"type":"object","required":["id","apiKeyId","requestedModel","internalFeature","endpoint","status","inputTokens","outputTokens","costUsd","builtinToolCalls","builtinToolUsd","latencyMs","cached","requestId","errorCode","ttftMs","toolsAuto","documentPages","documentScannedPages","via","createdAt"],"properties":{"id":{"type":"integer"},"apiKeyId":{"type":"string","format":"uuid","nullable":true},"requestedModel":{"type":"string","nullable":true,"description":"The model you asked for. Null when Roar made the call for you — see internalFeature"},"internalFeature":{"type":"string","nullable":true,"description":"Set when Roar made this call on your behalf: `insight` (app setup advice), `autofix` (automatic deploy fix), `explain` (deploy failure explainer). Null for your own requests"},"endpoint":{"type":"string"},"status":{"type":"integer","description":"HTTP status we answered with"},"inputTokens":{"type":"integer","nullable":true},"outputTokens":{"type":"integer","nullable":true},"costUsd":{"type":"string","description":"Decimal USD as a string"},"builtinToolCalls":{"type":"object","nullable":true,"additionalProperties":{"type":"integer"},"description":"Built-in tools that ran on this call, e.g. `{ \"web_search\": 2 }`; null when none did"},"builtinToolUsd":{"type":"string","description":"The part of `costUsd` charged for those tools — included in it, not added"},"latencyMs":{"type":"integer","nullable":true},"cached":{"type":"boolean"},"requestId":{"type":"string","nullable":true},"errorCode":{"type":"string","nullable":true},"ttftMs":{"type":"integer","nullable":true,"description":"Time to the first token, for a streamed call: when the first text, reasoning or tool call arrived. Null when not streamed, or when no output arrived"},"toolsAuto":{"type":"boolean","description":"Roar added your company's tools to this request itself — the app's “Let this app's AI use its tools” switch"},"documentPages":{"type":"integer","nullable":true,"description":"PDF pages we read for this request so the model could take them. Null when no document was converted"},"documentScannedPages":{"type":"integer","nullable":true,"description":"Of those pages, how many were scans sent to the model as page images. Null when no document was converted"},"ignoredParams":{"type":"array","items":{"type":"string"},"nullable":true,"description":"Sampling settings this request was served without, because the model runs at a fixed configuration (temperature, top_p). Also sent as the x-roar-ignored-params response header. Null when none were"},"via":{"type":"string","nullable":true,"enum":["mcp",null],"description":"mcp when an assistant connected over MCP made this call with the send_message or generate_image tool; null for every other call"},"createdAt":{"type":"string","format":"date-time"}}},"BudgetReport":{"type":"object","required":["data","summary","balanceUsd"],"properties":{"balanceUsd":{"type":"number","description":"The company's prepaid credit balance in USD — the number on Billing. Readable by any key with `usage:read`, so an app can warn before it runs out. The statement and top-ups are not available to keys."},"data":{"type":"array","items":{"$ref":"#/components/schemas/BudgetStatus"}},"summary":{"type":"object","required":["tracked","alerting","exceeded"],"properties":{"tracked":{"type":"integer"},"alerting":{"type":"integer"},"exceeded":{"type":"integer"}}}}},"BudgetStatus":{"type":"object","required":["scope","id","spendLimitUsd","budgetPeriod","hardCap","alertAtPct","periodStart","spendUsd","pctUsed","state"],"properties":{"scope":{"type":"string","enum":["client","project","key"]},"id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Absent for an unnamed key"},"spendLimitUsd":{"type":"number"},"budgetPeriod":{"type":"string","enum":["day","week","month","total"]},"hardCap":{"type":"boolean"},"alertAtPct":{"type":"integer"},"periodStart":{"type":"string","format":"date-time"},"spendUsd":{"type":"number"},"pctUsed":{"type":"number","nullable":true},"state":{"type":"string","enum":["ok","alerting","exceeded"]}}},"App":{"type":"object","required":["id","clientId","groupId","name","status","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"clientId":{"type":"string","format":"uuid"},"groupId":{"type":"string","format":"uuid","nullable":true,"description":"The project this app is filed under, if any"},"name":{"type":"string"},"status":{"type":"string"},"region":{"type":"string","nullable":true,"description":"The app's home cell — the region chosen at create time, not its current placement"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"AppList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AppListItem"}}}},"AppListItem":{"allOf":[{"$ref":"#/components/schemas/App"},{"type":"object","required":["url","lastDeploy","lastDeployedAt","permissions"],"properties":{"url":{"type":"string","nullable":true,"description":"The app's address, once a deploy has been confirmed serving on it"},"lastDeploy":{"type":"object","nullable":true,"required":["status","error","createdAt"],"properties":{"status":{"type":"string"},"error":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"lastDeployedAt":{"type":"string","format":"date-time","nullable":true,"description":"Null until the first successful deploy"},"serviceStatus":{"type":"string","nullable":true,"description":"The web service's status (creating|building|deploying|live|crashed|stopped); null when the app has no web service"},"permissions":{"$ref":"#/components/schemas/ProjectPermissions"}}}]},"OperationStarted":{"type":"object","required":["operationId","status"],"properties":{"operationId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["running"]}}},"OperationView":{"type":"object","required":["id","kind","status","result","error","startedAt","finishedAt"],"properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","description":"What ran: `autofix` or `app-insight` on this surface"},"status":{"type":"string","enum":["running","done","failed"]},"result":{"nullable":true,"description":"The body the starting route would have answered with. Null until `done`"},"error":{"type":"object","nullable":true,"required":["status","message"],"properties":{"status":{"type":"integer"},"message":{"type":"string"}},"description":"Set when `failed`: the status and message the route would have refused with"},"startedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time","nullable":true}}},"ProjectPermissions":{"type":"object","required":["role","canCreateKey","canDeploy","canListSecrets","canWriteSecrets","canManageSecrets","canManageMembers","canViewContent","canQueryData","canWriteData","canRestoreData","canSetBudget"],"properties":{"role":{"type":"string","enum":["viewer","developer","manager"],"nullable":true},"canCreateKey":{"type":"boolean"},"canDeploy":{"type":"boolean"},"canListSecrets":{"type":"boolean"},"canWriteSecrets":{"type":"boolean"},"canManageSecrets":{"type":"boolean"},"canManageMembers":{"type":"boolean"},"canViewContent":{"type":"boolean"},"canQueryData":{"type":"boolean"},"canWriteData":{"type":"boolean"},"canRestoreData":{"type":"boolean"},"canSetBudget":{"type":"boolean"}}},"AppDeployAccepted":{"type":"object","required":["project","service","deployment"],"properties":{"project":{"$ref":"#/components/schemas/App"},"service":{"$ref":"#/components/schemas/AppServiceView"},"deployment":{"$ref":"#/components/schemas/AppDeploymentQueued"}}},"AppDeploymentQueued":{"type":"object","required":["id","serviceId","status","commitSha","errorSummary","createdAt","finishedAt","timings"],"properties":{"id":{"type":"string","format":"uuid"},"serviceId":{"type":"string","format":"uuid"},"status":{"type":"string"},"commitSha":{"type":"string","nullable":true},"errorSummary":{"type":"string","nullable":true},"failureLabel":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time","nullable":true},"trigger":{"type":"string","nullable":true,"enum":["first_deploy","push","redeploy","restore","settings","database","autofix","retry","maintenance"]},"triggeredVia":{"type":"string","nullable":true,"enum":["console","api_key","mcp","github","platform"]},"triggeredBy":{"type":"string","nullable":true},"branch":{"type":"string","nullable":true},"commitMessage":{"type":"string","nullable":true},"restoredFromId":{"type":"string","format":"uuid","nullable":true},"timings":{"type":"object","nullable":true,"description":"Always null here — this deploy has not run, so there is nothing measured. GET /client/services/{id}/deployments carries the phase breakdown of a finished deploy."}}},"AppServiceView":{"type":"object","required":["id","projectId","kind","name","sourceRepo","status","runMigrations","deployOnPush","branch","memoryLimitMb","cpuLimit","emailFrom","aiUsesTools","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"projectId":{"type":"string","format":"uuid","description":"The APP's id — a service belongs to an app"},"kind":{"type":"string"},"name":{"type":"string"},"sourceRepo":{"type":"string","nullable":true,"description":"\"owner/name\" of the repo it deploys from"},"status":{"type":"string"},"runMigrations":{"type":"boolean"},"deployOnPush":{"type":"boolean"},"branch":{"type":"string","nullable":true},"memoryLimitMb":{"type":"integer","nullable":true,"description":"The most memory the app may use, in MB — a ceiling set automatically from the server it runs on, not a reservation and not a setting. Billing is on what the app actually uses. Null until the app has been placed on a server."},"cpuLimit":{"type":"number","nullable":true,"description":"The most CPU the app may use, in vCPU — set automatically, like memoryLimitMb. Null until the app has been placed on a server."},"emailFrom":{"type":"string","nullable":true},"aiUsesTools":{"type":"boolean","description":"“Let this app's AI use its tools”: Roar adds your company's tools to the app's own chat requests that bring none"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"AppAiTools":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"}}},"AppDeploymentView":{"type":"object","required":["id","serviceId","status","commitSha","errorSummary","createdAt","finishedAt","timings"],"properties":{"id":{"type":"string","format":"uuid"},"serviceId":{"type":"string","format":"uuid"},"status":{"type":"string"},"commitSha":{"type":"string","nullable":true},"errorSummary":{"type":"string","nullable":true},"failureLabel":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time","nullable":true},"trigger":{"type":"string","nullable":true,"enum":["first_deploy","push","redeploy","restore","settings","database","autofix","retry","maintenance"]},"triggeredVia":{"type":"string","nullable":true,"enum":["console","api_key","mcp","github","platform"]},"triggeredBy":{"type":"string","nullable":true},"branch":{"type":"string","nullable":true},"commitMessage":{"type":"string","nullable":true},"restoredFromId":{"type":"string","format":"uuid","nullable":true},"timings":{"$ref":"#/components/schemas/DeployTimings"}}},"AppDeploymentList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AppDeploymentView"}}}},"DeployTimings":{"type":"object","nullable":true,"required":["v","phases","imageBytes","source"],"properties":{"v":{"type":"integer","description":"Shape version. 1 today."},"phases":{"type":"array","description":"In the order they happened. A phase that did not run is ABSENT rather than zero — a deploy that reused an existing image did not build in 0ms.","items":{"type":"object","required":["name","ms"],"properties":{"name":{"type":"string","enum":["queue","clone","pull","build","migrate","start","health"]},"ms":{"type":"integer","description":"Wall clock for this phase."}}}},"imageBytes":{"type":"integer","nullable":true,"description":"Size of the image this deploy runs, or null if unknown."},"source":{"type":"string","enum":["built","pulled"],"description":"Whether this deploy built the app or reused the image already built from this commit."}}},"AppProgress":{"type":"object","required":["phase","runState","deployment","domain","url","serviceStatus","error","fix","canAutoFix","autoFixExhausted","failureKind","autoFixSummary","canRetry","skippedPush","securingReason","resourceWarning","memoryLimitMb","cpuLimit","updatedAt"],"properties":{"phase":{"type":"string","enum":["queued","building","deploying","securing","live","failed"],"description":"How the last DEPLOY went. `live` means it finished and its address was confirmed serving — not that the app is running now, which a later pause does not change. Read `runState` for that."},"runState":{"type":"string","enum":["running","starting","paused","crashed","unknown"],"description":"Whether the app is running right now. `paused` is an owner having stopped it — its URL is still reported, because that is the address it comes back on, but nothing answers there until it is started again."},"deployment":{"type":"object","nullable":true,"required":["id","status"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"}}},"domain":{"type":"object","nullable":true,"required":["domain","status"],"properties":{"domain":{"type":"string"},"status":{"type":"string"}}},"url":{"type":"string","nullable":true,"description":"The app's public address, once a deploy has been confirmed serving on it."},"serviceStatus":{"type":"string","nullable":true},"error":{"type":"string","nullable":true,"description":"Plain-language failure reason while `phase` is `failed`."},"fix":{"type":"string","nullable":true,"description":"A suggested fix for that failure, when we have one."},"canAutoFix":{"type":"boolean"},"autoFixExhausted":{"type":"boolean","description":"The fixer handles this failure, but the attempt budget for this episode is spent. Distinct from canAutoFix:false, which also covers failures we never fix."},"failureKind":{"type":"string","nullable":true,"enum":["start","code","settings","transient","platform","unsupported","unknown",null],"description":"What the failure needs: a start command, a code fix, a setting, a retry, us, or an unsupported app. Null while not failed."},"autoFixSummary":{"type":"string","nullable":true},"canRetry":{"type":"boolean","description":"Whether a failed app is far enough provisioned for a redeploy to be worth trying."},"skippedPush":{"type":"object","nullable":true,"required":["branch","commitSha","at"],"description":"A push that arrived and was deliberately not deployed, because the app was paused. Null once anything has deployed since.","properties":{"branch":{"type":"string"},"commitSha":{"type":"string","nullable":true},"at":{"type":"string","format":"date-time"}}},"securingReason":{"type":"string","nullable":true,"description":"Why the address is taking longer than the normal window. Null inside it."},"resourceWarning":{"type":"string","nullable":true,"description":"Plain-language note about the app's own resource use — today, that it was killed for running out of memory."},"memoryLimitMb":{"type":"integer","nullable":true,"description":"The most memory the app may use, in MB — a ceiling set automatically from the server it runs on, not a reservation and not a setting. Billing is on what the app actually uses. Null until the app has been placed on a server."},"cpuLimit":{"type":"number","nullable":true,"description":"The most CPU the app may use, in vCPU — set automatically, like memoryLimitMb. Null until the app has been placed on a server."},"updatedAt":{"type":"string","format":"date-time"}}},"DeployAppRequest":{"type":"object","required":["name","repo"],"additionalProperties":false,"properties":{"name":{"type":"string"},"repo":{"type":"string","description":"owner/name"},"branch":{"type":"string"},"rootDir":{"type":"string","description":"The app’s folder in a repository that holds several apps. Absent means found automatically."},"startCommand":{"type":"string","description":"How the app starts, when the repository does not say (one line, at most 300 characters). Stored as the build settings start command."},"env":{"type":"object","additionalProperties":{"type":"string"}},"projectId":{"type":"string","format":"uuid","nullable":true}}},"LaunchTemplateRequest":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","description":"Absent means the template’s own name, e.g. `n8n`."},"cell":{"type":"string","description":"The region, as offered by the app wizard. Absent means your default region."},"cloudflareProtection":{"type":"boolean","description":"Serve the app’s address behind protection. Absent means on."},"projectId":{"type":"string","format":"uuid","nullable":true}}},"Project":{"type":"object","required":["id","name","status","spendLimitUsd","budgetPeriod","hardCap","alertAtPct","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string","enum":["active","archived"]},"spendLimitUsd":{"type":"string","description":"Decimal USD as a string","nullable":true},"budgetPeriod":{"type":"string","enum":["day","week","month","total"]},"hardCap":{"type":"boolean"},"alertAtPct":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"}}},"ProjectList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ProjectListItem"}}}},"ProjectListItem":{"allOf":[{"$ref":"#/components/schemas/Project"},{"type":"object","required":["spendUsd","pctUsed","keys","apps","members","permissions"],"properties":{"spendUsd":{"type":"number","description":"Spend in the project's current budget period"},"pctUsed":{"type":"number","nullable":true,"description":"Null when the project has no cap"},"keys":{"type":"integer"},"apps":{"type":"integer"},"members":{"type":"integer"},"permissions":{"$ref":"#/components/schemas/ProjectPermissions"}}}]},"CreatedProject":{"allOf":[{"$ref":"#/components/schemas/Project"},{"type":"object","required":["clientId"],"properties":{"clientId":{"type":"string","format":"uuid"},"mcpDisabledAt":{"type":"string","format":"date-time","nullable":true,"description":"Set when Roar has switched the MCP gateway off for this project: no key in it can use gateway tools. Null = on."}}}]},"CreateProjectRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"spendLimitUsd":{"type":"number","nullable":true},"budgetPeriod":{"type":"string","enum":["day","week","month","total"]},"hardCap":{"type":"boolean"},"alertAtPct":{"type":"integer"}}},"WebhookEvent":{"type":"string","enum":["deploy.succeeded","deploy.failed","budget.alerting","budget.exceeded","credits.low","credits.exhausted","app.egress_spike","ping"]},"WebhookSubscription":{"type":"string","enum":["*","deploy.succeeded","deploy.failed","budget.alerting","budget.exceeded","credits.low","credits.exhausted","app.egress_spike","ping"]},"Webhook":{"type":"object","required":["id","url","events","description","enabled","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookSubscription"}},"description":{"type":"string","nullable":true},"enabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}},"WebhookListItem":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","required":["lastStatus","lastDeliveryAt"],"properties":{"lastStatus":{"type":"integer","nullable":true},"lastDeliveryAt":{"type":"string","format":"date-time","nullable":true}}}]},"WebhookList":{"type":"object","required":["data","events"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookListItem"}},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"},"description":"Every event that can be delivered"}}},"CreateWebhookRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Public https endpoint"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookSubscription"},"default":["*"]},"description":{"type":"string"}}},"UpdateWebhookRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookSubscription"}},"description":{"type":"string","nullable":true},"enabled":{"type":"boolean"}}},"CreatedWebhook":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","required":["secret","warning"],"properties":{"secret":{"type":"string","description":"HMAC signing secret — shown once"},"warning":{"type":"string"}}}]},"Ok":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"ServiceLogs":{"type":"object","required":["data","redacted","source","collectedAt","withheld","trimmed"],"properties":{"data":{"type":"array","items":{"type":"string"},"description":"Lines, oldest first"},"redacted":{"type":"boolean","description":"Always true for a key: our machine identity is stripped, so a line reads `<timestamp> <message>`"},"source":{"type":"string","enum":["runtime","build","none"],"description":"`runtime` is the app's own output; `build` is the fallback for an app that has never sent one"},"collectedAt":{"type":"string","format":"date-time","nullable":true,"description":"When the node last sent this tail. Null for build output and when we hold nothing"},"withheld":{"type":"integer","description":"Lines not shown to you because the node tagged them operator-only"},"trimmed":{"type":"boolean","nullable":true,"description":"Whether BUILD output is only its end. Meaningful when `source` is `build`; null means we do not know"}}},"AppEnvView":{"type":"object","required":["data","managed","canReveal","roarDefaults"],"properties":{"data":{"type":"object","additionalProperties":{"type":"string","nullable":true},"description":"Your own variables. A null VALUE means the variable exists and your role may not read it — not that it is blank"},"roarDefaults":{"type":"array","items":{"type":"string"},"description":"Names in `data` that still hold the value Roar set for your app (APP_NAME, APP_ENVIRONMENT, APP_URL, APP_PUBLIC_URL). You may change them — your value then replaces ours, and the name leaves this list"},"canReveal":{"type":"boolean","description":"False when every value above was withheld"},"managed":{"type":"array","description":"Variables Roar sets on the app. Read-only: they are refused at the write door","items":{"type":"object","required":["name","value","note"],"properties":{"name":{"type":"string"},"value":{"type":"string","nullable":true,"description":"Null on the same terms as `data` above"},"note":{"type":"string","description":"What it is for, in plain language"}}}}}},"WebhookDeliveryList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}}}},"WebhookDelivery":{"type":"object","required":["id","event","status","attempts","responseStatus","error","createdAt","deliveredAt"],"properties":{"id":{"type":"string","format":"uuid"},"event":{"type":"string"},"status":{"type":"string","enum":["pending","delivered","failed"]},"attempts":{"type":"integer"},"responseStatus":{"type":"integer","nullable":true},"error":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"deliveredAt":{"type":"string","format":"date-time","nullable":true}}},"McpUpstream":{"type":"object","required":["id","clientId","name","url","authType","authHeader","status","argsPolicy","toolsFetchedAt","lastError","createdAt","updatedAt","hasCredential"],"properties":{"id":{"type":"string","format":"uuid"},"clientId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"The prefix agents see on its tools: `<name>__<tool>`"},"url":{"type":"string","format":"uri","nullable":true,"description":"Null unless the caller may change tool servers: an owner or admin session, or an org-level key with mcp:write. An address can carry a token in its path."},"authType":{"type":"string","enum":["none","bearer","header","oauth"],"description":"`oauth`: an owner or admin SIGNS IN to it; its state is `GET /client/mcp/upstreams/{id}/oauth`"},"authHeader":{"type":"string","nullable":true,"description":"The header NAME for `header` auth — never its value. Null on the same terms as `url`"},"status":{"type":"string","enum":["active","disabled"]},"argsPolicy":{"type":"string","enum":["pass","redact","block"],"description":"What happens to personal data in a call’s ARGUMENTS. Results are always masked"},"toolsFetchedAt":{"type":"string","format":"date-time","nullable":true,"description":"Null until the first successful refresh"},"lastError":{"type":"string","nullable":true,"description":"Why the last refresh failed, in words safe to show. Null after a success"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"hasCredential":{"type":"boolean","description":"Whether a credential is stored. The credential itself is never returned"}}},"McpUpstreamList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/McpUpstream"}}}},"McpUpstreamAuth":{"type":"object","required":["type"],"description":"`{\"type\":\"none\"}`, `{\"type\":\"bearer\",\"token\":\"…\"}`, `{\"type\":\"header\",\"header\":\"X-Api-Key\",\"value\":\"…\"}` or `{\"type\":\"oauth\"}` (sign in after saving — nothing is pasted). Stored encrypted; never returned","properties":{"type":{"type":"string","enum":["none","bearer","header","oauth"]},"token":{"type":"string","description":"For `bearer`"},"header":{"type":"string","description":"For `header`: a header name the connection does not itself use"},"value":{"type":"string","description":"For `header`"}}},"CreateMcpUpstreamRequest":{"type":"object","required":["name","url"],"properties":{"name":{"type":"string","description":"Lower-case letters, digits and single hyphens, 1–20 characters. Unique in your organisation"},"url":{"type":"string","format":"uri","description":"The server’s Streamable HTTP endpoint. https only, no username or password in it"},"auth":{"$ref":"#/components/schemas/McpUpstreamAuth"},"argsPolicy":{"type":"string","enum":["pass","redact","block"],"default":"pass"}}},"UpdateMcpUpstreamRequest":{"type":"object","description":"At least one field. Moving a server that holds a credential to a different host requires `auth` in the same request","properties":{"url":{"type":"string","format":"uri"},"auth":{"$ref":"#/components/schemas/McpUpstreamAuth"},"status":{"type":"string","enum":["active","disabled"]},"argsPolicy":{"type":"string","enum":["pass","redact","block"]}}},"McpSetupState":{"type":"object","description":"How far setup has got, each step read from what exists now. The two steps about keys count only the projects you belong to","required":["serverAdded","toolsRead","toolsApproved","bundleMade","accessGiven","agentConnected","signInNeeded"],"properties":{"serverAdded":{"type":"boolean","description":"A tool server is registered and ready — it needs no sign-in, or it is signed in"},"toolsRead":{"type":"boolean","description":"A server’s tool list has been read"},"toolsApproved":{"type":"boolean","description":"A tool is approved at its current definition, listed and switched on"},"bundleMade":{"type":"boolean","description":"A bundle holds a tool"},"accessGiven":{"type":"boolean","description":"A key you can see would be served a tool now"},"agentConnected":{"type":"boolean","description":"An agent has used a tool, with a key you can see, in the last 30 days"},"signInNeeded":{"type":"object","nullable":true,"required":["name","again"],"properties":{"name":{"type":"string"},"again":{"type":"boolean","description":"Its sign-in stopped working, rather than never having happened"}},"description":"The oldest tool server that signs in and is not signed in; null when there is none"}}},"TestMcpUpstreamRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"The address to try. https only, no username or password in it"},"auth":{"$ref":"#/components/schemas/McpUpstreamAuth"},"upstreamId":{"type":"string","format":"uuid","description":"Test with THIS saved server’s credential when `auth` is left out. It is used only for the exact address it was saved for; to test another address, send `auth`"}}},"McpTestResult":{"type":"object","required":["count","tools","auth"],"properties":{"auth":{"$ref":"#/components/schemas/McpAuthDetection","nullable":true,"description":"What the server asks for, found by calling it without a credential — only when the test had none to send. With `oauth` or `token`, `tools` is empty. Null when a credential was sent"},"count":{"type":"integer","description":"How many tools the server lists"},"tools":{"type":"array","items":{"type":"object","required":["name","description"],"properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true,"description":"Cut to 300 characters"}}}}}},"McpAuthDetection":{"type":"object","required":["kind","signInHost"],"properties":{"kind":{"type":"string","enum":["oauth","token","none","unknown"],"description":"`oauth`: it names a sign-in server; `token`: it wants a credential it does not say how to get; `none`: it answered without one"},"signInHost":{"type":"string","nullable":true,"description":"For `oauth`: where a person would sign in"}}},"McpSignInStatus":{"type":"object","description":"Where one signed-in tool server stands. No token or secret is ever returned","required":["upstreamId","state","signedInBy","signedInByFormerMember","signedInAt","accountLabel","accountLabelVerified","signInHost","knownVendor","vendor","scopes","problem"],"properties":{"upstreamId":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["not_signed_in","connected","needs_sign_in"]},"signedInBy":{"type":"object","nullable":true,"required":["id","name","email"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"description":"The person who signed in. Null once they are deleted"},"signedInByFormerMember":{"type":"boolean","description":"The person who signed in has left the organisation: sign in again as someone current"},"signedInAt":{"type":"string","format":"date-time","nullable":true},"accountLabel":{"type":"string","nullable":true,"description":"The vendor account the sign-in page REPORTED, when it named one. Unverified"},"accountLabelVerified":{"type":"boolean","enum":[false],"description":"Always false: `accountLabel` is not verified"},"signInHost":{"type":"string","nullable":true,"description":"The sign-in server’s host"},"knownVendor":{"type":"boolean","description":"Whether the sign-in is a vendor Roar recognises, rather than a server’s own"},"vendor":{"type":"string","nullable":true,"description":"That vendor’s name, when `knownVendor`"},"scopes":{"type":"array","items":{"type":"string"},"description":"The permissions granted"},"problem":{"type":"string","nullable":true,"enum":["renewal_refused","server_refused","no_refresh_token","app_removed","unreadable",null],"description":"Why it needs signing in again. Null unless `state` is `needs_sign_in`"}}},"McpSignInStatusList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/McpSignInStatus"}}}},"McpSignOutResult":{"type":"object","required":["signedOut","revoked"],"properties":{"signedOut":{"type":"boolean","description":"False when it was not signed in"},"revoked":{"type":"boolean","description":"Whether the vendor confirmed revoking the tokens. They are deleted either way"}}},"McpRefreshResult":{"type":"object","required":["tools","added","changed","removed"],"properties":{"tools":{"type":"integer","description":"Tools the server lists now"},"added":{"type":"integer","description":"New since the last refresh — they wait for approval"},"changed":{"type":"integer","description":"Changed since the last refresh — hidden from agents until approved again"},"removed":{"type":"integer","description":"No longer listed"}}},"McpTool":{"type":"object","required":["id","clientId","upstreamId","name","definition","definitionHash","approvedHash","status","removedAt","effectOverride","effect","createdAt","updatedAt","namespacedName","state","fixedArgs","descriptionOverride","lockIssues"],"properties":{"id":{"type":"string","format":"uuid"},"clientId":{"type":"string","format":"uuid"},"upstreamId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"The server’s own name for the tool"},"definition":{"type":"object","required":["name"],"additionalProperties":true,"description":"The tool exactly as the server lists it (description, inputSchema, annotations …) — what an agent’s model reads","properties":{"name":{"type":"string"}}},"definitionHash":{"type":"string","description":"Fingerprint of the definition as listed now. Approving sends it back"},"approvedHash":{"type":"string","nullable":true,"description":"The fingerprint that was approved; null if never"},"status":{"type":"string","enum":["active","disabled"]},"removedAt":{"type":"string","format":"date-time","nullable":true,"description":"Set when the server stops listing it"},"effectOverride":{"type":"string","enum":["reads","changes"],"nullable":true,"description":"Your word on what the tool does; null to go by the server’s own `readOnlyHint`"},"effect":{"type":"string","enum":["reads","changes"],"description":"What the gateway treats it as: your override, else `reads` only when the server marks it read-only. Decides what “ask before changes” asks about"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"namespacedName":{"type":"string","description":"What agents call it: `<server>__<tool>`"},"state":{"type":"string","enum":["served","removed","disabled","unapproved","drifted","lock_mismatch","bad_name"],"description":"Only `served` reaches an agent. `drifted`: approved, then changed by the server. `lock_mismatch`: a locked input no longer fits the tool"},"fixedArgs":{"type":"object","nullable":true,"additionalProperties":{"type":["string","number","boolean"]},"description":"Locked inputs, `{ name: value }`: the gateway always sends these values, and agents never see these inputs. Null for none"},"descriptionOverride":{"type":"string","nullable":true,"description":"What agents read instead of the server’s description; null for the server’s own"},"lockIssues":{"type":"array","description":"Locks that no longer fit the tool as its server lists it today","items":{"type":"object","required":["name","problem"],"properties":{"name":{"type":"string"},"problem":{"type":"string"}}}}}},"McpToolList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/McpTool"}}}},"ApproveMcpToolsRequest":{"type":"object","required":["tools"],"properties":{"tools":{"type":"array","items":{"type":"object","required":["id","definitionHash"],"properties":{"id":{"type":"string","format":"uuid"},"definitionHash":{"type":"string","description":"As `GET /client/mcp/tools` showed it"}}}}}},"McpApproveResult":{"type":"object","required":["approved"],"properties":{"approved":{"type":"integer"}}},"McpToolGroup":{"type":"object","required":["id","clientId","name","description","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"clientId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"McpToolGroupListItem":{"allOf":[{"$ref":"#/components/schemas/McpToolGroup"},{"type":"object","required":["toolIds"],"properties":{"toolIds":{"type":"array","items":{"type":"string","format":"uuid"}}}}]},"McpToolGroupList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/McpToolGroupListItem"}}}},"McpGrant":{"type":"object","required":["id","clientId","groupId","apiKeyId","projectId","access","callLimit","callLimitPer","callsToday","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"clientId":{"type":"string","format":"uuid"},"groupId":{"type":"string","format":"uuid"},"apiKeyId":{"type":"string","format":"uuid","nullable":true,"description":"Set for a grant to one key"},"projectId":{"type":"string","format":"uuid","nullable":true,"description":"Set for a grant to every key in a project"},"access":{"$ref":"#/components/schemas/McpGrantAccess"},"callLimit":{"type":"integer","nullable":true,"description":"At most this many calls through the grant per `callLimitPer`; null for no limit"},"callLimitPer":{"type":"string","enum":["minute","day"],"nullable":true,"description":"A day is a UTC day"},"callsToday":{"type":"integer","nullable":true,"description":"Calls through the grant today (UTC), for a per-day limit only"},"createdAt":{"type":"string","format":"date-time"}}},"McpCallLimit":{"type":"object","nullable":true,"required":["count","per"],"description":"At most `count` calls a minute or a day (UTC) through the grant. Null for no limit","properties":{"count":{"type":"integer","minimum":1,"maximum":1000000},"per":{"type":"string","enum":["minute","day"]}}},"McpGrantAccess":{"type":"string","enum":["read_only","ask","full"],"description":"`read_only`: tools that change things are hidden. `ask`: each call to one waits for an owner or admin to approve it in the console. `full`: every tool runs. Where several grants serve one tool, the most permissive wins"},"McpGrantList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/McpGrant"}}}},"McpApproval":{"type":"object","required":["id","ref","status","toolId","toolName","apiKeyId","keyName","keyPrefix","requestedAt","expiresAt","decidedAt","decidedByName","usedAt","arguments"],"properties":{"id":{"type":"string","format":"uuid"},"ref":{"type":"string","description":"The short reference the agent was given"},"status":{"type":"string","enum":["pending","approved","denied","expired","used"]},"toolId":{"type":"string","format":"uuid"},"toolName":{"type":"string","description":"What agents call the tool: `<server>__<tool>`"},"apiKeyId":{"type":"string","format":"uuid"},"keyName":{"type":"string","nullable":true},"keyPrefix":{"type":"string"},"requestedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time","description":"Waiting: when it lapses (24 hours). Decided: until when the decision answers the agent’s retry (15 minutes)"},"decidedAt":{"type":"string","format":"date-time","nullable":true},"decidedByName":{"type":"string","nullable":true,"description":"Who decided, when it was one of your own people"},"usedAt":{"type":"string","format":"date-time","nullable":true,"description":"When the approval was used: the agent’s retry was sent on to the tool. Not whether the tool then succeeded"},"arguments":{"type":"object","nullable":true,"description":"Always null for an API key. In the console, a signed-in owner or admin sees `{ preview, truncated }` — the call’s arguments masked by your privacy detectors — while it waits; they are deleted once anyone decides"}}},"McpApprovalList":{"type":"object","required":["data","canDecide","argumentsWithheld"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/McpApproval"}},"canDecide":{"type":"boolean","description":"Whether you may approve or decline: a signed-in owner or admin. Never a key"},"argumentsWithheld":{"type":"object","nullable":true,"required":["country"],"properties":{"country":{"type":"string","nullable":true,"description":"Two-letter country code, lower case"}},"description":"Set when your data stays in one country: no call’s arguments are stored for approval, so `arguments` is null for every request and the approver sees the tool and the key only"}}},"McpSwitches":{"type":"object","required":["enabled","disabledAt","projects"],"properties":{"enabled":{"type":"boolean","description":"False when Roar has turned the gateway off for your organisation. Not settable here"},"disabledAt":{"type":"string","format":"date-time","nullable":true},"projects":{"type":"array","items":{"type":"object","required":["id","name","enabled","disabledAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"enabled":{"type":"boolean"},"disabledAt":{"type":"string","format":"date-time","nullable":true}}}}}},"McpKeyToolList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"type":"object","required":["name","upstreamId","upstreamToolName"],"properties":{"name":{"type":"string","description":"What the agent calls it"},"upstreamId":{"type":"string","format":"uuid"},"upstreamToolName":{"type":"string"}}}}}},"McpOverview":{"type":"object","required":["gatewayUrl","enabled","disabledAt","canManage","grantProjectIds","maxUpstreams","keys","setup"],"properties":{"gatewayUrl":{"type":"string","format":"uri","description":"Where an agent connects over MCP, sending `Authorization: Bearer <key>`"},"enabled":{"type":"boolean","description":"False when Roar has turned the gateway off for your organisation"},"disabledAt":{"type":"string","format":"date-time","nullable":true},"canManage":{"type":"boolean","description":"Whether this caller may change servers, tools, bundles, every grant and project switches"},"grantProjectIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Projects this caller may give bundles to and take them from"},"maxUpstreams":{"type":"integer","description":"The most tool servers your organisation may register"},"keys":{"type":"array","description":"Keys this caller can see that would be served at least one tool now","items":{"type":"object","required":["id","name","prefix","projectId","tools"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"prefix":{"type":"string"},"projectId":{"type":"string","format":"uuid","nullable":true},"tools":{"type":"array","items":{"type":"string"},"description":"Tool names as the agent sees them"}}}},"setup":{"$ref":"#/components/schemas/McpSetupState"}}}}}}