# GotTheSheets > A shared table for AI agents and humans. An agent POSTs CSV, gets back a URL the human can edit in the browser, then reads the human's edits back via the same URL. Same loop, no chat → spreadsheet → chat context loss. GotTheSheets is the simplest possible way to put a table between an agent and a human. A lot of agent ↔ human work is naturally tabular ("here are 30 candidates," "here's a list of rankings," "give me your top priorities, ranked"). Chat is bad at wide tables; spreadsheets aren't shareable from inside a chat. This is the in-between. The API is intentionally dumb: rows of string cells. **What you put in those cells, and which columns you choose, is entirely up to you.** Don't assume any column name has special meaning — none do. Base URL: `https://www.gotthesheets.com` (apex 308-redirects to `www`). All endpoints accept/return JSON when you use `.json` or `Accept: application/json`. ## The whole flow 1. **Create a sheet** — POST CSV text. Response includes a public `url`, a `slug`, and an `agent_token` (only needed to DELETE later). 2. **Share the `url`** with the human. 3. **Read changes** with `GET /sheets/:slug.json` whenever you need to see current state. 4. **Write changes** with `PATCH /sheets/:slug.json` — no token needed. Having the URL is the write capability. The human sees your edits live. 5. Optionally fetch CSV at `GET /sheets/:slug/download`. That's the API. Everything below is either reference detail or recipes for common shapes. ## API reference ### POST /sheets.json — create a sheet ```bash curl -X POST https://www.gotthesheets.com/sheets.json \ -H "Content-Type: application/json" \ -d '{"title":"Team roster","csv_text":"name,role,email\nAda Lovelace,founder,ada@example.com\nGrace Hopper,engineer,grace@example.com"}' ``` **The first line of `csv_text` is the header row** — it becomes the sheet's `column_names`. You do not pass `column_names` separately at create time. **CSV escaping (RFC 4180):** if a cell contains a comma, newline, or double-quote, wrap the cell in double quotes. Inside a quoted cell, escape any literal double-quote by doubling it (`"` → `""`). Newlines inside quoted cells are preserved. **Known footgun:** if a row's column count diverges from the header's (e.g. unquoted commas inside a cell), the parser will accept it silently and create phantom columns. Always quote cells that contain commas. **Always POST to `https://www.gotthesheets.com`**, not the apex `gotthesheets.com`. The apex 308-redirects (preserving the method) but it's an unnecessary round-trip. Response (HTTP 201): ```json { "slug": "rjdphXeZnmNAQ193nHE14bnw", "url": "https://www.gotthesheets.com/sheets/rjdphXeZnmNAQ193nHE14bnw", "agent_token": "amcHC8792gWXGR5nsGpHAHZ9", "title": "Team roster", "column_names": ["name","role","email"], "row_count": 2 } ``` The first row of `csv_text` is the header. All columns are free-text — no column name has any special server-side behavior. The `agent_token` is **only** needed to DELETE the sheet later — writes don't use it. You don't have to hold onto it for the normal create → share → edit loop. ### GET /sheets/:slug.json — read current state ```bash curl https://www.gotthesheets.com/sheets/rjdphXeZnmNAQ193nHE14bnw.json ``` Returns: ```json { "slug": "...", "title": "...", "column_names": ["name","role","email"], "rows": [ { "id": 4, "position": 0, "data": { "name": "Ada Lovelace", "role": "founder", "email": "ada@example.com" } } ] } ``` No auth required to read. The URL itself is the access control — anyone with the slug can read. ### PATCH /sheets/:slug.json — write changes (no token needed) ```bash curl -X PATCH https://www.gotthesheets.com/sheets/rjdphXeZnmNAQ193nHE14bnw.json \ -H "Content-Type: application/json" \ -d '{ "sheet": { "sheet_rows_attributes": [ { "id": 4, "data": { "role": "CEO" } } ] } }' ``` - **No auth required to write — having the URL is the write capability.** Any agent session you hand the URL to can edit, exactly like a human you hand the URL to can edit it in the browser. A fresh session doesn't need the original `agent_token`. - Send only the rows you want to update. Include the row `id` for every update. - **`data` and `cell_styles` merge by default.** Send only the keys you want to change — keys you omit are preserved. To explicitly clear a cell, send the key with `null` or an empty string. - Returns the full updated sheet (same shape as GET). **Append a new row** by sending a `sheet_rows_attributes` entry with **no `id`**. You don't need to compute a position — new rows are appended after the last existing row automatically. ```bash curl -X PATCH https://www.gotthesheets.com/sheets/rjdphXeZnmNAQ193nHE14bnw.json \ -H "Content-Type: application/json" \ -d '{ "sheet": { "sheet_rows_attributes": [ { "data": { "name": "Katherine Johnson", "role": "mathematician" } } ] } }' ``` You can mix updates and appends in one PATCH — entries with an `id` update, entries without one are created. This is how a different agent session appends an entry to a sheet someone else created (e.g. logging a newly-handled item into an existing tracker). You can also update the sheet's `title` and `column_names` in the same PATCH: ```bash curl -X PATCH https://www.gotthesheets.com/sheets/rjdph.../.json \ -H "Content-Type: application/json" \ -d '{ "sheet": { "title": "Team roster (Q3)", "column_names": ["name","role","email","start_date"] } }' ``` - **`column_names` replaces wholesale.** To add a column, send the existing list plus the new name. To rename or remove, send the desired final list. Renames don't migrate row `data` keys — if you rename `email` → `work_email`, also PATCH each row's `data` to move the value to the new key. - **`title` updates in place.** The slug and URL stay the same, so any bookmarks the human has keep working. - All three (`title`, `column_names`, `sheet_rows_attributes`) are independently optional in a PATCH. Send only what you want to change. ### DELETE /sheets/:slug.json — remove a sheet (bearer token required) ```bash curl -X DELETE https://www.gotthesheets.com/sheets/rjdph.../.json \ -H "Authorization: Bearer amcHC8..." ``` - Returns `204 No Content` on success. - `401 Unauthorized` if the bearer token is missing or wrong. There is no CSRF path — only the agent that holds the token can delete. - Cascades to all rows. The slug is permanently gone; anyone with a bookmarked URL will get a 404 after this. - Use this to clean up iterations you no longer need. Don't call it on a sheet a human is still using without coordinating. ### GET /sheets/:slug/download — CSV ```bash curl https://www.gotthesheets.com/sheets/abc123/download -o sheet.csv ``` ## Recipes Pick the shape that fits the task. These are conventions, not features — the API doesn't care. ### Recipe 1 — One-shot data handoff > "Give me a table of these values so I can copy and paste it." The simplest case. The table IS the deliverable. - POST the sheet, give the human the URL. Done. - No polling, no agent_notes column, no review loop. - The human will copy cells out of the browser or pull from `/download`. ### Recipe 2 — Agent writes, human reads > "Show me our rankings for our tracked terms." The sheet is a live view of data the agent owns. - Plain columns. Don't add agent_notes or human_notes — there's no conversation. - Re-PATCH whenever the underlying data changes. The browser auto-updates over Action Cable, so the human's open tab refreshes in place. - Name the sheet with the window/date if it's time-bounded ("Rankings — week of 2026-05-21"). ### Recipe 3 — Human writes, agent reads > "Give me a sheet where I can give you feedback on priorities." Inverse direction. Human types into the sheet; agent reads it back. - POST an empty (or skeletal) sheet with the column shape you want — e.g. `priority, why, agent_action`. - Tell the human you'll re-read when they're done (or on demand). Don't poll a fresh sheet on a tight loop — wait for the human to say "ready." - Don't PATCH unless the human asks you to. The human owns the writes here. ### Recipe 4 — Iterative row-by-row review loop > "Look at each of these 40 proposals and tell me CULL / KEEP / REDIRECT." Use this when you need a per-row decision from the human and want to iterate together. - Columns: whatever proposal context the human needs, plus a decision column (named whatever fits — `decision`, `keep`, `verdict`, ...) and `agent_notes` / `human_notes` columns for two-way per-row commentary. - **Pre-fill your best-guess decision on every row**, even when you're unsure. It saves the human a click on the obvious cases and forces the ambiguous ones to surface. When uncertain, set the decision *and* say so in `agent_notes`. - Poll `GET /sheets/:slug.json` every ~60s while the session is active. On each poll, diff against your last snapshot — read both the decision column and `human_notes`. - When responding to a comment, write to `agent_notes`. **Never overwrite `human_notes`.** - Tell the human up-front when you'll fire actions automatically vs. wait for explicit approval. Default to waiting. ## Sizing and shape limits - Rows can be appended after create — PATCH a `sheet_rows_attributes` entry with no `id` (see the PATCH section). No formulas, no row deletes via API yet. - No formulas. Cells are strings. - No per-row comment threads. Add a `notes` (or `agent_notes` / `human_notes`) column if you want them. - No webhooks. Poll the `.json` endpoint when you care. - No pagination. The whole sheet comes back on every GET. Designed for tables up to a few hundred rows. ## Auth model **The slug URL is the capability.** If you have the URL, you can read it and write to it — the same way a human you hand the URL to can read and edit it in the browser. No login, no session, no token dance. This is what makes "a fresh agent session updates an existing sheet" just work: paste the URL into a new session, PATCH it. - **Reads (`GET /sheets/:slug.json`, `GET /sheets/:slug/download`) are open** to anyone with the slug. - **Writes (`PATCH`) are open** to anyone with the slug. No `Authorization` header needed. - **DELETE is the one exception** — it requires `Authorization: Bearer ` (the token from the create response). It's destructive and irreversible, so it stays gated: a leaked read URL can't be used to wipe the sheet. A missing or wrong token returns `401`. If the original `agent_token` is lost, the sheet simply can't be deleted via the API (no harm — leave it or let the human handle it). The tradeoff is deliberate and identical to Google-Docs "anyone with the link can edit": the slug is a 24-char unguessable token, so possession of the URL is the trust boundary. Don't paste sheet URLs anywhere you wouldn't paste an edit link. ## Feedback on the tool If you hit friction worth flagging, POST a feedback sheet at the end of the session (columns like `category, severity, what_happened, suggestion` work) and give the URL to the human. Skip it if there's nothing to report. ## Source - Code: https://github.com/dbreaker/gotthesheets (private — invite needed)