# Grid82 > Build on Grid82 from a script, a terminal or an agent: the REST API, the MCP servers and the CLI, for the Studio and for every app, each acting as the person whose credential it is. Answer from these pages rather than from memory: they describe the current API version (/v1). Grid82 has two planes. The Studio (https://studio.grid82.app) is where builders define apps; its Builder API is the same for everyone. Each app runs in its suite's Runtime at https://{suite}.grid82.app/{app}, and its App API is generated per caller: the real OpenAPI document is https://{suite}.grid82.app/{app}/v1/openapi.json, masked to the tables and fields that caller can read, so it is fetched with the caller's own key (without one it answers 401). The App API reference here is the generic contract and names no real table or field. Never invent table or field names; read them from the caller's own schema. The OpenAPI documents at /docs/openapi/builder.json and /docs/openapi/app.json are authoritative for request and response shapes. Where a guide and the reference disagree, the reference wins. Every credential acts as one person and can never do more than that person. There is no admin key. Keys start with g82_. Grid82 is pre-release and not open to sign-ups. The CLI is on npm: `npx grid82`. Every page is also served as Markdown: append `.md` to its URL. Cite the HTML URL, which is the canonical one. --- # Grid82 developer documentation Source: https://grid82.com/docs A Grid82 app is a database plus permissions. Everything a person can do in it, a machine can do too: through the **REST API**, through **MCP** for agents, and soon through the **CLI**. All three are generated from one definition, and every one of them acts as a person, with that person's permissions and nothing more. > **Info: Grid82 is in private pre-release** > > Grid82 is not open to sign-ups yet, and only I use the Studio today. These pages describe the > API as it runs now. If someone invited you into an app, everything here works with your own > account: start with [Connect an agent](/docs/agents/setup) or the > [Quickstart](/docs/start/quickstart). ## Two products, two APIs Grid82 has two sides, and each has its own API. Knowing which one you need is most of getting started. | | **The Studio** | **An app** | | ----- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Who | builders, who define apps | the people invited into an app | | Where | `https://studio.grid82.app` | `https://{suite}.grid82.app/{app}` | | REST | the Builder API, `/v1` on the Studio | the App API, `/{app}/v1` on the suite | | MCP | `https://studio.grid82.app/mcp` | `https://{suite}.grid82.app/mcp`, one server for every app of the suite | | Can | create apps, change tables and fields, write permission rules, manage people | read and write the records you may see, save views, build your own pages | [Read how the two fit together](/docs/start/studio-and-runtime). [**Connect an agent**](/docs/agents/setup) One prompt to paste into Claude, ChatGPT, Cursor or VS Code. The agent connects itself and you sign in. [**Quickstart**](/docs/start/quickstart) Make a key, read a table, write a record: five steps with curl. [**App API reference**](/docs/app-api/records/list) Records, views, comments, files, SQL, undo and the change feed of an app. [**Builder API reference**](/docs/builder-api/apps/list) Organisations, suites, apps, schema, permission rules, people and domains. ## Three things that are true everywhere **A credential acts as a person.** An API key, an OAuth token and an agent's connection all belong to someone. A key made by someone in Sales sees what Sales sees. There is no service account with a view of everything. See [Authentication](/docs/start/authentication). **What you cannot read does not exist for you.** A field you may not read is not blanked: it is absent from the records, from the schema, from your OpenAPI document and from your agent's tools, and no error names it. See [Permissions](/docs/start/permissions). **Every change can be traced, and most can be undone.** Each write, from any surface, lands in the app's change history. See [Undo and history](/docs/api/undo). --- # The Studio and the Runtime Source: https://grid82.com/docs/start/studio-and-runtime Grid82 is two products built on one definition. The **Studio** is where an app is designed. The **Runtime** is where the people invited into it use it. The app's definition lives beside its data, so a published app keeps working without the Studio. ## Organisations, suites and apps ```text organisation who pays and who builds builders sign in at studio.grid82.app └── suite a set of apps with ONE login acme.grid82.app, or your own domain └── app tables, permission rules, pages acme.grid82.app/crm ``` A **suite** is a login boundary: its apps share one directory of people, one sign-in page and one address. The same person in two suites is two accounts. An **app** lives under its suite's address at its own path, so the CRM of the suite `acme` is `https://acme.grid82.app/crm`. ## The two planes | | **Studio (builder plane)** | **Runtime (app plane)** | | ----------- | ------------------------------------------ | ---------------------------------------------------------------- | | Who | builders of an organisation | people invited into a suite's apps | | Signs in at | `studio.grid82.app` | the suite's own address | | REST | `https://studio.grid82.app/v1` | `https://{suite}.grid82.app/{app}/v1` | | OpenAPI | one public document, the same for everyone | one per app and per person, generated on request | | MCP | `https://studio.grid82.app/mcp` | `https://{suite}.grid82.app/mcp`, one server for the whole suite | | Credentials | builder keys and OAuth grants | personal keys and OAuth grants, per suite | **The planes are a wall, not a filter.** A key made in an app is not a weaker builder key: the Studio does not recognise it at all (`401`), and the reverse is true too. An app credential can never be granted a builder scope such as `schema:write`, so no consent screen can even offer an end user's agent the power to change an app's structure. **A builder is not automatically a member of their own apps.** The Studio reaches every record of an app through its own routes (`/v1/apps/{appId}/tables/{tableId}/records`), with every write recorded under the builder's name. Inside the app's Runtime, a builder is a person like any other, and sees what their roles give them. ## Why an app has an API of its own Every app answers its own API under its own path, and that API is shaped like the person calling it: - `GET /{app}/v1/openapi.json` is generated on each request from the schema **you** can see: your tables, and only the fields you can read. A table you cannot open is not in the document. - The MCP tools you receive are built the same way. `tableId` is a closed list of your tables, and a record is typed with your readable fields only. - A suite on its own domain serves the same paths there, with no `grid82.com` in them: `https://portal.example.com/crm/v1`. That is why the [App API reference](/docs/app-api/records/list) on this site is a **generic contract**: the operations and their shapes, with no table or field named. Your real document is at `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential, and there is no anonymous version of it. The Studio's [Builder API](/docs/builder-api/apps/list) is the opposite case: it describes the platform, not anybody's data, so its document is public at `https://studio.grid82.app/v1/openapi.json` and is the same for every reader. ## One MCP server per suite A suite answers MCP at `https://{suite}.grid82.app/mcp`, one address for every app of the suite you belong to. Its first tool, `apps_list`, tells an agent which apps it can reach, and every app tool takes an `appId`. Adding an app to the suite does not make anybody reconfigure their client. When you approve a connection, you choose which of your apps it may reach: an integration that must touch one app and nothing else is approved for that app alone. See [MCP servers](/docs/agents/overview). --- # Quickstart Source: https://grid82.com/docs/start/quickstart This is five steps with curl and needs an app you can sign in to. Everything below acts as you: the key sees exactly what you see in the app, and nothing more. 1. **Make a key** In your app, open **Settings**, then **Connections**, then **For developers**, and create a key. Pick the scopes it needs (`records:read` and `records:write` for this guide). Copy it now: it is shown once. An app lives under its suite's address, at its own path: ```bash export GRID82_TOKEN="g82_live_…" export GRID82_APP="https://your-suite.grid82.app/your-app" ``` 2. **See what you can reach** ```bash curl "$GRID82_APP/v1/schema" -H "Authorization: Bearer $GRID82_TOKEN" ``` The answer lists your tables and, in each, the fields you can read. Table ids look like `tbl_8fj3kq2wmv5d` and field ids like `fld_3ns8wq5ke2pb`. Ids never change; names can. 3. **Read some records** ```bash curl "$GRID82_APP/v1/tables/tbl_8fj3kq2wmv5d/records?limit=5" \ -H "Authorization: Bearer $GRID82_TOKEN" ``` Every response has the same envelope. A record is keyed by field id, and `meta.fields` says which name each id has today, so you can show names without a second call: ```json { "data": [{ "id": "rec_4nqw8e2hzk7s", "fld_3ns8wq5ke2pb": "First record", "fld_7qk2mv9xp4ht": 20500 }], "meta": { "nextCursor": "eyJ…", "hasMore": true, "fields": { "fld_3ns8wq5ke2pb": "Name", "fld_7qk2mv9xp4ht": "Amount" } } } ``` 4. **Write one** Send an `Idempotency-Key` with every write. If the request is retried, and in a CI job it will be, you get the first answer back instead of a second row. ```bash curl -X POST "$GRID82_APP/v1/tables/tbl_8fj3kq2wmv5d/records" \ -H "Authorization: Bearer $GRID82_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"records":[{"Name":"Second record","Amount":1000}]}' ``` When you write, a field's exact name works in place of its id. A key that is neither is refused with a `422` naming it, so a typo never becomes an empty row. 5. **Hand it to an agent** In the same **Connections** screen, choose **Copy setup prompt** and paste it into Claude, ChatGPT, Cursor or VS Code. The agent adds the server and you sign in in your browser. See [Set up your agent](/docs/agents/setup). ## Where next - [Conventions](/docs/api/conventions): the envelope, pagination, filters and how records look on the wire. - [Retries and idempotency](/docs/api/idempotency): making a script safe to run twice. - [The App API reference](/docs/app-api/records/list): every operation of an app. - Your own OpenAPI document, which describes your tables: `curl "$GRID82_APP/v1/openapi.json" -H "Authorization: Bearer $GRID82_TOKEN"`. --- # Permissions Source: https://grid82.com/docs/start/permissions Every read and every write in an app, from the grid, the API, MCP or the built-in assistant, goes through the same permission check. There is no route around it, and no key that skips it. ## The model A builder gives each app **user roles**, and each role its own permissions: on which tables it may do which operations, optionally narrowed to some rows and some fields. A person can be in several roles. Their access is the **union** of their roles, taken per field and per operation. There is no order, no deny and no override. Adding a role can only ever add access, and what no role allows, nobody has. No role is special to the platform: there is no admin role in an app. Administration happens in the Studio, on the other plane, with its own roles for builders. ## Four questions, answered separately | Question | Example | | ------------- | -------------------------------------- | | Which tables | Support cannot see invoices at all | | Which rows | A rep sees the deals they own | | Which fields | Nobody outside Finance sees the margin | | Which actions | Read yes, delete no | ## What "cannot read" means A field you cannot read is **absent**, not blanked. It is not in the record, not in the schema you are served, not in your OpenAPI document, not in your agent's tool list, and not named in any error, because naming it would be the very leak the rule exists to prevent. A row you cannot see is answered as if it did not exist. That is also why an app's API reference on this site names no table and no field. Each person's document is generated from their own view of the app. See [Why an app has an API of its own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). ## Filters cannot be used to guess A filter can only narrow what you would see anyway. One case needs a rule: a field you may read on only **some** of your rows ("Finance sees the margin, on won deals"). Filtering or sorting by it would let you count rows outside your scope, so it is refused with `403`. `GET /v1/app` lists, per table, the fields you can see but not filter or sort by (`constrained`), so a client can grey them out before asking. ## Credentials narrow, never widen A key or an agent connection carries scopes (`records:read`, `records:write`, …). Scopes narrow what a credential may do; they never add to what its person may do. A key with `records:write`, made by someone who may only read, still cannot write. See [Authentication](/docs/start/authentication). ## Before changing a rule Builders can ask what someone would see, without granting anything. `rules.simulate` answers with the rows and fields a set of roles would get, broken down per role so you can see which role grants what: ```http POST https://studio.grid82.app/v1/apps/{appId}/rules/simulate ``` See [Ask what someone would see](/docs/builder-api/rules/simulate). --- # Authentication Source: https://grid82.com/docs/start/authentication Every credential Grid82 issues belongs to a person. An API key, an OAuth access token and an agent's connection all act **as** someone, with that person's permissions, narrowed by the scopes the credential was given. There is no ambient admin key and no service account. ## Send it as a bearer token ```http Authorization: Bearer g82_live_… ``` Keys start with `g82_`. Keys issued before Grid82 took its current name start with `g82_`; they keep working and need no change. An OAuth access token has the same format and is checked the same way. A credential is minted on **one plane** and is only accepted there. A key made in an app works in that app; an agent connected to a suite reaches the apps ticked when it was approved; a Studio key works on the Studio's Builder API. Presented to the other side, it is not a weaker credential, it is no credential at all (`401`). ## API keys | | In an app | In the Studio | | ------- | ------------------------------------------------- | ------------------------------------------------------------------ | | Where | **Settings**, **Connections**, **For developers** | the **Developer** page | | Acts as | you, in that app | you, as a builder of **one** organisation, chosen when you make it | | Expiry | 90 days | 90 days by default, or a number of days you choose, or never | A key is shown once, when it is made. Revoking it, in the same place, takes effect at once. **Keys are made and revoked by a person in a browser.** A key, or an agent connected by OAuth, cannot make another key or revoke one, and neither operation is an MCP tool. That way a key that leaks can be revoked and stays revoked: it cannot have minted a successor. **A Studio key acts in one organisation.** It cannot reach your other organisations, even though you can. ## Scopes | Scope | Lets a credential | Available in an app | | --------------- | ------------------------------------------------------------------------------------------------------ | ------------------- | | `records:read` | read the rows you can see, their comments and history, your saved views, SQL reads, the change history | yes | | `records:write` | create, change and delete rows, comment, upload files, undo | yes | | `schema:read` | see the tables and fields you have access to, and the app's rules | yes | | `schema:write` | change tables, fields and rules | Studio only | | `pages:read` | read pages, their code and history | yes | | `pages:write` | create, edit and publish pages: an app's in the Studio, your own in an app | yes | | `members:read` | see who is in an app and the directory | yes | | `members:write` | add people, change their roles, make invite links | Studio only | | `account:read` | see organisations, suites, apps, plan and usage | Studio only | | `account:write` | start, leave or delete an organisation, close your account | Studio only | A credential can only ever be **narrower** than the person behind it. A key with `records:write`, made by someone who may only read, still cannot write. Check what a credential is holding: ```bash curl "https://your-suite.grid82.app/your-app/v1/me" -H "Authorization: Bearer $GRID82_TOKEN" ``` ## OAuth 2.1, for agents and tools MCP clients and other tools connect with OAuth, so nobody pastes a key into them. The Studio and every suite are each their own authorisation server. - **Discovery.** `/.well-known/oauth-authorization-server` (RFC 8414) and `/.well-known/oauth-protected-resource` (RFC 9728) on the Studio and on every suite's address. An unauthenticated MCP request answers `401` with a `WWW-Authenticate` header naming where to authorise, which is why a client can connect with nothing but a URL. - **Authorization code with PKCE**, `S256` only. Every client is public; there is no client secret, no `plain` method and no implicit flow. - **Device flow** (`urn:ietf:params:oauth:grant-type:device_code`), for a machine with no browser: a CI runner, a server over SSH. The person types the code the device shows on the entry page and again on the approval screen; no link carries the code for them, so a link someone sends you cannot approve their device in one click. - **Dynamic client registration** (RFC 7591) at `/oauth/register`. A client that publishes its own metadata document needs no registration at all. - **Refresh tokens rotate.** Each refresh returns a new one. Presenting an old one again is treated as proof that somebody has a copy, and the whole grant is revoked. | Endpoint | Path, on the Studio or the suite's address | | -------------------- | ------------------------------------------ | | Authorize | `/authorize` | | Token | `/oauth/token` | | Device authorization | `/oauth/device` | | Registration | `/oauth/register` | | Revocation | `/oauth/revoke` | An access token lasts 30 days and its refresh token 90. ### Consent is a person in a browser The consent page shows the name the client gave itself **and** the address its answer will be sent to. Check that address before you approve. The scopes granted are the ones you tick, intersected with what you hold. On a suite, you also choose which of its apps the connection may reach; in the Studio, which organisation. ## Limits on signing in Signing in and the OAuth endpoints are limited per address per minute, and answer `429` with `rate_limited` when the limit is reached. Wait a minute and retry. --- # MCP servers Source: https://grid82.com/docs/agents/overview Grid82 speaks the Model Context Protocol over Streamable HTTP, with OAuth sign-in. An agent connected to it acts as the person who approved it, with that person's permissions. The fastest way to connect one is [Set up your agent](/docs/agents/setup); this page explains the addresses. ## The addresses | Address | What it reaches | Use it for | | ------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------- | | `https://{suite}.grid82.app/mcp` | every app of the suite you are in, as far as you ticked them at sign-in | Claude, ChatGPT, Cursor, a person's own agent, an integration | | `https://{suite}.grid82.app/mcp/code` | Code Mode, on the first app of the connection | an agent that writes code rather than calling tools one by one | | `https://studio.grid82.app/mcp` | the Studio: organisations, suites, apps, schema, rules, people | a builder's agent that designs apps | | `https://studio.grid82.app/mcp/code` | Code Mode on the Builder API | the same, as code | A suite on its own domain serves the same paths there. ## Why one server per suite A suite is one address with several apps under it. One server for all of them means one entry in a person's client, and adding an app to the suite does not make anybody reconfigure. Three things make that work: - **`apps_list`** returns the apps this connection can open, with the id to pass back. An agent can ask "the CRM or the support desk?" rather than guess. - **`appId`** on every app tool. It is optional when the connection reaches exactly one app, and required beyond that. Leaving it out is answered with a sentence that names your apps. - **`tableId` is a closed list** of the tables you can read, across the apps of the connection. A model cannot even name a table it may not read. The suite server does not attach every table's record shape to its tool descriptions, which would resend every app's schema on every turn of a conversation. An agent calls `schema_get` on the app it cares about. There is one server per suite and no address per app. An integration that must touch one app and nothing else is connected to the suite's server and approved for that app alone. ## What the agent is told is what you may see The tool list is generated for each connection from the same masked schema the API uses. A table you cannot read is not listed: not refused when called, absent. A field you cannot read is not in any tool's input or output. Tools outside the connection's scopes are not offered. When you approve a connection to a suite, you choose which of its apps it may reach. An agent connected to the suite but ticked only for one app sees nothing of the others, and membership is checked again on every call, so the list can only narrow. ## The Studio's server `https://studio.grid82.app/mcp` is the builder plane: create an app, add tables and fields, write permission rules, ask what a person would see with `rules_simulate`, manage the directory. It is bound to the one organisation chosen on the consent screen. It also serves `skills_list` and `skills_get`, the manual an agent reads before building: how permissions are shaped, how to check what a schema change would break, how page code is edited. ## This documentation has one too These docs have their own MCP server at `https://grid82.com/docs/mcp`, with `search`, `fetch` and `list_pages` over the public pages. It needs no sign-in and reaches no data. --- # Set up your agent Source: https://grid82.com/docs/agents/setup The quickest way to connect an agent is to let it connect itself. Grid82 hands you a short prompt that carries your own address; the agent reads a guide, works out which client it is running in, adds the server and asks you to sign in. 1. **Copy the setup prompt** In an app, open **Settings**, then **Connections**, and choose **Copy setup prompt**. In the Studio, the same button is on the **Developer** page. 2. **Paste it into your agent** Claude Code, the Claude app, ChatGPT, Cursor, VS Code, Codex or any other client that speaks remote MCP. 3. **Sign in when your browser opens** The consent page is on your own address. You can tick only some of your apps: the agent then sees nothing of the others. For a suite, the prompt reads like this, with your suite's address in place of `your-suite.grid82.app`: ```text Connect this agent to your-suite.grid82.app by following https://your-suite.grid82.app/agent-setup.md. The MCP server is https://your-suite.grid82.app/mcp (Streamable HTTP, OAuth sign-in in my browser). Then confirm what I can reach, without changing any data. ``` For the Studio: ```text Connect this agent to the Grid82 Studio by following https://studio.grid82.app/agent-setup.md. The MCP server is https://studio.grid82.app/mcp (Streamable HTTP, OAuth sign-in in my browser). Then confirm what I can reach, without changing any data. ``` Setting up changes no data. The agent's last step is to list what it can reach, and it is told not to create, update or delete anything to test the connection. ## Set it up yourself These are the steps the guide gives the agent, for a suite at `https://your-suite.grid82.app/mcp`. For the Studio, use `https://studio.grid82.app/mcp` and the name `grid82-studio`. **Claude Code** Run this in a terminal, in the project you want it for: ```bash title="Terminal" claude mcp add --transport http your-suite https://your-suite.grid82.app/mcp ``` Then type /mcp inside Claude Code, choose your-suite and Authenticate. Your browser opens a sign-in page. **Claude app** In Claude (web or desktop), open Customize, then Connectors, then Add custom connector. Paste and choose Connect. On a team plan an owner may have to add it first. **ChatGPT** Custom connectors depend on your ChatGPT plan and on what your workspace admin allows. In the desktop app: Settings, MCP servers, Add server. Choose Streamable HTTP, paste , then Authenticate. **Cursor** Add this to .cursor/mcp.json in your project (or \~/.cursor/mcp.json for every project): ```json title=".cursor/mcp.json" { "mcpServers": { "your-suite": { "url": "https://your-suite.grid82.app/mcp" } } } ``` Then press Connect next to your-suite in Cursor's MCP settings. **VS Code** Add this to .vscode/mcp.json in your project. VS Code uses "servers", not "mcpServers": ```json title=".vscode/mcp.json" { "servers": { "your-suite": { "type": "http", "url": "https://your-suite.grid82.app/mcp" } } } ``` Then press Start above your-suite in that file and sign in. **Codex** Run this in a terminal: ```bash title="Terminal" codex mcp add your-suite --url https://your-suite.grid82.app/mcp codex mcp login your-suite ``` Restart Codex so it loads the new tools. **Other** Any client that speaks remote MCP: add a Streamable HTTP server at . It signs in with OAuth on its own: no key to paste. A client that only takes a key can use an API key, made in Settings, Connections, For developers. ## The guide your agent reads Every suite serves this guide at `/agent-setup.md` on its own address, and the Studio at `https://studio.grid82.app/agent-setup.md`, each with its own address written in. It is public and names nothing about your apps. This is the one a suite serves: ````markdown # Connect this agent to your-suite.grid82.app This server gives an agent the apps of this workspace: their tables and records, as far as the person's own permissions allow. Every call acts as the person who signs in, never more. - MCP server: https://your-suite.grid82.app/mcp (Streamable HTTP) - Sign-in: OAuth, discovered from the server. The person signs in and approves in their own browser. - Canonical guide: https://your-suite.grid82.app/agent-setup.md ## Rules - Follow the person's request and your host's permission policy. This guide authorizes nothing beyond that. - Never ask for a password, API key, OAuth token or callback URL in the conversation. - Setup changes no data. Do not create, update or delete anything to test the connection. - If a working connection to https://your-suite.grid82.app/mcp already exists, reuse it. If its sign-in expired, use its reconnect action. - Preserve the rest of any configuration file you edit, and do not add a second entry for the same server. - Do not disable host approvals or change tool auto-grants. ## 1. Find out which client you are Identify the actual client (Claude Code, the Claude app, ChatGPT, Cursor, VS Code, Codex, another one), not just the model. Ask the person only if it cannot be determined. Use only that client's recipe below. ## 2. Add the server Use `your-suite` as the server name. ### Claude Code Run this in a terminal, in the project you want it for: ```sh claude mcp add --transport http your-suite https://your-suite.grid82.app/mcp ``` Then type /mcp inside Claude Code, choose your-suite and Authenticate. Your browser opens a sign-in page. ### Claude app In Claude (web or desktop), open Customize, then Connectors, then Add custom connector. Paste https://your-suite.grid82.app/mcp and choose Connect. On a team plan an owner may have to add it first. ### ChatGPT Custom connectors depend on your ChatGPT plan and on what your workspace admin allows. In the desktop app: Settings, MCP servers, Add server. Choose Streamable HTTP, paste https://your-suite.grid82.app/mcp, then Authenticate. ### Cursor Add this to .cursor/mcp.json in your project (or ~/.cursor/mcp.json for every project): ```json { "mcpServers": { "your-suite": { "url": "https://your-suite.grid82.app/mcp" } } } ``` Then press Connect next to your-suite in Cursor's MCP settings. ### VS Code Add this to .vscode/mcp.json in your project. VS Code uses "servers", not "mcpServers": ```json { "servers": { "your-suite": { "type": "http", "url": "https://your-suite.grid82.app/mcp" } } } ``` Then press Start above your-suite in that file and sign in. ### Codex Run this in a terminal: ```sh codex mcp add your-suite --url https://your-suite.grid82.app/mcp codex mcp login your-suite ``` Restart Codex so it loads the new tools. ### Other Any client that speaks remote MCP: add a Streamable HTTP server at https://your-suite.grid82.app/mcp. It signs in with OAuth on its own: no key to paste. A client that only takes a key can use an API key, made in Settings, Connections, For developers. If you cannot change the client's configuration yourself, give the person that client's steps above, at most four, and wait. A prompt alone does not install a connector: do not say it is connected. ## 3. Let the person sign in The first connection opens a consent page on https://your-suite.grid82.app. The person signs in and approves there. They may tick only some of the workspace's apps: respect that choice. A client may need a reload or a new session before the tools appear. ## 4. Verify, without changing anything Call `apps_list` (the apps this connection reaches and their tables), then `me_get` on one of them. Report the apps and tables back to the person. Report separately: server configured, sign-in completed, tools visible in this conversation, and any step left to the person (a reload, an approval). If a step is not verified, say so. Being listed in a configuration file is not proof of a connection. ```` --- # Tools Source: https://grid82.com/docs/agents/tools Every MCP tool is an operation of the REST API, generated from the same definition. A new endpoint is a new tool the day it ships, and the two cannot describe different things. ## Names A tool is named after the operation it mirrors, with the dots turned into underscores: `records.list` is `records_list`, `schema.get` is `schema_get`, `rules.simulate` is `rules_simulate`. Every page of the [App API](/docs/app-api/records/list) and [Builder API](/docs/builder-api/apps/list) references names its tool. Operation names never change silently: renaming one would break every agent and script written against it, so an old name is deprecated first and kept until a published date. ## Hints Each tool carries the MCP annotations a client uses to decide when to ask you first: | Hint | Set when | | ----------------- | ---------------------------------------------------------------------- | | `readOnlyHint` | the operation only reads | | `destructiveHint` | it deletes, or rewrites many rows at once: a bulk delete, a SQL update | | `idempotentHint` | calling it twice is the same as calling it once | A client that asks before destructive calls therefore asks at the right moments. ## Lists A list tool returns `{ data, meta }`. While `meta.hasMore` is true, pass `meta.nextCursor` back as `cursor` for the next page. The tool descriptions ask the model to narrow its filter first, because walking pages spends a conversation's context on paging rather than on thinking. Lists that are short by nature (a record's comments, your saved views of a table) take a `limit` and have no cursor. ## Errors come back as results A refusal is returned as a tool result marked `isError`, with the same `code` and `message` the REST API would give, not as a protocol error. The model reads it and adjusts. See [Errors](/docs/api/errors). ## What is not a tool, on purpose | Operation | Why not | Instead | | ---------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | Uploading a file | a tool call is JSON a model writes, and a file is bytes | upload with the REST API, then attach the returned descriptor with an ordinary record write, which is a tool | | Making or revoking a key | a credential that can mint credentials turns one leak into a permanent one | a person, in a signed-in browser | | A person's set-password link | it is a way to become that person | handed over by a builder, from the Studio | ## Large writes Saving a page's code sends the whole file. Through MCP that means the model writes every byte as tokens, which is slow and expensive for a large page. For anything big, use the REST API (`PUT …/pages/{pageId}/code`) from a script. --- # Code Mode Source: https://grid82.com/docs/agents/code-mode Code Mode serves the same API as two tools instead of one per operation. It suits an agent that writes code well and has a task of many steps: one script replaces a conversation of fifty tool calls, each costing a round trip and a share of the context window. | Address | Document it works against | | ------------------------------------- | ------------------------------------------------------------------------------------------------- | | `https://{suite}.grid82.app/mcp/code` | the first app of the connection: to choose it, tick only that app when you approve the connection | | `https://studio.grid82.app/mcp/code` | the Builder API | ## The two tools **`search`** takes a JavaScript filter and runs it over the OpenAPI document of what this connection may call. Operations outside its scopes are removed from the document. A listed operation can still be refused by role or resource permissions when executed. Omit `filter` for a compact index. A filtered search returns full contracts by default. For a broad search, pass `detail: "compact"`, then filter by the exact `operationId` to get its full contract and referenced schemas. **`execute`** runs one script. The script calls operations through the API, as you, with your permissions, and returns a result. ## The sandbox The script runs in an isolated Worker with **no outbound network**: it can reach Grid82's API through what it is given and nothing else on the internet. | Limit | Value | | -------------------- | ----------------------------- | | Run time | 30 seconds | | API requests per run | 100 | | Script size | 64 KB | | Result size | 256 KB, truncated beyond that | Credentials cannot be made or revoked from a script, any more than from a tool: those operations answer only a person in a browser. --- # Safety and undo Source: https://grid82.com/docs/agents/safety ## Writes happen as you A write from an MCP client is applied at once, with your permissions, exactly as if you had made the call yourself. It is not held for approval: asking first is your client's job, and most clients ask before running a tool marked destructive. An agent can never do more than you can, because it has no identity of its own to borrow. ## Everything lands in the history Every write, from any surface, is recorded with who made it, when, and how it arrived: the grid, a key, or an agent. A create, edit or delete of records can be undone from the change history, in the app or through the API (`changesets.mine`, then `changesets.undo`). One request is one entry, so an agent's import of five hundred rows is one thing to take back. See [Undo and history](/docs/api/undo). Deleting a table, a field, an app or a suite asks for its current name as `confirm`, from an agent as from a person, so a loop over the wrong list is refused rather than obeyed. ## Grid82's own assistant proposes The assistant built into Grid82 works differently: it **proposes** a change, shows you what it would do, and you apply it. Its proposals are recorded the same way, and an applied one is undone the same way. ## Give an agent no more than it needs - Connect it to the **suite**, and tick only the apps it should reach. - For a production integration, prefer a single app's address (`/{app}/mcp`) or a key with only the scopes it uses. - Revoke a connection from the same **Connections** screen; it stops at once. ## Setting up changes nothing The [setup guide](/docs/agents/setup) tells an agent to verify its connection by reading only (`apps_list`, then `me_get`), and never to create, update or delete anything to test it. --- # REST API overview Source: https://grid82.com/docs/api/overview Grid82 has two REST APIs, one per plane, built from the same registry as the MCP tools and the CLI. They share their conventions: the envelope, pagination, errors, idempotency and versioning work the same on both. | | **Builder API** | **App API** | | ----------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Base URL | `https://studio.grid82.app/v1` | `https://{suite}.grid82.app/{app}/v1` | | For | builders: organisations, suites, apps, schema, rules, people, domains | anyone invited into an app: records, views, comments, files, SQL, undo | | Credentials | Studio keys, Studio OAuth | your key in that app, or an OAuth connection to the suite | | OpenAPI | public, `https://studio.grid82.app/v1/openapi.json` | per person, `https://{suite}.grid82.app/{app}/v1/openapi.json` | | Reference | [Builder API](/docs/builder-api/apps/list) | [App API](/docs/app-api/records/list) | ## The App API is generated for you An app's OpenAPI document is generated on every request from the schema the caller can see. Its `tableId` parameter is a closed list of your tables, and each table's record is typed with only the fields you can read. Someone with a different role gets a different document from the same URL. There is no anonymous copy. So the [App API reference](/docs/app-api/records/list) here is the **generic contract**: every operation and its shape, with no table and no field named. To generate a client, or to give an agent exact types, fetch your own: ```bash curl "https://your-suite.grid82.app/your-app/v1/openapi.json" \ -H "Authorization: Bearer $GRID82_TOKEN" ``` The generic contracts are also published here, for tools that want a file: [`/docs/openapi/app.json`](/docs/openapi/app.json) and [`/docs/openapi/builder.json`](/docs/openapi/builder.json). ## Builders reach data through the Studio A builder is not a member of their own apps by default, and does not need to be. The Builder API reads and writes any app's records under `/v1/apps/{appId}/…`, with every write recorded under the builder's name. See [Data, as the builder](/docs/builder-api/data/records-list). ## Before you write code - [Conventions](/docs/api/conventions): the envelope, records on the wire, pagination. - [Errors](/docs/api/errors): the codes to switch on. - [Retries and idempotency](/docs/api/idempotency): make a write safe to repeat. - [Versioning](/docs/api/versioning): what can change, and how you are told. --- # Conventions Source: https://grid82.com/docs/api/conventions ## The envelope Success and failure have one shape each, and a list is an object with an array in it: ```json { "data": [], "meta": { "nextCursor": null, "hasMore": false } } ``` ```json { "error": { "code": "validation_failed", "message": "…", "details": {} } } ``` Switch on `error.code`, never on the message. See [Errors](/docs/api/errors). ## Records on the wire A record is keyed by **field id**, because an id survives a rename and a name does not. The id-to-name map travels with every list, in `meta.fields`: use it to show a name, never to key on one. ```json { "data": [{ "id": "rec_4nqw8e2hzk7s", "created_at": "2026-09-05T16:53:39.058Z", "fld_dd7kbtsjuhdc": "First record", "fld_jx99fwfvhv3x": 42 }], "meta": { "hasMore": false, "nextCursor": null, "fields": { "fld_dd7kbtsjuhdc": "Name", "fld_jx99fwfvhv3x": "Margin" } } } ``` Anything that points at a record or a person reads back with both halves, so nobody needs a second request to show it: | Field type | Reads back as | Written as | | ----------------------------- | ------------------------------------------------------------------------ | --------------------------------- | | link | `[{ "id", "label" }]`, the label being the linked record's primary field | ids, or the same objects | | user, created by, modified by | `[{ "id", "label" }]`, the label being a name or an email | ids | | attachment | `[{ "id", "name", "size", "mime", "url", … }]` | the descriptor an upload returned | Two values catch people: a **percent** holds the fraction (`0.15` is 15%), and a **duration** holds milliseconds (one hour is `3600000`). A select holds an option **id**, which is what lets its label be renamed. Dates are ISO 8601 in UTC. ## Writing by name On `records.create` and `records.update`, a key may be a field's id **or its exact name**, resolved before anything is stored. A key that is neither is refused with a `422` that names it, so a typo is loud rather than an empty row. `id` and `created_at` are ignored, so echoing a record back is safe. Email, URL and phone fields keep the text you send. A suspect format is still saved, and the response lists it under `warnings` (`code: "suspect_format"`). A warning is a successful save: do not retry it. ## Pagination Opaque cursors, `limit` capped at 200, never offsets: an offset over a table people are writing to skips and repeats rows silently. Pass `meta.nextCursor` back as `cursor` until `meta.hasMore` is false. A cursor the list did not give out is a `422`, not a silent restart. ```http GET /v1/tables/tbl_8fj3kq2wmv5d/records?limit=100 GET /v1/tables/tbl_8fj3kq2wmv5d/records?limit=100&cursor=eyJ… ``` Lists that are short by nature (a record's comments, a page's versions, your views of a table) take a `limit` and have no cursor; the reference says which. > **Note** > > Sorting and paging do not yet agree. The cursor follows creation order, so paging a list you > also **sort** can skip or repeat rows at page edges. If you need a sorted, complete list, fetch > the pages unsorted and sort them yourself, or ask for a saved view. ## Inputs are strict An unknown query parameter or body key is a `422` that names it, never silently ignored. A `limit` must be a whole number written in digits. ## Replacing a whole document A permission document, one role's permissions and a page's code are replaced whole. Reading one gives you its `revision` (or a page's version); send it back as `baseRevision` (or `baseVersion`) and a save made after somebody else changed it is refused with `409` instead of erasing their change. Without it, the last write wins. `records.update` takes an optional `baseVersion` too: the `changesetId` of the last write you saw on that record. ## Deleting Deleting an organisation, a suite, an app, a table or a field takes its current name as `?confirm=`, the same proof the Studio asks a person to type; closing an account takes its email address. A call without it is refused and deletes nothing. ```http DELETE /v1/apps/{appId}/tables/{tableId}?confirm=Old%20deals ``` Deleting a table or a field is also refused, with the list of what reads it (rules, views, pages, other fields), until you send `acknowledge=true`. --- # Errors and limits Source: https://grid82.com/docs/api/errors Every failure has the same shape, on both APIs and in MCP tool results: ```json { "error": { "code": "forbidden", "message": "…", "details": {} } } ``` Switch on `code`. The codes are stable across versions; the message is a sentence for a person and may be reworded. ## Codes | Code | HTTP | Means | | ------------------- | ---- | -------------------------------------------------------------------------------------- | | `unauthenticated` | 401 | No usable credential, or one from the other plane | | `forbidden` | 403 | Your permissions or your credential's scopes do not cover this | | `not_found` | 404 | No such thing, or none you can see | | `conflict` | 409 | Something changed underneath you, or an idempotency key was reused for another request | | `validation_failed` | 422 | The request is wrong; the message names the parameter or key | | `rate_limited` | 429 | Too many requests: wait and retry | | `payment_required` | 402 | The plan or the credit does not allow it: waiting will not help | | `not_implemented` | 501 | Not available on this deployment | | `internal` | 500 | Our fault. Retry with the same `Idempotency-Key` | ## What an error never contains The name of a field you cannot read. "You cannot read `salary`" tells you a field called `salary` exists, which is exactly what the rule was hiding. Errors name the operation, never the thing being protected, and a hidden field gets the same answer as a field that never existed. Likewise, something you cannot reach (missing, deleted, another organisation's) is the same `404` with the same sentence, whichever it is. ## Saved, with warnings Email, URL and phone fields keep the text you send. A suspect format still succeeds, and the response carries a `warnings` array (`code: "suspect_format"`, with the record, the field and a message) for the fields you can see. Do not retry a write because it has warnings. Invalid numbers, impossible dates, unknown choices and values over a field's limit fail with `validation_failed`. Send numbers or numeric strings, and ISO dates such as `2026-09-22`; timestamps must carry a timezone. A failed create batch saves none of its rows. ## Why 402 and 429 differ A rate limit is about speed and clears on its own. A spend limit is about money and does not. Conflating them would tell someone who ran out of credit to wait, and they would wait forever. ## Rate limits today - **Signing in and the OAuth endpoints** are limited per address per minute and answer `429` with `rate_limited` beyond that. - **`/v1` and MCP**, on both planes, are limited per credential (per person for a browser session): 1,200 requests a minute and 300 per 10 seconds. A request with no credential is limited per address, 600 a minute. Beyond that the answer is `429` with `rate_limited` and a `RateLimit-Policy` header describing the limit. A well-behaved client backs off and retries; with an `Idempotency-Key`, a retried write is safe. --- # Retries and idempotency Source: https://grid82.com/docs/api/idempotency Networks drop answers, and scripts retry. Without protection, a retried `POST` creates a second row. Send an `Idempotency-Key` header with every write, to an app or to the Studio: ```bash curl -X POST "https://your-suite.grid82.app/your-app/v1/tables/tbl_8fj3kq2wmv5d/records" \ -H "Authorization: Bearer $GRID82_TOKEN" \ -H "Idempotency-Key: 6f1c2b8e-1d2a-4f0e-9a51-2f1d7e0c3b44" \ -H "Content-Type: application/json" \ -d '{"records":[{"Name":"Imported row"}]}' ``` ## What happens | You send | You get | | ----------------------------------------------- | --------------------------------------------------------------------------- | | a new key | the request runs; its answer is kept for 24 hours | | the same key, the same request, within 24 hours | the first answer again, with `Idempotency-Replayed: true`, and nothing runs | | the same key with a different request | `409 conflict`: a key names one request | A key is any unique string up to 255 characters; a UUID per logical operation is the usual choice. Keys are scoped to the plane, the person and the operation, so two people can never collide. ## Which calls take it Every write the reference marks with an `Idempotency-Key` header. A `PUT` or a `PATCH` repeated is already the same as once, so the Studio keeps no answer for those: sending the same one twice is safe on its own. ## Together with retries - Retry `429`, `500` and network failures with the **same** key and an increasing delay. - Do not retry `4xx` answers other than `429`: the request itself is wrong, and will be wrong again. - A write that succeeded with `warnings` is a success. Do not retry it. --- # Versioning Source: https://grid82.com/docs/api/versioning ## The API The version is in the path: `/v1`. Every response names the contract version that answered in the `x-grid82-api-version` header, and the OpenAPI document carries the same number in `info.version`. - **Additive changes ship without a new path**: a new operation, a new optional parameter, a new key in a response. Write clients that ignore keys they do not know. - **Operation names never change silently.** They are also the MCP tool names and the CLI commands, so renaming one would break agents and scripts without warning. An operation being retired is marked `deprecated` in the reference and answers with a `Deprecation` header and a `Link` to what replaces it. Once it has a date after which it may stop answering, a `Sunset` header says when. No operation is deprecated today. - Renaming or removing a key in a body or a query goes through the same deprecation. - A breaking change to many operations at once would be a new path, `/v2`. ## Pages A page's code is stored as **immutable versions**. Every save builds a new version and keeps the old ones; publishing points the page at one of them. So: - rolling back is publishing an older version, with no rebuild; - two people using a page during a publish each keep the version they loaded; - `pages.versions` lists every version with who saved it, when, and whether it built. ```http GET /v1/apps/{appId}/pages/{pageId}/versions POST /v1/apps/{appId}/pages/{pageId}/publish { "version": 4 } ``` To keep editing from an older version, read its code with `GET …/code?version=4` and save it as a new version. ## Records Every write appends to the app's history: who, when, how it arrived, and the record before and after. See [Undo and history](/docs/api/undo). --- # Filtering, views and SQL Source: https://grid82.com/docs/api/filtering-and-sql Every way of narrowing a list is AND-ed with your permissions: it can only narrow what you would see anyway. ## Saved views Ask for a saved view instead of rebuilding its filters. It is one parameter, it stays correct when someone edits the view, and it is usually a much smaller answer. ```http GET /v1/tables/tbl_8fj3kq2wmv5d/records?viewId=viw_e4ks9mz2bh7w ``` A view belongs to whoever made it; you are handed your own views and nobody else's. ## Search, filters, sorts and where | Parameter | Takes | | --------- | --------------------------------------------------------- | | `search` | a substring, matched over the text fields you can read | | `filter` | a filter tree as JSON, the same shape a saved view stores | | `sorts` | a JSON array of `{ "fieldId", "direction" }` levels | | `where` | a SQL-style condition, in the dialect below | ```http GET /v1/tables/tbl_8fj3kq2wmv5d/records?filter={"id":"root","kind":"group","mode":"and","children":[{"id":"c1","kind":"condition","fieldId":"fld_7qk2mv9xp4ht","op":"gt","value":50000}]} GET /v1/tables/tbl_8fj3kq2wmv5d/records?where=Stage = 'won' AND Amount > 50000 ``` URL-encode the values in real requests. A field in a filter, a sort or a grouping may be named by id or by exact name. One you cannot read is refused with a `422` that lists the fields you can, and never names the hidden one. A field you can read on only some of your rows cannot be filtered or sorted on (`403`), because counting matches would reveal rows outside your scope. `GET /v1/app` lists those fields per table under `constrained`. To turn a sentence into a filter tree, `filters.suggest` takes the sentence and returns a tree you can inspect before you use it. ## SQL `POST /v1/sql/query` reads; `POST /v1/sql/execute` changes many rows. Nothing you send is run as written: the statement is parsed, every column is checked against what you may read, every value becomes a bound parameter, and your permissions are added to the `WHERE`. ```sql SELECT stage, COUNT(*) AS n, SUM(amount) AS total FROM Deals GROUP BY stage ORDER BY n DESC SELECT name, amount FROM Deals WHERE "Expected close" > CURRENT_DATE LIMIT 20 UPDATE Deals SET probability = 100 WHERE stage = 'won' DELETE FROM Deals WHERE stage = 'lost' AND amount < 1000 ``` What the dialect covers: - one statement over one table, named by name, plural or id; fields by name or id, with double quotes around a name that has spaces; - `AND OR NOT`, comparisons, `[NOT] LIKE`, `[NOT] IN (…)`, `IS [NOT] NULL`, `[NOT] BETWEEN`, both forms of `CASE`; - `LENGTH LOWER UPPER TRIM ABS ROUND COALESCE SUBSTR DATE STRFTIME IFNULL INSTR REPLACE`, and `COUNT SUM AVG MIN MAX` with `GROUP BY`; - on a link field, `HAS ANY ('rec_…', …)`, `HAS ALL (…)` and `IS EMPTY`. No `JOIN`, no subquery, no `INSERT` (use `records.create`). A `SELECT` returns at most 1,000 rows. A column you cannot read is "Unknown column", the same as one that does not exist. A field you can read on only some rows reads as `NULL` on the others, inside sums and expressions too. An `UPDATE` or `DELETE` touches up to 500 rows, each through the ordinary permission checks, and is **one** change in the history, undone in one step. Without a `WHERE` it is refused unless you send `confirm: true`. `dryRun: true` returns the count and the first ids and changes nothing. `sql.query` refuses a write and `sql.execute` refuses a `SELECT`, so a key holding only `records:read` gets exactly what its scope says. Builders have the same two operations on any app, under `/v1/apps/{appId}/sql/…` on the Studio. --- # Undo and history Source: https://grid82.com/docs/api/undo Every write to an app, from the grid, a key, an agent or a builder, is recorded: who, when, how it arrived, and each record before and after. A record's history, the change list and undo all come from that one record of changes. ## Undo a change **In an app**, `changesets.mine` lists the changes you made that you can still see, newest first, and says for each whether it can be undone (`revertible`) and, when not, where it is changed back instead. `changesets.undo` takes one back. ```http GET https://your-suite.grid82.app/your-app/v1/changesets POST https://your-suite.grid82.app/your-app/v1/changesets/{changesetId}/revert ``` **In the Studio**, a builder lists and undoes every change of an app: ```http GET https://studio.grid82.app/v1/apps/{appId}/changesets POST https://studio.grid82.app/v1/apps/{appId}/changesets/{changesetId}/revert ``` What to expect: - **One request is one change.** A batch of 500 creates, a bulk delete or a SQL update is one entry, undone in one step. - **All of it or none of it.** Every row is put back in one transaction. A row deleted since cannot be restored and is listed in `skipped` with the reason (`status: "partly reverted"`); any other refusal undoes nothing. - **A deleted record comes back whole**: same id, same autonumber, its links to records that still exist, and its comments. - **Someone changed it since?** The first attempt is refused with a sentence naming the field and whether it was you. Send `confirm: true` to go ahead. - **An undo is itself a change**, so undoing an undo is just undo again. - Record creates, edits and deletes can be undone this way. Comments, saved views, pages and the app's tables and fields are changed back elsewhere, and the list says where. ## A record's history `records.history` returns one entry per field that actually moved, plus when the record was created, commented on or deleted. A field you cannot read produces no entry you can see. ## The trash Deleting a table or a field is not undone from the change list, but it is not lost either: what it held goes to the app's trash for thirty days. `trash.restore` puts it back with the same ids, values, links and the rules that named it. ```http GET https://studio.grid82.app/v1/apps/{appId}/trash POST https://studio.grid82.app/v1/apps/{appId}/trash/{trashId}/restore ``` A deleted app can be restored for thirty days too, with `apps.restore`. --- # Events and webhooks Source: https://grid82.com/docs/api/events ## Pull what changed The events feed lists what changed in an app, oldest first, from the last event you saw: ```http GET https://your-suite.grid82.app/your-app/v1/events?since=chg_01J…&limit=200 ``` Pass `meta.nextCursor` back as `cursor` while `meta.hasMore` is true, and keep the last id you processed for next time. `since` and `cursor` both mean "the last change I saw". Events go through your permissions like a read: - a row you cannot see produces no event for you; - a field you cannot read is absent from the events you do get; - a deleted row is reported as the bare fact that it went, never its former contents; - someone who is not a member of the app is refused, rather than handed an empty feed. Builders follow changes from the Studio's change list (`GET /v1/apps/{appId}/changesets`); there is no builder events feed. ## Webhooks **Not available yet.** Grid82 does not send webhooks today. When it does, deliveries will follow [Standard Webhooks](https://www.standardwebhooks.com/): a signed id, timestamp and body, verifiable with any Standard Webhooks library, retried with backoff. The events feed above is how a webhook consumer will catch up after being down, so code written against it keeps its value. --- # What this app is, to you Source: https://grid82.com/docs/app-api/app/get `GET https://your-suite.grid82.app/your-app/v1/app` Get the app's name, address, enabled surfaces, theme, your roles and the pages those roles may open. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Never returns the app's full role list or other users' roles. Requires the `schema:read` scope. Operation `app.get`. MCP tool `app_get`. CLI: `grid82 app get` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.constrained` (`Record`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.slug` (`string`, required) - `data.surfaces` (`object`, required) - `data.surfaces.data` (`boolean`, required, default: `true`) - `data.surfaces.interface` (`boolean`, required, default: `true`) - `data.surfaces.assistant` (`boolean`, required, default: `true`) - `data.surfaces.api` (`boolean`, required, default: `true`) - `data.surfaces.mcp` (`boolean`, required, default: `true`) - `data.surfaces.cli` (`boolean`, required, default: `true`) - `data.theme` (`object`, required) - `data.theme.radius` (`number`, required, default: `6`, range: `0–24`) - `data.theme.accent` (`string`, required, default: `oklch(0.47 0.16 262)`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.theme.font` (`enum`, required, default: `inter`, one of: `system, geist, inter, figtree, manrope, jakarta, dm-sans, outfit, work-sans, nunito-sans, rubik, public-sans, source-serif, lora, fraunces, instrument-serif, geist-mono, jetbrains-mono, ibm-plex-mono`) - `data.theme.icon` (`string | null`, default: `null`) - `data.roles` (`object[]`, required) - `data.roles.id` (`string`, required) - `data.roles.name` (`string`, required) - `data.roles.description` (`string`, required) - `data.buildsPages` (`boolean`, required) - `data.buildsWorkflows` (`boolean`, required, default: `false`) - `data.pages` (`object[]`, required) - `data.pages.id` (`string`, required) - `data.pages.name` (`string`, required) - `data.pages.kind` (`enum`, required, one of: `declarative, code`) - `data.pages.mode` (`enum`, required, one of: `locked, template, canvas`) - `data.pages.tableId` (`string,null`, required) - `data.pages.owner` (`enum`, required, one of: `admin, user`) - `data.pages.version` (`number`, required) - `data.pages.updatedAt` (`string`, required) - `data.pages.bundleHash` (`string,null`, required) - `data.pages.forkedFrom` (`string,null`, required) - `data.pages.publishedVersion` (`number,null`, required, default: `null`) - `data.pages.theme` (`object | null`, required, default: `null`) - `data.pages.theme.accent` (`string`) - `data.pages.theme.radius` (`number`) ```json { "data": { "constrained": {}, "id": "", "name": "", "slug": "", "surfaces": { "data": true, "interface": true, "assistant": true, "api": true, "mcp": true, "cli": true }, "theme": { "radius": 6, "accent": "oklch(0.47 0.16 262)", "font": "inter", "icon": null }, "roles": [ { "id": "", "name": "", "description": "" } ], "buildsPages": true, "buildsWorkflows": false, "pages": [ { "id": "", "name": "", "kind": "declarative", "mode": "locked", "tableId": null, "owner": "admin", "version": 123.45, "updatedAt": "", "bundleHash": null, "forkedFrom": null, "publishedVersion": null, "theme": null } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/app' \ --header 'Authorization: Bearer ' ``` --- # Read the schema you can see Source: https://grid82.com/docs/app-api/schema/get `GET https://your-suite.grid82.app/your-app/v1/schema` Returns the tables and fields your permissions allow. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). A field you cannot read is absent. It is not listed as hidden, because listing it would leak it. Requires the `schema:read` scope. Operation `schema.get`. MCP tool `schema_get`. CLI: `grid82 schema get` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.tables` (`object[]`, required) - `data.tables.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.name` (`string`, required, length: `1–120 chars`) - `data.tables.plural` (`string`, length: `1–120 chars`) - `data.tables.description` (`string`, length: `≤ 2000 chars`) - `data.tables.icon` (`string | null`) - `data.tables.primaryFieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.fields` (`object[]`, required, items: `1–200 items`) - `data.tables.fields.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.fields.name` (`string`, required, length: `1–120 chars`) - `data.tables.fields.width` (`integer`, range: `60–900`) - `data.tables.fields.type` (`string`, required) - `data.tables.fields.config` (`object`, required, default: `{}`) ```json { "data": { "tables": [ { "id": "", "name": "", "plural": "", "description": "", "icon": "", "primaryFieldId": "", "fields": [ { "id": "", "name": "", "width": 123, "type": "", "config": {} } ] } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/schema' \ --header 'Authorization: Bearer ' ``` --- # List records Source: https://grid82.com/docs/app-api/records/list `GET https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records` Lists records from one table, filtered by your permissions and optionally by a saved view. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). `meta.rowsLimited` is true when your roles see only some of the table's rows: then no count or total you make is the table's. Paginated with an opaque cursor; ask for a view rather than paging when you only need what a user would see. Requires the `records:read` scope. Operation `records.list`. MCP tool `records_list`. CLI: `grid82 records list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | -------- | ------- | -------- | ---------------------- | | `limit` | integer | no | Default `50`. | | `cursor` | string | no | Up to 512 characters. | | `viewId` | string | no | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | | `filter` | string | no | Up to 4000 characters. | | `sorts` | string | no | Up to 1000 characters. | | `where` | string | no | Up to 4000 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`Record[]`, required) - `meta` (`object`, required) - `meta.nextCursor` (`string,null`, required): Pass as `cursor` for the next page. Null on the last one. - `meta.hasMore` (`boolean`, required) - `meta.fields` (`Record`): Field id to field name, for the table the records came from. - `meta.rowsLimited` (`boolean`): True when your roles see only some of the table's rows: this answer is about those rows, not the whole table. ```json { "data": [ {} ], "meta": { "nextCursor": null, "hasMore": true, "fields": {}, "rowsLimited": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records' \ --header 'Authorization: Bearer ' ``` --- # Create records Source: https://grid82.com/docs/app-api/records/create `POST https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records` Creates one or more records. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Each record is a flat object, not wrapped in `fields` as an update is. Keys are field ids or exact field names; a key that is not a field you can see is refused. A field you can see but not write is dropped and counted in `droppedFields`, and a record none of whose fields could be written is refused. Send an Idempotency-Key header to make a retry safe. Email, URL and phone text is preserved; visible suspect values are returned in warnings (code suspect\_format, recordId, fieldId, fieldName, message). A warning is a successful save, not a reason to retry. Invalid numeric values, ratings and ISO dates fail with validation\_failed/422. A failing create batch saves no rows. Requires the `records:write` scope. Operation `records.create`. MCP tool `records_create`. CLI: `grid82 records create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | --------- | -------- | --------------- | | `records` | object\[] | yes | 1 to 500 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.records` (`Record[]`, required) - `data.droppedFields` (`integer`, required) - `data.warnings` (`object[]`, required) - `data.warnings.code` (`string`, required) - `data.warnings.recordId` (`string`, required) - `data.warnings.fieldId` (`string`, required) - `data.warnings.fieldName` (`string`, required) - `data.warnings.message` (`string`, required) - `data.changesetId` (`string`) ```json { "data": { "records": [ {} ], "droppedFields": 123, "warnings": [ { "code": "", "recordId": "", "fieldId": "", "fieldName": "", "message": "" } ], "changesetId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "records": [ {} ] }' ``` --- # Count records Source: https://grid82.com/docs/app-api/records/count `GET https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/count` How many records match, for the same view, filter and search a list would use, among the rows your roles see (`meta.rowsLimited` says when that is only some). > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Ask this rather than paging to the end when you only need the number. Requires the `records:read` scope. Operation `records.count`. MCP tool `records_count`. CLI: `grid82 records count` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ---------------------- | | `viewId` | string | no | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | | `filter` | string | no | Up to 4000 characters. | | `sorts` | string | no | Up to 1000 characters. | | `where` | string | no | Up to 4000 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.count` (`number`, required) - `meta` (`object`, required) - `meta.rowsLimited` (`boolean`, required): True when your roles see only some of the table's rows: this answer is about those rows, not the whole table. ```json { "data": { "count": 123.45 }, "meta": { "rowsLimited": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records/count' \ --header 'Authorization: Bearer ' ``` --- # Total a column Source: https://grid82.com/docs/app-api/records/summarise `GET https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/summary` Reductions over the records a list would return: `summaries` is a JSON object of field id to one of sum, avg, median, min, max, range, stdev, count, filled, empty, unique, checked, unchecked or a percentage of those. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Values come back raw and unformatted, with `meta.rowsLimited` as on records.list. A pair that cannot be computed exactly answers null; a field you cannot read is refused, listing the fields you can. Requires the `records:read` scope. Operation `records.summarise`. MCP tool `records_summarise`. CLI: `grid82 records summarise` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------- | | `viewId` | string | no | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | | `filter` | string | no | Up to 4000 characters. | | `sorts` | string | no | Up to 1000 characters. | | `where` | string | no | Up to 4000 characters. | | `summaries` | string | yes | Up to 2000 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`Record`, required) - `meta` (`object`, required) - `meta.rowsLimited` (`boolean`, required): True when your roles see only some of the table's rows: this answer is about those rows, not the whole table. ```json { "data": {}, "meta": { "rowsLimited": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records/summary?summaries=' \ --header 'Authorization: Bearer ' ``` --- # Count records per value Source: https://grid82.com/docs/app-api/records/groups `GET https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/groups` Count records by each value in one column for the same set a list would return; a key has the shape the record's value has (a link is `[{ id, label }]`); order keys by that column's sort order. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Set `fieldId` to the field's ID or exact name. Refuses unreadable fields and lists the fields you can read. `meta.rowsLimited` as on records.list. Requires the `records:read` scope. Operation `records.groups`. MCP tool `records_groups`. CLI: `grid82 records groups` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ---------------------- | | `viewId` | string | no | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | | `filter` | string | no | Up to 4000 characters. | | `sorts` | string | no | Up to 1000 characters. | | `where` | string | no | Up to 4000 characters. | | `fieldId` | string | yes | Up to 200 characters. | | `limit` | integer | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.groups` (`object[]`, required) - `data.groups.key` (`object`, required) - `data.groups.count` (`number`, required) - `data.truncated` (`boolean`, required) - `meta` (`object`, required) - `meta.rowsLimited` (`boolean`, required): True when your roles see only some of the table's rows: this answer is about those rows, not the whole table. ```json { "data": { "groups": [ { "key": null, "count": 123.45 } ], "truncated": true }, "meta": { "rowsLimited": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records/groups?fieldId=' \ --header 'Authorization: Bearer ' ``` --- # Read a window of records by position Source: https://grid82.com/docs/app-api/records/window `GET https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/window` Rows `offset` to `offset + limit` of the set a list would return, in the same order, with the size of the whole set. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Meant for a grid that scrolls; to walk a table use records.list and its cursor, which cannot skip or repeat a row when somebody writes in between. Requires the `records:read` scope. Operation `records.window`. Not an MCP tool. A positional read for a grid that scrolls: it addresses rows by offset, which can skip or repeat a row when somebody writes between two calls. A model walks a table with records.list and its cursor, which cannot, and asks records.count for the size. CLI: `grid82 records window` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | -------- | ------- | -------- | ---------------------- | | `viewId` | string | no | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | | `filter` | string | no | Up to 4000 characters. | | `sorts` | string | no | Up to 1000 characters. | | `where` | string | no | Up to 4000 characters. | | `offset` | integer | no | Default `0`. | | `limit` | integer | no | Default `100`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.records` (`Record[]`, required) - `data.total` (`integer`, required) - `data.offset` (`integer`, required) - `data.fields` (`Record`, required) ```json { "data": { "records": [ {} ], "total": 123, "offset": 123, "fields": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records/window' \ --header 'Authorization: Bearer ' ``` --- # Read one record Source: https://grid82.com/docs/app-api/records/get `GET https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/{recordId}` Returns a single record with the fields your permissions allow. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Requires the `records:read` scope. Operation `records.get`. MCP tool `records_get`. CLI: `grid82 records get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `tableId` | string | yes | | | `recordId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`Record`, required) ```json { "data": {} } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records/' \ --header 'Authorization: Bearer ' ``` --- # Update a record Source: https://grid82.com/docs/app-api/records/update `PATCH https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/{recordId}` Applies a partial update; the body is {"fields":{…}}, keyed by field id or exact name. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). If the change moves the record outside what you can see, the response says so rather than pretending the record vanished. Email, URL and phone text is preserved; visible suspect values are returned in warnings (code suspect\_format, recordId, fieldId, fieldName, message). A warning is a successful save, not a reason to retry. Invalid numeric values, ratings and ISO dates fail with validation\_failed/422. A failing create batch saves no rows. Requires the `records:write` scope. Operation `records.update`. MCP tool `records_update`. CLI: `grid82 records update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `tableId` | string | yes | | | `recordId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------------- | ------ | -------- | -------------------- | | `fields` | object | yes | | | `baseVersion` | string | no | Up to 64 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.record` (`Record`, required) - `data.stillVisible` (`boolean`, required) - `data.droppedFields` (`integer`, required) - `data.warnings` (`object[]`, required) - `data.warnings.code` (`string`, required) - `data.warnings.recordId` (`string`, required) - `data.warnings.fieldId` (`string`, required) - `data.warnings.fieldName` (`string`, required) - `data.warnings.message` (`string`, required) - `data.changesetId` (`string`) ```json { "data": { "record": {}, "stillVisible": true, "droppedFields": 123, "warnings": [ { "code": "", "recordId": "", "fieldId": "", "fieldName": "", "message": "" } ], "changesetId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "fields": {} }' ``` --- # Delete a record Source: https://grid82.com/docs/app-api/records/delete `DELETE https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/{recordId}` Deletes one record. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Recorded in the op log with its links and its comments, so changesets.undo puts it back whole. Requires the `records:write` scope. Operation `records.delete`. MCP tool `records_delete`. CLI: `grid82 records delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `tableId` | string | yes | | | `recordId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records/' \ --header 'Authorization: Bearer ' ``` --- # Delete several records Source: https://grid82.com/docs/app-api/records/delete-many `POST https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/delete` Delete up to 500 records per request. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Check each row's permissions as for a single delete; report undeletable rows in `failed` without stopping other deletions. Log the batch as ONE op log entry so `changesetId` undoes all deletions at once. Requires the `records:write` scope. Operation `records.deleteMany`. MCP tool `records_deleteMany`. CLI: `grid82 records delete-many` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----------- | --------- | -------- | --------------- | | `recordIds` | string\[] | yes | 1 to 500 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.deleted` (`string[]`, required) - `data.failed` (`object[]`, required) - `data.failed.id` (`string`, required) - `data.failed.reason` (`string`, required) - `data.changesetId` (`string`) ```json { "data": { "deleted": [ "" ], "failed": [ { "id": "", "reason": "" } ], "changesetId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records/delete' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "recordIds": [ "" ] }' ``` --- # Duplicate a record Source: https://grid82.com/docs/app-api/records/duplicate `POST https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/{recordId}/duplicate` Copy readable, writable fields into a new record. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Leave empty any link whose other end holds a single record, so copying does not remove it from the original. Omit unreadable fields; copy only what you can see. Keep comments and history with the original record. Requires the `records:write` scope. Operation `records.duplicate`. MCP tool `records_duplicate`. CLI: `grid82 records duplicate` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `tableId` | string | yes | | | `recordId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`Record`, required) ```json { "data": {} } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records//duplicate' \ --header 'Authorization: Bearer ' ``` --- # Read a record's history Source: https://grid82.com/docs/app-api/records/history `GET https://your-suite.grid82.app/your-app/v1/records/{recordId}/history` List changes to this record, newest first, with one entry per field that changed; setting a field to its existing value creates no history. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Also includes record creation, comments, and deletion. Fields you cannot read produce no visible entry. Requires the `records:read` scope. Operation `records.history`. MCP tool `records_history`. CLI: `grid82 records history` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `recordId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ------------- | | `limit` | integer | no | Default `50`. | | `tableId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.tableId` (`string`, required) - `data.recordId` (`string`, required) - `data.authorId` (`string`, required) - `data.via` (`string`, required) - `data.kind` (`enum`, required, one of: `created, edited, commented, uncommented, deleted`) - `data.fieldId` (`string`) - `data.before` (`string | number | boolean | null | string[]`) - `data.after` (`string | number | boolean | null | string[]`) - `data.at` (`string`, required) ```json { "data": [ { "id": "", "tableId": "", "recordId": "", "authorId": "", "via": "", "kind": "created", "fieldId": "", "before": "", "after": "", "at": "" } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/records//history?tableId=' \ --header 'Authorization: Bearer ' ``` --- # List the saved views of a table Source: https://grid82.com/docs/app-api/views/list `GET https://your-suite.grid82.app/your-app/v1/tables/{tableId}/views` List your saved views of this table in your arranged order. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Views are user-specific; others' views and builders' views are not included because builders have none. Empty means you have no saved views; use the table directly. Requires the `records:read` scope. Operation `views.list`. MCP tool `views_list`. CLI: `grid82 views list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.filter` (`object`, required) - `data.filter.id` (`string`, required, length: `1–64 chars`) - `data.filter.kind` (`string`, required) - `data.filter.mode` (`enum`, required, one of: `and, or`) - `data.filter.children` (`object[]`, required, items: `≤ 50 items`) - `data.filter.children.id` (`string`, required, length: `1–64 chars`) - `data.filter.children.kind` (`string`, required) - `data.filter.children.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.filter.children.op` (`enum`, required, one of: `contains, not_contains, starts_with, ends_with, is, is_not, is_empty, is_not_empty, gt, lt, gte, lte, before, after, on_or_before, on_or_after, is_any_of, is_none_of, has_any_of, has_all_of, has_none_of, is_checked, is_unchecked`) - `data.filter.children.value` (`object`) - `data.sorts` (`object[]`, required, default: `[]`, items: `≤ 10 items`) - `data.sorts.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.sorts.desc` (`boolean`, required) - `data.groupBy` (`object[]`, required, default: `[]`, items: `≤ 3 items`) - `data.groupBy.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.groupBy.desc` (`boolean`, required) - `data.hidden` (`string[]`, required, default: `[]`) - `data.order` (`string[]`) - `data.rowHeight` (`enum`, required, default: `short`, one of: `short, medium, tall, extraTall`) - `data.search` (`string`, required, default: ``, length: `≤ 200 chars`) - `data.summaries` (`Record | null>`, required, default: `{}`) - `data.where` (`string`, length: `≤ 4000 chars`) - `data.repaired` (`object`) - `data.repaired.at` (`string`, required) - `data.repaired.removed` (`string[]`, required, items: `≤ 50 items`) - `data.repaired.note` (`string`, required, length: `≤ 400 chars`) - `data.tableId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.position` (`integer`, required) - `data.createdAt` (`string`, required) - `data.updatedAt` (`string`, required) ```json { "data": [ { "id": "", "name": "", "filter": { "id": "root", "kind": "group", "mode": "and", "children": [] }, "sorts": [], "groupBy": [], "hidden": [], "order": [ "" ], "rowHeight": "short", "search": "", "summaries": {}, "where": "", "repaired": { "at": "", "removed": [ "" ], "note": "" }, "tableId": "", "position": 123, "createdAt": "", "updatedAt": "" } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/tables//views' \ --header 'Authorization: Bearer ' ``` --- # Save a new view of a table Source: https://grid82.com/docs/app-api/views/create `POST https://your-suite.grid82.app/your-app/v1/tables/{tableId}/views` Visible to you alone; there is no other kind. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). `config` fills in defaults for whatever is left out: an empty filter, no sorts, nothing hidden. Requires the `records:write` scope. Operation `views.create`. MCP tool `views_create`. CLI: `grid82 views create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | ------ | -------- | -------------------- | | `name` | string | yes | Up to 50 characters. | | `config` | object | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.filter` (`object`, required) - `data.filter.id` (`string`, required, length: `1–64 chars`) - `data.filter.kind` (`string`, required) - `data.filter.mode` (`enum`, required, one of: `and, or`) - `data.filter.children` (`object[]`, required, items: `≤ 50 items`) - `data.filter.children.id` (`string`, required, length: `1–64 chars`) - `data.filter.children.kind` (`string`, required) - `data.filter.children.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.filter.children.op` (`enum`, required, one of: `contains, not_contains, starts_with, ends_with, is, is_not, is_empty, is_not_empty, gt, lt, gte, lte, before, after, on_or_before, on_or_after, is_any_of, is_none_of, has_any_of, has_all_of, has_none_of, is_checked, is_unchecked`) - `data.filter.children.value` (`object`) - `data.sorts` (`object[]`, required, default: `[]`, items: `≤ 10 items`) - `data.sorts.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.sorts.desc` (`boolean`, required) - `data.groupBy` (`object[]`, required, default: `[]`, items: `≤ 3 items`) - `data.groupBy.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.groupBy.desc` (`boolean`, required) - `data.hidden` (`string[]`, required, default: `[]`) - `data.order` (`string[]`) - `data.rowHeight` (`enum`, required, default: `short`, one of: `short, medium, tall, extraTall`) - `data.search` (`string`, required, default: ``, length: `≤ 200 chars`) - `data.summaries` (`Record | null>`, required, default: `{}`) - `data.where` (`string`, length: `≤ 4000 chars`) - `data.repaired` (`object`) - `data.repaired.at` (`string`, required) - `data.repaired.removed` (`string[]`, required, items: `≤ 50 items`) - `data.repaired.note` (`string`, required, length: `≤ 400 chars`) - `data.tableId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.position` (`integer`, required) - `data.createdAt` (`string`, required) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "filter": { "id": "root", "kind": "group", "mode": "and", "children": [] }, "sorts": [], "groupBy": [], "hidden": [], "order": [ "" ], "rowHeight": "short", "search": "", "summaries": {}, "where": "", "repaired": { "at": "", "removed": [ "" ], "note": "" }, "tableId": "", "position": 123, "createdAt": "", "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/tables//views' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }' ``` --- # Read one saved view Source: https://grid82.com/docs/app-api/views/get `GET https://your-suite.grid82.app/your-app/v1/views/{viewId}` One of yours. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Anybody else's is refused exactly like a record you cannot see, because the difference would confirm it exists. Requires the `records:read` scope. Operation `views.get`. MCP tool `views_get`. CLI: `grid82 views get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `viewId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.filter` (`object`, required) - `data.filter.id` (`string`, required, length: `1–64 chars`) - `data.filter.kind` (`string`, required) - `data.filter.mode` (`enum`, required, one of: `and, or`) - `data.filter.children` (`object[]`, required, items: `≤ 50 items`) - `data.filter.children.id` (`string`, required, length: `1–64 chars`) - `data.filter.children.kind` (`string`, required) - `data.filter.children.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.filter.children.op` (`enum`, required, one of: `contains, not_contains, starts_with, ends_with, is, is_not, is_empty, is_not_empty, gt, lt, gte, lte, before, after, on_or_before, on_or_after, is_any_of, is_none_of, has_any_of, has_all_of, has_none_of, is_checked, is_unchecked`) - `data.filter.children.value` (`object`) - `data.sorts` (`object[]`, required, default: `[]`, items: `≤ 10 items`) - `data.sorts.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.sorts.desc` (`boolean`, required) - `data.groupBy` (`object[]`, required, default: `[]`, items: `≤ 3 items`) - `data.groupBy.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.groupBy.desc` (`boolean`, required) - `data.hidden` (`string[]`, required, default: `[]`) - `data.order` (`string[]`) - `data.rowHeight` (`enum`, required, default: `short`, one of: `short, medium, tall, extraTall`) - `data.search` (`string`, required, default: ``, length: `≤ 200 chars`) - `data.summaries` (`Record | null>`, required, default: `{}`) - `data.where` (`string`, length: `≤ 4000 chars`) - `data.repaired` (`object`) - `data.repaired.at` (`string`, required) - `data.repaired.removed` (`string[]`, required, items: `≤ 50 items`) - `data.repaired.note` (`string`, required, length: `≤ 400 chars`) - `data.tableId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.position` (`integer`, required) - `data.createdAt` (`string`, required) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "filter": { "id": "root", "kind": "group", "mode": "and", "children": [] }, "sorts": [], "groupBy": [], "hidden": [], "order": [ "" ], "rowHeight": "short", "search": "", "summaries": {}, "where": "", "repaired": { "at": "", "removed": [ "" ], "note": "" }, "tableId": "", "position": 123, "createdAt": "", "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/views/' \ --header 'Authorization: Bearer ' ``` --- # Change a saved view Source: https://grid82.com/docs/app-api/views/update `PATCH https://your-suite.grid82.app/your-app/v1/views/{viewId}` Only a view that is yours. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). `config` merges shallowly: a key you send replaces that key, anything you leave out keeps its saved value. Requires the `records:write` scope. Operation `views.update`. MCP tool `views_update`. CLI: `grid82 views update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `viewId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | ------ | -------- | -------------------- | | `name` | string | no | Up to 50 characters. | | `config` | object | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.filter` (`object`, required) - `data.filter.id` (`string`, required, length: `1–64 chars`) - `data.filter.kind` (`string`, required) - `data.filter.mode` (`enum`, required, one of: `and, or`) - `data.filter.children` (`object[]`, required, items: `≤ 50 items`) - `data.filter.children.id` (`string`, required, length: `1–64 chars`) - `data.filter.children.kind` (`string`, required) - `data.filter.children.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.filter.children.op` (`enum`, required, one of: `contains, not_contains, starts_with, ends_with, is, is_not, is_empty, is_not_empty, gt, lt, gte, lte, before, after, on_or_before, on_or_after, is_any_of, is_none_of, has_any_of, has_all_of, has_none_of, is_checked, is_unchecked`) - `data.filter.children.value` (`object`) - `data.sorts` (`object[]`, required, default: `[]`, items: `≤ 10 items`) - `data.sorts.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.sorts.desc` (`boolean`, required) - `data.groupBy` (`object[]`, required, default: `[]`, items: `≤ 3 items`) - `data.groupBy.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.groupBy.desc` (`boolean`, required) - `data.hidden` (`string[]`, required, default: `[]`) - `data.order` (`string[]`) - `data.rowHeight` (`enum`, required, default: `short`, one of: `short, medium, tall, extraTall`) - `data.search` (`string`, required, default: ``, length: `≤ 200 chars`) - `data.summaries` (`Record | null>`, required, default: `{}`) - `data.where` (`string`, length: `≤ 4000 chars`) - `data.repaired` (`object`) - `data.repaired.at` (`string`, required) - `data.repaired.removed` (`string[]`, required, items: `≤ 50 items`) - `data.repaired.note` (`string`, required, length: `≤ 400 chars`) - `data.tableId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.position` (`integer`, required) - `data.createdAt` (`string`, required) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "filter": { "id": "root", "kind": "group", "mode": "and", "children": [] }, "sorts": [], "groupBy": [], "hidden": [], "order": [ "" ], "rowHeight": "short", "search": "", "summaries": {}, "where": "", "repaired": { "at": "", "removed": [ "" ], "note": "" }, "tableId": "", "position": 123, "createdAt": "", "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://your-suite.grid82.app/your-app/v1/views/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "config": { "filter": { "id": "root", "kind": "group", "mode": "and", "children": [] }, "sorts": [], "groupBy": [], "hidden": [], "order": [ "" ], "rowHeight": "short", "search": "", "summaries": {}, "where": "", "repaired": { "at": "", "removed": [ "" ], "note": "" } } }' ``` --- # Delete a saved view Source: https://grid82.com/docs/app-api/views/delete `DELETE https://your-suite.grid82.app/your-app/v1/views/{viewId}` Only a view that is yours, which is every view you can see. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). A table with none left is the table itself, so there is no last one to protect. Requires the `records:write` scope. Operation `views.delete`. MCP tool `views_delete`. CLI: `grid82 views delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `viewId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://your-suite.grid82.app/your-app/v1/views/' \ --header 'Authorization: Bearer ' ``` --- # Reorder a table's views Source: https://grid82.com/docs/app-api/views/reorder `POST https://your-suite.grid82.app/your-app/v1/tables/{tableId}/views/reorder` `ids` in the order you want them. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). An id that is not one of yours is ignored. Requires the `records:write` scope. Operation `views.reorder`. MCP tool `views_reorder`. CLI: `grid82 views reorder` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----- | --------- | -------- | --------------- | | `ids` | string\[] | yes | 1 to 200 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.ok` (`boolean`, required) ```json { "data": { "ok": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/tables//views/reorder' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "ids": [ "" ] }' ``` --- # Read a record's comments Source: https://grid82.com/docs/app-api/comments/list `GET https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/{recordId}/comments` List notes users left on this record, oldest first. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Comments are about the record, not its fields. If you cannot read the record, refuse rather than report that it has no comments, to avoid confirming its existence. Requires the `records:read` scope. Operation `comments.list`. MCP tool `comments_list`. CLI: `grid82 comments list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `tableId` | string | yes | | | `recordId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | ------- | ------- | -------- | ------------- | | `limit` | integer | no | Default `50`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.tableId` (`string`, required) - `data.recordId` (`string`, required) - `data.authorId` (`string`, required) - `data.text` (`string`, required, length: `1–4000 chars`) - `data.mentions` (`string[]`, required) - `data.createdAt` (`string`, required) - `data.editedAt` (`string,null`, required) ```json { "data": [ { "id": "", "tableId": "", "recordId": "", "authorId": "", "text": "", "mentions": [ "" ], "createdAt": "", "editedAt": null } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records//comments' \ --header 'Authorization: Bearer ' ``` --- # Comment on a record Source: https://grid82.com/docs/app-api/comments/create `POST https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/{recordId}/comments` Leave a note on a record. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Set `mentions` to the user IDs named with @ in the body; users who cannot see the record are dropped to avoid revealing its existence or sending them a refusal. Ask `comments.mentionable` which users you may name. Requires the `records:write` scope. Operation `comments.create`. MCP tool `comments_create`. CLI: `grid82 comments create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `tableId` | string | yes | | | `recordId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | --------- | -------- | ---------------------- | | `text` | string | yes | Up to 4000 characters. | | `mentions` | string\[] | no | Up to 20 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.tableId` (`string`, required) - `data.recordId` (`string`, required) - `data.authorId` (`string`, required) - `data.text` (`string`, required, length: `1–4000 chars`) - `data.mentions` (`string[]`, required) - `data.createdAt` (`string`, required) - `data.editedAt` (`string,null`, required) ```json { "data": { "id": "", "tableId": "", "recordId": "", "authorId": "", "text": "", "mentions": [ "" ], "createdAt": "", "editedAt": null } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records//comments' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "text": "" }' ``` --- # Who may be named on this record Source: https://grid82.com/docs/app-api/comments/mentionable `GET https://your-suite.grid82.app/your-app/v1/tables/{tableId}/records/{recordId}/comments/mentionable` List users who can currently see this record and may be named in an @. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Evaluate per record, not per table, since rows in the same table may have different audiences. Requires the `records:read` scope. Operation `comments.mentionable`. MCP tool `comments_mentionable`. CLI: `grid82 comments mentionable` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `tableId` | string | yes | | | `recordId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.userId` (`string`, required) - `data.email` (`string,null`, required) - `data.name` (`string,null`, required) ```json { "data": [ { "userId": "", "email": null, "name": null } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/tables//records//comments/mentionable' \ --header 'Authorization: Bearer ' ``` --- # Edit your own comment Source: https://grid82.com/docs/app-api/comments/update `PATCH https://your-suite.grid82.app/your-app/v1/comments/{commentId}` Only your own, refused on the server rather than hidden in the interface. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). The comment is marked as edited. Requires the `records:write` scope. Operation `comments.update`. MCP tool `comments_update`. CLI: `grid82 comments update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ----------- | ------ | -------- | ----------- | | `commentId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | --------- | -------- | ---------------------- | | `text` | string | yes | Up to 4000 characters. | | `mentions` | string\[] | no | Up to 20 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.tableId` (`string`, required) - `data.recordId` (`string`, required) - `data.authorId` (`string`, required) - `data.text` (`string`, required, length: `1–4000 chars`) - `data.mentions` (`string[]`, required) - `data.createdAt` (`string`, required) - `data.editedAt` (`string,null`, required) ```json { "data": { "id": "", "tableId": "", "recordId": "", "authorId": "", "text": "", "mentions": [ "" ], "createdAt": "", "editedAt": null } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://your-suite.grid82.app/your-app/v1/comments/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "text": "" }' ``` --- # Delete your own comment Source: https://grid82.com/docs/app-api/comments/delete `DELETE https://your-suite.grid82.app/your-app/v1/comments/{commentId}` Delete only your own comment. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). The record's history retains both the comment-added and comment-removed events. Requires the `records:write` scope. Operation `comments.delete`. MCP tool `comments_delete`. CLI: `grid82 comments delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ----------- | ------ | -------- | ----------- | | `commentId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://your-suite.grid82.app/your-app/v1/comments/' \ --header 'Authorization: Bearer ' ``` --- # Upload a file for an attachment field Source: https://grid82.com/docs/app-api/files/upload `POST https://your-suite.grid82.app/your-app/v1/tables/{tableId}/files` Upload file bytes as `multipart/form-data` under `file` and return `{ id, name, size, mime, key, uploadedAt }`. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Set `recordId` to identify the record the file will belong to; unused uploads can be swept. Write the descriptor to an attachment field in a separate ordinary record write, with the same permission check, op log, and undo as any other value. Reject descriptors not returned by this operation: `key` identifies an R2 object, and inventing one could name someone else's bytes. Requires the `records:write` scope. Operation `files.upload`. Not an MCP tool. A model cannot send bytes: a tool call is JSON, and an upload is a multipart file. Upload with the CLI (`--file ./path`) or from the grid, then attach the returned descriptor with an ordinary record write, which is a tool. CLI: `grid82 files upload` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body Sent as `multipart/form-data`. | Field | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------------------------------------------------------- | | `file` | string | yes | The bytes. | | `recordId` | string | yes | The record the file will be attached to, so an upload never attached can be swept. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.size` (`number`, required) - `data.mime` (`string`, required) - `data.key` (`string`, required) - `data.uploadedAt` (`string`, required) - `data.width` (`number`) - `data.height` (`number`) ```json { "data": { "id": "", "name": "", "size": 123.45, "mime": "", "key": "", "uploadedAt": "", "width": 123.45, "height": 123.45 } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/tables//files' \ --header 'Authorization: Bearer ' ``` --- # Read with SQL Source: https://grid82.com/docs/app-api/sql/query `POST https://your-suite.grid82.app/your-app/v1/sql/query` Runs one SELECT as you and returns rows and columns. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Fast for questions a filter cannot ask: aggregates, GROUP BY, CASE, functions. The dialect: SELECT \ FROM \ \[WHERE …] \[GROUP BY …] \[ORDER BY …] \[LIMIT n OFFSET m]; UPDATE \
SET col = expr \[WHERE …]; DELETE FROM \
\[WHERE …]. One table per statement; field names or ids (double-quote names with spaces); 'text', numbers, TRUE, FALSE, NULL, CURRENT\_DATE; AND OR NOT, = \<> \< \<= > >=, \[NOT] LIKE, \[NOT] IN (…), IS \[NOT] NULL, \[NOT] BETWEEN, CASE WHEN cond THEN v … \[ELSE v] END and CASE expr WHEN value THEN v … \[ELSE v] END; a select takes its choice's label (any case) or id; functions LENGTH LOWER UPPER TRIM ABS ROUND COALESCE SUBSTR DATE STRFTIME IFNULL INSTR REPLACE; link columns: col HAS ANY ('id', …), col HAS ALL (…), col IS EMPTY. Your text is never executed: it is compiled against your own permissions, and a column you may not read is simply an unknown column. At most 1000 rows; add LIMIT. Requires the `records:read` scope. Operation `sql.query`. MCP tool `sql_query`. CLI: `grid82 sql query` (`npx grid82`). ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----- | ------ | -------- | ----------------------- | | `sql` | string | yes | Up to 20000 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.kind` (`string`, required) - `data.columns` (`string[]`, required) - `data.rows` (`object[][]`, required) - `data.rowCount` (`integer`, required) - `data.truncated` (`boolean`, required) ```json { "data": { "kind": "", "columns": [ "" ], "rows": [ [ null ] ], "rowCount": 123, "truncated": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/sql/query' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "sql": "" }' ``` --- # Change many rows with SQL Source: https://grid82.com/docs/app-api/sql/execute `POST https://your-suite.grid82.app/your-app/v1/sql/execute` Runs one UPDATE or DELETE as you. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). The statement names the rows and the new values; each row is then written through the same checks as a single edit, so a row you may not change is skipped. The whole statement is one changeset, returned as changesetId, so one revert undoes it. Up to 500 rows per statement. Without a WHERE it refuses unless confirm is true; dryRun counts first. The dialect: SELECT \ FROM \
\[WHERE …] \[GROUP BY …] \[ORDER BY …] \[LIMIT n OFFSET m]; UPDATE \
SET col = expr \[WHERE …]; DELETE FROM \
\[WHERE …]. One table per statement; field names or ids (double-quote names with spaces); 'text', numbers, TRUE, FALSE, NULL, CURRENT\_DATE; AND OR NOT, = \<> \< \<= > >=, \[NOT] LIKE, \[NOT] IN (…), IS \[NOT] NULL, \[NOT] BETWEEN, CASE WHEN cond THEN v … \[ELSE v] END and CASE expr WHEN value THEN v … \[ELSE v] END; a select takes its choice's label (any case) or id; functions LENGTH LOWER UPPER TRIM ABS ROUND COALESCE SUBSTR DATE STRFTIME IFNULL INSTR REPLACE; link columns: col HAS ANY ('id', …), col HAS ALL (…), col IS EMPTY. Your text is never executed: it is compiled against your own permissions, and a column you may not read is simply an unknown column. Requires the `records:write` scope. Operation `sql.execute`. MCP tool `sql_execute`. CLI: `grid82 sql execute` (`npx grid82`). ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------- | -------- | ----------------------- | | `sql` | string | yes | Up to 20000 characters. | | `confirm` | boolean | no | | | `dryRun` | boolean | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.kind` (`enum`, required, one of: `update, delete`) - `data.affected` (`integer`, required) - `data.ids` (`string[]`, required) - `data.failures` (`string[]`) - `data.warnings` (`object[]`) - `data.warnings.code` (`string`, required) - `data.warnings.recordId` (`string`, required) - `data.warnings.fieldId` (`string`, required) - `data.warnings.fieldName` (`string`, required) - `data.warnings.message` (`string`, required) - `data.dryRun` (`boolean`, required) - `data.changesetId` (`string`) ```json { "data": { "kind": "update", "affected": 123, "ids": [ "" ], "failures": [ "" ], "warnings": [ { "code": "", "recordId": "", "fieldId": "", "fieldName": "", "message": "" } ], "dryRun": true, "changesetId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/sql/execute' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "sql": "" }' ``` --- # Turn a sentence into a filter Source: https://grid82.com/docs/app-api/filters/suggest `POST https://your-suite.grid82.app/your-app/v1/tables/{tableId}/filters/suggest` Return the complete filter to apply, given the table's fields, visible example values, and existing filter; return a plain-word refusal when the sentence is not about narrowing rows. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). This is a small model call, not the assistant, and costs credits. Requires the `records:read` scope. Operation `filters.suggest`. MCP tool `filters_suggest`. CLI: `grid82 filters suggest` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | ---------------- | -------- | --------------------- | | `sentence` | string | yes | Up to 600 characters. | | `current` | FilterGroupInput | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.ok` (`boolean`, required) - `data.filter` (`object`, required) - `data.filter.id` (`string`, required, length: `1–64 chars`) - `data.filter.kind` (`string`, required) - `data.filter.mode` (`enum`, required, one of: `and, or`) - `data.filter.children` (`object[]`, required, items: `≤ 50 items`) - `data.filter.children.id` (`string`, required, length: `1–64 chars`) - `data.filter.children.kind` (`string`, required) - `data.filter.children.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.filter.children.op` (`enum`, required, one of: `contains, not_contains, starts_with, ends_with, is, is_not, is_empty, is_not_empty, gt, lt, gte, lte, before, after, on_or_before, on_or_after, is_any_of, is_none_of, has_any_of, has_all_of, has_none_of, is_checked, is_unchecked`) - `data.filter.children.value` (`object`) ```json { "data": { "ok": true, "filter": { "id": "", "kind": "", "mode": "and", "children": [ { "id": "", "kind": "", "fieldId": "", "op": "contains", "value": null } ] } } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/tables//filters/suggest' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "sentence": "" }' ``` --- # What was changed here, and what can be undone Source: https://grid82.com/docs/app-api/changesets/mine `GET https://your-suite.grid82.app/your-app/v1/changesets` List this app's recent write bundles, newest first. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Each bundle groups one user's operations performed atomically at one moment and indicates whether it has since been undone and whether it is `revertible`. The log is for the current app; your permissions determine which operations you may UNDO, one at a time. Requires the `records:read` scope. Operation `changesets.mine`. MCP tool `changesets_mine`. CLI: `grid82 changesets mine` (`npx grid82`). ## Query parameters | Name | Type | Required | Description | | ------- | ------- | -------- | ------------- | | `limit` | integer | no | Default `50`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.title` (`string`, required) - `data.status` (`string`, required) - `data.createdAt` (`string`, required) - `data.actor` (`string`, required) - `data.reverted` (`boolean`, required) - `data.revertible` (`boolean`, required) - `data.notRevertible` (`string`) ```json { "data": [ { "id": "", "title": "", "status": "", "createdAt": "", "actor": "", "reverted": true, "revertible": true, "notRevertible": "" } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/changesets' \ --header 'Authorization: Bearer ' ``` --- # Undo a change Source: https://grid82.com/docs/app-api/changesets/undo `POST https://your-suite.grid82.app/your-app/v1/changesets/{changesetId}/revert` Undo a bundle by replaying its operations backwards through the standard write permission compiler; undoing a delete recreates the row only if you have permission to create it. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Restore only fields changed by an update, preserving colleagues' edits to other fields. If anyone changed a field being restored, refuse and identify who; send `confirm: true` to overwrite their changes. Only bundles of record creates, edits, and deletes are undoable; the changesets list marks each as `revertible`. Refuse other bundles (comments, saved views, pages, or app-shape changes) up front with the reason, without partially applying them. Undo all restorable rows in ONE transaction; list rows deleted since in `skipped` without failing the rest. Requires the `records:write` scope. Operation `changesets.undo`. MCP tool `changesets_undo`. CLI: `grid82 changesets undo` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------- | ------ | -------- | ----------- | | `changesetId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `confirm` | boolean | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.revertId` (`string`) - `data.status` (`string`, required) - `data.skipped` (`object[]`) - `data.skipped.tableId` (`string`, required) - `data.skipped.recordId` (`string`, required) - `data.skipped.reason` (`string`, required) ```json { "data": { "id": "", "revertId": "", "status": "", "skipped": [ { "tableId": "", "recordId": "", "reason": "" } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/changesets//revert' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "confirm": true }' ``` --- # Catch up on what changed Source: https://grid82.com/docs/app-api/events/list `GET https://your-suite.grid82.app/your-app/v1/events` Events since a cursor, so a consumer that was down can catch up itself instead of asking us to replay. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). This is the pull side of webhooks. Requires the `records:read` scope. Operation `events.list`. MCP tool `events_list`. CLI: `grid82 events list` (`npx grid82`). ## Query parameters | Name | Type | Required | Description | | -------- | ------- | -------- | --------------------- | | `limit` | integer | no | Default `50`. | | `cursor` | string | no | Up to 512 characters. | | `since` | string | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.at` (`string`, required) - `data.kind` (`string`, required) - `data.payload` (`object`, required) - `meta` (`object`, required) - `meta.nextCursor` (`string,null`, required): Pass as `cursor` for the next page. Null on the last one. - `meta.hasMore` (`boolean`, required) - `meta.fields` (`Record`): Field id to field name, for the table the records came from. ```json { "data": [ { "id": "", "at": "", "kind": "", "payload": null } ], "meta": { "nextCursor": null, "hasMore": true, "fields": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/events' \ --header 'Authorization: Bearer ' ``` --- # Make a Template page your own Source: https://grid82.com/docs/app-api/my/pages-fork `POST https://your-suite.grid82.app/your-app/v1/pages/{pageId}/fork` Fork the Template page into your own editable, deletable page, visible only to you; it replaces the Template for you. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Refused unless one of your roles may build pages; otherwise, the Template is as fixed as a locked page. Requires the `pages:write` scope. Operation `my.pages.fork`. MCP tool `my_pages_fork`. CLI: `grid82 my pages fork` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `pageId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.kind` (`enum`, required, one of: `declarative, code`) - `data.mode` (`enum`, required, one of: `locked, template, canvas`) - `data.owner` (`enum`, required, one of: `admin, user`) - `data.ownerUserId` (`string`, length: `≤ 64 chars`) - `data.tableId` (`string`, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.forkedFrom` (`string`, length: `≤ 64 chars`) - `data.visibleTo` (`string | string[]`, required, default: `*`) - `data.reads` (`object`) - `data.reads.tables` (`string[]`, required, default: `[]`) - `data.reads.fields` (`string[]`, required, default: `[]`) - `data.reads.views` (`string[]`, required, default: `[]`) - `data.bundleHash` (`string`, length: `≤ 128 chars`) - `data.publishedVersion` (`integer`, range: `≥ 1`) - `data.theme` (`object`) - `data.theme.accent` (`string`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.theme.radius` (`number`, range: `0–24`) - `data.document` (`object`) - `data.version` (`integer`, required, default: `1`, range: `≥ 1`) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "kind": "declarative", "mode": "locked", "owner": "admin", "ownerUserId": "", "tableId": "", "forkedFrom": "", "visibleTo": "*", "reads": { "tables": [], "fields": [], "views": [] }, "bundleHash": "", "publishedVersion": 123, "theme": { "accent": "", "radius": 123.45 }, "document": null, "version": 1, "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/pages//fork' \ --header 'Authorization: Bearer ' ``` --- # Rename a page you own Source: https://grid82.com/docs/app-api/my/pages-rename `PATCH https://your-suite.grid82.app/your-app/v1/pages/{pageId}` Changes the name of a copy you took, and nothing else about it. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). A builder's page keeps the name the builder gave it. Requires the `pages:write` scope. Operation `my.pages.rename`. MCP tool `my_pages_rename`. CLI: `grid82 my pages rename` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `pageId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------ | ------ | -------- | -------------------- | | `name` | string | yes | Up to 60 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.kind` (`enum`, required, one of: `declarative, code`) - `data.mode` (`enum`, required, one of: `locked, template, canvas`) - `data.owner` (`enum`, required, one of: `admin, user`) - `data.ownerUserId` (`string`, length: `≤ 64 chars`) - `data.tableId` (`string`, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.forkedFrom` (`string`, length: `≤ 64 chars`) - `data.visibleTo` (`string | string[]`, required, default: `*`) - `data.reads` (`object`) - `data.reads.tables` (`string[]`, required, default: `[]`) - `data.reads.fields` (`string[]`, required, default: `[]`) - `data.reads.views` (`string[]`, required, default: `[]`) - `data.bundleHash` (`string`, length: `≤ 128 chars`) - `data.publishedVersion` (`integer`, range: `≥ 1`) - `data.theme` (`object`) - `data.theme.accent` (`string`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.theme.radius` (`number`, range: `0–24`) - `data.document` (`object`) - `data.version` (`integer`, required, default: `1`, range: `≥ 1`) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "kind": "declarative", "mode": "locked", "owner": "admin", "ownerUserId": "", "tableId": "", "forkedFrom": "", "visibleTo": "*", "reads": { "tables": [], "fields": [], "views": [] }, "bundleHash": "", "publishedVersion": 123, "theme": { "accent": "", "radius": 123.45 }, "document": null, "version": 1, "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://your-suite.grid82.app/your-app/v1/pages/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }' ``` --- # Delete a page you own Source: https://grid82.com/docs/app-api/my/pages-delete `DELETE https://your-suite.grid82.app/your-app/v1/pages/{pageId}` Only your own copies. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). A builder's page is not yours to remove; a copy you no longer want is. Requires the `pages:write` scope. Operation `my.pages.delete`. MCP tool `my_pages_delete`. CLI: `grid82 my pages delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `pageId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://your-suite.grid82.app/your-app/v1/pages/' \ --header 'Authorization: Bearer ' ``` --- # Create a page of your own Source: https://grid82.com/docs/app-api/my/pages-create `POST https://your-suite.grid82.app/your-app/v1/pages` A new page that is yours alone, empty until you save code to it. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Refused unless one of your roles may build pages. Requires the `pages:write` scope. Operation `my.pages.create`. MCP tool `my_pages_create`. CLI: `grid82 my pages create` (`npx grid82`). ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------ | -------- | -------------------- | | `name` | string | yes | Up to 60 characters. | | `tableId` | string | no | Up to 64 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.kind` (`enum`, required, one of: `declarative, code`) - `data.mode` (`enum`, required, one of: `locked, template, canvas`) - `data.owner` (`enum`, required, one of: `admin, user`) - `data.ownerUserId` (`string`, length: `≤ 64 chars`) - `data.tableId` (`string`, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.forkedFrom` (`string`, length: `≤ 64 chars`) - `data.visibleTo` (`string | string[]`, required, default: `*`) - `data.reads` (`object`) - `data.reads.tables` (`string[]`, required, default: `[]`) - `data.reads.fields` (`string[]`, required, default: `[]`) - `data.reads.views` (`string[]`, required, default: `[]`) - `data.bundleHash` (`string`, length: `≤ 128 chars`) - `data.publishedVersion` (`integer`, range: `≥ 1`) - `data.theme` (`object`) - `data.theme.accent` (`string`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.theme.radius` (`number`, range: `0–24`) - `data.document` (`object`) - `data.version` (`integer`, required, default: `1`, range: `≥ 1`) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "kind": "declarative", "mode": "locked", "owner": "admin", "ownerUserId": "", "tableId": "", "forkedFrom": "", "visibleTo": "*", "reads": { "tables": [], "fields": [], "views": [] }, "bundleHash": "", "publishedVersion": 123, "theme": { "accent": "", "radius": 123.45 }, "document": null, "version": 1, "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/pages' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }' ``` --- # Read a page's code Source: https://grid82.com/docs/app-api/my/pages-code-get `GET https://your-suite.grid82.app/your-app/v1/pages/{pageId}/code` The current version of a page you can open, or an earlier one by number. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Null when nothing has been saved yet. Requires the `pages:read` scope. Operation `my.pages.code.get`. MCP tool `my_pages_code_get`. CLI: `grid82 my pages code get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `pageId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `version` | integer | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object | null`, required) - `data.version` (`integer`, required) - `data.author` (`string`, required) - `data.note` (`string,null`, required) - `data.at` (`string`, required) - `data.source` (`string`, required) ```json { "data": { "version": 123, "author": "", "note": null, "at": "", "source": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/pages//code' \ --header 'Authorization: Bearer ' ``` --- # Save a new version of your page's code Source: https://grid82.com/docs/app-api/my/pages-code-set `PUT https://your-suite.grid82.app/your-app/v1/pages/{pageId}/code` Only a page you own. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Never overwrites: every save is a version. A note says why, for the history. Send `baseVersion` (the version you read) to be refused with 409 if somebody saved since. Requires the `pages:write` scope. Operation `my.pages.code.set`. MCP tool `my_pages_code_set`. CLI: `grid82 my pages code set` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `pageId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------------- | ------------------------------------------- | -------- | ------------------------ | | `source` | string | yes | Up to 400000 characters. | | `note` | string | no | Up to 200 characters. | | `via` | "assistant" or "hand" or "api" or "restore" | no | | | `baseVersion` | integer | no | At least 0. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.version` (`integer`, required) - `data.at` (`string`, required) - `data.bundleHash` (`string,null`, required, default: `null`) - `data.buildError` (`string,null`, required, default: `null`) - `data.warnings` (`string[]`, required, default: `[]`) ```json { "data": { "version": 123, "at": "", "bundleHash": null, "buildError": null, "warnings": [] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PUT \ --url 'https://your-suite.grid82.app/your-app/v1/pages//code' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "source": "" }' ``` --- # The history of a page you can open Source: https://grid82.com/docs/app-api/my/pages-versions `GET https://your-suite.grid82.app/your-app/v1/pages/{pageId}/versions` Every saved version, newest first: who saved it, when, the note they left, whether it builds, and which one users are looking at. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Requires the `pages:read` scope. Operation `my.pages.versions`. MCP tool `my_pages_versions`. CLI: `grid82 my pages versions` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `pageId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.version` (`integer`, required) - `data.author` (`string`, required) - `data.note` (`string,null`, required) - `data.at` (`string`, required) - `data.bytes` (`integer`, required) - `data.bundleHash` (`string,null`, required, default: `null`) - `data.buildError` (`string,null`, required, default: `null`) - `data.warnings` (`string[]`, required, default: `[]`) - `data.via` (`enum | null`, required, default: `null`) - `data.report` (`Record | null`, required, default: `null`) - `data.published` (`boolean`, required, default: `false`) ```json { "data": [ { "version": 123, "author": "", "note": null, "at": "", "bytes": 123, "bundleHash": null, "buildError": null, "warnings": [], "via": null, "report": null, "published": false } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/pages//versions' \ --header 'Authorization: Bearer ' ``` --- # Publish a version of your page Source: https://grid82.com/docs/app-api/my/pages-publish `POST https://your-suite.grid82.app/your-app/v1/pages/{pageId}/publish` Point the page to an already-built version: the latest by default, or `version` to select an earlier one. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Saves always build, but this operation never compiles code or fails on it; if a version did not build, report that instead of publishing it. Requires the `pages:write` scope. Operation `my.pages.publish`. MCP tool `my_pages_publish`. CLI: `grid82 my pages publish` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `pageId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `version` | integer | no | At least 1. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.bundleHash` (`string`, required) - `data.version` (`integer`, required) ```json { "data": { "bundleHash": "", "version": 123 } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/pages//publish' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "version": 123 }' ``` --- # Which apps you are in Source: https://grid82.com/docs/app-api/my/apps `GET https://your-suite.grid82.app/your-app/v1/my/apps` List every app in this organisation you can access and your roles in each. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). The app switcher reads this list; agents should call it before assuming which app they are viewing. Any valid credential for this API: no scope is needed. Operation `my.apps`. MCP tool `my_apps`. CLI: `grid82 my apps` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.appId` (`string`, required) - `data.name` (`string`, required) - `data.slug` (`string`, required) - `data.url` (`string`, required) - `data.roles` (`string[]`, required) ```json { "data": [ { "appId": "", "name": "", "slug": "", "url": "", "roles": [ "" ] } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/my/apps' \ --header 'Authorization: Bearer ' ``` --- # List your workflows Source: https://grid82.com/docs/app-api/my/workflows-list `GET https://your-suite.grid82.app/your-app/v1/workflows` Every workflow here (your own workflows in this app, which act as you): name, status, trigger, the version that is live and the last run. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Requires the `workflows:read` scope. Operation `my.workflows.list`. MCP tool `my_workflows_list`. CLI: `grid82 my workflows list` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.mode` (`enum`, required, one of: `locked, personal`) - `data.owner` (`string`, required) - `data.status` (`enum`, required, one of: `draft, active, paused`) - `data.latestVersion` (`integer`, required) - `data.activeVersion` (`integer | null`, required) - `data.trigger` (`Record | null`, required) - `data.updatedAt` (`string`, required) - `data.lastRun` (`object | null`, required) - `data.lastRun.id` (`string`, required) - `data.lastRun.status` (`string`, required) - `data.lastRun.startedAt` (`string`, required) - `data.lastRun.error` (`string,null`, required) ```json { "data": [ { "id": "", "name": "", "mode": "locked", "owner": "", "status": "draft", "latestVersion": 123, "activeVersion": 123, "trigger": {}, "updatedAt": "", "lastRun": { "id": "", "status": "", "startedAt": "", "error": null } } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/workflows' \ --header 'Authorization: Bearer ' ``` --- # Create a workflow of your own Source: https://grid82.com/docs/app-api/my/workflows-create `POST https://your-suite.grid82.app/your-app/v1/workflows` Saves version 1 of a new workflow from its source. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). It starts as a draft: nothing runs until a person activates a version. The answer is the compiler's verdict on this version: `ok`, every error with its line and the fix, warnings, the graph the Automations tab draws and what the workflow can touch. A version that does not compile is kept and cannot be activated: fix the errors and save again. Needs a role that may create workflows. Requires the scopes `workflows:write`, `records:read`, `records:write`, `schema:read`. Operation `my.workflows.create`. MCP tool `my_workflows_create`. CLI: `grid82 my workflows create` (`npx grid82`). ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | ------------------------------------------- | -------- | ----------------------- | | `source` | string | yes | Up to 65536 characters. | | `note` | string | no | Up to 200 characters. | | `via` | "assistant" or "hand" or "api" or "restore" | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.version` (`integer`, required) - `data.at` (`string`, required) - `data.ok` (`boolean`, required) - `data.errors` (`object[]`, required) - `data.errors.line` (`integer`, required) - `data.errors.message` (`string`, required) - `data.warnings` (`object[]`, required) - `data.warnings.line` (`integer`, required) - `data.warnings.message` (`string`, required) - `data.graph` (`Record | null`) - `data.impact` (`Record | null`) ```json { "data": { "id": "", "version": 123, "at": "", "ok": true, "errors": [ { "line": 123, "message": "" } ], "warnings": [ { "line": 123, "message": "" } ], "graph": {}, "impact": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/workflows' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "source": "" }' ``` --- # Read a workflow Source: https://grid82.com/docs/app-api/my/workflows-get `GET https://your-suite.grid82.app/your-app/v1/workflows/{workflowId}` The workflow with one version in full (the latest unless `version` names another): its source, the compiler's verdict, the graph, the notes, and the list of every version. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Requires the `workflows:read` scope. Operation `my.workflows.get`. MCP tool `my_workflows_get`. CLI: `grid82 my workflows get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `workflowId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `version` | integer | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.mode` (`enum`, required, one of: `locked, personal`) - `data.owner` (`string`, required) - `data.status` (`enum`, required, one of: `draft, active, paused`) - `data.latestVersion` (`integer`, required) - `data.activeVersion` (`integer | null`, required) - `data.trigger` (`Record | null`, required) - `data.updatedAt` (`string`, required) - `data.lastRun` (`object | null`, required) - `data.lastRun.id` (`string`, required) - `data.lastRun.status` (`string`, required) - `data.lastRun.startedAt` (`string`, required) - `data.lastRun.error` (`string,null`, required) - `data.version` (`object | null`, required) - `data.version.version` (`integer`, required) - `data.version.source` (`string`, required) - `data.version.note` (`string,null`, required) - `data.version.via` (`string,null`, required) - `data.version.author` (`string`, required) - `data.version.at` (`string`, required) - `data.version.ok` (`boolean`, required) - `data.version.errors` (`object[]`, required) - `data.version.errors.line` (`integer`, required) - `data.version.errors.message` (`string`, required) - `data.version.warnings` (`object[]`, required) - `data.version.warnings.line` (`integer`, required) - `data.version.warnings.message` (`string`, required) - `data.version.config` (`Record | null`, required) - `data.version.graph` (`Record | null`, required) - `data.version.manifest` (`object[]`, required) - `data.version.impact` (`Record | null`, required) - `data.version.notes` (`Record`, required) - `data.version.bundleHash` (`string,null`, required) - `data.versions` (`object[]`, required) - `data.versions.version` (`integer`, required) - `data.versions.note` (`string,null`, required) - `data.versions.via` (`string,null`, required) - `data.versions.author` (`string`, required) - `data.versions.at` (`string`, required) - `data.versions.ok` (`boolean`, required) - `data.versions.bundleHash` (`string,null`, required) - `data.versions.active` (`boolean`, required) ```json { "data": { "id": "", "name": "", "mode": "locked", "owner": "", "status": "draft", "latestVersion": 123, "activeVersion": 123, "trigger": {}, "updatedAt": "", "lastRun": { "id": "", "status": "", "startedAt": "", "error": null }, "version": { "version": 123, "source": "", "note": null, "via": null, "author": "", "at": "", "ok": true, "errors": [ { "line": 123, "message": "" } ], "warnings": [ { "line": 123, "message": "" } ], "config": {}, "graph": {}, "manifest": [ null ], "impact": {}, "notes": {}, "bundleHash": null }, "versions": [ { "version": 123, "note": null, "via": null, "author": "", "at": "", "ok": true, "bundleHash": null, "active": true } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/workflows/' \ --header 'Authorization: Bearer ' ``` --- # Rename a workflow Source: https://grid82.com/docs/app-api/my/workflows-rename `PATCH https://your-suite.grid82.app/your-app/v1/workflows/{workflowId}` Changes the name people see. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). The name is a label of the workflow, not part of its code: no version is saved, nothing is recompiled, and what runs does not change. The `name` in the code is only the name a new workflow starts with. Requires the `workflows:write` scope. Operation `my.workflows.rename`. MCP tool `my_workflows_rename`. CLI: `grid82 my workflows rename` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------ | ------ | -------- | -------------------- | | `name` | string | yes | Up to 80 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) ```json { "data": { "id": "", "name": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://your-suite.grid82.app/your-app/v1/workflows/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }' ``` --- # Delete a workflow Source: https://grid82.com/docs/app-api/my/workflows-delete `DELETE https://your-suite.grid82.app/your-app/v1/workflows/{workflowId}` The workflow, its versions and its run history. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Records it changed stay as they are. Requires the `workflows:write` scope. Operation `my.workflows.delete`. MCP tool `my_workflows_delete`. CLI: `grid82 my workflows delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://your-suite.grid82.app/your-app/v1/workflows/' \ --header 'Authorization: Bearer ' ``` --- # Save a new version of a workflow Source: https://grid82.com/docs/app-api/my/workflows-code-set `PUT https://your-suite.grid82.app/your-app/v1/workflows/{workflowId}/code` Never overwrites: every save is a version, and what runs does not change until a person activates one. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Send `baseVersion` (the latest version when you read it) to be refused with 409 if somebody saved since. The answer is the compiler's verdict on this version: `ok`, every error with its line and the fix, warnings, the graph the Automations tab draws and what the workflow can touch. A version that does not compile is kept and cannot be activated: fix the errors and save again. Requires the scopes `workflows:write`, `records:read`, `records:write`, `schema:read`. Operation `my.workflows.code.set`. MCP tool `my_workflows_code_set`. CLI: `grid82 my workflows code set` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------------- | ------------------------------------------- | -------- | ----------------------- | | `source` | string | yes | Up to 65536 characters. | | `note` | string | no | Up to 200 characters. | | `via` | "assistant" or "hand" or "api" or "restore" | no | | | `baseVersion` | integer | no | At least 0. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.version` (`integer`, required) - `data.at` (`string`, required) - `data.ok` (`boolean`, required) - `data.errors` (`object[]`, required) - `data.errors.line` (`integer`, required) - `data.errors.message` (`string`, required) - `data.warnings` (`object[]`, required) - `data.warnings.line` (`integer`, required) - `data.warnings.message` (`string`, required) - `data.graph` (`Record | null`) - `data.impact` (`Record | null`) ```json { "data": { "id": "", "version": 123, "at": "", "ok": true, "errors": [ { "line": 123, "message": "" } ], "warnings": [ { "line": 123, "message": "" } ], "graph": {}, "impact": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PUT \ --url 'https://your-suite.grid82.app/your-app/v1/workflows//code' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "source": "" }' ``` --- # Check a workflow's code without saving it Source: https://grid82.com/docs/app-api/my/workflows-check `POST https://your-suite.grid82.app/your-app/v1/workflows/check` Compiles the source exactly as a save would and answers with the same verdict, but saves nothing: no version, no bundle, no history. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Use it whenever you are unsure what the compiler accepts, to try a call or a name, and to get the code right BEFORE the one save. Never save a version to find out. The answer is the compiler's verdict on this version: `ok`, every error with its line and the fix, warnings, the graph the Automations tab draws and what the workflow can touch. Nothing is kept. Requires the scopes `workflows:read`, `schema:read`. Operation `my.workflows.check`. MCP tool `my_workflows_check`. CLI: `grid82 my workflows check` (`npx grid82`). ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | ------ | -------- | ----------------------- | | `source` | string | yes | Up to 65536 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.ok` (`boolean`, required) - `data.errors` (`object[]`, required) - `data.errors.line` (`integer`, required) - `data.errors.message` (`string`, required) - `data.warnings` (`object[]`, required) - `data.warnings.line` (`integer`, required) - `data.warnings.message` (`string`, required) - `data.graph` (`Record | null`) - `data.impact` (`Record | null`) ```json { "data": { "ok": true, "errors": [ { "line": 123, "message": "" } ], "warnings": [ { "line": 123, "message": "" } ], "graph": {}, "impact": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/workflows/check' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "source": "" }' ``` --- # Test a version without changing anything Source: https://grid82.com/docs/app-api/my/workflows-test `POST https://your-suite.grid82.app/your-app/v1/workflows/{workflowId}/test` Runs a version now (the latest unless `version` says otherwise) against the real records: reads are real; record writes, POSTs and approvals are captured instead of done; waits are skipped. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). A workflow that starts from a record is tested on `recordId`, or on the newest record of its table you can see. The answer is the trace, step by step. AI steps run for real and spend AI credits. Requires the scopes `workflows:read`, `records:read`. Operation `my.workflows.test`. MCP tool `my_workflows_test`. CLI: `grid82 my workflows test` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----------- | ------- | -------- | --------------------- | | `version` | integer | no | At least 1. | | `stopAfter` | string | no | Up to 200 characters. | | `pins` | object | no | | | `input` | object | no | | | `recordId` | string | no | Up to 64 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.ok` (`boolean`, required) - `data.error` (`string,null`, required) - `data.ms` (`number`, required) - `data.output` (`object`, required) - `data.recordId` (`string`) - `data.steps` (`object[]`, required) - `data.steps.name` (`string`, required) - `data.steps.kind` (`string`, required) - `data.steps.status` (`string`, required) - `data.steps.ms` (`number`, required) - `data.steps.output` (`object`) - `data.steps.error` (`string`) - `data.steps.detail` (`string`) ```json { "data": { "ok": true, "error": null, "ms": 123.45, "output": null, "recordId": "", "steps": [ { "name": "", "kind": "", "status": "", "ms": 123.45, "output": null, "error": "", "detail": "" } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/workflows//test' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "version": 123, "stopAfter": "", "pins": {}, "input": {}, "recordId": "" }' ``` --- # Activate a version Source: https://grid82.com/docs/app-api/my/workflows-activate `POST https://your-suite.grid82.app/your-app/v1/workflows/{workflowId}/activate` Makes a version the one that runs (the latest unless `version` names another): its trigger is armed, and new runs use it. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Runs already started finish on the version they started on. Refused for a version that does not compile. Requires the scopes `workflows:write`, `records:read`, `records:write`, `schema:read`. Operation `my.workflows.activate`. MCP tool `my_workflows_activate`. CLI: `grid82 my workflows activate` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `version` | integer | no | At least 1. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.version` (`integer`, required) - `data.nextRunAt` (`string,null`, required) ```json { "data": { "id": "", "version": 123, "nextRunAt": null } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/workflows//activate' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "version": 123 }' ``` --- # Pause a workflow Source: https://grid82.com/docs/app-api/my/workflows-pause `POST https://your-suite.grid82.app/your-app/v1/workflows/{workflowId}/pause` Nothing new starts until a version is activated again. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Runs already started carry on. Requires the `workflows:write` scope. Operation `my.workflows.pause`. MCP tool `my_workflows_pause`. CLI: `grid82 my workflows pause` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.status` (`string`, required) ```json { "data": { "id": "", "status": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/workflows//pause' \ --header 'Authorization: Bearer ' ``` --- # Run a workflow now Source: https://grid82.com/docs/app-api/my/workflows-run `POST https://your-suite.grid82.app/your-app/v1/workflows/{workflowId}/run` Starts a real run of the active version, as if its trigger fired. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). For a manual trigger, pass `input` (a workflow that works on one record declares an input of type `"record"` and receives its id). `recordId` is for a workflow that starts from a record (`record.created`, `record.updated`, `record.deleted`): it runs on that record. Requires the scopes `workflows:write`, `records:read`, `records:write`, `schema:read`. Operation `my.workflows.run`. MCP tool `my_workflows_run`. CLI: `grid82 my workflows run` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | ------ | -------- | -------------------- | | `input` | object | no | | | `recordId` | string | no | Up to 64 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.runId` (`string`, required) ```json { "data": { "runId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/workflows//run' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "input": {}, "recordId": "" }' ``` --- # A workflow's runs, newest first Source: https://grid82.com/docs/app-api/my/workflows-runs-list `GET https://your-suite.grid82.app/your-app/v1/workflows/{workflowId}/runs` Status, version, when it started and ended, how many records it changed, and the error of a run that failed. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Requires the `workflows:read` scope. Operation `my.workflows.runs.list`. MCP tool `my_workflows_runs_list`. CLI: `grid82 my workflows runs list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `workflowId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | ------- | ------- | -------- | ------------- | | `limit` | integer | no | Default `50`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.version` (`integer`, required) - `data.status` (`string`, required) - `data.trigger` (`string,null`, required) - `data.startedAt` (`string`, required) - `data.endedAt` (`string,null`, required) - `data.error` (`string,null`, required) - `data.writes` (`integer`, required) ```json { "data": [ { "id": "", "version": 123, "status": "", "trigger": null, "startedAt": "", "endedAt": null, "error": null, "writes": 123 } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/workflows//runs' \ --header 'Authorization: Bearer ' ``` --- # Read one run, step by step Source: https://grid82.com/docs/app-api/my/workflows-runs-get `GET https://your-suite.grid82.app/your-app/v1/workflow-runs/{runId}` Every step of the run in order with its status, its result or its error. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Read the `workflow-runs` skill to diagnose a failure. Requires the `workflows:read` scope. Operation `my.workflows.runs.get`. MCP tool `my_workflows_runs_get`. CLI: `grid82 my workflows runs get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `runId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.version` (`integer`, required) - `data.status` (`string`, required) - `data.trigger` (`string,null`, required) - `data.startedAt` (`string`, required) - `data.endedAt` (`string,null`, required) - `data.error` (`string,null`, required) - `data.writes` (`integer`, required) - `data.workflowId` (`string`, required) - `data.output` (`object`, required) - `data.steps` (`object[]`, required) - `data.steps.name` (`string`, required) - `data.steps.status` (`string`, required) - `data.steps.startedAt` (`string,null`, required) - `data.steps.endedAt` (`string,null`, required) - `data.steps.output` (`object`, required) - `data.steps.error` (`string,null`, required) - `data.steps.detail` (`string,null`, required) ```json { "data": { "id": "", "version": 123, "status": "", "trigger": null, "startedAt": "", "endedAt": null, "error": null, "writes": 123, "workflowId": "", "output": null, "steps": [ { "name": "", "status": "", "startedAt": null, "endedAt": null, "output": null, "error": null, "detail": null } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/workflow-runs/' \ --header 'Authorization: Bearer ' ``` --- # Stop a run Source: https://grid82.com/docs/app-api/my/workflows-runs-cancel `POST https://your-suite.grid82.app/your-app/v1/workflow-runs/{runId}/cancel` The run stops where it is. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). What it already changed stays changed. Requires the `workflows:write` scope. Operation `my.workflows.runs.cancel`. MCP tool `my_workflows_runs_cancel`. CLI: `grid82 my workflows runs cancel` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `runId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.runId` (`string`, required) ```json { "data": { "runId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/workflow-runs//cancel' \ --header 'Authorization: Bearer ' ``` --- # Retry a run Source: https://grid82.com/docs/app-api/my/workflows-runs-retry `POST https://your-suite.grid82.app/your-app/v1/workflow-runs/{runId}/retry` Only a run that failed or was stopped. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). It restarts at the step that did not finish: every step before it keeps its saved result and does not run again. `from` names that step; null means the run restarted from the beginning, where the steps before it run again, except record writes, which return the record they wrote the first time. Use it after fixing the data or the roles, not for a code error (save and activate a new version). Requires the scopes `workflows:write`, `records:read`, `records:write`, `schema:read`. Operation `my.workflows.runs.retry`. MCP tool `my_workflows_runs_retry`. CLI: `grid82 my workflows runs retry` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `runId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.runId` (`string`, required) - `data.from` (`string,null`, required) ```json { "data": { "runId": "", "from": null } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/workflow-runs//retry' \ --header 'Authorization: Bearer ' ``` --- # Who this token acts as Source: https://grid82.com/docs/app-api/me/get `GET https://your-suite.grid82.app/your-app/v1/me` Get the user, their roles, and how the request arrived. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). An API key cannot have more permissions than its user. Any valid credential for this API: no scope is needed. Operation `me.get`. MCP tool `me_get`. CLI: `grid82 me get` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.userId` (`string`, required, length: `1–64 chars`) - `data.email` (`string`, pattern: `^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$`) - `data.roles` (`string[]`, required) - `data.via` (`enum`, required, one of: `ui, api, cli, mcp, agent, assistant, system, studio, workflow`) - `data.workflow` (`object`) - `data.workflow.id` (`string`, required, length: `1–64 chars`) - `data.workflow.runId` (`string`, required, length: `1–64 chars`) - `data.workflow.depth` (`integer`, required, range: `0–10`) - `data.plane` (`enum`, one of: `app, builder`) ```json { "data": { "userId": "", "email": "", "roles": [ "" ], "via": "ui", "workflow": { "id": "", "runId": "", "depth": 123 }, "plane": "app" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/me' \ --header 'Authorization: Bearer ' ``` --- # Your keys and connected agents in this app Source: https://grid82.com/docs/app-api/me/keys-list `GET https://your-suite.grid82.app/your-app/v1/me/keys` Every credential acting as you here: the keys you made and the AI clients you connected. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Never the secrets themselves. Any valid credential for this API: no scope is needed. Operation `me.keys.list`. MCP tool `me_keys_list`. CLI: `grid82 me keys list` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.kind` (`enum`, required, one of: `key, oauth`) - `data.name` (`string`, required) - `data.keyId` (`string`, required) - `data.scopes` (`string[]`, required) - `data.clientId` (`string,null`, required) - `data.appIds` (`string[] | null`, required) - `data.createdAt` (`string`, required) - `data.expiresAt` (`string,null`, required) - `data.lastUsedAt` (`string,null`, required) ```json { "data": [ { "id": "", "kind": "key", "name": "", "keyId": "", "scopes": [ "" ], "clientId": null, "appIds": [ "" ], "createdAt": "", "expiresAt": null, "lastUsedAt": null } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/me/keys' \ --header 'Authorization: Bearer ' ``` --- # Create a personal API key for this app Source: https://grid82.com/docs/app-api/me/keys-create `POST https://your-suite.grid82.app/your-app/v1/me/keys` Mint a key that acts as you in this app, with no more permissions than you have. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Only signed-in browsers can mint keys; keys and connected agents cannot mint keys. Keys expire in 90 days unless configured otherwise; `expiresInDays: null` keeps the key active until revoked. The key is returned once and never stored in clear. Any valid credential for this API: no scope is needed. Operation `me.keys.create`. Not an MCP tool. Minting a credential from a credential is how one leaked token becomes a permanent one. Keys are made by a person in a signed-in browser, never by a connected agent. CLI: `grid82 me keys create` (`npx grid82`). ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------------- | --------------- | -------- | -------------------- | | `name` | string | yes | Up to 60 characters. | | `scopes` | string\[] | yes | | | `expiresInDays` | integer or null | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.kind` (`enum`, required, one of: `key, oauth`) - `data.name` (`string`, required) - `data.keyId` (`string`, required) - `data.scopes` (`string[]`, required) - `data.clientId` (`string,null`, required) - `data.appIds` (`string[] | null`, required) - `data.createdAt` (`string`, required) - `data.expiresAt` (`string,null`, required) - `data.lastUsedAt` (`string,null`, required) - `data.key` (`string`, required) ```json { "data": { "id": "", "kind": "key", "name": "", "keyId": "", "scopes": [ "" ], "clientId": null, "appIds": [ "" ], "createdAt": "", "expiresAt": null, "lastUsedAt": null, "key": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/me/keys' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "scopes": [ "" ] }' ``` --- # Revoke one of your credentials Source: https://grid82.com/docs/app-api/me/keys-revoke `DELETE https://your-suite.grid82.app/your-app/v1/me/keys/{credentialId}` Stops a key or a connected agent working, immediately. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Only your own, and only from a signed-in browser. Any valid credential for this API: no scope is needed. Operation `me.keys.revoke`. Not an MCP tool. A connected agent revoking the person's other credentials is not a thing it should be able to do on their behalf. Revocation is done by the person, in a signed-in browser. CLI: `grid82 me keys revoke` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------------- | ------ | -------- | ----------- | | `credentialId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://your-suite.grid82.app/your-app/v1/me/keys/' \ --header 'Authorization: Bearer ' ``` --- # Decisions waiting for you Source: https://grid82.com/docs/app-api/approvals/list `GET https://your-suite.grid82.app/your-app/v1/approvals` Approvals workflows asked of one of your roles in this app, oldest first, with exactly what the workflow chose to show. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). Requires the `records:read` scope. Operation `approvals.list`. MCP tool `approvals_list`. CLI: `grid82 approvals list` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.workflowId` (`string`, required) - `data.workflowName` (`string`, required) - `data.label` (`string`, required) - `data.role` (`string,null`, required) - `data.question` (`string,null`, required) - `data.show` (`object`, required) - `data.createdAt` (`string`, required) - `data.expiresAt` (`string`, required) ```json { "data": [ { "id": "", "workflowId": "", "workflowName": "", "label": "", "role": null, "question": null, "show": null, "createdAt": "", "expiresAt": "" } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://your-suite.grid82.app/your-app/v1/approvals' \ --header 'Authorization: Bearer ' ``` --- # Approve or reject Source: https://grid82.com/docs/app-api/approvals/decide `POST https://your-suite.grid82.app/your-app/v1/approvals/{approvalId}` Your decision wakes the workflow that asked. > **Note: Generic contract** > > Your own version of this operation, with your tables and the fields you can read, is in > your app's document: `https://{suite}.grid82.app/{app}/v1/openapi.json`, behind your credential. > [Why each person gets their own](/docs/start/studio-and-runtime#why-an-app-has-an-api-of-its-own). A person's decision, never an assistant's. Requires the `records:write` scope. Operation `approvals.decide`. MCP tool `approvals_decide`. CLI: `grid82 approvals decide` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `approvalId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | ------- | -------- | ---------------------- | | `approved` | boolean | yes | | | `note` | string | no | Up to 1000 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.approved` (`boolean`, required) ```json { "data": { "id": "", "approved": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://your-suite.grid82.app/your-app/v1/approvals/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "approved": true }' ``` --- # Who is signed in Source: https://grid82.com/docs/builder-api/session/get `GET https://studio.grid82.app/v1/session` Identify the acting builder, how it arrived (browser session, key, or connected agent), and its scopes. Organisations and everything below them are available through other routes. This is what `grid82 auth status` checks. Any valid credential for this API: no scope is needed. Operation `session.get`. MCP tool `session_get`. CLI: `grid82 session get` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.user` (`object`, required) - `data.user.id` (`string`, required) - `data.user.name` (`string,null`, required) - `data.user.email` (`string`, required) - `data.via` (`enum`, required, one of: `session, key, oauth, assistant`) - `data.scopes` (`string[]`, required) ```json { "data": { "user": { "id": "", "name": null, "email": "" }, "via": "session", "scopes": [ "" ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/session' \ --header 'Authorization: Bearer ' ``` --- # The organisations you build for Source: https://grid82.com/docs/builder-api/orgs/list `GET https://studio.grid82.app/v1/organizations` Every organisation you are a builder of. An organisation pays, holds the plan, and contains suites; this is what the switcher at the top of the Studio reads. Requires the `account:read` scope. Operation `orgs.list`. MCP tool `orgs_list`. CLI: `grid82 orgs list` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.plan` (`enum`, required, one of: `free, starter, pro, custom`) - `data.role` (`string`, required) - `data.slug` (`string,null`, required) - `data.suites` (`integer`, required) - `data.apps` (`integer`, required) - `data.soleAdmin` (`boolean`, required) - `data.canCreateSuites` (`boolean`, required) - `data.builders` (`integer | null`, required) - `data.subscriptionLive` (`boolean,null`, required) ```json { "data": [ { "id": "", "name": "", "plan": "free", "role": "", "slug": null, "suites": 123, "apps": 123, "soleAdmin": true, "canCreateSuites": true, "builders": 123, "subscriptionLive": null } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/organizations' \ --header 'Authorization: Bearer ' ``` --- # Start another organisation Source: https://grid82.com/docs/builder-api/orgs/create `POST https://studio.grid82.app/v1/organizations` Create a separate organisation for another company, with its own plan, invoice, suites, builders, and billing. Do not create one for a second customer of the same company; use a suite instead. Requires the `account:write` scope. Operation `orgs.create`. MCP tool `orgs_create`. CLI: `grid82 orgs create` (`npx grid82`). ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------ | ------ | -------- | -------------------- | | `name` | string | yes | Up to 60 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) ```json { "data": { "id": "", "name": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/organizations' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }' ``` --- # Read an organisation Source: https://grid82.com/docs/builder-api/orgs/get `GET https://studio.grid82.app/v1/organizations/{organizationId}` Its name, its plan, when it was created, and the models its builders' own assistant offers in the Studio. Billing detail is a separate call, because most screens do not need it. Requires the `account:read` scope. Operation `orgs.get`. MCP tool `orgs_get`. CLI: `grid82 orgs get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.plan` (`enum`, required, one of: `free, starter, pro, custom`) - `data.createdAt` (`string`, required) - `data.status` (`string`, required) - `data.assistantModels` (`object[]`, required) - `data.assistantModels.label` (`string`, required, length: `1–24 chars`) - `data.assistantModels.model` (`string`, required, length: `1–120 chars`) - `data.assistantModels.reasoning` (`enum | null`, required) ```json { "data": { "id": "", "name": "", "plan": "free", "createdAt": "", "status": "", "assistantModels": [ { "label": "", "model": "", "reasoning": "none" } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/organizations/' \ --header 'Authorization: Bearer ' ``` --- # Change an organisation Source: https://grid82.com/docs/builder-api/orgs/update `PATCH https://studio.grid82.app/v1/organizations/{organizationId}` Set your team's Studio name, shown at the top of the Studio without changing any suite login page, and the models your assistant offers in the Studio. This menu applies only to the Studio; set app users' options per suite, where they see a label rather than a model name. Requires the `schema:write` scope. Operation `orgs.update`. MCP tool `orgs_update`. CLI: `grid82 orgs update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----------------- | --------- | -------- | -------------------- | | `name` | string | no | Up to 60 characters. | | `assistantModels` | object\[] | no | 1 to 5 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.assistantModels` (`object[]`, required) - `data.assistantModels.label` (`string`, required, length: `1–24 chars`) - `data.assistantModels.model` (`string`, required, length: `1–120 chars`) - `data.assistantModels.reasoning` (`enum | null`, required) ```json { "data": { "id": "", "name": "", "assistantModels": [ { "label": "", "model": "", "reasoning": "none" } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/organizations/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "assistantModels": [ { "label": "", "model": "", "reasoning": "none" } ] }' ``` --- # Delete an organisation Source: https://grid82.com/docs/builder-api/orgs/delete `DELETE https://studio.grid82.app/v1/organizations/{organizationId}` Delete the organisation; admins only. Send its name as `confirm`. This deletes every suite, app, and directory, and makes every address stop answering. App data remains through the grace period so deletion can be undone, but addresses are released immediately. Deletion is refused while a paid subscription is attached; cancel it with `billing.cancel` and wait for the period to end first, because deleting the organisation does not stop charges. Requires the `account:write` scope. Operation `orgs.delete`. MCP tool `orgs_delete`. CLI: `grid82 orgs delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------ | -------- | --------------------- | | `confirm` | string | no | Up to 200 characters. | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/organizations/' \ --header 'Authorization: Bearer ' ``` --- # Leave an organisation Source: https://grid82.com/docs/builder-api/orgs/leave `POST https://studio.grid82.app/v1/organizations/{organizationId}/leave` Leave the organisation and lose every suite under it; nothing in the organisation is deleted. The last admin cannot leave: make someone else an admin or delete the organisation. Requires the `account:write` scope. Operation `orgs.leave`. MCP tool `orgs_leave`. CLI: `grid82 orgs leave` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/organizations//leave' \ --header 'Authorization: Bearer ' ``` --- # Close your account Source: https://grid82.com/docs/builder-api/account/delete `DELETE https://studio.grid82.app/v1/me` Delete your account and sign-in; delete organisations where you are the only builder, including their suites and apps, and leave organisations with another admin. Refused if you are the only admin of an organisation where other builders work (make one an admin first on that organisation's Builders page) or if an organisation to be deleted still has a paid subscription. The impact is shown before confirmation. Send your account email address as `confirm`. Requires the `account:write` scope. Operation `account.delete`. MCP tool `account_delete`. CLI: `grid82 account delete` (`npx grid82`). ## Query parameters | Name | Type | Required | Description | | --------- | ------ | -------- | --------------------- | | `confirm` | string | no | Up to 320 characters. | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.deletedOrganizations` (`string[]`, required) - `data.leftOrganizations` (`string[]`, required) ```json { "data": { "deletedOrganizations": [ "" ], "leftOrganizations": [ "" ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/me' \ --header 'Authorization: Bearer ' ``` --- # The builders of an organisation Source: https://grid82.com/docs/builder-api/builders/list `GET https://studio.grid82.app/v1/organizations/{organizationId}/builders` List builders who can open this Studio, their roles, and the suites each may work on. Excludes app users, who are listed in a suite's directory and cannot access the Studio. Requires the `members:read` scope. Operation `builders.list`. MCP tool `builders_list`. CLI: `grid82 builders list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.userId` (`string`, required) - `data.email` (`string,null`, required) - `data.name` (`string,null`, required) - `data.role` (`enum`, required, one of: `admin, member`) - `data.joinedAt` (`string,null`, required) - `data.allSuites` (`boolean`, required) - `data.suiteIds` (`string[]`, required) - `data.canCreateSuites` (`boolean`, required) ```json { "data": [ { "userId": "", "email": null, "name": null, "role": "admin", "joinedAt": null, "allSuites": true, "suiteIds": [ "" ], "canCreateSuites": true } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/organizations//builders' \ --header 'Authorization: Bearer ' ``` --- # Change what a builder is, or which suites they may work on Source: https://grid82.com/docs/builder-api/builders/update `PATCH https://studio.grid82.app/v1/organizations/{organizationId}/builders/{userId}` Restrict updates to admins. Admins can do everything and need no suite list; members see only their assigned suites, or every suite in the organisation when `allSuites` is on. Prevent demoting the last admin so the organisation always has someone who can change it. Requires the `members:write` scope. Operation `builders.update`. MCP tool `builders_update`. CLI: `grid82 builders update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | | `userId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----------------- | ------------------- | -------- | ----------- | | `role` | "admin" or "member" | no | | | `allSuites` | boolean | no | | | `suiteIds` | string\[] | no | | | `canCreateSuites` | boolean | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.userId` (`string`, required) - `data.role` (`enum`, required, one of: `admin, member`) - `data.allSuites` (`boolean`, required) - `data.suiteIds` (`string[]`, required) - `data.canCreateSuites` (`boolean`, required) ```json { "data": { "userId": "", "role": "admin", "allSuites": true, "suiteIds": [ "" ], "canCreateSuites": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/organizations//builders/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "role": "admin", "allSuites": true, "suiteIds": [ "" ], "canCreateSuites": true }' ``` --- # The organisation's builder invite link Source: https://grid82.com/docs/builder-api/builders/invite-link-get `GET https://studio.grid82.app/v1/organizations/{organizationId}/builders/invite-link` Admins only. Whoever opens this link and signs in can join the organisation as a member with no suite, until it expires seven days after it was turned on. The link is a credential: send it to the people you mean, never paste it anywhere public. Requires the `members:read` scope. Operation `builders.inviteLink.get`. MCP tool `builders_inviteLink_get`. CLI: `grid82 builders invite-link get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.enabled` (`boolean`, required) ```json { "data": { "enabled": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/organizations//builders/invite-link' \ --header 'Authorization: Bearer ' ``` --- # Turn the builder invite link on, renew it, or turn it off Source: https://grid82.com/docs/builder-api/builders/invite-link-set `PUT https://studio.grid82.app/v1/organizations/{organizationId}/builders/invite-link` Admins only. `enabled: true` makes a new link valid for seven days and the previous one stops working at once, so it also renews an expired link. `enabled: false` stops the current link; nobody already in the organisation is removed. Requires the `members:write` scope. Operation `builders.inviteLink.set`. MCP tool `builders_inviteLink_set`. CLI: `grid82 builders invite-link set` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `enabled` | boolean | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.enabled` (`boolean`, required) ```json { "data": { "enabled": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PUT \ --url 'https://studio.grid82.app/v1/organizations//builders/invite-link' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "enabled": true }' ``` --- # Plan, usage and limits Source: https://grid82.com/docs/builder-api/billing/get `GET https://studio.grid82.app/v1/organizations/{organizationId}/billing` Retrieve the organisation's plan, usage, and allowed features. `plan` is the id: `free` is the plan named Solo. Limits are counted against the organisation that pays; suites are never billed for existing. Requires the `account:read` scope. Operation `billing.get`. MCP tool `billing_get`. CLI: `grid82 billing get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.organizationId` (`string`, required) - `data.plan` (`enum`, required, one of: `free, starter, pro, custom`) - `data.planStatus` (`string`, required) - `data.renewsAt` (`string,null`, required) - `data.hasPaymentMethod` (`boolean`, required) - `data.usage` (`object`, required) - `data.usage.suites` (`integer`, required) - `data.usage.apps` (`integer`, required) - `data.usage.users` (`integer`, required) - `data.usage.builders` (`integer`, required) - `data.usage.storageBytes` (`integer`) - `data.usage.customDomains` (`integer`) - `data.limits` (`object`, required) - `data.limits.builders` (`integer | null`, required) - `data.limits.suites` (`integer | null`, required) - `data.limits.apps` (`integer | null`, required) - `data.limits.users` (`integer | null`, required) - `data.limits.rowsPerApp` (`integer | null`, required) - `data.limits.storageBytes` (`integer | null`, required) - `data.limits.customDomains` (`integer | null`, required) - `data.limits.assistantCredits` (`integer | null`, required) - `data.addons` (`Record`, required) - `data.subscription` (`object`, required) - `data.subscription.live` (`boolean`, required) - `data.subscription.interval` (`enum | null`, required) - `data.subscription.cancelAt` (`string,null`, required) - `data.subscription.pendingChange` (`object | null`, required) - `data.subscription.pendingChange.plan` (`enum`, required, one of: `free, starter, pro, custom`) - `data.subscription.pendingChange.interval` (`enum | null`, required) - `data.subscription.pendingChange.effectiveAt` (`string,null`, required) - `data.checkout` (`object`, required) - `data.checkout.connected` (`boolean`, required) - `data.checkout.reason` (`string,null`, required) - `data.checkout.purchasable` (`object[]`, required) - `data.checkout.purchasable.plan` (`enum`, required, one of: `free, starter, pro, custom`) - `data.checkout.purchasable.interval` (`enum`, required, one of: `monthly, yearly`) - `data.grants` (`object`, required) - `data.grants.availableCredits` (`number`, required) - `data.grants.pendingCredits` (`number`, required) - `data.pricingIsPlaceholder` (`boolean`, required) ```json { "data": { "organizationId": "", "plan": "free", "planStatus": "", "renewsAt": null, "hasPaymentMethod": true, "usage": { "suites": 123, "apps": 123, "users": 123, "builders": 123, "storageBytes": 123, "customDomains": 123 }, "limits": { "builders": 123, "suites": 123, "apps": 123, "users": 123, "rowsPerApp": 123, "storageBytes": 123, "customDomains": 123, "assistantCredits": 123 }, "addons": {}, "subscription": { "live": true, "interval": "monthly", "cancelAt": null, "pendingChange": { "plan": "free", "interval": "monthly", "effectiveAt": null } }, "checkout": { "connected": true, "reason": null, "purchasable": [ { "plan": "free", "interval": "monthly" } ] }, "grants": { "availableCredits": 123.45, "pendingCredits": 123.45 }, "pricingIsPlaceholder": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/organizations//billing' \ --header 'Authorization: Bearer ' ``` --- # Start paying for a plan Source: https://grid82.com/docs/builder-api/billing/checkout `POST https://studio.grid82.app/v1/organizations/{organizationId}/billing/checkout` Create a Stripe Checkout address for an organisation with no paid subscription, where a person enters a card and the plan applies after completion; nothing is charged until then. Admins only. For organisations already paying, use billing.change instead. Returns 501 while billing is not connected. Requires the `account:write` scope. Operation `billing.checkout`. MCP tool `billing_checkout`. CLI: `grid82 billing checkout` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | ---------------------------------------- | -------- | -------------------- | | `plan` | "free" or "starter" or "pro" or "custom" | yes | | | `interval` | "monthly" or "yearly" | no | Default `"monthly"`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.url` (`string`, required) ```json { "data": { "url": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/organizations//billing/checkout' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "plan": "free" }' ``` --- # What changing plan would do, and cost today Source: https://grid82.com/docs/builder-api/billing/preview `POST https://studio.grid82.app/v1/organizations/{organizationId}/billing/preview` Admins only. Changes nothing. Says whether the change is immediate (an upgrade, charged now and prorated, with Stripe's own figure for today), scheduled for the end of the paid period (a downgrade), or needs a checkout (no subscription yet). Refused with 501 while billing is not connected. Requires the `account:read` scope. Operation `billing.preview`. MCP tool `billing_preview`. CLI: `grid82 billing preview` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | ---------------------------------------- | -------- | -------------------- | | `plan` | "free" or "starter" or "pro" or "custom" | yes | | | `interval` | "monthly" or "yearly" | no | Default `"monthly"`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.kind` (`enum`, required, one of: `first_subscription, upgrade_immediate, downgrade_scheduled`) - `data.plan` (`enum`, required, one of: `free, starter, pro, custom`) - `data.interval` (`enum`, required, one of: `monthly, yearly`) - `data.effectiveImmediately` (`boolean`, required) - `data.effectiveAt` (`string,null`, required) - `data.amountDueToday` (`number,null`, required) - `data.currency` (`string,null`, required) - `data.breakdown` (`object | null`, required) - `data.breakdown.unusedCredit` (`number`, required) - `data.breakdown.newCharge` (`number`, required) - `data.breakdown.tax` (`number`, required) ```json { "data": { "kind": "first_subscription", "plan": "free", "interval": "monthly", "effectiveImmediately": true, "effectiveAt": null, "amountDueToday": null, "currency": null, "breakdown": { "unusedCredit": 123.45, "newCharge": 123.45, "tax": 123.45 } } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/organizations//billing/preview' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "plan": "free" }' ``` --- # Change plan Source: https://grid82.com/docs/builder-api/billing/change `POST https://studio.grid82.app/v1/organizations/{organizationId}/billing/change` Admins only. Up a tier, or monthly to yearly: immediate, prorated and charged now to the card on file; a declined card refuses the change. Down a tier, or yearly to monthly: scheduled for the end of the period already paid for, and withdrawable with billing.cancel (what: pendingChange). With no subscription yet, returns `checkoutUrl` instead. Call billing.preview first to see the figure. Refused with 501 while billing is not connected. Requires the `account:write` scope. Operation `billing.change`. MCP tool `billing_change`. CLI: `grid82 billing change` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | ---------------------------------------- | -------- | -------------------- | | `plan` | "free" or "starter" or "pro" or "custom" | yes | | | `interval` | "monthly" or "yearly" | no | Default `"monthly"`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.kind` (`enum`, required, one of: `first_subscription, upgrade_immediate, downgrade_scheduled`) - `data.checkoutUrl` (`string,null`, required) - `data.effectiveAt` (`string,null`, required) ```json { "data": { "kind": "first_subscription", "checkoutUrl": null, "effectiveAt": null } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/organizations//billing/change' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "plan": "free" }' ``` --- # Cancel the subscription, or a scheduled change Source: https://grid82.com/docs/builder-api/billing/cancel `POST https://studio.grid82.app/v1/organizations/{organizationId}/billing/cancel` Admins only. `what: subscription` (the default) stops the subscription at the end of the period already paid for; the plan stays until then and billing.resume undoes it. `what: pendingChange` withdraws a scheduled downgrade and keeps the current plan. Refused with 501 while billing is not connected. Requires the `account:write` scope. Operation `billing.cancel`. MCP tool `billing_cancel`. CLI: `grid82 billing cancel` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------ | --------------------------------- | -------- | ------------------------- | | `what` | "subscription" or "pendingChange" | no | Default `"subscription"`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.cancelAt` (`string,null`, required) - `data.pendingChangeWithdrawn` (`boolean`, required) ```json { "data": { "cancelAt": null, "pendingChangeWithdrawn": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/organizations//billing/cancel' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "what": "subscription" }' ``` --- # Keep the plan after cancelling it Source: https://grid82.com/docs/builder-api/billing/resume `POST https://studio.grid82.app/v1/organizations/{organizationId}/billing/resume` Admins only. Undoes a cancellation that has not taken effect yet, so the subscription renews as before and the card is charged at the next renewal. Refused with 501 while billing is not connected. Requires the `account:write` scope. Operation `billing.resume`. MCP tool `billing_resume`. CLI: `grid82 billing resume` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.resumed` (`boolean`, required) ```json { "data": { "resumed": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/organizations//billing/resume' \ --header 'Authorization: Bearer ' ``` --- # Open Stripe's billing portal Source: https://grid82.com/docs/builder-api/billing/portal `POST https://studio.grid82.app/v1/organizations/{organizationId}/billing/portal` Return a short-lived Stripe address for a person to update the card, billing address, and tax ID and download invoices; admins only. The card is never visible to us. Refuse with 501 while billing is not connected; refuse if the organisation has never paid. Requires the `account:write` scope. Operation `billing.portal`. MCP tool `billing_portal`. CLI: `grid82 billing portal` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.url` (`string`, required) ```json { "data": { "url": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/organizations//billing/portal' \ --header 'Authorization: Bearer ' ``` --- # What is happening across an organisation Source: https://grid82.com/docs/builder-api/analytics/get `GET https://studio.grid82.app/v1/organizations/{organizationId}/analytics` Retrieve activity and cost for a window, filterable to one suite or one app. Counts come from the platform's event log, which records that something happened, never its contents: for example, a row was created in a table, not the row. Answer where work is happening, who is using an app, and what the assistant is costing. Requires the `account:read` scope. Operation `analytics.get`. MCP tool `analytics_get`. CLI: `grid82 analytics get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ------------- | | `suiteId` | string | no | | | `appId` | string | no | | | `days` | integer | no | Default `30`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.window` (`object`, required) - `data.window.days` (`number`, required) - `data.window.from` (`string`, required) - `data.window.to` (`string`, required) - `data.totals` (`object`, required) - `data.totals.suites` (`number`, required) - `data.totals.apps` (`number`, required) - `data.totals.users` (`number`, required) - `data.totals.builders` (`number`, required) - `data.totals.activeUsers` (`number`, required) - `data.totals.recordsWritten` (`number`, required) - `data.totals.machineCalls` (`number`, required) - `data.totals.signIns` (`number`, required) - `data.daily` (`object[]`, required) - `data.daily.day` (`string`, required) - `data.daily.activeUsers` (`number`, required) - `data.daily.records` (`number`, required) - `data.daily.calls` (`number`, required) - `data.byApp` (`object[]`, required) - `data.byApp.appId` (`string`, required) - `data.byApp.name` (`string`, required) - `data.byApp.suiteId` (`string`, required) - `data.byApp.activeUsers` (`number`, required) - `data.byApp.records` (`number`, required) - `data.byApp.calls` (`number`, required) - `data.byApp.members` (`number`, required) - `data.bySurface` (`object[]`, required) - `data.bySurface.via` (`string`, required) - `data.bySurface.count` (`number`, required) - `data.assistant` (`object`, required) - `data.assistant.runs` (`number`, required) - `data.assistant.proposed` (`number`, required) - `data.assistant.applied` (`number`, required) - `data.assistant.costUsd` (`number`, required) - `data.assistant.tokens` (`number`, required) - `data.plan` (`object`, required) - `data.plan.id` (`enum`, required, one of: `free, starter, pro, custom`) - `data.plan.usage` (`object`, required) - `data.plan.usage.suites` (`integer`, required) - `data.plan.usage.apps` (`integer`, required) - `data.plan.usage.users` (`integer`, required) - `data.plan.usage.builders` (`integer`, required) - `data.plan.usage.storageBytes` (`integer`) - `data.plan.usage.customDomains` (`integer`) - `data.plan.limits` (`Record`, required) ```json { "data": { "window": { "days": 123.45, "from": "", "to": "" }, "totals": { "suites": 123.45, "apps": 123.45, "users": 123.45, "builders": 123.45, "activeUsers": 123.45, "recordsWritten": 123.45, "machineCalls": 123.45, "signIns": 123.45 }, "daily": [ { "day": "", "activeUsers": 123.45, "records": 123.45, "calls": 123.45 } ], "byApp": [ { "appId": "", "name": "", "suiteId": "", "activeUsers": 123.45, "records": 123.45, "calls": 123.45, "members": 123.45 } ], "bySurface": [ { "via": "", "count": 123.45 } ], "assistant": { "runs": 123.45, "proposed": 123.45, "applied": 123.45, "costUsd": 123.45, "tokens": 123.45 }, "plan": { "id": "free", "usage": { "suites": 123, "apps": 123, "users": 123, "builders": 123, "storageBytes": 123, "customDomains": 123 }, "limits": {} } } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/organizations//analytics' \ --header 'Authorization: Bearer ' ``` --- # The suites of an organisation Source: https://grid82.com/docs/builder-api/suites/list `GET https://studio.grid82.app/v1/organizations/{organizationId}/suites` Each suite is one user directory and one login page, holding one or more apps. An agency makes one per client; a company usually needs one. Requires the `account:read` scope. Operation `suites.list`. MCP tool `suites_list`. CLI: `grid82 suites list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.organizationId` (`string`, required) - `data.name` (`string`, required, length: `1–120 chars`) - `data.slug` (`string`, required, length: `2–40 chars`, pattern: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`) - `data.slugPrefix` (`string`) - `data.authPassword` (`boolean`, required) - `data.authGoogle` (`boolean`, required) - `data.allowSelfSignup` (`boolean`, required) - `data.accent` (`string | null`, required) - `data.logoUrl` (`string | null`, required) - `data.createdAt` (`string`, required) - `data.assistantMonthlyCredits` (`integer | null`, required) - `data.apps` (`integer`, required) - `data.users` (`integer`, required) - `data.assistantCreditsUsed` (`number`) - `data.assistantCreditsLimit` (`number,null`) - `data.assistantPresets` (`object[]`, required) - `data.assistantPresets.label` (`string`, required, length: `1–24 chars`) - `data.assistantPresets.model` (`string`, required, length: `1–120 chars`) - `data.assistantPresets.reasoning` (`enum | null`, required) - `data.assistantPresetsEditable` (`boolean`) ```json { "data": [ { "id": "", "organizationId": "", "name": "", "slug": "", "slugPrefix": "", "authPassword": true, "authGoogle": true, "allowSelfSignup": true, "accent": "", "logoUrl": "", "createdAt": "", "assistantMonthlyCredits": 123, "apps": 123, "users": 123, "assistantCreditsUsed": 123.45, "assistantCreditsLimit": null, "assistantPresets": [ { "label": "", "model": "", "reasoning": "none" } ], "assistantPresetsEditable": true } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/organizations//suites' \ --header 'Authorization: Bearer ' ``` --- # Create a suite Source: https://grid82.com/docs/builder-api/suites/create `POST https://studio.grid82.app/v1/organizations/{organizationId}/suites` Create an empty directory and login page; nobody is added, and creation grants no access to apps inside. Set `slug` to the subdomain; if it is taken, creation is refused rather than adding a suffix. Omit `slug` to derive it from the name. Requires the `schema:write` scope. Operation `suites.create`. MCP tool `suites_create`. CLI: `grid82 suites create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------ | ------ | -------- | -------------------- | | `name` | string | yes | Up to 60 characters. | | `slug` | string | no | Up to 40 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.organizationId` (`string`, required) - `data.name` (`string`, required, length: `1–120 chars`) - `data.slug` (`string`, required, length: `2–40 chars`, pattern: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`) - `data.slugPrefix` (`string`) - `data.authPassword` (`boolean`, required) - `data.authGoogle` (`boolean`, required) - `data.allowSelfSignup` (`boolean`, required) - `data.accent` (`string | null`, required) - `data.logoUrl` (`string | null`, required) - `data.createdAt` (`string`, required) - `data.assistantMonthlyCredits` (`integer | null`, required) - `data.apps` (`integer`, required) - `data.users` (`integer`, required) - `data.assistantCreditsUsed` (`number`) - `data.assistantCreditsLimit` (`number,null`) - `data.assistantPresets` (`object[]`, required) - `data.assistantPresets.label` (`string`, required, length: `1–24 chars`) - `data.assistantPresets.model` (`string`, required, length: `1–120 chars`) - `data.assistantPresets.reasoning` (`enum | null`, required) - `data.assistantPresetsEditable` (`boolean`) ```json { "data": { "id": "", "organizationId": "", "name": "", "slug": "", "slugPrefix": "", "authPassword": true, "authGoogle": true, "allowSelfSignup": true, "accent": "", "logoUrl": "", "createdAt": "", "assistantMonthlyCredits": 123, "apps": 123, "users": 123, "assistantCreditsUsed": 123.45, "assistantCreditsLimit": null, "assistantPresets": [ { "label": "", "model": "", "reasoning": "none" } ], "assistantPresetsEditable": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/organizations//suites' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }' ``` --- # Read a suite Source: https://grid82.com/docs/builder-api/suites/get `GET https://studio.grid82.app/v1/suites/{suiteId}` The suite's name, branding and login switches: which providers its users may sign in with, and whether a stranger may sign themselves up. Requires the `account:read` scope. Operation `suites.get`. MCP tool `suites_get`. CLI: `grid82 suites get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.organizationId` (`string`, required) - `data.name` (`string`, required, length: `1–120 chars`) - `data.slug` (`string`, required, length: `2–40 chars`, pattern: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`) - `data.slugPrefix` (`string`) - `data.authPassword` (`boolean`, required) - `data.authGoogle` (`boolean`, required) - `data.allowSelfSignup` (`boolean`, required) - `data.accent` (`string | null`, required) - `data.logoUrl` (`string | null`, required) - `data.createdAt` (`string`, required) - `data.assistantMonthlyCredits` (`integer | null`, required) - `data.apps` (`integer`, required) - `data.users` (`integer`, required) - `data.assistantCreditsUsed` (`number`) - `data.assistantCreditsLimit` (`number,null`) - `data.assistantPresets` (`object[]`, required) - `data.assistantPresets.label` (`string`, required, length: `1–24 chars`) - `data.assistantPresets.model` (`string`, required, length: `1–120 chars`) - `data.assistantPresets.reasoning` (`enum | null`, required) - `data.assistantPresetsEditable` (`boolean`) ```json { "data": { "id": "", "organizationId": "", "name": "", "slug": "", "slugPrefix": "", "authPassword": true, "authGoogle": true, "allowSelfSignup": true, "accent": "", "logoUrl": "", "createdAt": "", "assistantMonthlyCredits": 123, "apps": 123, "users": 123, "assistantCreditsUsed": 123.45, "assistantCreditsLimit": null, "assistantPresets": [ { "label": "", "model": "", "reasoning": "none" } ], "assistantPresetsEditable": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/suites/' \ --header 'Authorization: Bearer ' ``` --- # Change a suite Source: https://grid82.com/docs/builder-api/suites/update `PATCH https://studio.grid82.app/v1/suites/{suiteId}` Renames a suite, changes its subdomain, sets its login page's colour and logo, or flips its providers. A subdomain already taken by another suite is refused rather than suffixed: an address is not a name, and quietly giving somebody `acme-2` would be worse than saying no. Turning a provider off signs nobody out; it stops the next sign-in with it. Requires the `schema:write` scope. Operation `suites.update`. MCP tool `suites_update`. CLI: `grid82 suites update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------------------------- | --------------- | -------- | -------------------- | | `name` | string | no | Up to 60 characters. | | `slug` | string | no | Up to 40 characters. | | `authPassword` | boolean | no | | | `authGoogle` | boolean | no | | | `allowSelfSignup` | boolean | no | | | `accent` | string or null | no | | | `logoUrl` | string or null | no | | | `assistantMonthlyCredits` | integer or null | no | | | `assistantPresets` | object\[] | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.organizationId` (`string`, required) - `data.name` (`string`, required, length: `1–120 chars`) - `data.slug` (`string`, required, length: `2–40 chars`, pattern: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`) - `data.slugPrefix` (`string`) - `data.authPassword` (`boolean`, required) - `data.authGoogle` (`boolean`, required) - `data.allowSelfSignup` (`boolean`, required) - `data.accent` (`string | null`, required) - `data.logoUrl` (`string | null`, required) - `data.createdAt` (`string`, required) - `data.assistantMonthlyCredits` (`integer | null`, required) - `data.apps` (`integer`, required) - `data.users` (`integer`, required) - `data.assistantCreditsUsed` (`number`) - `data.assistantCreditsLimit` (`number,null`) - `data.assistantPresets` (`object[]`, required) - `data.assistantPresets.label` (`string`, required, length: `1–24 chars`) - `data.assistantPresets.model` (`string`, required, length: `1–120 chars`) - `data.assistantPresets.reasoning` (`enum | null`, required) - `data.assistantPresetsEditable` (`boolean`) ```json { "data": { "id": "", "organizationId": "", "name": "", "slug": "", "slugPrefix": "", "authPassword": true, "authGoogle": true, "allowSelfSignup": true, "accent": "", "logoUrl": "", "createdAt": "", "assistantMonthlyCredits": 123, "apps": 123, "users": 123, "assistantCreditsUsed": 123.45, "assistantCreditsLimit": null, "assistantPresets": [ { "label": "", "model": "", "reasoning": "none" } ], "assistantPresetsEditable": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/suites/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "slug": "", "authPassword": true, "authGoogle": true, "allowSelfSignup": true, "accent": "", "logoUrl": "", "assistantMonthlyCredits": 123, "assistantPresets": [ { "label": "", "model": "", "reasoning": "none" } ] }' ``` --- # Delete a suite Source: https://grid82.com/docs/builder-api/suites/delete `DELETE https://studio.grid82.app/v1/suites/{suiteId}` Delete the suite, its apps, directory, and addresses together. Release hostnames first, so a suite marked deleted can no longer answer. Delete apps as `apps.delete` does: softly, leaving their rows available to undo mistakes during the grace period. Requires an organisation administrator; Studio requires two separate confirmations. Send the suite name as `confirm`. Never offered to the built-in assistant. Requires the `schema:write` scope. Operation `suites.delete`. MCP tool `suites_delete`. CLI: `grid82 suites delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------ | -------- | --------------------- | | `confirm` | string | no | Up to 200 characters. | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/suites/' \ --header 'Authorization: Bearer ' ``` --- # List your apps Source: https://grid82.com/docs/builder-api/apps/list `GET https://studio.grid82.app/v1/apps` Every app you build, newest first, across every organisation and suite. Start here when you do not know an app id: every other builder tool needs one. Requires the `account:read` scope. Operation `apps.list`. MCP tool `apps_list`. CLI: `grid82 apps list` (`npx grid82`). ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | --------------------- | | `limit` | integer | no | Default `50`. | | `cursor` | string | no | Up to 512 characters. | | `suiteId` | string | no | Up to 64 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.slug` (`string`, required) - `data.suiteId` (`string`, required) - `data.updatedAt` (`string`, required) - `data.status` (`string`, required) - `data.hostname` (`string,null`, required) - `data.url` (`string,null`, required) - `data.icon` (`string,null`) - `meta` (`object`, required) - `meta.nextCursor` (`string,null`, required): Pass as `cursor` for the next page. Null on the last one. - `meta.hasMore` (`boolean`, required) - `meta.fields` (`Record`): Field id to field name, for the table the records came from. ```json { "data": [ { "id": "", "name": "", "slug": "", "suiteId": "", "updatedAt": "", "status": "", "hostname": null, "url": null, "icon": null } ], "meta": { "nextCursor": null, "hasMore": true, "fields": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps' \ --header 'Authorization: Bearer ' ``` --- # Create an app Source: https://grid82.com/docs/builder-api/apps/create `POST https://studio.grid82.app/v1/apps` Create an app at the suite's address followed by its slug. The builder is not automatically a member; no role has special platform privileges. Starts with `Role 1`, full access to all tables and no members: rename it and narrow access with `rules.set` before adding anyone. Without `tables`, creates a sample `Project` table and rows. Supply `tables` for your own structure; each field is `{ name, type, config? }`, and `fields.types` lists the settings each type takes. Requires the `schema:write` scope. Operation `apps.create`. MCP tool `apps_create`. CLI: `grid82 apps create` (`npx grid82`). ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | --------- | -------- | -------------------- | | `suiteId` | string | yes | Up to 64 characters. | | `name` | string | yes | Up to 50 characters. | | `slug` | string | no | | | `tables` | object\[] | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.schemaVersion` (`number`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.slug` (`string`, required, pattern: `^[a-z0-9][a-z0-9-]{1,62}$`) - `data.tables` (`object[]`, required, items: `≤ 200 items`) - `data.tables.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.name` (`string`, required, length: `1–120 chars`) - `data.tables.plural` (`string`, length: `1–120 chars`) - `data.tables.description` (`string`, length: `≤ 2000 chars`) - `data.tables.icon` (`string | null`) - `data.tables.primaryFieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.fields` (`object[]`, required, items: `1–200 items`) - `data.tables.fields.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.fields.name` (`string`, required, length: `1–120 chars`) - `data.tables.fields.width` (`integer`, range: `60–900`) - `data.tables.fields.type` (`string`, required) - `data.tables.fields.config` (`object`, required, default: `{}`) - `data.views` (`Record`, required, default: `{}`) - `data.rules` (`object`, required) - `data.rules.version` (`number`, required) - `data.rules.roles` (`object[]`, required, items: `1–100 items`) - `data.rules.roles.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.rules.roles.name` (`string`, required, length: `1–120 chars`) - `data.rules.roles.description` (`string`, length: `≤ 500 chars`) - `data.rules.roles.assistantMonthlyCredits` (`integer`, range: `0–10000000`) - `data.rules.roles.interface` (`enum`, one of: `assigned, own`) - `data.rules.roles.automations` (`enum`, one of: `none, own`) - `data.rules.permissions` (`object[]`, required, items: `≤ 100 items`) - `data.rules.permissions.role` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.rules.permissions.tables` (`string | Record`, required) - `data.pages` (`object[]`, required, default: `[]`) - `data.pages.id` (`string`, required, length: `1–64 chars`) - `data.pages.name` (`string`, required, length: `1–120 chars`) - `data.pages.kind` (`enum`, required, one of: `declarative, code`) - `data.pages.mode` (`enum`, required, one of: `locked, template, canvas`) - `data.pages.owner` (`enum`, required, one of: `admin, user`) - `data.pages.ownerUserId` (`string`, length: `≤ 64 chars`) - `data.pages.tableId` (`string`, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.pages.forkedFrom` (`string`, length: `≤ 64 chars`) - `data.pages.visibleTo` (`string | string[]`, required, default: `*`) - `data.pages.reads` (`object`) - `data.pages.reads.tables` (`string[]`, required, default: `[]`) - `data.pages.reads.fields` (`string[]`, required, default: `[]`) - `data.pages.reads.views` (`string[]`, required, default: `[]`) - `data.pages.bundleHash` (`string`, length: `≤ 128 chars`) - `data.pages.publishedVersion` (`integer`, range: `≥ 1`) - `data.pages.theme` (`object`) - `data.pages.theme.accent` (`string`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.pages.theme.radius` (`number`, range: `0–24`) - `data.pages.document` (`object`) - `data.pages.version` (`integer`, required, default: `1`, range: `≥ 1`) - `data.pages.updatedAt` (`string`, required) - `data.members` (`object[]`, required, default: `[]`) - `data.members.userId` (`string`, required, length: `1–64 chars`) - `data.members.email` (`string`, required, pattern: `^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$`) - `data.members.name` (`string`, length: `≤ 120 chars`) - `data.members.roles` (`string[]`, required, items: `≥ 1 items`) - `data.members.status` (`enum`, required, default: `invited`, one of: `invited, active, suspended`) - `data.members.invitedAt` (`string`) - `data.theme` (`object`, required, default: `{"radius":6,"accent":"oklch(0.47 0.16 262)","font":"inter"}`) - `data.theme.radius` (`number`, required, default: `6`, range: `0–24`) - `data.theme.accent` (`string`, required, default: `oklch(0.47 0.16 262)`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.theme.font` (`enum`, required, default: `inter`, one of: `system, geist, inter, figtree, manrope, jakarta, dm-sans, outfit, work-sans, nunito-sans, rubik, public-sans, source-serif, lora, fraunces, instrument-serif, geist-mono, jetbrains-mono, ibm-plex-mono`) - `data.theme.icon` (`string | null`, default: `null`) - `data.surfaces` (`object`, required, default: `{"data":true,"interface":true,"assistant":true,"api":true,"mcp":true,"cli":true}`) - `data.surfaces.data` (`boolean`, required, default: `true`) - `data.surfaces.interface` (`boolean`, required, default: `true`) - `data.surfaces.assistant` (`boolean`, required, default: `true`) - `data.surfaces.api` (`boolean`, required, default: `true`) - `data.surfaces.mcp` (`boolean`, required, default: `true`) - `data.surfaces.cli` (`boolean`, required, default: `true`) - `data.updatedAt` (`string`, required) ```json { "data": { "schemaVersion": 123.45, "id": "", "name": "", "slug": "", "tables": [ { "id": "", "name": "", "plural": "", "description": "", "icon": "", "primaryFieldId": "", "fields": [ { "id": "", "name": "", "width": 123, "type": "", "config": {} } ] } ], "views": {}, "rules": { "version": 123.45, "roles": [ { "id": "", "name": "", "description": "", "assistantMonthlyCredits": 123, "interface": "assigned", "automations": "none" } ], "permissions": [ { "role": "", "tables": "" } ] }, "pages": [], "members": [], "theme": { "radius": 6, "accent": "oklch(0.47 0.16 262)", "font": "inter" }, "surfaces": { "data": true, "interface": true, "assistant": true, "api": true, "mcp": true, "cli": true }, "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "suiteId": "", "name": "" }' ``` --- # Read an app definition Source: https://grid82.com/docs/builder-api/apps/get `GET https://studio.grid82.app/v1/apps/{appId}` The whole definition: tables, views, rules, pages, members, theme, surfaces. Requires the `schema:read` scope. Operation `apps.get`. MCP tool `apps_get`. CLI: `grid82 apps get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.schemaVersion` (`number`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.slug` (`string`, required, pattern: `^[a-z0-9][a-z0-9-]{1,62}$`) - `data.tables` (`object[]`, required, items: `≤ 200 items`) - `data.tables.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.name` (`string`, required, length: `1–120 chars`) - `data.tables.plural` (`string`, length: `1–120 chars`) - `data.tables.description` (`string`, length: `≤ 2000 chars`) - `data.tables.icon` (`string | null`) - `data.tables.primaryFieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.fields` (`object[]`, required, items: `1–200 items`) - `data.tables.fields.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.fields.name` (`string`, required, length: `1–120 chars`) - `data.tables.fields.width` (`integer`, range: `60–900`) - `data.tables.fields.type` (`string`, required) - `data.tables.fields.config` (`object`, required, default: `{}`) - `data.views` (`Record`, required, default: `{}`) - `data.rules` (`object`, required) - `data.rules.version` (`number`, required) - `data.rules.roles` (`object[]`, required, items: `1–100 items`) - `data.rules.roles.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.rules.roles.name` (`string`, required, length: `1–120 chars`) - `data.rules.roles.description` (`string`, length: `≤ 500 chars`) - `data.rules.roles.assistantMonthlyCredits` (`integer`, range: `0–10000000`) - `data.rules.roles.interface` (`enum`, one of: `assigned, own`) - `data.rules.roles.automations` (`enum`, one of: `none, own`) - `data.rules.permissions` (`object[]`, required, items: `≤ 100 items`) - `data.rules.permissions.role` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.rules.permissions.tables` (`string | Record`, required) - `data.pages` (`object[]`, required, default: `[]`) - `data.pages.id` (`string`, required, length: `1–64 chars`) - `data.pages.name` (`string`, required, length: `1–120 chars`) - `data.pages.kind` (`enum`, required, one of: `declarative, code`) - `data.pages.mode` (`enum`, required, one of: `locked, template, canvas`) - `data.pages.owner` (`enum`, required, one of: `admin, user`) - `data.pages.ownerUserId` (`string`, length: `≤ 64 chars`) - `data.pages.tableId` (`string`, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.pages.forkedFrom` (`string`, length: `≤ 64 chars`) - `data.pages.visibleTo` (`string | string[]`, required, default: `*`) - `data.pages.reads` (`object`) - `data.pages.reads.tables` (`string[]`, required, default: `[]`) - `data.pages.reads.fields` (`string[]`, required, default: `[]`) - `data.pages.reads.views` (`string[]`, required, default: `[]`) - `data.pages.bundleHash` (`string`, length: `≤ 128 chars`) - `data.pages.publishedVersion` (`integer`, range: `≥ 1`) - `data.pages.theme` (`object`) - `data.pages.theme.accent` (`string`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.pages.theme.radius` (`number`, range: `0–24`) - `data.pages.document` (`object`) - `data.pages.version` (`integer`, required, default: `1`, range: `≥ 1`) - `data.pages.updatedAt` (`string`, required) - `data.members` (`object[]`, required, default: `[]`) - `data.members.userId` (`string`, required, length: `1–64 chars`) - `data.members.email` (`string`, required, pattern: `^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$`) - `data.members.name` (`string`, length: `≤ 120 chars`) - `data.members.roles` (`string[]`, required, items: `≥ 1 items`) - `data.members.status` (`enum`, required, default: `invited`, one of: `invited, active, suspended`) - `data.members.invitedAt` (`string`) - `data.theme` (`object`, required, default: `{"radius":6,"accent":"oklch(0.47 0.16 262)","font":"inter"}`) - `data.theme.radius` (`number`, required, default: `6`, range: `0–24`) - `data.theme.accent` (`string`, required, default: `oklch(0.47 0.16 262)`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.theme.font` (`enum`, required, default: `inter`, one of: `system, geist, inter, figtree, manrope, jakarta, dm-sans, outfit, work-sans, nunito-sans, rubik, public-sans, source-serif, lora, fraunces, instrument-serif, geist-mono, jetbrains-mono, ibm-plex-mono`) - `data.theme.icon` (`string | null`, default: `null`) - `data.surfaces` (`object`, required, default: `{"data":true,"interface":true,"assistant":true,"api":true,"mcp":true,"cli":true}`) - `data.surfaces.data` (`boolean`, required, default: `true`) - `data.surfaces.interface` (`boolean`, required, default: `true`) - `data.surfaces.assistant` (`boolean`, required, default: `true`) - `data.surfaces.api` (`boolean`, required, default: `true`) - `data.surfaces.mcp` (`boolean`, required, default: `true`) - `data.surfaces.cli` (`boolean`, required, default: `true`) - `data.updatedAt` (`string`, required) ```json { "data": { "schemaVersion": 123.45, "id": "", "name": "", "slug": "", "tables": [ { "id": "", "name": "", "plural": "", "description": "", "icon": "", "primaryFieldId": "", "fields": [ { "id": "", "name": "", "width": 123, "type": "", "config": {} } ] } ], "views": {}, "rules": { "version": 123.45, "roles": [ { "id": "", "name": "", "description": "", "assistantMonthlyCredits": 123, "interface": "assigned", "automations": "none" } ], "permissions": [ { "role": "", "tables": "" } ] }, "pages": [], "members": [], "theme": { "radius": 6, "accent": "oklch(0.47 0.16 262)", "font": "inter" }, "surfaces": { "data": true, "interface": true, "assistant": true, "api": true, "mcp": true, "cli": true }, "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps/' \ --header 'Authorization: Bearer ' ``` --- # Rename an app, change its path, or its icon, theme and surfaces Source: https://grid82.com/docs/builder-api/apps/update `PATCH https://studio.grid82.app/v1/apps/{appId}` Update the app's name, Studio icon (a name from the table icon library, or `null` for the default), appearance (accent, radius, font, density), `slug`, and enabled surfaces: data grid, interface, assistant, API, MCP, and CLI. Move `slug` to another path under the suite's address, such as `/crm` to `/sales`; links and bookmarks to the old path stop working, and people must sign in again at the new path. Refuses a `slug` held by another app in the suite, including a deleted app, or a word the suite owns, such as `login`. Within a minute of disabling a surface, `api` refuses all calls using a key or connected token, including CLI calls; the app's own screens continue working in a signed-in session. `mcp` refuses the app's MCP endpoints and removes it from the suite's MCP server. `cli` refuses the official command-line tool by its user agent; with API enabled, this stops the tool and nothing else. `assistant` removes the in-app assistant; `data` and `interface` hide their respective tabs. Disabling any surface deletes nothing. Requires the `schema:write` scope. Operation `apps.update`. MCP tool `apps_update`. CLI: `grid82 apps update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | -------------- | -------- | -------------------- | | `name` | string | no | Up to 50 characters. | | `slug` | string | no | | | `icon` | string or null | no | | | `theme` | object | no | | | `surfaces` | object | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.schemaVersion` (`number`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.slug` (`string`, required, pattern: `^[a-z0-9][a-z0-9-]{1,62}$`) - `data.tables` (`object[]`, required, items: `≤ 200 items`) - `data.tables.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.name` (`string`, required, length: `1–120 chars`) - `data.tables.plural` (`string`, length: `1–120 chars`) - `data.tables.description` (`string`, length: `≤ 2000 chars`) - `data.tables.icon` (`string | null`) - `data.tables.primaryFieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.fields` (`object[]`, required, items: `1–200 items`) - `data.tables.fields.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.tables.fields.name` (`string`, required, length: `1–120 chars`) - `data.tables.fields.width` (`integer`, range: `60–900`) - `data.tables.fields.type` (`string`, required) - `data.tables.fields.config` (`object`, required, default: `{}`) - `data.views` (`Record`, required, default: `{}`) - `data.rules` (`object`, required) - `data.rules.version` (`number`, required) - `data.rules.roles` (`object[]`, required, items: `1–100 items`) - `data.rules.roles.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.rules.roles.name` (`string`, required, length: `1–120 chars`) - `data.rules.roles.description` (`string`, length: `≤ 500 chars`) - `data.rules.roles.assistantMonthlyCredits` (`integer`, range: `0–10000000`) - `data.rules.roles.interface` (`enum`, one of: `assigned, own`) - `data.rules.roles.automations` (`enum`, one of: `none, own`) - `data.rules.permissions` (`object[]`, required, items: `≤ 100 items`) - `data.rules.permissions.role` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.rules.permissions.tables` (`string | Record`, required) - `data.pages` (`object[]`, required, default: `[]`) - `data.pages.id` (`string`, required, length: `1–64 chars`) - `data.pages.name` (`string`, required, length: `1–120 chars`) - `data.pages.kind` (`enum`, required, one of: `declarative, code`) - `data.pages.mode` (`enum`, required, one of: `locked, template, canvas`) - `data.pages.owner` (`enum`, required, one of: `admin, user`) - `data.pages.ownerUserId` (`string`, length: `≤ 64 chars`) - `data.pages.tableId` (`string`, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.pages.forkedFrom` (`string`, length: `≤ 64 chars`) - `data.pages.visibleTo` (`string | string[]`, required, default: `*`) - `data.pages.reads` (`object`) - `data.pages.reads.tables` (`string[]`, required, default: `[]`) - `data.pages.reads.fields` (`string[]`, required, default: `[]`) - `data.pages.reads.views` (`string[]`, required, default: `[]`) - `data.pages.bundleHash` (`string`, length: `≤ 128 chars`) - `data.pages.publishedVersion` (`integer`, range: `≥ 1`) - `data.pages.theme` (`object`) - `data.pages.theme.accent` (`string`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.pages.theme.radius` (`number`, range: `0–24`) - `data.pages.document` (`object`) - `data.pages.version` (`integer`, required, default: `1`, range: `≥ 1`) - `data.pages.updatedAt` (`string`, required) - `data.members` (`object[]`, required, default: `[]`) - `data.members.userId` (`string`, required, length: `1–64 chars`) - `data.members.email` (`string`, required, pattern: `^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$`) - `data.members.name` (`string`, length: `≤ 120 chars`) - `data.members.roles` (`string[]`, required, items: `≥ 1 items`) - `data.members.status` (`enum`, required, default: `invited`, one of: `invited, active, suspended`) - `data.members.invitedAt` (`string`) - `data.theme` (`object`, required, default: `{"radius":6,"accent":"oklch(0.47 0.16 262)","font":"inter"}`) - `data.theme.radius` (`number`, required, default: `6`, range: `0–24`) - `data.theme.accent` (`string`, required, default: `oklch(0.47 0.16 262)`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.theme.font` (`enum`, required, default: `inter`, one of: `system, geist, inter, figtree, manrope, jakarta, dm-sans, outfit, work-sans, nunito-sans, rubik, public-sans, source-serif, lora, fraunces, instrument-serif, geist-mono, jetbrains-mono, ibm-plex-mono`) - `data.theme.icon` (`string | null`, default: `null`) - `data.surfaces` (`object`, required, default: `{"data":true,"interface":true,"assistant":true,"api":true,"mcp":true,"cli":true}`) - `data.surfaces.data` (`boolean`, required, default: `true`) - `data.surfaces.interface` (`boolean`, required, default: `true`) - `data.surfaces.assistant` (`boolean`, required, default: `true`) - `data.surfaces.api` (`boolean`, required, default: `true`) - `data.surfaces.mcp` (`boolean`, required, default: `true`) - `data.surfaces.cli` (`boolean`, required, default: `true`) - `data.updatedAt` (`string`, required) ```json { "data": { "schemaVersion": 123.45, "id": "", "name": "", "slug": "", "tables": [ { "id": "", "name": "", "plural": "", "description": "", "icon": "", "primaryFieldId": "", "fields": [ { "id": "", "name": "", "width": 123, "type": "", "config": {} } ] } ], "views": {}, "rules": { "version": 123.45, "roles": [ { "id": "", "name": "", "description": "", "assistantMonthlyCredits": 123, "interface": "assigned", "automations": "none" } ], "permissions": [ { "role": "", "tables": "" } ] }, "pages": [], "members": [], "theme": { "radius": 6, "accent": "oklch(0.47 0.16 262)", "font": "inter" }, "surfaces": { "data": true, "interface": true, "assistant": true, "api": true, "mcp": true, "cli": true }, "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/apps/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "slug": "", "icon": "", "theme": { "radius": 6, "accent": "oklch(0.47 0.16 262)", "font": "inter", "icon": "" }, "surfaces": { "data": true, "interface": true, "assistant": true, "api": true, "mcp": true, "cli": true } }' ``` --- # Delete an app Source: https://grid82.com/docs/builder-api/apps/delete `DELETE https://studio.grid82.app/v1/apps/{appId}` Remove the app from its suite address, then mark it deleted. Keep its object, rows, and files for 30 days; `apps.deleted` lists it, and `apps.restore` restores it at the same path. After 30 days, permanently purge its object storage, files, assistant conversations, and control-plane rows. Revoke credentials and invite links naming the app; they stay revoked. Pass the app's name as `confirm`. Requires the `schema:write` scope. Operation `apps.delete`. MCP tool `apps_delete`. CLI: `grid82 apps delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------ | -------- | --------------------- | | `confirm` | string | no | Up to 200 characters. | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/apps/' \ --header 'Authorization: Bearer ' ``` --- # List a suite's deleted apps that can still be restored Source: https://grid82.com/docs/builder-api/apps/deleted `GET https://studio.grid82.app/v1/suites/{suiteId}/deleted-apps` Apps of this suite deleted in the last thirty days and not yet purged, with when each was deleted and after when it is purged. A deleted suite's apps are not here: its directory went with it. Requires the `schema:read` scope. Operation `apps.deleted`. MCP tool `apps_deleted`. CLI: `grid82 apps deleted` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.slug` (`string`, required) - `data.deletedAt` (`string`, required) - `data.purgeAfter` (`string`, required) ```json { "data": [ { "id": "", "name": "", "slug": "", "deletedAt": "", "purgeAfter": "" } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/suites//deleted-apps' \ --header 'Authorization: Bearer ' ``` --- # Restore a deleted app Source: https://grid82.com/docs/builder-api/apps/restore `POST https://studio.grid82.app/v1/apps/{appId}/restore` Brings back an app deleted in the last thirty days, at its own path, with everything in it. Counted against the plan like a new app. The credentials and invite links its deletion revoked stay revoked. Requires the `schema:write` scope. Operation `apps.restore`. MCP tool `apps_restore`. CLI: `grid82 apps restore` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.status` (`string`, required) ```json { "data": { "id": "", "status": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//restore' \ --header 'Authorization: Bearer ' ``` --- # Read the app's definition log Source: https://grid82.com/docs/builder-api/apps/log `GET https://studio.grid82.app/v1/apps/{appId}/log` Who changed the schema, rules, members, automations, pages and views, newest first; no row data. `by`: a user id. Via `ui`: the Studio. Requires the `members:read` scope. Operation `apps.log`. MCP tool `apps_log`. CLI: `grid82 apps log` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | -------- | ------- | -------- | --------------------- | | `limit` | integer | no | Default `50`. | | `cursor` | string | no | Up to 512 characters. | | `from` | string | no | Up to 40 characters. | | `to` | string | no | Up to 40 characters. | | `by` | string | no | Up to 64 characters. | | `what` | string | no | Up to 200 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.at` (`string`, required) - `data.actor` (`object`, required) - `data.actor.userId` (`string`, required) - `data.actor.via` (`string`, required) - `data.ops` (`object[]`, required) - `data.ops.kind` (`string`, required) - `data.ops.member` (`object`) - `data.ops.member.userId` (`string`) - `data.ops.member.roles` (`string[]`) - `data.ops.member.status` (`string`) - `meta` (`object`, required) - `meta.nextCursor` (`string,null`, required): Pass as `cursor` for the next page. Null on the last one. - `meta.hasMore` (`boolean`, required) - `meta.fields` (`Record`): Field id to field name, for the table the records came from. ```json { "data": [ { "id": "", "at": "", "actor": { "userId": "", "via": "" }, "ops": [ { "kind": "", "member": { "userId": "", "roles": [ "" ], "status": "" } } ] } ], "meta": { "nextCursor": null, "hasMore": true, "fields": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//log' \ --header 'Authorization: Bearer ' ``` --- # Add a table Source: https://grid82.com/docs/builder-api/tables/create `POST https://studio.grid82.app/v1/apps/{appId}/tables` Add a table. Omit `id` and every field's `id` to receive platform-minted opaque `tbl_…` and `fld_…` IDs in the response; send IDs only when recreating an item whose IDs you already hold. Each field is `{ name, type, config? }`, and `fields.types` lists the settings each type takes. Requires the `schema:write` scope. Operation `tables.create`. MCP tool `tables_create`. CLI: `grid82 tables create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------------- | -------------- | -------- | ---------------------- | | `name` | string | yes | Up to 60 characters. | | `plural` | string | no | Up to 120 characters. | | `description` | string | no | Up to 2000 characters. | | `icon` | string or null | no | | | `id` | string | no | | | `primaryField` | string | no | Up to 120 characters. | | `primaryFieldId` | string | no | | | `fields` | FieldInput\[] | yes | 1 to 200 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.plural` (`string`, length: `1–120 chars`) - `data.description` (`string`, length: `≤ 2000 chars`) - `data.icon` (`string | null`) - `data.primaryFieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.fields` (`object[]`, required, items: `1–200 items`) - `data.fields.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.fields.name` (`string`, required, length: `1–120 chars`) - `data.fields.width` (`integer`, range: `60–900`) - `data.fields.type` (`string`, required) - `data.fields.config` (`object`, required, default: `{}`) ```json { "data": { "id": "", "name": "", "plural": "", "description": "", "icon": "", "primaryFieldId": "", "fields": [ { "id": "", "name": "", "width": 123, "type": "", "config": {} } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//tables' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "fields": [ { "name": "", "type": "" } ] }' ``` --- # Rename a table, change its Studio icon or primary field Source: https://grid82.com/docs/builder-api/tables/update `PATCH https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}` The id never changes: it is the column name and what every rule and page points at. The name is what people read. The icon is a Lucide name used only in the Studio; null restores the default table icon. Requires the `schema:write` scope. Operation `tables.update`. MCP tool `tables_update`. CLI: `grid82 tables update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------------- | -------------- | -------- | --------------------- | | `name` | string | no | Up to 60 characters. | | `plural` | string | no | Up to 120 characters. | | `icon` | string or null | no | | | `primaryFieldId` | string | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.plural` (`string`, length: `1–120 chars`) - `data.description` (`string`, length: `≤ 2000 chars`) - `data.icon` (`string | null`) - `data.primaryFieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.fields` (`object[]`, required, items: `1–200 items`) - `data.fields.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.fields.name` (`string`, required, length: `1–120 chars`) - `data.fields.width` (`integer`, range: `60–900`) - `data.fields.type` (`string`, required) - `data.fields.config` (`object`, required, default: `{}`) ```json { "data": { "id": "", "name": "", "plural": "", "description": "", "icon": "", "primaryFieldId": "", "fields": [ { "id": "", "name": "", "width": 123, "type": "", "config": {} } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/apps//tables/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "plural": "", "icon": "", "primaryFieldId": "" }' ``` --- # Delete a table and its rows Source: https://grid82.com/docs/builder-api/tables/delete `DELETE https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}` Drop the table and all its rows; remove links to it from other tables and references to it in rules. Rows, links, and grants go to the app's trash for 30 days; `trash.restore` can restore the table with the same IDs. Send its current name as `confirm`. Returns 409 with readers in `details.impact` until you send `acknowledge=true`. Requires the `schema:write` scope. Operation `tables.delete`. MCP tool `tables_delete`. CLI: `grid82 tables delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | ------------- | ------- | -------- | --------------------- | | `confirm` | string | no | Up to 200 characters. | | `acknowledge` | boolean | no | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/apps//tables/' \ --header 'Authorization: Bearer ' ``` --- # Every field type, its settings and its limits Source: https://grid82.com/docs/builder-api/fields/types `GET https://studio.grid82.app/v1/field-types` List field types, storage, writable and systemWritten flags, setting names, `configSchema`, and limits. For creation use `createConfigSchema` when present, otherwise `configSchema`; `configSchema` describes the stored shape. Keys not defined for the type fail validation. Requires the `schema:read` scope. Operation `fields.types`. MCP tool `fields_types`. CLI: `grid82 fields types` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.types` (`Record[]`, required) ```json { "data": { "types": [ {} ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/field-types' \ --header 'Authorization: Bearer ' ``` --- # Every function a formula may call Source: https://grid82.com/docs/builder-api/fields/functions `GET https://studio.grid82.app/v1/formula-functions` List each function's name, group, purpose, example, return value, and argument count. Functions not listed here do not exist; guessing a function from another product causes the formula to be refused when saved. Requires the `schema:read` scope. Operation `fields.functions`. MCP tool `fields_functions`. CLI: `grid82 fields functions` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.functions` (`Record[]`, required) ```json { "data": { "functions": [ {} ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/formula-functions' \ --header 'Authorization: Bearer ' ``` --- # Add a field Source: https://grid82.com/docs/builder-api/fields/create `POST https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/fields` Add a field. Omit `id`; the platform returns an opaque `fld_…`. `config` is type-specific; keys for another type fail validation. Read `fields.types` for config shapes. Computed fields (lookup, rollup, formula, button) have no column and are derived at read time. A `link` creates its mirror on the other table in the same write; both are deleted together. Requires the `schema:write` scope. Operation `fields.create`. MCP tool `fields_create`. CLI: `grid82 fields create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body A object. **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.width` (`integer`, range: `60–900`) - `data.type` (`string`, required) - `data.config` (`object`, required, default: `{}`) ```json { "data": { "id": "", "name": "", "width": 123, "type": "", "config": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//tables//fields' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "type": "" }' ``` --- # Change a field's name, type, width or options Source: https://grid82.com/docs/builder-api/fields/update `PATCH https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/fields/{fieldId}` Update a field; its `id` is its column and never changes. Merge `config` with existing settings and validate against the resulting type. Type changes convert values in one transaction: keep values readable as the new type, clear others. Explain effects on pages, view filters and permission conditions before proposing a type change. Cannot convert to or from a relation; add a new field and delete the old one. Select options replace the whole list: keep each option's `id` to preserve its rows. Removed options are cleared from all records in the same transaction; single choices become empty, multiselects keep other choices. Restore cleared values with `trash.restore`. Requires the `schema:write` scope. Operation `fields.update`. MCP tool `fields_update`. CLI: `grid82 fields update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | | `fieldId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | ------------- | ------- | -------- | ----------- | | `acknowledge` | boolean | no | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------------- | | `name` | string | no | Up to 60 characters. | | `type` | "text" or "longtext" or "email" or "url" or "phone" or "json" or "number" or "currency" or "percent" or "rating" or "duration" or "select" or "multiselect" or "checkbox" or "user" or "attachment" or "date" or "recordId" or "autonumber" or "createdTime" or "createdBy" or "modifiedTime" or "modifiedBy" or "link" or "lookup" or "rollup" or "formula" or "button" | no | | | `width` | integer | no | At least 60. At most 900. | | `config` | object | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.width` (`integer`, range: `60–900`) - `data.type` (`string`, required) - `data.config` (`object`, required, default: `{}`) ```json { "data": { "id": "", "name": "", "width": 123, "type": "", "config": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/apps//tables//fields/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "type": "text", "width": 123, "config": {} }' ``` --- # Delete a field Source: https://grid82.com/docs/builder-api/fields/delete `DELETE https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/fields/{fieldId}` Drop a field and its values after sending its current name as `confirm`. The field, values, and grants naming it go to the app's trash for thirty days; `trash.restore` can restore them with the same id. Record the app's point-in-time bookmark. The operation returns 409 with readers in `details.impact` until you send `acknowledge=true`; `schema.impact` reports the same impact without changing anything. Requires the `schema:write` scope. Operation `fields.delete`. MCP tool `fields_delete`. CLI: `grid82 fields delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | | `fieldId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | ------------- | ------- | -------- | --------------------- | | `confirm` | string | no | Up to 200 characters. | | `acknowledge` | boolean | no | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/apps//tables//fields/' \ --header 'Authorization: Bearer ' ``` --- # What a schema change would break Source: https://grid82.com/docs/builder-api/schema/impact `POST https://studio.grid82.app/v1/apps/{appId}/schema/impact` List permission rules, saved views, pages, and other fields that read the fields or tables you propose to remove. Returns the same list that causes `fields.delete` and `tables.delete` to be refused until you send `acknowledge=true`. Matches pages by the names in their last build; references use IDs, so renaming breaks nothing and returns no results. Advisory only; changes nothing. Requires the `schema:read` scope. Operation `schema.impact`. MCP tool `schema_impact`. CLI: `grid82 schema impact` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | --------- | -------- | ------------- | | `drops` | object\[] | no | Default `[]`. | | `renames` | object\[] | no | Default `[]`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.pages` (`object[]`, required) - `data.pages.pageId` (`string`, required) - `data.pages.name` (`string`, required) - `data.pages.reads` (`string[]`, required) - `data.pages.repairable` (`boolean`, required) - `data.views` (`object[]`, required) - `data.views.viewId` (`string`, required) - `data.views.name` (`string`, required) - `data.views.reads` (`string[]`, required) - `data.rules` (`object[]`, required) - `data.rules.ruleId` (`string`, required) - `data.rules.name` (`string`) - `data.rules.reads` (`string[]`, required) - `data.fields` (`object[]`) - `data.fields.fieldId` (`string`, required) - `data.fields.name` (`string`, required) - `data.fields.reads` (`string[]`, required) ```json { "data": { "pages": [ { "pageId": "", "name": "", "reads": [ "" ], "repairable": true } ], "views": [ { "viewId": "", "name": "", "reads": [ "" ] } ], "rules": [ { "ruleId": "", "name": "", "reads": [ "" ] } ], "fields": [ { "fieldId": "", "name": "", "reads": [ "" ] } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//schema/impact' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "drops": [], "renames": [] }' ``` --- # List a table's records, as the builder Source: https://grid82.com/docs/builder-api/data/records-list `GET https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records` Every row the table holds, paginated with an opaque cursor. Takes the same `filter`, `sorts`, `where` and `search` as the app's own list, and `meta.fields` names each field id. Rows are read with `records:read` and written with `records:write` on both planes; `schema:*` is the tables and fields themselves. Requires the `records:read` scope. Operation `apps.records.list`. MCP tool `apps_records_list`. CLI: `grid82 apps records list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | -------- | ------- | -------- | ---------------------- | | `limit` | integer | no | Default `50`. | | `cursor` | string | no | Up to 512 characters. | | `viewId` | string | no | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | | `filter` | string | no | Up to 4000 characters. | | `sorts` | string | no | Up to 1000 characters. | | `where` | string | no | Up to 4000 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`Record[]`, required) - `meta` (`object`, required) - `meta.nextCursor` (`string,null`, required): Pass as `cursor` for the next page. Null on the last one. - `meta.hasMore` (`boolean`, required) - `meta.fields` (`Record`): Field id to field name, for the table the records came from. ```json { "data": [ {} ], "meta": { "nextCursor": null, "hasMore": true, "fields": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//tables//records' \ --header 'Authorization: Bearer ' ``` --- # Create records, as the builder Source: https://grid82.com/docs/builder-api/data/records-create `POST https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records` Creates one to 500 records in one call, all or nothing: one refused row saves none. Each record is a flat object, not wrapped in `fields` as an update is. Keys are field ids or exact field names; an unknown key is refused rather than dropped. The whole batch is one changeset, returned as `changesetId`. Email, URL and phone text is preserved; visible suspect values are returned in warnings (code suspect\_format, recordId, fieldId, fieldName, message). A warning is a successful save, not a reason to retry. Invalid numeric values, ratings and ISO dates fail with validation\_failed/422. A failing create batch saves no rows. Requires the `records:write` scope. Operation `apps.records.create`. MCP tool `apps_records_create`. CLI: `grid82 apps records create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | --------- | -------- | --------------- | | `records` | object\[] | yes | 1 to 500 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.records` (`Record[]`, required) - `data.droppedFields` (`integer`, required) - `data.warnings` (`object[]`, required) - `data.warnings.code` (`string`, required) - `data.warnings.recordId` (`string`, required) - `data.warnings.fieldId` (`string`, required) - `data.warnings.fieldName` (`string`, required) - `data.warnings.message` (`string`, required) - `data.changesetId` (`string`) ```json { "data": { "records": [ {} ], "droppedFields": 123, "warnings": [ { "code": "", "recordId": "", "fieldId": "", "fieldName": "", "message": "" } ], "changesetId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//tables//records' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "records": [ {} ] }' ``` --- # Count a table's records, as the builder Source: https://grid82.com/docs/builder-api/data/records-count `GET https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/count` How many rows match, for the same view, filter and search a list would use. Requires the `records:read` scope. Operation `apps.records.count`. MCP tool `apps_records_count`. CLI: `grid82 apps records count` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ---------------------- | | `viewId` | string | no | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | | `filter` | string | no | Up to 4000 characters. | | `sorts` | string | no | Up to 1000 characters. | | `where` | string | no | Up to 4000 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.count` (`number`, required) ```json { "data": { "count": 123.45 } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//tables//records/count' \ --header 'Authorization: Bearer ' ``` --- # Total a column, as the builder Source: https://grid82.com/docs/builder-api/data/records-summarise `GET https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/summary` Reductions over the rows a list would return. `summaries` is a JSON object of field id (or exact name) to reduction; values come back raw and unformatted. An unknown field is refused, listing the table's fields. Requires the `records:read` scope. Operation `apps.records.summarise`. MCP tool `apps_records_summarise`. CLI: `grid82 apps records summarise` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------- | | `viewId` | string | no | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | | `filter` | string | no | Up to 4000 characters. | | `sorts` | string | no | Up to 1000 characters. | | `where` | string | no | Up to 4000 characters. | | `summaries` | string | yes | Up to 2000 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`Record`, required) ```json { "data": {} } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//tables//records/summary?summaries=' \ --header 'Authorization: Bearer ' ``` --- # Count a table's records per value, as the builder Source: https://grid82.com/docs/builder-api/data/records-groups `GET https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/groups` How many rows sit under each value of one column, ordered the way that column sorts. `fieldId` takes the field's id or exact name; an unknown one is refused, listing the table's fields. Requires the `records:read` scope. Operation `apps.records.groups`. MCP tool `apps_records_groups`. CLI: `grid82 apps records groups` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ---------------------- | | `viewId` | string | no | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | | `filter` | string | no | Up to 4000 characters. | | `sorts` | string | no | Up to 1000 characters. | | `where` | string | no | Up to 4000 characters. | | `fieldId` | string | yes | Up to 200 characters. | | `limit` | integer | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.groups` (`object[]`, required) - `data.groups.key` (`object`, required) - `data.groups.count` (`number`, required) - `data.truncated` (`boolean`, required) ```json { "data": { "groups": [ { "key": null, "count": 123.45 } ], "truncated": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//tables//records/groups?fieldId=' \ --header 'Authorization: Bearer ' ``` --- # Read a window of a table's records by position, as the builder Source: https://grid82.com/docs/builder-api/data/records-window `GET https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/window` Rows `offset` to `offset + limit` of the set a list would return, in the same order, with the size of the whole set. For a grid that scrolls; to walk a table use apps.records.list and its cursor. Requires the `records:read` scope. Operation `apps.records.window`. Not an MCP tool. A positional read for a grid that scrolls: it addresses rows by offset, which can skip or repeat a row when somebody writes between two calls. A model walks a table with records.list and its cursor, which cannot, and asks records.count for the size. CLI: `grid82 apps records window` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | -------- | ------- | -------- | ---------------------- | | `viewId` | string | no | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | | `filter` | string | no | Up to 4000 characters. | | `sorts` | string | no | Up to 1000 characters. | | `where` | string | no | Up to 4000 characters. | | `offset` | integer | no | Default `0`. | | `limit` | integer | no | Default `100`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.records` (`Record[]`, required) - `data.total` (`integer`, required) - `data.offset` (`integer`, required) - `data.fields` (`Record`, required) ```json { "data": { "records": [ {} ], "total": 123, "offset": 123, "fields": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//tables//records/window' \ --header 'Authorization: Bearer ' ``` --- # Read one record, as the builder Source: https://grid82.com/docs/builder-api/data/records-get `GET https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/{recordId}` The whole row, every field. Nothing is masked on the builder plane. Requires the `records:read` scope. Operation `apps.records.get`. MCP tool `apps_records_get`. CLI: `grid82 apps records get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | | `recordId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`Record`, required) ```json { "data": {} } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//tables//records/' \ --header 'Authorization: Bearer ' ``` --- # Update a record, as the builder Source: https://grid82.com/docs/builder-api/data/records-update `PATCH https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/{recordId}` A partial update: only the fields sent are changed. The body is \{"fields":\{…}}, keyed by field id or exact name. Logged as this builder, from the Studio. Email, URL and phone text is preserved; visible suspect values are returned in warnings (code suspect\_format, recordId, fieldId, fieldName, message). A warning is a successful save, not a reason to retry. Invalid numeric values, ratings and ISO dates fail with validation\_failed/422. A failing create batch saves no rows. Requires the `records:write` scope. Operation `apps.records.update`. MCP tool `apps_records_update`. CLI: `grid82 apps records update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | | `recordId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------------- | ------ | -------- | -------------------- | | `fields` | object | yes | | | `baseVersion` | string | no | Up to 64 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.record` (`Record`, required) - `data.stillVisible` (`boolean`, required) - `data.droppedFields` (`integer`, required) - `data.warnings` (`object[]`, required) - `data.warnings.code` (`string`, required) - `data.warnings.recordId` (`string`, required) - `data.warnings.fieldId` (`string`, required) - `data.warnings.fieldName` (`string`, required) - `data.warnings.message` (`string`, required) - `data.changesetId` (`string`) ```json { "data": { "record": {}, "stillVisible": true, "droppedFields": 123, "warnings": [ { "code": "", "recordId": "", "fieldId": "", "fieldName": "", "message": "" } ], "changesetId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/apps//tables//records/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "fields": {} }' ``` --- # Delete a record, as the builder Source: https://grid82.com/docs/builder-api/data/records-delete `DELETE https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/{recordId}` Delete the row, its links, and its comments; the op log retains all three, so `changesets.revert` restores the row with the same id and number, links to records that still exist, and comments. Returns the bundle id as `changesetId`. Requires the `records:write` scope. Operation `apps.records.delete`. MCP tool `apps_records_delete`. CLI: `grid82 apps records delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | | `recordId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.changesetId` (`string`) ```json { "data": { "id": "", "changesetId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/apps//tables//records/' \ --header 'Authorization: Bearer ' ``` --- # Upload a file for an attachment field, as the builder Source: https://grid82.com/docs/builder-api/data/files-upload `POST https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/files` The builder plane's half of `files.upload`. Takes the BYTES as `multipart/form-data` under `file`, with `recordId` saying which record they will belong to, and answers with the descriptor to write into an attachment field. Attaching it is an ordinary record write, so the file arrives through the same op log and the same undo as any other value. Requires the `records:write` scope. Operation `apps.files.upload`. Not an MCP tool. A model cannot send bytes: a tool call is JSON, and an upload is a multipart file. Upload with the CLI (`--file ./path`) or from the grid, then attach the returned descriptor with an ordinary record write, which is a tool. CLI: `grid82 apps files upload` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body Sent as `multipart/form-data`. | Field | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------------------------------------------------------- | | `file` | string | yes | The bytes. | | `recordId` | string | yes | The record the file will be attached to, so an upload never attached can be swept. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.size` (`number`, required) - `data.mime` (`string`, required) - `data.key` (`string`, required) - `data.uploadedAt` (`string`, required) - `data.width` (`number`) - `data.height` (`number`) ```json { "data": { "id": "", "name": "", "size": 123.45, "mime": "", "key": "", "uploadedAt": "", "width": 123.45, "height": 123.45 } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//tables//files' \ --header 'Authorization: Bearer ' ``` --- # Duplicate a record, as the builder Source: https://grid82.com/docs/builder-api/data/records-duplicate `POST https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/{recordId}/duplicate` A new record with the same values, inserted after the original. Comments and history stay with the original. Requires the `records:write` scope. Operation `apps.records.duplicate`. MCP tool `apps_records_duplicate`. CLI: `grid82 apps records duplicate` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | | `recordId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`Record`, required) ```json { "data": {} } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//tables//records//duplicate' \ --header 'Authorization: Bearer ' ``` --- # Delete several records, as the builder Source: https://grid82.com/docs/builder-api/data/records-delete-many `POST https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/delete` Delete up to 500 records per request. Report refused rows in `failed` without stopping other deletions. Log the batch as ONE entry; `changesetId` undoes the entire batch at once. Requires the `records:write` scope. Operation `apps.records.deleteMany`. MCP tool `apps_records_deleteMany`. CLI: `grid82 apps records delete-many` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----------- | --------- | -------- | --------------- | | `recordIds` | string\[] | yes | 1 to 500 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.deleted` (`string[]`, required) - `data.failed` (`object[]`, required) - `data.failed.id` (`string`, required) - `data.failed.reason` (`string`, required) - `data.changesetId` (`string`) ```json { "data": { "deleted": [ "" ], "failed": [ { "id": "", "reason": "" } ], "changesetId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//tables//records/delete' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "recordIds": [ "" ] }' ``` --- # A record's history, as the builder Source: https://grid82.com/docs/builder-api/data/records-history `GET https://studio.grid82.app/v1/apps/{appId}/records/{recordId}/history` Every change to the record, one entry per field that moved, newest first. `tableId` names the table it is in. Requires the `records:read` scope. Operation `apps.records.history`. MCP tool `apps_records_history`. CLI: `grid82 apps records history` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `recordId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ------------- | | `limit` | integer | no | Default `50`. | | `tableId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.tableId` (`string`, required) - `data.recordId` (`string`, required) - `data.authorId` (`string`, required) - `data.via` (`string`, required) - `data.kind` (`enum`, required, one of: `created, edited, commented, uncommented, deleted`) - `data.fieldId` (`string`) - `data.before` (`string | number | boolean | null | string[]`) - `data.after` (`string | number | boolean | null | string[]`) - `data.at` (`string`, required) ```json { "data": [ { "id": "", "tableId": "", "recordId": "", "authorId": "", "via": "", "kind": "created", "fieldId": "", "before": "", "after": "", "at": "" } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//records//history?tableId=' \ --header 'Authorization: Bearer ' ``` --- # A record's comments, as the builder Source: https://grid82.com/docs/builder-api/data/comments-list `GET https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/{recordId}/comments` The notes users have left on this record, oldest first. Requires the `records:read` scope. Operation `apps.comments.list`. MCP tool `apps_comments_list`. CLI: `grid82 apps comments list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | | `recordId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | ------- | ------- | -------- | ------------- | | `limit` | integer | no | Default `50`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.tableId` (`string`, required) - `data.recordId` (`string`, required) - `data.authorId` (`string`, required) - `data.text` (`string`, required, length: `1–4000 chars`) - `data.mentions` (`string[]`, required) - `data.createdAt` (`string`, required) - `data.editedAt` (`string,null`, required) ```json { "data": [ { "id": "", "tableId": "", "recordId": "", "authorId": "", "text": "", "mentions": [ "" ], "createdAt": "", "editedAt": null } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//tables//records//comments' \ --header 'Authorization: Bearer ' ``` --- # Comment on a record, as the builder Source: https://grid82.com/docs/builder-api/data/comments-create `POST https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/{recordId}/comments` Adds a comment under this builder's own identity. Mentions may only name users who can see the record. Requires the `records:write` scope. Operation `apps.comments.create`. MCP tool `apps_comments_create`. CLI: `grid82 apps comments create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | | `recordId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | --------- | -------- | ---------------------- | | `text` | string | yes | Up to 4000 characters. | | `mentions` | string\[] | no | Up to 20 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.tableId` (`string`, required) - `data.recordId` (`string`, required) - `data.authorId` (`string`, required) - `data.text` (`string`, required, length: `1–4000 chars`) - `data.mentions` (`string[]`, required) - `data.createdAt` (`string`, required) - `data.editedAt` (`string,null`, required) ```json { "data": { "id": "", "tableId": "", "recordId": "", "authorId": "", "text": "", "mentions": [ "" ], "createdAt": "", "editedAt": null } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//tables//records//comments' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "text": "" }' ``` --- # Who may be named on this record Source: https://grid82.com/docs/builder-api/data/comments-mentionable `GET https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/records/{recordId}/comments/mentionable` The app users who can currently see this record, the only users an @ may name. Requires the `records:read` scope. Operation `apps.comments.mentionable`. MCP tool `apps_comments_mentionable`. CLI: `grid82 apps comments mentionable` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | | `recordId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.userId` (`string`, required) - `data.email` (`string,null`, required) - `data.name` (`string,null`, required) ```json { "data": [ { "userId": "", "email": null, "name": null } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//tables//records//comments/mentionable' \ --header 'Authorization: Bearer ' ``` --- # Edit your own comment, as the builder Source: https://grid82.com/docs/builder-api/data/comments-update `PATCH https://studio.grid82.app/v1/apps/{appId}/comments/{commentId}` Only the author may edit a comment, and the builder is the author of the ones they wrote. Requires the `records:write` scope. Operation `apps.comments.update`. MCP tool `apps_comments_update`. CLI: `grid82 apps comments update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ----------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `commentId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | --------- | -------- | ---------------------- | | `text` | string | yes | Up to 4000 characters. | | `mentions` | string\[] | no | Up to 20 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.tableId` (`string`, required) - `data.recordId` (`string`, required) - `data.authorId` (`string`, required) - `data.text` (`string`, required, length: `1–4000 chars`) - `data.mentions` (`string[]`, required) - `data.createdAt` (`string`, required) - `data.editedAt` (`string,null`, required) ```json { "data": { "id": "", "tableId": "", "recordId": "", "authorId": "", "text": "", "mentions": [ "" ], "createdAt": "", "editedAt": null } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/apps//comments/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "text": "" }' ``` --- # Delete your own comment, as the builder Source: https://grid82.com/docs/builder-api/data/comments-delete `DELETE https://studio.grid82.app/v1/apps/{appId}/comments/{commentId}` Removes a comment this builder wrote. Only the author may delete a comment, whichever plane they stand on. Requires the `records:write` scope. Operation `apps.comments.delete`. MCP tool `apps_comments_delete`. CLI: `grid82 apps comments delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ----------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `commentId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/apps//comments/' \ --header 'Authorization: Bearer ' ``` --- # Read with SQL, as the builder Source: https://grid82.com/docs/builder-api/data/sql-query `POST https://studio.grid82.app/v1/apps/{appId}/sql/query` One SELECT over one table of the app, every row. The dialect: SELECT \ FROM \
\[WHERE …] \[GROUP BY …] \[ORDER BY …] \[LIMIT n OFFSET m]; UPDATE \
SET col = expr \[WHERE …]; DELETE FROM \
\[WHERE …]. One table per statement; field names or ids (double-quote names with spaces); 'text', numbers, TRUE, FALSE, NULL, CURRENT\_DATE; AND OR NOT, = \<> \< \<= > >=, \[NOT] LIKE, \[NOT] IN (…), IS \[NOT] NULL, \[NOT] BETWEEN, CASE WHEN cond THEN v … \[ELSE v] END and CASE expr WHEN value THEN v … \[ELSE v] END; a select takes its choice's label (any case) or id; functions LENGTH LOWER UPPER TRIM ABS ROUND COALESCE SUBSTR DATE STRFTIME IFNULL INSTR REPLACE; link columns: col HAS ANY ('id', …), col HAS ALL (…), col IS EMPTY. Your text is never executed: it is compiled against your own permissions, and a column you may not read is simply an unknown column. Requires the `records:read` scope. Operation `apps.sql.query`. MCP tool `apps_sql_query`. CLI: `grid82 apps sql query` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----- | ------ | -------- | ----------------------- | | `sql` | string | yes | Up to 20000 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.kind` (`string`, required) - `data.columns` (`string[]`, required) - `data.rows` (`object[][]`, required) - `data.rowCount` (`integer`, required) - `data.truncated` (`boolean`, required) ```json { "data": { "kind": "", "columns": [ "" ], "rows": [ [ null ] ], "rowCount": 123, "truncated": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//sql/query' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "sql": "" }' ``` --- # Change many rows with SQL, as the builder Source: https://grid82.com/docs/builder-api/data/sql-execute `POST https://studio.grid82.app/v1/apps/{appId}/sql/execute` One UPDATE or DELETE; each row is written through the ordinary path, and the statement is one changeset (changesetId), undone by one revert. Up to 500 rows per statement; without a WHERE it needs confirm: true; dryRun counts first. The dialect: SELECT \ FROM \
\[WHERE …] \[GROUP BY …] \[ORDER BY …] \[LIMIT n OFFSET m]; UPDATE \
SET col = expr \[WHERE …]; DELETE FROM \
\[WHERE …]. One table per statement; field names or ids (double-quote names with spaces); 'text', numbers, TRUE, FALSE, NULL, CURRENT\_DATE; AND OR NOT, = \<> \< \<= > >=, \[NOT] LIKE, \[NOT] IN (…), IS \[NOT] NULL, \[NOT] BETWEEN, CASE WHEN cond THEN v … \[ELSE v] END and CASE expr WHEN value THEN v … \[ELSE v] END; a select takes its choice's label (any case) or id; functions LENGTH LOWER UPPER TRIM ABS ROUND COALESCE SUBSTR DATE STRFTIME IFNULL INSTR REPLACE; link columns: col HAS ANY ('id', …), col HAS ALL (…), col IS EMPTY. Your text is never executed: it is compiled against your own permissions, and a column you may not read is simply an unknown column. Requires the `records:write` scope. Operation `apps.sql.execute`. MCP tool `apps_sql_execute`. CLI: `grid82 apps sql execute` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------- | -------- | ----------------------- | | `sql` | string | yes | Up to 20000 characters. | | `confirm` | boolean | no | | | `dryRun` | boolean | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.kind` (`enum`, required, one of: `update, delete`) - `data.affected` (`integer`, required) - `data.ids` (`string[]`, required) - `data.failures` (`string[]`) - `data.warnings` (`object[]`) - `data.warnings.code` (`string`, required) - `data.warnings.recordId` (`string`, required) - `data.warnings.fieldId` (`string`, required) - `data.warnings.fieldName` (`string`, required) - `data.warnings.message` (`string`, required) - `data.dryRun` (`boolean`, required) - `data.changesetId` (`string`) ```json { "data": { "kind": "update", "affected": 123, "ids": [ "" ], "failures": [ "" ], "warnings": [ { "code": "", "recordId": "", "fieldId": "", "fieldName": "", "message": "" } ], "dryRun": true, "changesetId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//sql/execute' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "sql": "" }' ``` --- # Turn a sentence into a filter, as the builder Source: https://grid82.com/docs/builder-api/data/filters-suggest `POST https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/filters/suggest` Generate a complete filter to apply from the table's fields, example values, and current filter, or return a plain-language refusal. Costs the organisation credits. Requires the `records:read` scope. Operation `apps.filters.suggest`. MCP tool `apps_filters_suggest`. CLI: `grid82 apps filters suggest` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | ----------- | -------- | --------------------- | | `sentence` | string | yes | Up to 600 characters. | | `current` | FilterGroup | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.ok` (`boolean`, required) - `data.filter` (`object`, required) - `data.filter.id` (`string`, required, length: `1–64 chars`) - `data.filter.kind` (`string`, required) - `data.filter.mode` (`enum`, required, one of: `and, or`) - `data.filter.children` (`object[]`, required, items: `≤ 50 items`) - `data.filter.children.id` (`string`, required, length: `1–64 chars`) - `data.filter.children.kind` (`string`, required) - `data.filter.children.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.filter.children.op` (`enum`, required, one of: `contains, not_contains, starts_with, ends_with, is, is_not, is_empty, is_not_empty, gt, lt, gte, lte, before, after, on_or_before, on_or_after, is_any_of, is_none_of, has_any_of, has_all_of, has_none_of, is_checked, is_unchecked`) - `data.filter.children.value` (`object`) ```json { "data": { "ok": true, "filter": { "id": "", "kind": "", "mode": "and", "children": [ { "id": "", "kind": "", "fieldId": "", "op": "contains", "value": null } ] } } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//tables//filters/suggest' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "sentence": "" }' ``` --- # Your saved views of a table, as the builder Source: https://grid82.com/docs/builder-api/data/views-list `GET https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/views` List the views you saved for this table, in your order. Only you can see them; nobody else's views are included. Empty is ordinary. Requires the `records:read` scope. Operation `apps.views.list`. MCP tool `apps_views_list`. CLI: `grid82 apps views list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.filter` (`object`, required) - `data.filter.id` (`string`, required, length: `1–64 chars`) - `data.filter.kind` (`string`, required) - `data.filter.mode` (`enum`, required, one of: `and, or`) - `data.filter.children` (`object[]`, required, items: `≤ 50 items`) - `data.filter.children.id` (`string`, required, length: `1–64 chars`) - `data.filter.children.kind` (`string`, required) - `data.filter.children.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.filter.children.op` (`enum`, required, one of: `contains, not_contains, starts_with, ends_with, is, is_not, is_empty, is_not_empty, gt, lt, gte, lte, before, after, on_or_before, on_or_after, is_any_of, is_none_of, has_any_of, has_all_of, has_none_of, is_checked, is_unchecked`) - `data.filter.children.value` (`object`) - `data.sorts` (`object[]`, required, default: `[]`, items: `≤ 10 items`) - `data.sorts.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.sorts.desc` (`boolean`, required) - `data.groupBy` (`object[]`, required, default: `[]`, items: `≤ 3 items`) - `data.groupBy.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.groupBy.desc` (`boolean`, required) - `data.hidden` (`string[]`, required, default: `[]`) - `data.order` (`string[]`) - `data.rowHeight` (`enum`, required, default: `short`, one of: `short, medium, tall, extraTall`) - `data.search` (`string`, required, default: ``, length: `≤ 200 chars`) - `data.summaries` (`Record | null>`, required, default: `{}`) - `data.where` (`string`, length: `≤ 4000 chars`) - `data.repaired` (`object`) - `data.repaired.at` (`string`, required) - `data.repaired.removed` (`string[]`, required, items: `≤ 50 items`) - `data.repaired.note` (`string`, required, length: `≤ 400 chars`) - `data.tableId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.position` (`integer`, required) - `data.createdAt` (`string`, required) - `data.updatedAt` (`string`, required) ```json { "data": [ { "id": "", "name": "", "filter": { "id": "root", "kind": "group", "mode": "and", "children": [] }, "sorts": [], "groupBy": [], "hidden": [], "order": [ "" ], "rowHeight": "short", "search": "", "summaries": {}, "where": "", "repaired": { "at": "", "removed": [ "" ], "note": "" }, "tableId": "", "position": 123, "createdAt": "", "updatedAt": "" } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//tables//views' \ --header 'Authorization: Bearer ' ``` --- # Save a view of a table, as the builder Source: https://grid82.com/docs/builder-api/data/views-create `POST https://studio.grid82.app/v1/apps/{appId}/tables/{tableId}/views` A view visible to you alone. `config` fills in defaults for whatever is left out: an empty filter, no sorts, nothing hidden. Pass its id as `viewId` to apps.records.list to read through it. Requires the `records:write` scope. Operation `apps.views.create`. MCP tool `apps_views_create`. CLI: `grid82 apps views create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `tableId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | ------ | -------- | -------------------- | | `name` | string | yes | Up to 50 characters. | | `config` | object | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.filter` (`object`, required) - `data.filter.id` (`string`, required, length: `1–64 chars`) - `data.filter.kind` (`string`, required) - `data.filter.mode` (`enum`, required, one of: `and, or`) - `data.filter.children` (`object[]`, required, items: `≤ 50 items`) - `data.filter.children.id` (`string`, required, length: `1–64 chars`) - `data.filter.children.kind` (`string`, required) - `data.filter.children.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.filter.children.op` (`enum`, required, one of: `contains, not_contains, starts_with, ends_with, is, is_not, is_empty, is_not_empty, gt, lt, gte, lte, before, after, on_or_before, on_or_after, is_any_of, is_none_of, has_any_of, has_all_of, has_none_of, is_checked, is_unchecked`) - `data.filter.children.value` (`object`) - `data.sorts` (`object[]`, required, default: `[]`, items: `≤ 10 items`) - `data.sorts.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.sorts.desc` (`boolean`, required) - `data.groupBy` (`object[]`, required, default: `[]`, items: `≤ 3 items`) - `data.groupBy.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.groupBy.desc` (`boolean`, required) - `data.hidden` (`string[]`, required, default: `[]`) - `data.order` (`string[]`) - `data.rowHeight` (`enum`, required, default: `short`, one of: `short, medium, tall, extraTall`) - `data.search` (`string`, required, default: ``, length: `≤ 200 chars`) - `data.summaries` (`Record | null>`, required, default: `{}`) - `data.where` (`string`, length: `≤ 4000 chars`) - `data.repaired` (`object`) - `data.repaired.at` (`string`, required) - `data.repaired.removed` (`string[]`, required, items: `≤ 50 items`) - `data.repaired.note` (`string`, required, length: `≤ 400 chars`) - `data.tableId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.position` (`integer`, required) - `data.createdAt` (`string`, required) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "filter": { "id": "root", "kind": "group", "mode": "and", "children": [] }, "sorts": [], "groupBy": [], "hidden": [], "order": [ "" ], "rowHeight": "short", "search": "", "summaries": {}, "where": "", "repaired": { "at": "", "removed": [ "" ], "note": "" }, "tableId": "", "position": 123, "createdAt": "", "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//tables//views' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }' ``` --- # Read one of your saved views, as the builder Source: https://grid82.com/docs/builder-api/data/views-get `GET https://studio.grid82.app/v1/apps/{appId}/views/{viewId}` One of your own views. A view that is not yours is refused exactly like one that does not exist. Requires the `records:read` scope. Operation `apps.views.get`. MCP tool `apps_views_get`. CLI: `grid82 apps views get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `viewId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.filter` (`object`, required) - `data.filter.id` (`string`, required, length: `1–64 chars`) - `data.filter.kind` (`string`, required) - `data.filter.mode` (`enum`, required, one of: `and, or`) - `data.filter.children` (`object[]`, required, items: `≤ 50 items`) - `data.filter.children.id` (`string`, required, length: `1–64 chars`) - `data.filter.children.kind` (`string`, required) - `data.filter.children.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.filter.children.op` (`enum`, required, one of: `contains, not_contains, starts_with, ends_with, is, is_not, is_empty, is_not_empty, gt, lt, gte, lte, before, after, on_or_before, on_or_after, is_any_of, is_none_of, has_any_of, has_all_of, has_none_of, is_checked, is_unchecked`) - `data.filter.children.value` (`object`) - `data.sorts` (`object[]`, required, default: `[]`, items: `≤ 10 items`) - `data.sorts.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.sorts.desc` (`boolean`, required) - `data.groupBy` (`object[]`, required, default: `[]`, items: `≤ 3 items`) - `data.groupBy.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.groupBy.desc` (`boolean`, required) - `data.hidden` (`string[]`, required, default: `[]`) - `data.order` (`string[]`) - `data.rowHeight` (`enum`, required, default: `short`, one of: `short, medium, tall, extraTall`) - `data.search` (`string`, required, default: ``, length: `≤ 200 chars`) - `data.summaries` (`Record | null>`, required, default: `{}`) - `data.where` (`string`, length: `≤ 4000 chars`) - `data.repaired` (`object`) - `data.repaired.at` (`string`, required) - `data.repaired.removed` (`string[]`, required, items: `≤ 50 items`) - `data.repaired.note` (`string`, required, length: `≤ 400 chars`) - `data.tableId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.position` (`integer`, required) - `data.createdAt` (`string`, required) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "filter": { "id": "root", "kind": "group", "mode": "and", "children": [] }, "sorts": [], "groupBy": [], "hidden": [], "order": [ "" ], "rowHeight": "short", "search": "", "summaries": {}, "where": "", "repaired": { "at": "", "removed": [ "" ], "note": "" }, "tableId": "", "position": 123, "createdAt": "", "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//views/' \ --header 'Authorization: Bearer ' ``` --- # Change one of your saved views, as the builder Source: https://grid82.com/docs/builder-api/data/views-update `PATCH https://studio.grid82.app/v1/apps/{appId}/views/{viewId}` Only a view that is yours. `config` merges shallowly: a key you send replaces that key, anything you leave out keeps its saved value. Requires the `records:write` scope. Operation `apps.views.update`. MCP tool `apps_views_update`. CLI: `grid82 apps views update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `viewId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | ------ | -------- | -------------------- | | `name` | string | no | Up to 50 characters. | | `config` | object | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.filter` (`object`, required) - `data.filter.id` (`string`, required, length: `1–64 chars`) - `data.filter.kind` (`string`, required) - `data.filter.mode` (`enum`, required, one of: `and, or`) - `data.filter.children` (`object[]`, required, items: `≤ 50 items`) - `data.filter.children.id` (`string`, required, length: `1–64 chars`) - `data.filter.children.kind` (`string`, required) - `data.filter.children.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.filter.children.op` (`enum`, required, one of: `contains, not_contains, starts_with, ends_with, is, is_not, is_empty, is_not_empty, gt, lt, gte, lte, before, after, on_or_before, on_or_after, is_any_of, is_none_of, has_any_of, has_all_of, has_none_of, is_checked, is_unchecked`) - `data.filter.children.value` (`object`) - `data.sorts` (`object[]`, required, default: `[]`, items: `≤ 10 items`) - `data.sorts.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.sorts.desc` (`boolean`, required) - `data.groupBy` (`object[]`, required, default: `[]`, items: `≤ 3 items`) - `data.groupBy.fieldId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.groupBy.desc` (`boolean`, required) - `data.hidden` (`string[]`, required, default: `[]`) - `data.order` (`string[]`) - `data.rowHeight` (`enum`, required, default: `short`, one of: `short, medium, tall, extraTall`) - `data.search` (`string`, required, default: ``, length: `≤ 200 chars`) - `data.summaries` (`Record | null>`, required, default: `{}`) - `data.where` (`string`, length: `≤ 4000 chars`) - `data.repaired` (`object`) - `data.repaired.at` (`string`, required) - `data.repaired.removed` (`string[]`, required, items: `≤ 50 items`) - `data.repaired.note` (`string`, required, length: `≤ 400 chars`) - `data.tableId` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.position` (`integer`, required) - `data.createdAt` (`string`, required) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "filter": { "id": "root", "kind": "group", "mode": "and", "children": [] }, "sorts": [], "groupBy": [], "hidden": [], "order": [ "" ], "rowHeight": "short", "search": "", "summaries": {}, "where": "", "repaired": { "at": "", "removed": [ "" ], "note": "" }, "tableId": "", "position": 123, "createdAt": "", "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/apps//views/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "config": { "filter": { "id": "root", "kind": "group", "mode": "and", "children": [] }, "sorts": [], "groupBy": [], "hidden": [], "order": [ "" ], "rowHeight": "short", "search": "", "summaries": {}, "where": "", "repaired": { "at": "", "removed": [ "" ], "note": "" } } }' ``` --- # Delete one of your saved views, as the builder Source: https://grid82.com/docs/builder-api/data/views-delete `DELETE https://studio.grid82.app/v1/apps/{appId}/views/{viewId}` Only a view that is yours. The table itself is untouched: a view is an arrangement of rows, never the rows. Requires the `records:write` scope. Operation `apps.views.delete`. MCP tool `apps_views_delete`. CLI: `grid82 apps views delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `viewId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/apps//views/' \ --header 'Authorization: Bearer ' ``` --- # List the user roles Source: https://grid82.com/docs/builder-api/roles/list `GET https://studio.grid82.app/v1/apps/{appId}/roles` Start here to inspect permissions. List every app role, its member count, and one sentence per table describing what each role may see and do. All app permissions are decided by roles; rules never cross between roles. Add one with `roles.create`; check one with `rules.simulate`, without adding anyone. Requires the `schema:read` scope. Operation `roles.list`. MCP tool `roles_list`. CLI: `grid82 roles list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.roles` (`object[]`, required) - `data.roles.id` (`string`, required) - `data.roles.name` (`string`, required) - `data.roles.description` (`string`, required) - `data.roles.members` (`integer`, required) - `data.roles.wholeDatabase` (`boolean`, required) - `data.roles.tables` (`integer`, required) - `data.roles.summary` (`string`, required) ```json { "data": { "roles": [ { "id": "", "name": "", "description": "", "members": 123, "wholeDatabase": true, "tables": 123, "summary": "" } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//roles' \ --header 'Authorization: Bearer ' ``` --- # Add a role Source: https://grid82.com/docs/builder-api/roles/create `POST https://studio.grid82.app/v1/apps/{appId}/roles` Add a role, with its permissions in the same call. Without `tables` it reaches no table; `tables` takes the shape shown in `roles.setPermissions`. Other roles are untouched. Returns the role and its `revision`; check what it allows with `rules.simulate`, without adding anyone. Requires the `schema:write` scope. Operation `roles.create`. MCP tool `roles_create`. CLI: `grid82 roles create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------------- | ------------------- | -------- | --------------------- | | `name` | string | yes | Up to 120 characters. | | `description` | string | no | Up to 500 characters. | | `tables` | "\*" or object | no | | | `interface` | "assigned" or "own" | no | | | `automations` | "none" or "own" | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.role` (`object`, required) - `data.role.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.role.name` (`string`, required, length: `1–120 chars`) - `data.role.description` (`string`, length: `≤ 500 chars`) - `data.role.assistantMonthlyCredits` (`integer`, range: `0–10000000`) - `data.role.interface` (`enum`, one of: `assigned, own`) - `data.role.automations` (`enum`, one of: `none, own`) - `data.revision` (`string`, required) ```json { "data": { "role": { "id": "", "name": "", "description": "", "assistantMonthlyCredits": 123, "interface": "assigned", "automations": "none" }, "revision": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//roles' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }' ``` --- # Read one role's permissions Source: https://grid82.com/docs/builder-api/roles/get `GET https://studio.grid82.app/v1/apps/{appId}/roles/{roleId}` Retrieve one role's table-by-table permissions: rows it can see, fields it can read, fields it can change, and rows on which it can add, edit, or delete. Include the same sentence shown in Studio for each table so tools and screens describe the role consistently. Requires the `schema:read` scope. Operation `roles.get`. MCP tool `roles_get`. CLI: `grid82 roles get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `roleId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.role` (`object`, required) - `data.role.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.role.name` (`string`, required, length: `1–120 chars`) - `data.role.description` (`string`, length: `≤ 500 chars`) - `data.role.assistantMonthlyCredits` (`integer`, range: `0–10000000`) - `data.role.interface` (`enum`, one of: `assigned, own`) - `data.role.automations` (`enum`, one of: `none, own`) - `data.tables` (`string | object[]`, required) - `data.revision` (`string`, required) ```json { "data": { "role": { "id": "", "name": "", "description": "", "assistantMonthlyCredits": 123, "interface": "assigned", "automations": "none" }, "tables": "", "revision": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//roles/' \ --header 'Authorization: Bearer ' ``` --- # Rename or describe a role Source: https://grid82.com/docs/builder-api/roles/update `PATCH https://studio.grid82.app/v1/apps/{appId}/roles/{roleId}` Change a role's name, description, or what its members may do with pages (`interface`) and workflows (`automations`). Its permissions change with `roles.setPermissions`. Requires the `schema:write` scope. Operation `roles.update`. MCP tool `roles_update`. CLI: `grid82 roles update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `roleId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------------- | ------------------- | -------- | --------------------- | | `name` | string | no | Up to 120 characters. | | `description` | string | no | Up to 500 characters. | | `interface` | "assigned" or "own" | no | | | `automations` | "none" or "own" | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.description` (`string`, length: `≤ 500 chars`) - `data.assistantMonthlyCredits` (`integer`, range: `0–10000000`) - `data.interface` (`enum`, one of: `assigned, own`) - `data.automations` (`enum`, one of: `none, own`) ```json { "data": { "id": "", "name": "", "description": "", "assistantMonthlyCredits": 123, "interface": "assigned", "automations": "none" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/apps//roles/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "description": "", "interface": "assigned", "automations": "none" }' ``` --- # Remove a role Source: https://grid82.com/docs/builder-api/roles/delete `DELETE https://studio.grid82.app/v1/apps/{appId}/roles/{roleId}` Remove a role and its permissions. Refused while anyone is in it: move them with `members.update` first. The last role cannot be removed. Requires the `schema:write` scope. Operation `roles.delete`. MCP tool `roles_delete`. CLI: `grid82 roles delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `roleId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/apps//roles/' \ --header 'Authorization: Bearer ' ``` --- # Replace one role's permissions Source: https://grid82.com/docs/builder-api/roles/set-permissions `PUT https://studio.grid82.app/v1/apps/{appId}/roles/{roleId}/permissions` Set permissions for this role only; other roles remain untouched, and permissions never cross between roles or require ordering. Include every table the role should access; omitted tables become inaccessible. `tables` is `"*"` (everything, future tables included) or a map of table id to grant. Example, read all rows, edit only `stage`, hide every other field: `{"tables":{"tbl_deals":{"access":"granted","fields":{"mode":"selected","entries":{"fld_name":{"see":true},"fld_stage":{"see":true,"edit":true}}},"update":"all"}}}`. A field absent from `entries` is hidden; `fields: {"mode":"all","edit":"none"}` shows every field, future ones included. `rows`, `update` and `delete` take a filter group as views do, to limit rows. Simulate first if unsure who is affected. Send the `revision` from roles.get as `baseRevision`; if someone changed this role after that revision, the save is refused with 409 instead of erasing their change. Requires the `schema:write` scope. Operation `roles.setPermissions`. MCP tool `roles_setPermissions`. CLI: `grid82 roles set-permissions` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `roleId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------------- | -------------- | -------- | ----------- | | `tables` | "\*" or object | yes | | | `baseRevision` | string | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.version` (`number`, required) - `data.roles` (`object[]`, required, items: `1–100 items`) - `data.roles.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.roles.name` (`string`, required, length: `1–120 chars`) - `data.roles.description` (`string`, length: `≤ 500 chars`) - `data.roles.assistantMonthlyCredits` (`integer`, range: `0–10000000`) - `data.roles.interface` (`enum`, one of: `assigned, own`) - `data.roles.automations` (`enum`, one of: `none, own`) - `data.permissions` (`object[]`, required, items: `≤ 100 items`) - `data.permissions.role` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.permissions.tables` (`string | Record`, required) - `data.revision` (`string`, required) ```json { "data": { "version": 123.45, "roles": [ { "id": "", "name": "", "description": "", "assistantMonthlyCredits": 123, "interface": "assigned", "automations": "none" } ], "permissions": [ { "role": "", "tables": "" } ], "revision": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PUT \ --url 'https://studio.grid82.app/v1/apps//roles//permissions' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "tables": "" }' ``` --- # Read the whole permission document Source: https://grid82.com/docs/builder-api/rules/get `GET https://studio.grid82.app/v1/apps/{appId}/rules` Return every role and its permissions in one object. Deny by default; a user's permissions are the union of their roles, so adding a role can only add permissions. Prefer `roles.get` for one role. `revision` identifies this document version; send it as `baseRevision` when replacing the document. Requires the `schema:read` scope. Operation `rules.get`. MCP tool `rules_get`. CLI: `grid82 rules get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.version` (`number`, required) - `data.roles` (`object[]`, required, items: `1–100 items`) - `data.roles.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.roles.name` (`string`, required, length: `1–120 chars`) - `data.roles.description` (`string`, length: `≤ 500 chars`) - `data.roles.assistantMonthlyCredits` (`integer`, range: `0–10000000`) - `data.roles.interface` (`enum`, one of: `assigned, own`) - `data.roles.automations` (`enum`, one of: `none, own`) - `data.permissions` (`object[]`, required, items: `≤ 100 items`) - `data.permissions.role` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.permissions.tables` (`string | Record`, required) - `data.revision` (`string`, required) ```json { "data": { "version": 123.45, "roles": [ { "id": "", "name": "", "description": "", "assistantMonthlyCredits": 123, "interface": "assigned", "automations": "none" } ], "permissions": [ { "role": "", "tables": "" } ], "revision": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//rules' \ --header 'Authorization: Bearer ' ``` --- # Replace the whole permission document Source: https://grid82.com/docs/builder-api/rules/set `PUT https://studio.grid82.app/v1/apps/{appId}/rules` Replace every role and permission at once. To add, rename or remove one role, prefer `roles.create`, `roles.update` and `roles.delete`. Each `permissions[].tables` has the shape shown in `roles.setPermissions`. To change one role's permissions, prefer `roles.setPermissions`, which cannot affect others. Send the `revision` returned by `rules.get` as `baseRevision`; if someone else changed the document since, the save is refused with 409 instead of overwriting their change. Requires the `schema:write` scope. Operation `rules.set`. Not an MCP tool. Replacing every role at once is how one agent's edit erases another role's. A model adds, renames, removes and narrows roles one at a time with roles.create, roles.update, roles.delete and roles.setPermissions, none of which can reach another role. The whole document stays on the API and the CLI. CLI: `grid82 rules set` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------------- | --------- | -------- | ---------------- | | `version` | 2 | yes | | | `roles` | object\[] | yes | 1 to 100 items. | | `permissions` | object\[] | yes | Up to 100 items. | | `baseRevision` | string | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.version` (`number`, required) - `data.roles` (`object[]`, required, items: `1–100 items`) - `data.roles.id` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.roles.name` (`string`, required, length: `1–120 chars`) - `data.roles.description` (`string`, length: `≤ 500 chars`) - `data.roles.assistantMonthlyCredits` (`integer`, range: `0–10000000`) - `data.roles.interface` (`enum`, one of: `assigned, own`) - `data.roles.automations` (`enum`, one of: `none, own`) - `data.permissions` (`object[]`, required, items: `≤ 100 items`) - `data.permissions.role` (`string`, required, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.permissions.tables` (`string | Record`, required) - `data.revision` (`string`, required) ```json { "data": { "version": 123.45, "roles": [ { "id": "", "name": "", "description": "", "assistantMonthlyCredits": 123, "interface": "assigned", "automations": "none" } ], "permissions": [ { "role": "", "tables": "" } ], "revision": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PUT \ --url 'https://studio.grid82.app/v1/apps//rules' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "version": 123.45, "roles": [ { "id": "", "name": "" } ], "permissions": [ { "role": "", "tables": "" } ] }' ``` --- # Ask what someone would see Source: https://grid82.com/docs/builder-api/rules/simulate `POST https://studio.grid82.app/v1/apps/{appId}/rules/simulate` Preview the rows and fields a set of roles would see on a table, without granting access; show the result for each role to identify which role grants access. Run before changing rules. Requires the `schema:read` scope. Operation `rules.simulate`. MCP tool `rules_simulate`. CLI: `grid82 rules simulate` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------------------------------------------ | -------- | ----------- | | `roles` | string\[] | yes | | | `tableId` | string | yes | | | `op` | "read" or "create" or "update" or "delete" | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.allowed` (`boolean`, required) - `data.readableFields` (`string[]`, required) - `data.writableFields` (`string[]`, required) - `data.matchedRows` (`integer`, required) - `data.byRole` (`object[]`, required) - `data.byRole.role` (`string`, required) - `data.byRole.allowed` (`boolean`, required) - `data.byRole.readableFields` (`string[]`, required) ```json { "data": { "allowed": true, "readableFields": [ "" ], "writableFields": [ "" ], "matchedRows": 123, "byRole": [ { "role": "", "allowed": true, "readableFields": [ "" ] } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//rules/simulate' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "roles": [ "" ], "tableId": "", "op": "read" }' ``` --- # List an app's pages Source: https://grid82.com/docs/builder-api/pages/list `GET https://studio.grid82.app/v1/apps/{appId}/pages` Every page of the app: the builder's, with their mode and the roles they are shown to, and the pages app users built of their own. Requires the `pages:read` scope. Operation `pages.list`. MCP tool `pages_list`. CLI: `grid82 pages list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.kind` (`enum`, required, one of: `declarative, code`) - `data.mode` (`enum`, required, one of: `locked, template, canvas`) - `data.owner` (`enum`, required, one of: `admin, user`) - `data.ownerUserId` (`string`, length: `≤ 64 chars`) - `data.tableId` (`string`, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.forkedFrom` (`string`, length: `≤ 64 chars`) - `data.visibleTo` (`string | string[]`, required, default: `*`) - `data.reads` (`object`) - `data.reads.tables` (`string[]`, required, default: `[]`) - `data.reads.fields` (`string[]`, required, default: `[]`) - `data.reads.views` (`string[]`, required, default: `[]`) - `data.bundleHash` (`string`, length: `≤ 128 chars`) - `data.publishedVersion` (`integer`, range: `≥ 1`) - `data.theme` (`object`) - `data.theme.accent` (`string`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.theme.radius` (`number`, range: `0–24`) - `data.document` (`object`) - `data.version` (`integer`, required, default: `1`, range: `≥ 1`) - `data.updatedAt` (`string`, required) ```json { "data": [ { "id": "", "name": "", "kind": "declarative", "mode": "locked", "owner": "admin", "ownerUserId": "", "tableId": "", "forkedFrom": "", "visibleTo": "*", "reads": { "tables": [], "fields": [], "views": [] }, "bundleHash": "", "publishedVersion": 123, "theme": { "accent": "", "radius": 123.45 }, "document": null, "version": 1, "updatedAt": "" } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//pages' \ --header 'Authorization: Bearer ' ``` --- # Add a page Source: https://grid82.com/docs/builder-api/pages/create `POST https://studio.grid82.app/v1/apps/{appId}/pages` Create a page definition with its name, mode, roles that can see it, and optional associated table. It has no code; save a version with `pages.code.set` or ask the assistant to draft one. Requires the `pages:write` scope. Operation `pages.create`. MCP tool `pages_create`. CLI: `grid82 pages create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----------- | ----------------------- | -------- | -------------------- | | `name` | string | yes | Up to 60 characters. | | `kind` | "declarative" or "code" | no | Default `"code"`. | | `mode` | "locked" or "template" | no | Default `"locked"`. | | `tableId` | string | no | Up to 64 characters. | | `visibleTo` | "\*" or string\[] | no | Default `"*"`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.kind` (`enum`, required, one of: `declarative, code`) - `data.mode` (`enum`, required, one of: `locked, template, canvas`) - `data.owner` (`enum`, required, one of: `admin, user`) - `data.ownerUserId` (`string`, length: `≤ 64 chars`) - `data.tableId` (`string`, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.forkedFrom` (`string`, length: `≤ 64 chars`) - `data.visibleTo` (`string | string[]`, required, default: `*`) - `data.reads` (`object`) - `data.reads.tables` (`string[]`, required, default: `[]`) - `data.reads.fields` (`string[]`, required, default: `[]`) - `data.reads.views` (`string[]`, required, default: `[]`) - `data.bundleHash` (`string`, length: `≤ 128 chars`) - `data.publishedVersion` (`integer`, range: `≥ 1`) - `data.theme` (`object`) - `data.theme.accent` (`string`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.theme.radius` (`number`, range: `0–24`) - `data.document` (`object`) - `data.version` (`integer`, required, default: `1`, range: `≥ 1`) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "kind": "declarative", "mode": "locked", "owner": "admin", "ownerUserId": "", "tableId": "", "forkedFrom": "", "visibleTo": "*", "reads": { "tables": [], "fields": [], "views": [] }, "bundleHash": "", "publishedVersion": 123, "theme": { "accent": "", "radius": 123.45 }, "document": null, "version": 1, "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//pages' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }' ``` --- # Rename a page, change its mode, or change who sees it Source: https://grid82.com/docs/builder-api/pages/update `PATCH https://studio.grid82.app/v1/apps/{appId}/pages/{pageId}` Any of name, mode, tableId and visibleTo. A page has one mode for everyone: it cannot be locked for one role and a Template for another. Requires the `pages:write` scope. Operation `pages.update`. MCP tool `pages_update`. CLI: `grid82 pages update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `pageId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----------- | ----------------------- | -------- | -------------------- | | `name` | string | no | Up to 60 characters. | | `kind` | "declarative" or "code" | no | | | `mode` | "locked" or "template" | no | | | `tableId` | string | no | Up to 64 characters. | | `visibleTo` | "\*" or string\[] | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required, length: `1–64 chars`) - `data.name` (`string`, required, length: `1–120 chars`) - `data.kind` (`enum`, required, one of: `declarative, code`) - `data.mode` (`enum`, required, one of: `locked, template, canvas`) - `data.owner` (`enum`, required, one of: `admin, user`) - `data.ownerUserId` (`string`, length: `≤ 64 chars`) - `data.tableId` (`string`, pattern: `^[a-z][a-z0-9_]{0,62}$`) - `data.forkedFrom` (`string`, length: `≤ 64 chars`) - `data.visibleTo` (`string | string[]`, required, default: `*`) - `data.reads` (`object`) - `data.reads.tables` (`string[]`, required, default: `[]`) - `data.reads.fields` (`string[]`, required, default: `[]`) - `data.reads.views` (`string[]`, required, default: `[]`) - `data.bundleHash` (`string`, length: `≤ 128 chars`) - `data.publishedVersion` (`integer`, range: `≥ 1`) - `data.theme` (`object`) - `data.theme.accent` (`string`, pattern: `^(?:#[0-9a-fA-F]{3,8}|oklch\([0-9.%\s/]{1,60}\))$`) - `data.theme.radius` (`number`, range: `0–24`) - `data.document` (`object`) - `data.version` (`integer`, required, default: `1`, range: `≥ 1`) - `data.updatedAt` (`string`, required) ```json { "data": { "id": "", "name": "", "kind": "declarative", "mode": "locked", "owner": "admin", "ownerUserId": "", "tableId": "", "forkedFrom": "", "visibleTo": "*", "reads": { "tables": [], "fields": [], "views": [] }, "bundleHash": "", "publishedVersion": 123, "theme": { "accent": "", "radius": 123.45 }, "document": null, "version": 1, "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/apps//pages/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "kind": "declarative", "mode": "locked", "tableId": "", "visibleTo": "" }' ``` --- # Delete a page Source: https://grid82.com/docs/builder-api/pages/delete `DELETE https://studio.grid82.app/v1/apps/{appId}/pages/{pageId}` The page, every version of its code, and every copy an app user took of it. Requires the `pages:write` scope. Operation `pages.delete`. MCP tool `pages_delete`. CLI: `grid82 pages delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `pageId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/apps//pages/' \ --header 'Authorization: Bearer ' ``` --- # Read a page's code Source: https://grid82.com/docs/builder-api/pages/code-get `GET https://studio.grid82.app/v1/apps/{appId}/pages/{pageId}/code` The current version unless `version` names an earlier one. Null when nothing has been saved yet. Requires the `pages:read` scope. Operation `pages.code.get`. MCP tool `pages_code_get`. CLI: `grid82 pages code get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `pageId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `version` | integer | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object | null`, required) - `data.version` (`integer`, required) - `data.author` (`string`, required) - `data.note` (`string,null`, required) - `data.at` (`string`, required) - `data.source` (`string`, required) ```json { "data": { "version": 123, "author": "", "note": null, "at": "", "source": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//pages//code' \ --header 'Authorization: Bearer ' ``` --- # Save a new version of a page's code Source: https://grid82.com/docs/builder-api/pages/code-set `PUT https://studio.grid82.app/v1/apps/{appId}/pages/{pageId}/code` Save a new version without overwriting; the page points to the latest version. Include a note explaining why for the history. Send `baseVersion` (the page's `version` when read) to receive 409 if someone has saved since, rather than burying their version. Requires the `pages:write` scope. Operation `pages.code.set`. MCP tool `pages_code_set`. CLI: `grid82 pages code set` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `pageId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------------- | ------------------------------------------- | -------- | ------------------------ | | `source` | string | yes | Up to 400000 characters. | | `note` | string | no | Up to 200 characters. | | `via` | "assistant" or "hand" or "api" or "restore" | no | | | `baseVersion` | integer | no | At least 0. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.version` (`integer`, required) - `data.at` (`string`, required) - `data.bundleHash` (`string,null`, required, default: `null`) - `data.buildError` (`string,null`, required, default: `null`) - `data.warnings` (`string[]`, required, default: `[]`) ```json { "data": { "version": 123, "at": "", "bundleHash": null, "buildError": null, "warnings": [] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PUT \ --url 'https://studio.grid82.app/v1/apps//pages//code' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "source": "" }' ``` --- # A page's version history, newest first Source: https://grid82.com/docs/builder-api/pages/versions `GET https://studio.grid82.app/v1/apps/{appId}/pages/{pageId}/versions` Who saved each version and when, with the note they left. The code itself is read by version with pages.code.get. Requires the `pages:read` scope. Operation `pages.versions`. MCP tool `pages_versions`. CLI: `grid82 pages versions` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `pageId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.version` (`integer`, required) - `data.author` (`string`, required) - `data.note` (`string,null`, required) - `data.at` (`string`, required) - `data.bytes` (`integer`, required) - `data.bundleHash` (`string,null`, required, default: `null`) - `data.buildError` (`string,null`, required, default: `null`) - `data.warnings` (`string[]`, required, default: `[]`) - `data.via` (`enum | null`, required, default: `null`) - `data.report` (`Record | null`, required, default: `null`) - `data.published` (`boolean`, required, default: `false`) ```json { "data": [ { "version": 123, "author": "", "note": null, "at": "", "bytes": 123, "bundleHash": null, "buildError": null, "warnings": [], "via": null, "report": null, "published": false } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//pages//versions' \ --header 'Authorization: Bearer ' ``` --- # Record what a version did when it ran Source: https://grid82.com/docs/builder-api/pages/report `POST https://studio.grid82.app/v1/apps/{appId}/pages/{pageId}/report` Written by the preview, never by a page and never by an agent: which tables the page asked for, how many rows came back, what was refused, and what it drew. It is what lets `page_check` answer "this page shows an empty table over 140 rows" instead of "it compiled". Requires the `pages:write` scope. Operation `pages.report`. MCP tool `pages_report`. CLI: `grid82 pages report` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `pageId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `version` | integer | yes | At least 1. | | `report` | object | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.ok` (`boolean`, required) ```json { "data": { "ok": true } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//pages//report' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "version": 123, "report": {} }' ``` --- # Publish a version of a page Source: https://grid82.com/docs/builder-api/pages/publish `POST https://studio.grid82.app/v1/apps/{appId}/pages/{pageId}/publish` Point the page to a built version: the latest by default, or an earlier one via `version`. Saves always build; publishing does not compile, cannot fail on code, and shows the same result as the preview. Refuse versions that did not build, with the reason. Use `files` when the caller supplies its own build. Requires the `pages:write` scope. Operation `pages.publish`. MCP tool `pages_publish`. CLI: `grid82 pages publish` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `pageId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | --------- | -------- | -------------- | | `version` | integer | no | At least 1. | | `files` | object\[] | no | 1 to 50 items. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.bundleHash` (`string`, required) - `data.version` (`integer`, required) ```json { "data": { "bundleHash": "", "version": 123 } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//pages//publish' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "version": 123, "files": [ { "path": "", "content": "" } ] }' ``` --- # What was changed, and what can be undone Source: https://grid82.com/docs/builder-api/changesets/list `GET https://studio.grid82.app/v1/apps/{appId}/changesets` List recent write bundles, newest first. Each bundle is one atomic group of one user's operations, reports whether it has been undone, and includes `revertible` to indicate whether it can be undone. Requires the `records:read` scope. Operation `changesets.list`. MCP tool `changesets_list`. CLI: `grid82 changesets list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | ------- | ------- | -------- | ------------- | | `limit` | integer | no | Default `50`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.title` (`string`, required) - `data.status` (`string`, required) - `data.createdAt` (`string`, required) - `data.actor` (`string`, required) - `data.reverted` (`boolean`, required) - `data.revertible` (`boolean`, required) - `data.notRevertible` (`string`) ```json { "data": [ { "id": "", "title": "", "status": "", "createdAt": "", "actor": "", "reverted": true, "revertible": true, "notRevertible": "" } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//changesets' \ --header 'Authorization: Bearer ' ``` --- # Undo a changeset Source: https://grid82.com/docs/builder-api/changesets/revert `POST https://studio.grid82.app/v1/apps/{appId}/changesets/{changesetId}/revert` Replay a bundle's operations in reverse through the normal permission compiler; undoing a delete recreates the row only if you could create it. Updates restore only changed fields, preserving edits to other fields; if someone changed the same fields, refuse and identify them unless `confirm: true`, which overwrites their changes. Record the revert so reverting it undoes it. Only bundles of record creates, edits, and deletes are revertible; the list marks each `revertible`. Refuse comments, saved views, pages, and app SHAPE changes up front with the reason: restoring a definition would run its migration in reverse, and a returning column loses its contents. Restore deleted fields or tables, and columns converted by a type change, from the trash with `trash.restore`, including values. Undo all restorable rows in one transaction; list rows deleted since in `skipped` rather than failing the rest. Requires the `records:write` scope. Operation `changesets.revert`. MCP tool `changesets_revert`. CLI: `grid82 changesets revert` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `changesetId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `confirm` | boolean | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.status` (`string`, required) - `data.skipped` (`object[]`) - `data.skipped.tableId` (`string`, required) - `data.skipped.recordId` (`string`, required) - `data.skipped.reason` (`string`, required) ```json { "data": { "id": "", "status": "", "skipped": [ { "tableId": "", "recordId": "", "reason": "" } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//changesets//revert' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "confirm": true }' ``` --- # What was deleted, and can still be put back Source: https://grid82.com/docs/builder-api/trash/list `GET https://studio.grid82.app/v1/apps/{appId}/trash` List deleted tables and fields, plus columns converted by type changes; show how many records' values each holds and when it expires (thirty days after the change). Restore an item with trash.restore. Requires the `schema:read` scope. Operation `trash.list`. MCP tool `trash_list`. CLI: `grid82 trash list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.kind` (`string`, required) - `data.label` (`string`, required) - `data.at` (`string`, required) - `data.actor` (`string`, required) - `data.rows` (`number`, required) - `data.expiresAt` (`string`, required) - `data.restoredAt` (`string,null`, required) ```json { "data": [ { "id": "", "kind": "", "label": "", "at": "", "actor": "", "rows": 123.45, "expiresAt": "", "restoredAt": null } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//trash' \ --header 'Authorization: Bearer ' ``` --- # Put a deleted table or field back Source: https://grid82.com/docs/builder-api/trash/restore `POST https://studio.grid82.app/v1/apps/{appId}/trash/{trashId}/restore` Restore tables with rows and links, fields with values, converted columns with previous values, and their grants, using the same IDs. Records created since keep their values. Restore values only to existing records and links only when both records exist. Refused if an ID is now in use; restore the table before its fields. Requires the `schema:write` scope. Operation `trash.restore`. MCP tool `trash_restore`. CLI: `grid82 trash restore` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `trashId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.kind` (`string`, required) - `data.label` (`string`, required) ```json { "data": { "id": "", "kind": "", "label": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//trash//restore' \ --header 'Authorization: Bearer ' ``` --- # A suite's directory Source: https://grid82.com/docs/builder-api/users/list `GET https://studio.grid82.app/v1/users` Check existing suite accounts before inviting a user again. List users and their app memberships; each has one suite account and can belong to multiple apps. `apps` includes those apps and the user's roles in each. Requires the `members:read` scope. Operation `users.list`. MCP tool `users_list`. CLI: `grid82 users list` (`npx grid82`). ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | --------------------- | | `limit` | integer | no | Default `50`. | | `cursor` | string | no | Up to 512 characters. | | `suiteId` | string | yes | Up to 64 characters. | | `search` | string | no | Up to 200 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.userId` (`string`, required) - `data.email` (`string`, required) - `data.name` (`string,null`, required) - `data.status` (`string`, required) - `data.kind` (`enum`, required, one of: `builder, member`) - `data.apps` (`object[]`, required) - `data.apps.id` (`string`, required) - `data.apps.name` (`string`, required) - `data.apps.roles` (`string[]`, required) - `meta` (`object`, required) - `meta.nextCursor` (`string,null`, required): Pass as `cursor` for the next page. Null on the last one. - `meta.hasMore` (`boolean`, required) - `meta.fields` (`Record`): Field id to field name, for the table the records came from. ```json { "data": [ { "userId": "", "email": "", "name": null, "status": "", "kind": "builder", "apps": [ { "id": "", "name": "", "roles": [ "" ] } ] } ], "meta": { "nextCursor": null, "hasMore": true, "fields": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/users?suiteId=' \ --header 'Authorization: Bearer ' ``` --- # Add someone to a suite's directory Source: https://grid82.com/docs/builder-api/users/invite `POST https://studio.grid82.app/v1/users` Create a directory entry; this grants no access. Add the user to an app with `members.add` afterwards. Directory membership and app access are separate so onboarding a colleague does not require deciding what they may see in the CRM. Requires the `members:write` scope. Operation `users.invite`. MCP tool `users_invite`. CLI: `grid82 users invite` (`npx grid82`). ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | --------------------- | -------- | -------------------- | | `suiteId` | string | yes | Up to 64 characters. | | `email` | string | yes | | | `name` | string | no | Up to 80 characters. | | `kind` | "builder" or "member" | no | Default `"member"`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.userId` (`string`, required) - `data.email` (`string`, required) - `data.status` (`string`, required) ```json { "data": { "userId": "", "email": "", "status": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/users' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "suiteId": "", "email": "" }' ``` --- # Rename or suspend someone Source: https://grid82.com/docs/builder-api/users/update `PATCH https://studio.grid82.app/v1/users/{userId}` Suspending keeps the user in the directory and in their apps, but refuses their next sign-in and every request from their credentials. Reinstating is the same call with status active. Requires the `members:write` scope. Operation `users.update`. MCP tool `users_update`. CLI: `grid82 users update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `userId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | ------------------------------------ | -------- | ----------- | | `name` | string or null | no | | | `status` | "active" or "invited" or "suspended" | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.userId` (`string`, required) - `data.name` (`string,null`, required) - `data.status` (`string`, required) ```json { "data": { "userId": "", "name": null, "status": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/users/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "", "status": "active" }' ``` --- # Remove someone from a suite Source: https://grid82.com/docs/builder-api/users/remove `DELETE https://studio.grid82.app/v1/users/{userId}` Remove users from the directory and every app in this suite. Their sign-in stops working immediately. Keep records they created; those belong to the app, not to them. Requires the `members:write` scope. Operation `users.remove`. MCP tool `users_remove`. CLI: `grid82 users remove` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `userId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.userId` (`string`, required) ```json { "data": { "userId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/users/' \ --header 'Authorization: Bearer ' ``` --- # Get someone's personal set-password link Source: https://grid82.com/docs/builder-api/users/password-link `POST https://studio.grid82.app/v1/users/{userId}/password-link` Create a password-setting link for this person only, at the suite's address. Directory membership does not prove ownership; hand them the link for access without Google because the platform sends no email. Valid once for seven days; a new link retires the previous one. Refused for someone who can already sign in. An unverified self-sign-up does not count: using the link replaces its password and ends its sessions and keys. URL returned once, never stored in clear. Requires the `members:write` scope. Operation `users.passwordLink`. Not an MCP tool. A set-password link is a credential for another person: whoever holds it becomes them. It is handed over by a builder, from the Studio or the CLI, and never passes through a model's context. CLI: `grid82 users password-link` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `userId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.userId` (`string`, required) - `data.url` (`string`, required) - `data.expiresAt` (`string`, required) ```json { "data": { "userId": "", "url": "", "expiresAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/users//password-link' \ --header 'Authorization: Bearer ' ``` --- # List members Source: https://grid82.com/docs/builder-api/members/list `GET https://studio.grid82.app/v1/apps/{appId}/members` Everyone invited into this app and which roles they are in. Requires the `members:read` scope. Operation `members.list`. MCP tool `members_list`. CLI: `grid82 members list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | -------- | ------- | -------- | --------------------- | | `limit` | integer | no | Default `50`. | | `cursor` | string | no | Up to 512 characters. | | `search` | string | no | Up to 200 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.userId` (`string`, required) - `data.email` (`string`, required) - `data.roles` (`string[]`, required) - `data.status` (`string`, required) - `meta` (`object`, required) - `meta.nextCursor` (`string,null`, required): Pass as `cursor` for the next page. Null on the last one. - `meta.hasMore` (`boolean`, required) - `meta.fields` (`Record`): Field id to field name, for the table the records came from. ```json { "data": [ { "userId": "", "email": "", "roles": [ "" ], "status": "" } ], "meta": { "nextCursor": null, "hasMore": true, "fields": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//members' \ --header 'Authorization: Bearer ' ``` --- # Put someone in an app Source: https://grid82.com/docs/builder-api/members/add `POST https://studio.grid82.app/v1/apps/{appId}/members` Grant app access in one or more roles. Access is the union of role grants; adding a role only adds access. An unknown email is added to the suite directory automatically; do not call `users.invite` first. Requires the `members:write` scope. Operation `members.add`. MCP tool `members_add`. CLI: `grid82 members add` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | --------- | -------- | -------------------- | | `userId` | string | no | Up to 64 characters. | | `email` | string | no | | | `roles` | string\[] | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.userId` (`string`, required) - `data.email` (`string`, required) - `data.roles` (`string[]`, required) - `data.status` (`string`, required) ```json { "data": { "userId": "", "email": "", "roles": [ "" ], "status": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//members' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "roles": [ "" ] }' ``` --- # Change someone's roles in an app Source: https://grid82.com/docs/builder-api/members/update `PATCH https://studio.grid82.app/v1/apps/{appId}/members/{userId}` Replace this user's entire role list in this app; recompute access as the union of the new roles. Change nothing else about the user in this or any other app. Requires the `members:write` scope. Operation `members.update`. MCP tool `members_update`. CLI: `grid82 members update` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `userId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------- | --------- | -------- | ----------- | | `roles` | string\[] | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.userId` (`string`, required) - `data.roles` (`string[]`, required) ```json { "data": { "userId": "", "roles": [ "" ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/apps//members/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "roles": [ "" ] }' ``` --- # Remove someone from an app Source: https://grid82.com/docs/builder-api/members/remove `DELETE https://studio.grid82.app/v1/apps/{appId}/members/{userId}` Remove access to this app only; keep the member in the directory and all other apps, and retain records they created because those belong to the app. Requires the `members:write` scope. Operation `members.remove`. MCP tool `members_remove`. CLI: `grid82 members remove` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `userId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.userId` (`string`, required) ```json { "data": { "userId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/apps//members/' \ --header 'Authorization: Bearer ' ``` --- # List invite links Source: https://grid82.com/docs/builder-api/invites/list `GET https://studio.grid82.app/v1/suites/{suiteId}/invites` List every invite link across the suite's apps, including its app, granted roles, use count, and whether it is still live. The URL is never returned after creation. Requires the `members:read` scope. Operation `invites.list`. MCP tool `invites_list`. CLI: `grid82 invites list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | -------- | ------- | -------- | --------------------- | | `limit` | integer | no | Default `50`. | | `cursor` | string | no | Up to 512 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.appId` (`string`, required) - `data.roles` (`string[]`, required) - `data.label` (`string,null`, required) - `data.createdAt` (`string`, required) - `data.expiresAt` (`string`, required) - `data.maxUses` (`number,null`, required) - `data.uses` (`number`, required) - `data.status` (`enum`, required, one of: `live, expired, exhausted, revoked`) - `meta` (`object`, required) - `meta.nextCursor` (`string,null`, required): Pass as `cursor` for the next page. Null on the last one. - `meta.hasMore` (`boolean`, required) - `meta.fields` (`Record`): Field id to field name, for the table the records came from. ```json { "data": [ { "id": "", "appId": "", "roles": [ "" ], "label": null, "createdAt": "", "expiresAt": "", "maxUses": null, "uses": 123.45, "status": "live" } ], "meta": { "nextCursor": null, "hasMore": true, "fields": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/suites//invites' \ --header 'Authorization: Bearer ' ``` --- # Create an invite link Source: https://grid82.com/docs/builder-api/invites/create `POST https://studio.grid82.app/v1/suites/{suiteId}/invites` Create a single-use link that adds anyone who opens it to one app in this suite with exactly these roles, after signing in at the suite's address. It works when the suite is invitation-only and can add a new person to the directory. It cannot set a password for an address already in the directory; that person needs their own link (`users.passwordLink`). Pass `maxUses` to allow multiple uses, or `null` for unlimited. The URL is returned once and never stored in clear; copy it now. Requires the `members:write` scope. Operation `invites.create`. MCP tool `invites_create`. CLI: `grid82 invites create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------------- | --------------- | -------- | ------------------------------------- | | `appId` | string | yes | Up to 64 characters. | | `roles` | string\[] | yes | | | `label` | string | no | Up to 60 characters. | | `expiresInDays` | integer | no | Default `14`. At least 1. At most 90. | | `maxUses` | integer or null | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.url` (`string`, required) - `data.appId` (`string`, required) - `data.roles` (`string[]`, required) - `data.label` (`string,null`, required) - `data.expiresAt` (`string`, required) - `data.maxUses` (`number,null`, required) ```json { "data": { "id": "", "url": "", "appId": "", "roles": [ "" ], "label": null, "expiresAt": "", "maxUses": null } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/suites//invites' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "appId": "", "roles": [ "" ] }' ``` --- # Revoke an invite link Source: https://grid82.com/docs/builder-api/invites/revoke `DELETE https://studio.grid82.app/v1/suites/{suiteId}/invites/{inviteId}` Stops a link working from now on. Users who already joined through it keep their membership; revoking a link is not removing members. Requires the `members:write` scope. Operation `invites.revoke`. MCP tool `invites_revoke`. CLI: `grid82 invites revoke` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | | `inviteId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.status` (`string`, required) ```json { "data": { "id": "", "status": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/suites//invites/' \ --header 'Authorization: Bearer ' ``` --- # The hostnames a suite answers on Source: https://grid82.com/docs/builder-api/domains/list `GET https://studio.grid82.app/v1/suites/{suiteId}/domains` Every hostname attached to this suite (active, pending its DNS proof, provisioning its certificate, or released), with when it was verified. A pending one carries the records to publish; a provisioning one carries its certificate's progress. Its apps are reached underneath each active one. Requires the `account:read` scope. Operation `domains.list`. MCP tool `domains_list`. CLI: `grid82 domains list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.hostname` (`string`, required) - `data.status` (`string`, required) - `data.createdAt` (`string`, required) - `data.verifiedAt` (`string,null`, required) - `data.verification` (`object`) - `data.verification.type` (`string`, required) - `data.verification.name` (`string`, required) - `data.verification.value` (`string`, required) - `data.dns` (`object`) - `data.dns.type` (`string`, required) - `data.dns.name` (`string`, required) - `data.dns.value` (`string`, required) - `data.certificate` (`object`) - `data.certificate.host` (`string`, required) - `data.certificate.ssl` (`string`, required) - `data.certificate.errors` (`string[]`, required) ```json { "data": [ { "hostname": "", "status": "", "createdAt": "", "verifiedAt": null, "verification": { "type": "", "name": "", "value": "" }, "dns": { "type": "", "name": "", "value": "" }, "certificate": { "host": "", "ssl": "", "errors": [ "" ] } } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/suites//domains' \ --header 'Authorization: Bearer ' ``` --- # Attach a hostname to a suite Source: https://grid82.com/docs/builder-api/domains/attach `POST https://studio.grid82.app/v1/suites/{suiteId}/domains` Route a hostname to this suite so people can sign in there and each app responds under its slug, such as `/crm` or `/hr`; one address serves every app. The platform subdomain is active immediately. Custom hostnames are a paid option and must be subdomains, such as `app.example.com`, not `example.com`; they start as `pending`, and the response includes a TXT record to prove ownership and a CNAME to route traffic here. Publish both records, then call `domains.verify`: after the TXT record appears, the hostname becomes `provisioning` while a certificate is issued, then `active` when it serves. Refuse hostnames already answered by another organisation. Requires the `schema:write` scope. Operation `domains.attach`. MCP tool `domains_attach`. CLI: `grid82 domains attach` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | ------ | -------- | --------------------- | | `hostname` | string | yes | Up to 253 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.hostname` (`string`, required) - `data.status` (`enum`, required, one of: `active, pending, provisioning`) - `data.dns` (`object`, required) - `data.dns.type` (`string`, required) - `data.dns.name` (`string`, required) - `data.dns.value` (`string`, required) - `data.verification` (`object`) - `data.verification.type` (`string`, required) - `data.verification.name` (`string`, required) - `data.verification.value` (`string`, required) ```json { "data": { "hostname": "", "status": "active", "dns": { "type": "", "name": "", "value": "" }, "verification": { "type": "", "name": "", "value": "" } } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/suites//domains' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "hostname": "" }' ``` --- # Check a hostname's DNS and certificate, and activate it Source: https://grid82.com/docs/builder-api/domains/verify `POST https://studio.grid82.app/v1/suites/{suiteId}/domains/{hostname}/verify` Check a hostname's status and advance it one step. `pending`: look up the TXT record requested by `domains.attach`; when it appears, the hostname becomes `provisioning` while Cloudflare validates it and issues a certificate. This takes minutes and requires the CNAME to point here. `certificate` includes both states and any Cloudflare complaints. `active`: the hostname serves and starts routing to this suite. Safe to call repeatedly while DNS propagates. Requires the `schema:write` scope. Operation `domains.verify`. MCP tool `domains_verify`. CLI: `grid82 domains verify` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | | `hostname` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.hostname` (`string`, required) - `data.status` (`enum`, required, one of: `active, pending, provisioning`) - `data.verification` (`object`) - `data.verification.type` (`string`, required) - `data.verification.name` (`string`, required) - `data.verification.value` (`string`, required) - `data.dns` (`object`) - `data.dns.type` (`string`, required) - `data.dns.name` (`string`, required) - `data.dns.value` (`string`, required) - `data.certificate` (`object`) - `data.certificate.host` (`string`, required) - `data.certificate.ssl` (`string`, required) - `data.certificate.errors` (`string[]`, required) ```json { "data": { "hostname": "", "status": "active", "verification": { "type": "", "name": "", "value": "" }, "dns": { "type": "", "name": "", "value": "" }, "certificate": { "host": "", "ssl": "", "errors": [ "" ] } } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/suites//domains//verify' \ --header 'Authorization: Bearer ' ``` --- # Detach a hostname Source: https://grid82.com/docs/builder-api/domains/detach `DELETE https://studio.grid82.app/v1/suites/{suiteId}/domains/{hostname}` The hostname stops routing to this suite at once, for every app under it; people signed in there are signed out by the next request. Records, users and members are untouched. Requires the `schema:write` scope. Operation `domains.detach`. MCP tool `domains_detach`. CLI: `grid82 domains detach` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | | `hostname` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.hostname` (`string`, required) ```json { "data": { "hostname": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/suites//domains/' \ --header 'Authorization: Bearer ' ``` --- # Your builder keys and connected agents Source: https://grid82.com/docs/builder-api/keys/list `GET https://studio.grid82.app/v1/keys` Every credential acting as you on the builder plane: keys you made and AI clients you connected to the Studio. Never the secrets. Any valid credential for this API: no scope is needed. Operation `keys.list`. MCP tool `keys_list`. CLI: `grid82 keys list` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.kind` (`enum`, required, one of: `key, oauth`) - `data.name` (`string`, required) - `data.keyId` (`string`, required) - `data.scopes` (`string[]`, required) - `data.clientId` (`string,null`, required) - `data.appIds` (`string[] | null`, required) - `data.createdAt` (`string`, required) - `data.expiresAt` (`string,null`, required) - `data.lastUsedAt` (`string,null`, required) ```json { "data": [ { "id": "", "kind": "key", "name": "", "keyId": "", "scopes": [ "" ], "clientId": null, "appIds": [ "" ], "createdAt": "", "expiresAt": null, "lastUsedAt": null } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/keys' \ --header 'Authorization: Bearer ' ``` --- # Create a builder API key Source: https://grid82.com/docs/builder-api/keys/create `POST https://studio.grid82.app/v1/keys` Mint a key acting as you, the builder, in one named organisation, narrowed to chosen scopes. Requires a signed-in browser; keys and connected agents cannot mint keys. Expires in 90 days unless otherwise specified; `expiresInDays: null` means it lives until revoked. Returned once and never stored in clear. For CI and CLI use without a browser. Any valid credential for this API: no scope is needed. Operation `keys.create`. Not an MCP tool. Minting a credential from a credential is how one leaked token becomes a permanent one. Keys are made by a person in a signed-in browser, never by a connected agent. CLI: `grid82 keys create` (`npx grid82`). ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------------- | --------------- | -------- | -------------------- | | `organizationId` | string | yes | | | `name` | string | yes | Up to 60 characters. | | `scopes` | string\[] | yes | | | `expiresInDays` | integer or null | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.kind` (`enum`, required, one of: `key, oauth`) - `data.name` (`string`, required) - `data.keyId` (`string`, required) - `data.scopes` (`string[]`, required) - `data.clientId` (`string,null`, required) - `data.appIds` (`string[] | null`, required) - `data.createdAt` (`string`, required) - `data.expiresAt` (`string,null`, required) - `data.lastUsedAt` (`string,null`, required) - `data.key` (`string`, required) ```json { "data": { "id": "", "kind": "key", "name": "", "keyId": "", "scopes": [ "" ], "clientId": null, "appIds": [ "" ], "createdAt": "", "expiresAt": null, "lastUsedAt": null, "key": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/keys' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "organizationId": "", "name": "", "scopes": [ "" ] }' ``` --- # Revoke one of your builder credentials Source: https://grid82.com/docs/builder-api/keys/revoke `DELETE https://studio.grid82.app/v1/keys/{credentialId}` Stops a key or a connected agent working, immediately. Only your own, and only from a signed-in browser. Any valid credential for this API: no scope is needed. Operation `keys.revoke`. Not an MCP tool. A connected agent revoking the person's other credentials is not a thing it should be able to do on their behalf. Revocation is done by the person, in a signed-in browser. CLI: `grid82 keys revoke` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | -------------- | ------ | -------- | ----------- | | `credentialId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/keys/' \ --header 'Authorization: Bearer ' ``` --- # List the skills Source: https://grid82.com/docs/builder-api/skills/list `GET https://studio.grid82.app/v1/skills` List skill IDs and one-line summaries. Each skill contains detailed instructions for an area, such as permissions, an app's database, or its interface, written for the tool user. Check this list before non-trivial work; read the relevant skill with `skills.get` rather than guessing its contents. Any valid credential for this API: no scope is needed. Operation `skills.list`. MCP tool `skills_list`. CLI: `grid82 skills list` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.skills` (`object[]`, required) - `data.skills.id` (`string`, required) - `data.skills.title` (`string`, required) - `data.skills.description` (`string`, required) ```json { "data": { "skills": [ { "id": "", "title": "", "description": "" } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/skills' \ --header 'Authorization: Bearer ' ``` --- # Read one skill Source: https://grid82.com/docs/builder-api/skills/get `GET https://studio.grid82.app/v1/skills/{skillId}` Retrieve the full Markdown text of one skill, explaining how its tools fit together, common mistakes, and actions to never take. Reading it is cheap and can prevent costly permission-change errors. Any valid credential for this API: no scope is needed. Operation `skills.get`. MCP tool `skills_get`. CLI: `grid82 skills get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `skillId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.title` (`string`, required) - `data.description` (`string`, required) - `data.body` (`string`, required) ```json { "data": { "id": "", "title": "", "description": "", "body": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/skills/' \ --header 'Authorization: Bearer ' ``` --- # The models an assistant menu may offer Source: https://grid82.com/docs/builder-api/models/list `GET https://studio.grid82.app/v1/models` The platform's model catalogue: each model's id, name, what it is good at and how fast, clever and expensive it is. The same list for everybody; an organisation's own menu and a suite's are chosen from it. Any valid credential for this API: no scope is needed. Operation `models.list`. MCP tool `models_list`. CLI: `grid82 models list` (`npx grid82`). **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.models` (`Record[]`, required) ```json { "data": { "models": [ {} ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/models' \ --header 'Authorization: Bearer ' ``` --- # This organisation's referral code and what it has earned Source: https://grid82.com/docs/builder-api/referrals/get `GET https://studio.grid82.app/v1/organizations/{organizationId}/referrals` Retrieve the sign-up link code, the referrals it brought (status and date, never who), and earned credits: a bonus for the referred organisation when it signs up, then for you when it first uses Grid82 and first subscribes. While sign-ups are closed, the code cannot bring anybody in. Available to admins and builders on every suite. Requires the `account:read` scope. Operation `referrals.get`. MCP tool `referrals_get`. CLI: `grid82 referrals get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------- | | `organizationId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.code` (`string`, required) - `data.link` (`string`, required) - `data.signupsOpen` (`boolean`, required) - `data.referrals` (`object[]`, required) - `data.referrals.status` (`enum`, required, one of: `pending, rewarded, void`) - `data.referrals.createdAt` (`string`, required) - `data.rewards` (`object`, required) - `data.rewards.pendingCredits` (`number`, required) - `data.rewards.earnedCredits` (`number`, required) - `data.rewards.perReferral` (`object`, required) - `data.rewards.perReferral.referred` (`number`, required) - `data.rewards.perReferral.firstUse` (`number`, required) - `data.rewards.perReferral.firstPurchase` (`number`, required) ```json { "data": { "code": "", "link": "", "signupsOpen": true, "referrals": [ { "status": "pending", "createdAt": "" } ], "rewards": { "pendingCredits": 123.45, "earnedCredits": 123.45, "perReferral": { "referred": 123.45, "firstUse": 123.45, "firstPurchase": 123.45 } } } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/organizations//referrals' \ --header 'Authorization: Bearer ' ``` --- # List an app's workflows Source: https://grid82.com/docs/builder-api/workflows/list `GET https://studio.grid82.app/v1/apps/{appId}/workflows` Every workflow here (the app's workflows, which act as themselves, with the Studio's reach over the apps they list): name, status, trigger, the version that is live and the last run. Requires the `workflows:read` scope. Operation `workflows.list`. MCP tool `workflows_list`. CLI: `grid82 workflows list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.mode` (`enum`, required, one of: `locked, personal`) - `data.owner` (`string`, required) - `data.status` (`enum`, required, one of: `draft, active, paused`) - `data.latestVersion` (`integer`, required) - `data.activeVersion` (`integer | null`, required) - `data.trigger` (`Record | null`, required) - `data.updatedAt` (`string`, required) - `data.lastRun` (`object | null`, required) - `data.lastRun.id` (`string`, required) - `data.lastRun.status` (`string`, required) - `data.lastRun.startedAt` (`string`, required) - `data.lastRun.error` (`string,null`, required) ```json { "data": [ { "id": "", "name": "", "mode": "locked", "owner": "", "status": "draft", "latestVersion": 123, "activeVersion": 123, "trigger": {}, "updatedAt": "", "lastRun": { "id": "", "status": "", "startedAt": "", "error": null } } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//workflows' \ --header 'Authorization: Bearer ' ``` --- # Create a workflow Source: https://grid82.com/docs/builder-api/workflows/create `POST https://studio.grid82.app/v1/apps/{appId}/workflows` Saves version 1 of a new workflow from its source. It starts as a draft: nothing runs until a person activates a version. The answer is the compiler's verdict on this version: `ok`, every error with its line and the fix, warnings, the graph the Automations tab draws and what the workflow can touch. A version that does not compile is kept and cannot be activated: fix the errors and save again. Requires the scopes `workflows:write`, `records:read`, `records:write`, `schema:read`. Operation `workflows.create`. MCP tool `workflows_create`. CLI: `grid82 workflows create` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | ------------------------------------------- | -------- | ----------------------- | | `source` | string | yes | Up to 65536 characters. | | `note` | string | no | Up to 200 characters. | | `via` | "assistant" or "hand" or "api" or "restore" | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **201**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.version` (`integer`, required) - `data.at` (`string`, required) - `data.ok` (`boolean`, required) - `data.errors` (`object[]`, required) - `data.errors.line` (`integer`, required) - `data.errors.message` (`string`, required) - `data.warnings` (`object[]`, required) - `data.warnings.line` (`integer`, required) - `data.warnings.message` (`string`, required) - `data.graph` (`Record | null`) - `data.impact` (`Record | null`) ```json { "data": { "id": "", "version": 123, "at": "", "ok": true, "errors": [ { "line": 123, "message": "" } ], "warnings": [ { "line": 123, "message": "" } ], "graph": {}, "impact": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//workflows' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "source": "" }' ``` --- # Read a workflow Source: https://grid82.com/docs/builder-api/workflows/get `GET https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}` The workflow with one version in full (the latest unless `version` names another): its source, the compiler's verdict, the graph, the notes, and the list of every version. Requires the `workflows:read` scope. Operation `workflows.get`. MCP tool `workflows_get`. CLI: `grid82 workflows get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `version` | integer | no | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) - `data.mode` (`enum`, required, one of: `locked, personal`) - `data.owner` (`string`, required) - `data.status` (`enum`, required, one of: `draft, active, paused`) - `data.latestVersion` (`integer`, required) - `data.activeVersion` (`integer | null`, required) - `data.trigger` (`Record | null`, required) - `data.updatedAt` (`string`, required) - `data.lastRun` (`object | null`, required) - `data.lastRun.id` (`string`, required) - `data.lastRun.status` (`string`, required) - `data.lastRun.startedAt` (`string`, required) - `data.lastRun.error` (`string,null`, required) - `data.version` (`object | null`, required) - `data.version.version` (`integer`, required) - `data.version.source` (`string`, required) - `data.version.note` (`string,null`, required) - `data.version.via` (`string,null`, required) - `data.version.author` (`string`, required) - `data.version.at` (`string`, required) - `data.version.ok` (`boolean`, required) - `data.version.errors` (`object[]`, required) - `data.version.errors.line` (`integer`, required) - `data.version.errors.message` (`string`, required) - `data.version.warnings` (`object[]`, required) - `data.version.warnings.line` (`integer`, required) - `data.version.warnings.message` (`string`, required) - `data.version.config` (`Record | null`, required) - `data.version.graph` (`Record | null`, required) - `data.version.manifest` (`object[]`, required) - `data.version.impact` (`Record | null`, required) - `data.version.notes` (`Record`, required) - `data.version.bundleHash` (`string,null`, required) - `data.versions` (`object[]`, required) - `data.versions.version` (`integer`, required) - `data.versions.note` (`string,null`, required) - `data.versions.via` (`string,null`, required) - `data.versions.author` (`string`, required) - `data.versions.at` (`string`, required) - `data.versions.ok` (`boolean`, required) - `data.versions.bundleHash` (`string,null`, required) - `data.versions.active` (`boolean`, required) ```json { "data": { "id": "", "name": "", "mode": "locked", "owner": "", "status": "draft", "latestVersion": 123, "activeVersion": 123, "trigger": {}, "updatedAt": "", "lastRun": { "id": "", "status": "", "startedAt": "", "error": null }, "version": { "version": 123, "source": "", "note": null, "via": null, "author": "", "at": "", "ok": true, "errors": [ { "line": 123, "message": "" } ], "warnings": [ { "line": 123, "message": "" } ], "config": {}, "graph": {}, "manifest": [ null ], "impact": {}, "notes": {}, "bundleHash": null }, "versions": [ { "version": 123, "note": null, "via": null, "author": "", "at": "", "ok": true, "bundleHash": null, "active": true } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//workflows/' \ --header 'Authorization: Bearer ' ``` --- # Rename a workflow Source: https://grid82.com/docs/builder-api/workflows/rename `PATCH https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}` Changes the name people see. The name is a label of the workflow, not part of its code: no version is saved, nothing is recompiled, and what runs does not change. The `name` in the code is only the name a new workflow starts with. Requires the `workflows:write` scope. Operation `workflows.rename`. MCP tool `workflows_rename`. CLI: `grid82 workflows rename` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------ | ------ | -------- | -------------------- | | `name` | string | yes | Up to 80 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.name` (`string`, required) ```json { "data": { "id": "", "name": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PATCH \ --url 'https://studio.grid82.app/v1/apps//workflows/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }' ``` --- # Delete a workflow Source: https://grid82.com/docs/builder-api/workflows/delete `DELETE https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}` The workflow, its versions and its run history. Records it changed stay as they are. Requires the `workflows:write` scope. Operation `workflows.delete`. MCP tool `workflows_delete`. CLI: `grid82 workflows delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) ```json { "data": { "id": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/apps//workflows/' \ --header 'Authorization: Bearer ' ``` --- # Save a new version of a workflow Source: https://grid82.com/docs/builder-api/workflows/code-set `PUT https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}/code` Never overwrites: every save is a version, and what runs does not change until a person activates one. Send `baseVersion` (the latest version when you read it) to be refused with 409 if somebody saved since. The answer is the compiler's verdict on this version: `ok`, every error with its line and the fix, warnings, the graph the Automations tab draws and what the workflow can touch. A version that does not compile is kept and cannot be activated: fix the errors and save again. Requires the scopes `workflows:write`, `records:read`, `records:write`, `schema:read`. Operation `workflows.code.set`. MCP tool `workflows_code_set`. CLI: `grid82 workflows code set` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------------- | ------------------------------------------- | -------- | ----------------------- | | `source` | string | yes | Up to 65536 characters. | | `note` | string | no | Up to 200 characters. | | `via` | "assistant" or "hand" or "api" or "restore" | no | | | `baseVersion` | integer | no | At least 0. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.version` (`integer`, required) - `data.at` (`string`, required) - `data.ok` (`boolean`, required) - `data.errors` (`object[]`, required) - `data.errors.line` (`integer`, required) - `data.errors.message` (`string`, required) - `data.warnings` (`object[]`, required) - `data.warnings.line` (`integer`, required) - `data.warnings.message` (`string`, required) - `data.graph` (`Record | null`) - `data.impact` (`Record | null`) ```json { "data": { "id": "", "version": 123, "at": "", "ok": true, "errors": [ { "line": 123, "message": "" } ], "warnings": [ { "line": 123, "message": "" } ], "graph": {}, "impact": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PUT \ --url 'https://studio.grid82.app/v1/apps//workflows//code' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "source": "" }' ``` --- # Check a workflow's code without saving it Source: https://grid82.com/docs/builder-api/workflows/check `POST https://studio.grid82.app/v1/apps/{appId}/workflows/check` Compiles the source exactly as a save would and answers with the same verdict, but saves nothing: no version, no bundle, no history. Use it whenever you are unsure what the compiler accepts, to try a call or a name, and to get the code right BEFORE the one save. Never save a version to find out. The answer is the compiler's verdict on this version: `ok`, every error with its line and the fix, warnings, the graph the Automations tab draws and what the workflow can touch. Nothing is kept. Requires the scopes `workflows:read`, `schema:read`. Operation `workflows.check`. MCP tool `workflows_check`. CLI: `grid82 workflows check` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | -------- | ------ | -------- | ----------------------- | | `source` | string | yes | Up to 65536 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.ok` (`boolean`, required) - `data.errors` (`object[]`, required) - `data.errors.line` (`integer`, required) - `data.errors.message` (`string`, required) - `data.warnings` (`object[]`, required) - `data.warnings.line` (`integer`, required) - `data.warnings.message` (`string`, required) - `data.graph` (`Record | null`) - `data.impact` (`Record | null`) ```json { "data": { "ok": true, "errors": [ { "line": 123, "message": "" } ], "warnings": [ { "line": 123, "message": "" } ], "graph": {}, "impact": {} } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//workflows/check' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "source": "" }' ``` --- # Test a version without changing anything Source: https://grid82.com/docs/builder-api/workflows/test `POST https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}/test` Runs a version now (the latest unless `version` says otherwise) against the real records: reads are real; record writes, POSTs and approvals are captured instead of done; waits are skipped. A workflow that starts from a record is tested on `recordId`, or on the newest record of its table you can see. The answer is the trace, step by step. AI steps run for real and spend AI credits. Requires the scopes `workflows:read`, `records:read`. Operation `workflows.test`. MCP tool `workflows_test`. CLI: `grid82 workflows test` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ----------- | ------- | -------- | --------------------- | | `version` | integer | no | At least 1. | | `stopAfter` | string | no | Up to 200 characters. | | `pins` | object | no | | | `input` | object | no | | | `recordId` | string | no | Up to 64 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.ok` (`boolean`, required) - `data.error` (`string,null`, required) - `data.ms` (`number`, required) - `data.output` (`object`, required) - `data.recordId` (`string`) - `data.steps` (`object[]`, required) - `data.steps.name` (`string`, required) - `data.steps.kind` (`string`, required) - `data.steps.status` (`string`, required) - `data.steps.ms` (`number`, required) - `data.steps.output` (`object`) - `data.steps.error` (`string`) - `data.steps.detail` (`string`) ```json { "data": { "ok": true, "error": null, "ms": 123.45, "output": null, "recordId": "", "steps": [ { "name": "", "kind": "", "status": "", "ms": 123.45, "output": null, "error": "", "detail": "" } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//workflows//test' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "version": 123, "stopAfter": "", "pins": {}, "input": {}, "recordId": "" }' ``` --- # Activate a version Source: https://grid82.com/docs/builder-api/workflows/activate `POST https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}/activate` Makes a version the one that runs (the latest unless `version` names another): its trigger is armed, and new runs use it. Runs already started finish on the version they started on. Refused for a version that does not compile. Requires the scopes `workflows:write`, `records:read`, `records:write`, `schema:read`. Operation `workflows.activate`. MCP tool `workflows_activate`. CLI: `grid82 workflows activate` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `version` | integer | no | At least 1. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.version` (`integer`, required) - `data.nextRunAt` (`string,null`, required) ```json { "data": { "id": "", "version": 123, "nextRunAt": null } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//workflows//activate' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "version": 123 }' ``` --- # Pause a workflow Source: https://grid82.com/docs/builder-api/workflows/pause `POST https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}/pause` Nothing new starts until a version is activated again. Runs already started carry on. Requires the `workflows:write` scope. Operation `workflows.pause`. MCP tool `workflows_pause`. CLI: `grid82 workflows pause` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.status` (`string`, required) ```json { "data": { "id": "", "status": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//workflows//pause' \ --header 'Authorization: Bearer ' ``` --- # Run a workflow now Source: https://grid82.com/docs/builder-api/workflows/run `POST https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}/run` Starts a real run of the active version, as if its trigger fired. For a manual trigger, pass `input` (a workflow that works on one record declares an input of type `"record"` and receives its id). `recordId` is for a workflow that starts from a record (`record.created`, `record.updated`, `record.deleted`): it runs on that record. Requires the scopes `workflows:write`, `records:read`, `records:write`, `schema:read`. Operation `workflows.run`. MCP tool `workflows_run`. CLI: `grid82 workflows run` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ---------- | ------ | -------- | -------------------- | | `input` | object | no | | | `recordId` | string | no | Up to 64 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.runId` (`string`, required) ```json { "data": { "runId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//workflows//run' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "input": {}, "recordId": "" }' ``` --- # A workflow's runs, newest first Source: https://grid82.com/docs/builder-api/workflows/runs-list `GET https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}/runs` Status, version, when it started and ended, how many records it changed, and the error of a run that failed. Requires the `workflows:read` scope. Operation `workflows.runs.list`. MCP tool `workflows_runs_list`. CLI: `grid82 workflows runs list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | ## Query parameters | Name | Type | Required | Description | | ------- | ------- | -------- | ------------- | | `limit` | integer | no | Default `50`. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.id` (`string`, required) - `data.version` (`integer`, required) - `data.status` (`string`, required) - `data.trigger` (`string,null`, required) - `data.startedAt` (`string`, required) - `data.endedAt` (`string,null`, required) - `data.error` (`string,null`, required) - `data.writes` (`integer`, required) ```json { "data": [ { "id": "", "version": 123, "status": "", "trigger": null, "startedAt": "", "endedAt": null, "error": null, "writes": 123 } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//workflows//runs' \ --header 'Authorization: Bearer ' ``` --- # Read one run, step by step Source: https://grid82.com/docs/builder-api/workflows/runs-get `GET https://studio.grid82.app/v1/apps/{appId}/workflow-runs/{runId}` Every step of the run in order with its status, its result or its error. Read the `workflow-runs` skill to diagnose a failure. Requires the `workflows:read` scope. Operation `workflows.runs.get`. MCP tool `workflows_runs_get`. CLI: `grid82 workflows runs get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `runId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.id` (`string`, required) - `data.version` (`integer`, required) - `data.status` (`string`, required) - `data.trigger` (`string,null`, required) - `data.startedAt` (`string`, required) - `data.endedAt` (`string,null`, required) - `data.error` (`string,null`, required) - `data.writes` (`integer`, required) - `data.workflowId` (`string`, required) - `data.output` (`object`, required) - `data.steps` (`object[]`, required) - `data.steps.name` (`string`, required) - `data.steps.status` (`string`, required) - `data.steps.startedAt` (`string,null`, required) - `data.steps.endedAt` (`string,null`, required) - `data.steps.output` (`object`, required) - `data.steps.error` (`string,null`, required) - `data.steps.detail` (`string,null`, required) ```json { "data": { "id": "", "version": 123, "status": "", "trigger": null, "startedAt": "", "endedAt": null, "error": null, "writes": 123, "workflowId": "", "output": null, "steps": [ { "name": "", "status": "", "startedAt": null, "endedAt": null, "output": null, "error": null, "detail": null } ] } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//workflow-runs/' \ --header 'Authorization: Bearer ' ``` --- # Stop a run Source: https://grid82.com/docs/builder-api/workflows/runs-cancel `POST https://studio.grid82.app/v1/apps/{appId}/workflow-runs/{runId}/cancel` The run stops where it is. What it already changed stays changed. Requires the `workflows:write` scope. Operation `workflows.runs.cancel`. MCP tool `workflows_runs_cancel`. CLI: `grid82 workflows runs cancel` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `runId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.runId` (`string`, required) ```json { "data": { "runId": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//workflow-runs//cancel' \ --header 'Authorization: Bearer ' ``` --- # Retry a run Source: https://grid82.com/docs/builder-api/workflows/runs-retry `POST https://studio.grid82.app/v1/apps/{appId}/workflow-runs/{runId}/retry` Only a run that failed or was stopped. It restarts at the step that did not finish: every step before it keeps its saved result and does not run again. `from` names that step; null means the run restarted from the beginning, where the steps before it run again, except record writes, which return the record they wrote the first time. Use it after fixing the data or the roles, not for a code error (save and activate a new version). Requires the scopes `workflows:write`, `records:read`, `records:write`, `schema:read`. Operation `workflows.runs.retry`. MCP tool `workflows_runs_retry`. CLI: `grid82 workflows runs retry` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------- | ------ | -------- | ----------- | | `appId` | string | yes | | | `runId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.runId` (`string`, required) - `data.from` (`string,null`, required) ```json { "data": { "runId": "", "from": null } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//workflow-runs//retry' \ --header 'Authorization: Bearer ' ``` --- # Get a workflow's webhook address and secret Source: https://grid82.com/docs/builder-api/workflows/webhook-get `GET https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}/webhook` For a workflow of the app whose trigger is `webhook()`. The secret is created on first read. A POST to the address starts a run when the workflow is active: signed (Standard Webhooks) or with the secret as a bearer token, JSON body of the declared inputs, 64 KB at most; it answers 202 with the run's id, or 400 if an input is missing or wrong. Requires the `workflows:write` scope. Operation `workflows.webhook.get`. MCP tool `workflows_webhook_get`. CLI: `grid82 workflows webhook get` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.url` (`string,null`, required) - `data.secret` (`string`, required) ```json { "data": { "url": null, "secret": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/apps//workflows//webhook' \ --header 'Authorization: Bearer ' ``` --- # Replace a workflow's webhook secret Source: https://grid82.com/docs/builder-api/workflows/webhook-rotate `POST https://studio.grid82.app/v1/apps/{appId}/workflows/{workflowId}/webhook/rotate` The old secret stops working at once. Give the new one to the sender. Requires the `workflows:write` scope. Operation `workflows.webhook.rotate`. MCP tool `workflows_webhook_rotate`. CLI: `grid82 workflows webhook rotate` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `appId` | string | yes | | | `workflowId` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.url` (`string,null`, required) - `data.secret` (`string`, required) ```json { "data": { "url": null, "secret": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request POST \ --url 'https://studio.grid82.app/v1/apps//workflows//webhook/rotate' \ --header 'Authorization: Bearer ' ``` --- # List a suite's secrets, by name Source: https://grid82.com/docs/builder-api/secrets/list `GET https://studio.grid82.app/v1/suites/{suiteId}/secrets` Names and dates only. A value is never returned to anybody, by any route. Requires the `members:read` scope. Operation `secrets.list`. MCP tool `secrets_list`. CLI: `grid82 secrets list` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object[]`, required) - `data.name` (`string`, required) - `data.updatedAt` (`string`, required) ```json { "data": [ { "name": "", "updatedAt": "" } ] } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request GET \ --url 'https://studio.grid82.app/v1/suites//secrets' \ --header 'Authorization: Bearer ' ``` --- # Set a secret Source: https://grid82.com/docs/builder-api/secrets/set `PUT https://studio.grid82.app/v1/suites/{suiteId}/secrets/{name}` Stores a value workflows of this suite can use by name (`"secret:NAME"`) without ever seeing it: a webhook address, an API key. `OPENROUTER_API_KEY` makes the suite's AI steps use its own OpenRouter key instead of credits. Names are UPPER\_SNAKE\_CASE. Requires the `members:write` scope. Operation `secrets.set`. MCP tool `secrets_set`. CLI: `grid82 secrets set` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | | `name` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | ## Request body | Field | Type | Required | Description | | ------- | ------ | -------- | ---------------------- | | `value` | string | yes | Up to 4096 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.name` (`string`, required) - `data.updatedAt` (`string`, required) ```json { "data": { "name": "", "updatedAt": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request PUT \ --url 'https://studio.grid82.app/v1/suites//secrets/' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "value": "" }' ``` --- # Delete a secret Source: https://grid82.com/docs/builder-api/secrets/delete `DELETE https://studio.grid82.app/v1/suites/{suiteId}/secrets/{name}` Workflows that use it fail on that step with a sentence naming it. Requires the `members:write` scope. Operation `secrets.delete`. MCP tool `secrets_delete`. CLI: `grid82 secrets delete` (`npx grid82`). ## Path parameters | Name | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `suiteId` | string | yes | | | `name` | string | yes | | ## Headers | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Idempotency-Key` | string | no | Any unique string. Sending the same key with the same request again returns the first response (marked `Idempotency-Replayed: true`) instead of running it twice. Reusing it for a different request is refused with 409. Kept for 24 hours. Up to 255 characters. | **Authorizations** - `Authorization` (bearer, in header, required) ## Response **200**: Success - `data` (`object`, required) - `data.name` (`string`, required) ```json { "data": { "name": "" } } ``` **4XX**: Refused - `error` (`object`, required) - `error.code` (`enum`, required, one of: `not_found, forbidden, unauthenticated, validation_failed, conflict, rate_limited, payment_required, not_implemented, internal`) - `error.message` (`string`, required) - `error.details` (`Record`) ```json { "error": { "code": "not_found", "message": "", "details": {} } } ``` **Example request** ```bash curl --request DELETE \ --url 'https://studio.grid82.app/v1/suites//secrets/' \ --header 'Authorization: Bearer ' ``` --- # The Grid82 CLI Source: https://grid82.com/docs/cli/overview ## Install Node.js 22.12 or later. Run it without installing, or install it once: ```bash npx grid82 --version npm install -g grid82 ``` The binary is `grid82`. ## Commands Every operation of both APIs is a command, named after it: `records.list` is `grid82 records list`. Path parameters come first, in order; everything else is a flag, or `--body` with the whole request as JSON. A new endpoint is a new command the day it ships, with its help text already correct. ```bash grid82 records list tbl_8fj3kq2wmv5d --view-id viw_e4ks9mz2bh7w grid82 records get tbl_8fj3kq2wmv5d rec_4nqw8e2hzk7s grid82 records create tbl_8fj3kq2wmv5d --records '[{"Name":"New record","Amount":1000}]' grid82 files upload tbl_8fj3kq2wmv5d --file ./contract.pdf --record-id rec_4nqw8e2hzk7s grid82 rules simulate app_5mg8rw2kx4tn --roles rol_v6nh3ta8rk2e --table-id tbl_8fj3kq2wmv5d --op read ``` Each page of the [App API](/docs/app-api/records/list) and [Builder API](/docs/builder-api/apps/list) references names its command. ## Signing in Three ways, one per situation: ```bash grid82 auth login # the Studio, in your browser grid82 auth login --target https://your-suite.grid82.app/your-app # one app grid82 auth login --device # a machine with no browser ``` In CI, skip the login: set `GRID82_TOKEN` to one of your keys and `GRID82_URL` to the Studio or the app it is for. On a desktop, tokens are kept in the system keychain. ## Made for scripts and agents - **Output is JSON whenever it is not going to a terminal**, so nobody has to remember `--json`. - Cursor-paginated JSON commands return `{ data, meta }`. Pass `meta.nextCursor` as `--cursor` until `meta.hasMore` is false. Table output shows the next cursor; NDJSON outputs rows only. - **The exit code says what kind of failure it was**: 2 usage, 3 authentication, 4 not found, 5 rate limited, 6 conflict, 7 plan. The API's error envelope goes to stderr. - **A document comes from a file**: `--source @relance.ts` sends a workflow's code from disk and `--body @body.json` a whole request body (`@-` reads stdin). - **Deleting asks for the name**, as the Studio does: `grid82 tables delete app_5mg8rw2kx4tn tbl_x2c7nw4hq9fk --confirm "Old deals"`. - `grid82 mcp` bridges a local stdio client to the MCP server, and `grid82 doctor` checks the connection, the credential, the version and the clock. ## More than MCP, by design Uploading files is possible from the CLI and not from MCP, because a terminal can send bytes and a model cannot. Everything else is at parity by construction. --- # Changelog Source: https://grid82.com/docs/changelog ## 2026-10-22: Check a workflow without saving it `workflows.check` (and `my.workflows.check` for your own workflows) compiles a workflow's source exactly like a save and returns the same answer, errors with their fix, warnings and the graph, without creating a version. Use it to find out what the compiler accepts, then save once. The compiler also says what a workflow does not have. There is no email or notification step: every spelling of `notify` or `email` is refused with the options that exist (`http.post` to a chat or automation address kept as a suite secret, a record in a table people watch, or `approval.ask`). A secret can only be used as a whole address, never inside a header or a body. A name your code uses but did not list as received is now an error at compile time, not a `ReferenceError` at run time. ## 2026-10-22: Apps have an icon `apps.update` accepts `icon`, a name from the table icon library, or `null` for the default. `apps.list` returns it. The Studio shows it in the sidebar, in lists and next to the app's name. ## 2026-10-15: One MCP server per suite Every app of a suite is reached through the suite's own server, `https://{suite}.grid82.app/mcp`, and you choose which apps an agent may reach when you approve the connection. The per-app addresses (`/{app}/mcp` and `/{app}/mcp/code`) are retired: they answer `410` with the suite's address. Reconnect an agent that used one to the suite's address. Connected to Claude or ChatGPT, the suite's server can now open a table as an interactive grid, or an Interface page, inside the conversation (`show_table`, `show_page`). ## 2026-10-06: The developer documentation, rebuilt These pages were rebuilt around Grid82's two planes. The App API and Builder API references are now generated from the platform's own OpenAPI documents, one page per operation, and [Set up your agent](/docs/agents/setup) is generated from the same source as the setup prompt in the product, so the two cannot disagree. For agents: every page is available as Markdown (append `.md`), the whole site as [llms.txt](/docs/llms.txt) and [llms-full.txt](/docs/llms-full.txt), and the documentation has its own MCP server at `https://grid82.com/docs/mcp`. ## 2026-10-06: A new name: Grid82 The product is now called grid82. New API keys start with `g82_`; keys issued before the rename start with `g82_` and keep working unchanged. The contract version header is `x-grid82-api-version`. ## 2026-10-05: Connect an agent with one prompt **Settings, Connections** in an app and the Studio's **Developer** page now lead with **Copy setup prompt**: paste it into your agent and it connects itself. Each suite serves the guide the agent follows at `/agent-setup.md`. A suite's MCP server also accepts clients that send an OAuth `resource`, which some were refused for before.