{"openapi":"3.0.3","info":{"title":"AppStruct Slides API","version":"1.0.0","description":"\nProgrammatic read and write access to your **AppStruct Slides** presentations —\nlist them, read what is on every slide, build a deck from an outline, and edit\nthe text on slides that already exist.\n\n### Authentication\nEvery request carries an API key, created from your AppStruct dashboard. Send it\nas either header:\n\n```\nAuthorization: Bearer ak_live_xxxxxxxxxxxxxxxxxxxxxxxx\n# or\nx-api-key: ak_live_xxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nKeys are shown **once**, at creation. A key reaches the presentations its own\naccount owns and nothing else — decks shared *with* that account, and anything\nopened by a share link, are not visible through the API.\n\n### Scopes\n- `*`\n- `slides:read`\n- `slides:write`\n\n### AI assistants (MCP)\nThe same key opens a **Model Context Protocol** server at `POST /v1/slides/mcp`,\nso Claude, Claude Code, Cursor or any other MCP client can read and write these\npresentations directly. It speaks Streamable HTTP and takes the same\n`Authorization: Bearer` header as the endpoints below.\n\nThe tools an assistant is offered are derived from the key's scopes — a\n`slides:read` key never sees the write tools at all — and every tool runs\nthrough the same service and the same limits as the REST calls documented here.\nCopy-paste configuration for each client is in the dashboard:\nhttps://appstruct.ai/dashboard/slides/api/connect\n\nBrowser clients are refused: the endpoint rejects any request carrying an\n`Origin` header, which is what the MCP specification requires against DNS\nrebinding, and real MCP clients are not browsers and send none.\n\n### Work from ids, not from names\nEvery call after the first takes an id, and every id you will need comes back\nfrom the call before it. List presentations to find the one you mean; read it to\nget its slide ids and the ids of the objects on each slide; then edit.\n\nThree ids and they do not mix. A **presentation id** is the path segment.\nA **slide id** is what `deleteObject`, `duplicateObject`,\n`updateSlideContent`, `updateSpeakerNotes` and `updateSlideProperties`\ntake — and it is the *same string* as a slide's `objectId`, which is why both\nspellings come back. An **element id** is what `insertText`, `deleteText`\nand `setText` take, and you rarely need one: every placeholder on a slide is\nreachable by name through `updateSlideContent`, and element ids exist for the\ntext boxes that have no name.\n\nReading a presentation is cheap and reading a stale id is not, so read before\nyou write. There is no version history here and no undo: text this API replaces\nor deletes is gone.\n\n### The text you read is the text you write\nA read gives every text object its content as an array of lines, one line per\nparagraph, and those lines are in exactly the form a write takes back. No list\nmarker is ever stored or returned — bullets belong to the layout, not to the\ntext — and a paragraph's outline level comes back as two spaces of indent per\nlevel. So `setText` of the lines a read just returned leaves the presentation\nbyte-for-byte unchanged, which is what makes \"read it, change one line, write it\nback\" safe.\n\nWriting goes the same way. A body is **one paragraph per line**, whether you\nsend a string with newlines or an array of strings. A leading `-`, `*`,\n`•`, `1.` or `1)` is a marker you typed and is stripped, so pasting a\nMarkdown list does the obvious thing; two spaces of indent make one outline\nlevel, up to eight. The exceptions are worth knowing: speaker **notes** are\nstored exactly as sent, with no grammar applied, and `insertText` addresses\nthe element's *stored* text — a **0-based** UTF-16 offset with paragraphs joined\nby newlines — which does not include the indent a read prints. A **newline in\ninsertText starts a new paragraph**, inheriting the style of the one it split.\nThere is no way to send formatting, and no way to send a deck: raw deck JSON\ncannot be uploaded as a create body, and `?view=full` is a read-only door.\n\n### Where text lands on a slide\nText is placed by NAME, not by geometry: `title`, `subtitle`, `body` and\n`columns[i]` are matched against the placeholders the layout defines.\n`body` is `columns[0]`, and sending both is refused. A field the layout has\nno slot for is refused too, naming the layout and the field — a `quote` layout\nhas no title, and quietly dropping the title you sent would be worse than a 400.\n\n| Layout | Name | Text you can write | Other placeholders |\n| ------ | ---- | ------------------ | ------------------ |\n| `title` | Title slide | `title`, `subtitle` | — |\n| `sectionHeader` | Section header | `title`, `subtitle` | — |\n| `titleOnly` | Title only | `title` | — |\n| `blank` | Blank | — | — |\n| `titleBody` | Title and body | `title`, `body` | — |\n| `twoColumn` | Two columns | `title`, `columns[0]`, `columns[1]` | — |\n| `comparison` | Comparison | `title`, `columns[0]`, `columns[1]`, `columns[2]`, `columns[3]` | — |\n| `agenda` | Agenda | `title`, `body` | — |\n| `appendix` | Appendix | `title`, `body` | — |\n| `captionImage` | Image and caption | `title`, `body` | image |\n| `timeline` | Timeline | `title`, `columns[0]`, `columns[1]`, `columns[2]`, `columns[3]` | — |\n| `chartLeft` | Chart and takeaway | `title`, `body` | chart |\n| `diagramFull` | Diagram | `title`, `body` | — |\n| `imageBleed` | Full-bleed image | `title`, `subtitle` | image |\n| `bigNumber` | Big number | `title`, `columns[0]`, `columns[1]` | — |\n| `quote` | Quote | `columns[0]`, `columns[1]` | — |\n| `statement` | Statement | `title` | — |\n\n`columns[0]` is the same slot as `body`, so a one-column layout can take\neither name. Read the numbered slots layout by layout: on `comparison`, `columns[0]` and\n`columns[1]` are the two headings and `columns[2]` and `columns[3]` the\ntext under them; on `bigNumber`, `columns[0]` is the number and\n`columns[1]` is what it means; on `quote`, `columns[0]` is the quote and\n`columns[1]` the attribution; on `timeline`, the four columns are the four\nsteps. Picture, chart and table placeholders stay empty — there is no way to\nfill them through the API yet.\n\nGoogle's `predefinedLayout` names are accepted anywhere a layout key is:\n\n| Google name | AppStruct layout |\n| ----------- | ---------------- |\n| `BLANK` | `blank` |\n| `TITLE` | `title` |\n| `TITLE_AND_BODY` | `titleBody` |\n| `TITLE_AND_TWO_COLUMNS` | `twoColumn` |\n| `TITLE_ONLY` | `titleOnly` |\n| `SECTION_HEADER` | `sectionHeader` |\n| `SECTION_TITLE_AND_DESCRIPTION` | `sectionHeader` |\n| `ONE_COLUMN_TEXT` | `titleBody` |\n| `MAIN_POINT` | `statement` |\n| `BIG_NUMBER` | `bigNumber` |\n| `CAPTION_ONLY` | `captionImage` |\n\n### Editing\n`:batchUpdate` applies a list of edits **in order**, in one save. Each edit\nsees the presentation as the edits before it left it, so a `createSlide`\nfollowed by an `updateSlideContent` naming the id from its reply is not a\nlegal batch — you do not have the id yet. Put the content on the\n`createSlide` instead; that is what its `title`, `body` and `columns`\nfields are for.\n\nA batch either applies whole or changes nothing. An edit the server cannot\nunderstand is refused with the position of the request that was wrong\n(`Request 3: …`), and nothing before it is written. Replies line up with\nrequests one for one and use Google's envelope: `{\"createSlide\": {\"objectId\":\n\"…\"}}`, `{\"duplicateObject\": {\"objectId\": \"…\"}}`,\n`{\"replaceAllText\": {\"occurrencesChanged\": 2}}`, and `{}` for everything\nelse.\n\nFields this API cannot honour are refused by name rather than ignored —\n`createSlide.objectId`, `placeholderIdMappings`,\n`slideLayoutReference.layoutId`, `cellLocation`,\n`containsText.searchByRegex`, a `textRange` narrower than `ALL`. A 200\nthat quietly did something else is the failure this API is built to avoid.\n\n### Limits\n- **300 presentations per account** and **400 slides\n  per presentation**. Reaching either answers `400`.\n- **100 edits** per `:batchUpdate` call, and at most\n  **2,000,000 characters** of text in\n  one request body.\n- **20,000 characters** per text field and\n  per line, **200 characters** for a title or subtitle, and\n  **1,000 characters** for a search\n  string.\n- **12 MiB per presentation**, counting the pictures and charts stored inside it.\n  A write that would exceed it answers `413`.\n- List endpoints take `?limit=` (1–200, default\n  50) and `?offset=`, and return `{ data, meta }`.\n- **120 requests per minute per key**, plus a coarser\n  **600 per minute per IP**. Both answer `429` with\n  `Retry-After`; every response carries `X-RateLimit-Limit`,\n  `X-RateLimit-Remaining` and `X-RateLimit-Reset` (seconds), and the per-IP\n  tier reports the same three suffixed `-ip`.\n- **100,000 requests per rolling\n  24 hours per account**, shared across every key. Each other AppStruct API\n  meters its own separate allowance, so traffic there does not spend this one.\n  Reported in `X-AppStruct-RateLimit-Daily` and\n  `X-AppStruct-RateLimit-Daily-Remaining`.\n- **25 live keys per account**. Revoke one you no longer\n  use before minting another.\n\n### Errors\nStandard HTTP status codes with a JSON body. Switch on `statusCode`, not on\nthe human-readable `message`.\n\n| Status | Meaning |\n| ------ | ------- |\n| 400 | The request, or one edit in it, could not be understood |\n| 401 | Missing, invalid, expired or revoked API key |\n| 403 | The key lacks the required scope |\n| 404 | No such presentation on this account, or no such slide on it |\n| 409 | The presentation is in the trash, or it changed since the revision you read |\n| 413 | The saved presentation would exceed its size limit |\n| 429 | A rate limit or the daily budget was exceeded |\n\n### Concurrent editing\nWrites to one presentation are serialised on the server, and\n`writeControl.requiredRevisionId` lets two API callers refuse to overwrite\neach other: read a presentation, compute your batch, send the `revisionId` you\nread, and a `409` tells you somebody got there first.\n\nIt does **not** protect you from the owner's editor. A presentation open in an\neditor tab is saved whole from that tab on its next edit or reconnect, and that\nsave will overwrite changes made here. A library change made offline — a rename,\na star, a move to the trash or into a folder — is re-applied over an API rename\non the next sync. And changes made here appear in an open editor only after it\nis reloaded.\n\nSo: prefer writing to presentations nobody is editing. If somebody is, ask them\nto close it before you write and to reopen it afterwards.\n\n### Not available yet\nThumbnails, PDF and PPTX export — there is no renderer on the server. Inserting\na picture by URL, or any picture at all. Shares and comments, including\npresentations shared *with* you. Anything about geometry, colour, fonts or\nanimation. Uploading a deck as raw JSON. All of these are absent rather than\npartial: nothing in this version silently half-does one of them.\n"},"servers":[{"url":"/","description":"Current host"}],"tags":[{"name":"Presentations","description":"Finding presentations, reading what is on their slides, and creating new ones"},{"name":"Editing","description":"Changing a presentation that already exists — slides, text and speaker notes"}],"components":{"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","bearerFormat":"API key","description":"Your Slides API key, e.g. ak_live_… (also accepted in the x-api-key header)."},"ApiKeyHeader":{"type":"apiKey","in":"header","name":"x-api-key","description":"Your Slides API key, as an alternative to Authorization: Bearer."}},"schemas":{"Error":{"type":"object","properties":{"statusCode":{"type":"integer","example":403},"error":{"type":"string","example":"FORBIDDEN"},"message":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}}},"PresentationSummary":{"type":"object","description":"A presentation without its slides. This is what the listing carries — a page of 50 decks with their slides inlined would be hundreds of megabytes, because a deck stores its pictures and charts as inline data.","properties":{"presentationId":{"type":"string"},"title":{"type":"string"},"slideCount":{"type":"integer"},"starred":{"type":"boolean"},"trashed":{"type":"boolean","description":"In the trash. Still owned and still readable; every write to it is refused with 409."},"folderId":{"type":"string","nullable":true,"description":"The library folder the presentation sits in, or null at the root."},"updatedAt":{"type":"string","format":"date-time","description":"When the presentation was last SAVED, from the row itself rather than from anything a client wrote into the deck."}}},"PageMeta":{"type":"object","properties":{"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"},"hasMore":{"type":"boolean"}}},"PresentationList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PresentationSummary"}},"meta":{"$ref":"#/components/schemas/PageMeta"}}},"ElementDigest":{"type":"object","description":"One text-bearing object on a slide. Lines, arrows, groups and empty picture frames are left out — printing an empty `text` for four hundred of them is noise, not information.","properties":{"objectId":{"type":"string","description":"The id every edit addressing this element takes — `insertText`, `deleteText`, `setText`."},"type":{"type":"string","enum":["shape","table","chart","image","video","audio"]},"role":{"type":"string","description":"The placeholder role, when the element has one: `title`, `centeredTitle`, `subtitle`, `body`, `picture`, `chart`, `table`. An element with a role is reachable by NAME through `updateSlideContent`, and you never need its id."},"index":{"type":"integer","description":"Which slot of that role this is. `body` index 1 is `columns[1]`."},"name":{"type":"string"},"text":{"type":"array","items":{"type":"string"},"description":"The element's text, one string per paragraph, in exactly the form a write takes back: no list marker, and an outline level written as two spaces of indent per level. These lines can be handed straight to `setText`."}}},"SlideDigest":{"type":"object","properties":{"slideId":{"type":"string"},"objectId":{"type":"string","description":"Identical to `slideId`. Both spellings are returned because Google's requests call it `objectId` and a reader looking for a slide looks for a slide id."},"index":{"type":"integer","description":"0-based position in the deck."},"slideNumber":{"type":"integer","description":"1-based, the number a person sees."},"layout":{"type":"string","description":"The layout key this slide was created with, as stored — not guessed from what is on it."},"skipped":{"type":"boolean"},"title":{"type":"string","description":"The slide's title text, collapsed to one line for display. Write it back through `title`, not through this string."},"elements":{"type":"array","items":{"$ref":"#/components/schemas/ElementDigest"}},"notes":{"type":"string","description":"Speaker notes, when the slide has any."}}},"Presentation":{"type":"object","properties":{"presentationId":{"type":"string"},"title":{"type":"string"},"slideCount":{"type":"integer"},"starred":{"type":"boolean"},"trashed":{"type":"boolean"},"folderId":{"type":"string","nullable":true},"updatedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"size":{"type":"object","description":"The page size in points, and which preset it is.","properties":{"width":{"type":"number"},"height":{"type":"number"},"preset":{"type":"string","enum":["16:9","4:3","16:10","a4","custom"]}}},"revisionId":{"type":"string","description":"An opaque token that changes when, and only when, the content of the presentation changes. Read it, remember it, and send it back as `writeControl.requiredRevisionId` to have a write refused rather than applied on top of somebody else's. It is not a timestamp and carries no meaning beyond equality."},"slides":{"type":"array","items":{"$ref":"#/components/schemas/SlideDigest"}},"deck":{"type":"object","description":"The stored deck, verbatim, and only when you ask for `?view=full`. Large: it carries every picture and rasterised chart inline. Read it to archive a presentation; it cannot be sent back."}}},"ParagraphDigest":{"type":"object","properties":{"paragraphId":{"type":"string","description":"Stable across edits that reword the line, so a comment anchored to it survives."},"text":{"type":"string"},"level":{"type":"integer","description":"Outline depth, 1–8, absent at the top level. `text` in the element's `text` array carries this as leading indent; here it is printed as a number so you can discount it when counting characters for `insertText`."},"bullet":{"type":"string","description":"The bullet the layout gives this paragraph, when it has one."}}},"ElementResource":{"type":"object","properties":{"objectId":{"type":"string"},"type":{"type":"string"},"role":{"type":"string"},"index":{"type":"integer"},"name":{"type":"string"},"text":{"type":"array","items":{"type":"string"}},"frame":{"type":"object","description":"Where the element sits on the page, in points, with the rotation in degrees. Read-only in this version.","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number"},"height":{"type":"number"},"rotation":{"type":"number"}}},"paragraphs":{"type":"array","items":{"$ref":"#/components/schemas/ParagraphDigest"}},"altText":{"type":"string"}}},"Slide":{"type":"object","description":"One slide in full: everything the digest carries, plus each element's geometry, its paragraph ids and its alt text.","properties":{"slideId":{"type":"string"},"objectId":{"type":"string"},"index":{"type":"integer"},"slideNumber":{"type":"integer"},"layout":{"type":"string"},"skipped":{"type":"boolean"},"title":{"type":"string"},"elements":{"type":"array","items":{"$ref":"#/components/schemas/ElementResource"}},"notes":{"type":"string"}}},"SlideSpec":{"type":"object","description":"What goes on a slide, by NAME rather than by element id. The same shape is used by create, by `createSlide` and by `updateSlideContent`, so one description covers all three. Which names a layout accepts is in the table above; a field the layout has no slot for is refused rather than dropped.","properties":{"layout":{"type":"string","description":"A layout key, or one of Google's predefined layout names. Omitted means `titleBody` — or `title` for the first slide of a new presentation. (Google defaults to a blank slide; a blank slide has nowhere to put the title almost every caller sends.)"},"title":{"type":"string","maxLength":200},"subtitle":{"type":"string","maxLength":200},"body":{"oneOf":[{"type":"string","maxLength":20000},{"type":"array","maxItems":500,"items":{"type":"string","maxLength":20000}}],"description":"The main text, one paragraph per line. `body` is `columns[0]`; sending both is refused. A leading `-`, `*`, `•` or `1.` is stripped — bullets come from the layout — and two spaces of indent make an outline level."},"columns":{"type":"array","maxItems":4,"items":{"type":"array","maxItems":500,"items":{"type":"string","maxLength":20000}},"description":"One entry per body slot, for the layouts that have more than one. What each slot means depends on the layout — see the table above."},"notes":{"type":"string","maxLength":20000,"description":"Speaker notes. An empty string clears them. Notes are stored as you send them: the outline grammar above does not apply here."},"skipped":{"type":"boolean","description":"Skip this slide when the deck is presented."}}},"CreateSlideRequest":{"type":"object","description":"Add a slide, optionally with its text already written.","properties":{"layout":{"type":"string"},"slideLayoutReference":{"type":"object","description":"Google's spelling of `layout`. Only `predefinedLayout` is read; `layoutId` names a layout object this API does not expose and is refused.","properties":{"predefinedLayout":{"type":"string"}}},"insertionIndex":{"type":"integer","minimum":0,"description":"Where the new slide goes, 0 for first. Omitted appends."},"title":{"type":"string","maxLength":200},"subtitle":{"type":"string","maxLength":200},"body":{"oneOf":[{"type":"string","maxLength":20000},{"type":"array","maxItems":500,"items":{"type":"string","maxLength":20000}}]},"columns":{"type":"array","maxItems":4,"items":{"type":"array","maxItems":500,"items":{"type":"string","maxLength":20000}}},"notes":{"type":"string","maxLength":20000},"skipped":{"type":"boolean"}}},"DeleteObjectRequest":{"type":"object","required":["objectId"],"description":"Delete a slide, or one element on a slide. Deleting a group deletes what is inside it, and anything pointing at what went — an animation, a connector — goes with it. The last slide of a presentation cannot be deleted.","properties":{"objectId":{"type":"string"}}},"DuplicateObjectRequest":{"type":"object","required":["objectId"],"description":"Copy one slide, inserted immediately after the original. Every id inside the copy is new. Elements cannot be duplicated on their own in this version.","properties":{"objectId":{"type":"string"}}},"UpdateSlidesPositionRequest":{"type":"object","required":["slideObjectIds","insertionIndex"],"description":"Move slides. They keep their order relative to each other.","properties":{"slideObjectIds":{"type":"array","maxItems":400,"items":{"type":"string"},"description":"The slides to move. No duplicates."},"insertionIndex":{"type":"integer","minimum":0,"description":"Counted in the order BEFORE the move — 0 puts them first, the slide count puts them last. This is Google's rule and it is the only one a drag-and-drop target can produce."}}},"UpdateSlidePropertiesRequest":{"type":"object","required":["objectId","slideProperties"],"description":"Skip a slide, or stop skipping it.","properties":{"objectId":{"type":"string"},"slideProperties":{"type":"object","required":["isSkipped"],"properties":{"isSkipped":{"type":"boolean"}}},"fields":{"type":"string","description":"Google's field mask. `isSkipped` or `*`; anything else is refused rather than silently ignored, because a mask naming a property this API cannot change is a caller expecting something to happen."}}},"InsertTextRequest":{"type":"object","required":["objectId","text"],"description":"Insert a run of text into one element.","properties":{"objectId":{"type":"string"},"text":{"type":"string","maxLength":20000,"description":"The text to insert. A newline in `insertText` starts a new paragraph, which inherits the style and outline level of the paragraph it was split out of."},"insertionIndex":{"type":"integer","minimum":0,"description":"Where to insert, as a 0-based offset into the element's stored text with its paragraphs joined by newlines. Omit to append. An offset past the end is refused rather than clamped. Note that the stored text does not include the outline indent that a read returns, so discount `paragraphs[].level` when you count."}}},"DeleteTextRequest":{"type":"object","required":["objectId"],"description":"Empty one element's text. The element stays where it is, with its first paragraph id intact, so it looks the way the layout says when text is typed into it again.","properties":{"objectId":{"type":"string"},"textRange":{"type":"object","description":"Only `{\"type\": \"ALL\"}` is supported. A narrower range would need an index space this version does not expose for deletes.","properties":{"type":{"type":"string","enum":["ALL"]}}}}},"SetTextRequest":{"type":"object","required":["objectId"],"description":"Replace one element's text outright. An AppStruct extension, and the one edit that takes an element id on purpose: it is how you write a text box that has no placeholder role. Paragraph ids and run styling survive line by line, so restating a body with one line changed keeps every anchor on the others.","properties":{"objectId":{"type":"string"},"text":{"type":"string","maxLength":20000},"lines":{"type":"array","maxItems":500,"items":{"type":"string","maxLength":20000},"description":"Send exactly one of `text` or `lines`."}}},"ReplaceAllTextRequest":{"type":"object","required":["containsText","replaceText"],"description":"Find and replace across the presentation — shapes, table cells and speaker notes. Layouts and masters are never touched, so replacing \"2024\" does not rewrite the prompt on every empty placeholder in the deck.","properties":{"containsText":{"oneOf":[{"type":"string","maxLength":1000},{"type":"object","properties":{"text":{"type":"string","maxLength":1000},"matchCase":{"type":"boolean"}}}],"description":"The text to look for, as a plain substring. Google's object form is accepted as well as the plain string."},"replaceText":{"type":"string","maxLength":20000,"description":"What to put in its place. Required: an empty string deletes every match, but only when you send one — an omitted `replaceText` is refused rather than treated as a deletion."},"matchCase":{"type":"boolean","default":false,"description":"Whether the search is case-sensitive. Defaults to false."},"pageObjectIds":{"type":"array","maxItems":400,"items":{"type":"string"},"description":"Restrict the replacement to these slides. Omitted means every slide."}}},"UpdateSpeakerNotesRequest":{"type":"object","required":["objectId","text"],"description":"Replace a slide's speaker notes. An AppStruct extension. An empty string removes them entirely.","properties":{"objectId":{"type":"string","description":"The SLIDE id, not an element id."},"text":{"type":"string","maxLength":20000}}},"UpdateTitleRequest":{"type":"object","required":["title"],"description":"Rename the presentation.","properties":{"title":{"type":"string","maxLength":200,"description":"The new name in the library. Nothing is written onto a slide."}}},"UpdateSlideContentRequest":{"type":"object","required":["objectId"],"description":"Rewrite the named slots of a slide that already exists — an AppStruct extension, and the edit to reach for first. An omitted field is left alone; `\"\"` or `[]` clears that slot. The slide keeps its layout, its paragraph ids and its run styling.","properties":{"objectId":{"type":"string","description":"The SLIDE id."},"title":{"type":"string","maxLength":200},"subtitle":{"type":"string","maxLength":200},"body":{"oneOf":[{"type":"string","maxLength":20000},{"type":"array","maxItems":500,"items":{"type":"string","maxLength":20000}}]},"columns":{"type":"array","maxItems":4,"items":{"type":"array","maxItems":500,"items":{"type":"string","maxLength":20000}}},"notes":{"type":"string","maxLength":20000}}},"BatchUpdateRequest":{"type":"object","minProperties":1,"maxProperties":1,"description":"One edit. Exactly one field is set; a request object naming two kinds of edit is rejected rather than guessed at.","properties":{"createSlide":{"$ref":"#/components/schemas/CreateSlideRequest"},"deleteObject":{"$ref":"#/components/schemas/DeleteObjectRequest"},"duplicateObject":{"$ref":"#/components/schemas/DuplicateObjectRequest"},"insertText":{"$ref":"#/components/schemas/InsertTextRequest"},"deleteText":{"$ref":"#/components/schemas/DeleteTextRequest"},"setText":{"$ref":"#/components/schemas/SetTextRequest"},"replaceAllText":{"$ref":"#/components/schemas/ReplaceAllTextRequest"},"updateSlideContent":{"$ref":"#/components/schemas/UpdateSlideContentRequest"},"updateSlideProperties":{"$ref":"#/components/schemas/UpdateSlidePropertiesRequest"},"updateSlidesPosition":{"$ref":"#/components/schemas/UpdateSlidesPositionRequest"},"updateSpeakerNotes":{"$ref":"#/components/schemas/UpdateSpeakerNotesRequest"},"updateTitle":{"$ref":"#/components/schemas/UpdateTitleRequest"}}},"WriteControl":{"type":"object","description":"Refuse the whole batch if the presentation has changed since you read it.","properties":{"requiredRevisionId":{"type":"string","description":"The `revisionId` from the read you computed this batch against. A mismatch answers 409 and writes nothing."}}},"BatchUpdateReply":{"type":"object","description":"The reply to the request at the same position, in Google's envelope: the field is named after the edit that produced it. Edits with nothing to report answer with an empty object, so the two arrays always line up.","properties":{"createSlide":{"type":"object","properties":{"objectId":{"type":"string","description":"The id of the slide that was created."}}},"duplicateObject":{"type":"object","properties":{"objectId":{"type":"string","description":"The id of the copy."}}},"replaceAllText":{"type":"object","properties":{"occurrencesChanged":{"type":"integer","description":"How many matches were replaced. Zero is a success, not an error."}}}}},"BatchUpdateResponse":{"type":"object","properties":{"presentationId":{"type":"string"},"replies":{"type":"array","items":{"$ref":"#/components/schemas/BatchUpdateReply"}},"revisionId":{"type":"string","description":"The revision the presentation is at now. Send it as the next batch's `requiredRevisionId` to chain writes safely."}}}}},"paths":{"/v1/slides/presentations":{"get":{"operationId":"listPresentations","tags":["Presentations"],"summary":"List presentations","description":"Every presentation this account owns, most recently saved first. Slides are left out — fetch one presentation to read what is on it.\n\nRequires the `slides:read` scope.","security":[{"ApiKey":[]},{"ApiKeyHeader":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"How many presentations to return."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0},"description":"How many to skip, for paging."},{"name":"includeTrashed","in":"query","schema":{"type":"boolean","default":false},"description":"Include presentations in the trash. They are still owned and still readable; every write to one is refused."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresentationList"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found on this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit or daily budget exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createPresentation","tags":["Presentations"],"summary":"Create a presentation","description":"Make a new presentation, with its slides already written if you send them. Send `slides` as an outline — one entry per slide, each naming a layout and the text that goes in its named slots — and the whole deck exists after one call.\n\nRequires the `slides:write` scope.","security":[{"ApiKey":[]},{"ApiKeyHeader":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title"],"properties":{"title":{"type":"string","maxLength":200},"size":{"type":"string","enum":["16:9","4:3","16:10","a4"],"default":"16:9","description":"The page shape."},"slides":{"type":"array","maxItems":400,"items":{"$ref":"#/components/schemas/SlideSpec"},"description":"The slides to create. Omit for a presentation with one blank title slide."},"folderId":{"type":"string","maxLength":160,"description":"File the new presentation in this library folder. It must already exist. Omit to leave it at the root."}}},"example":{"title":"Q3 review","size":"16:9","slides":[{"layout":"title","title":"Q3 review","subtitle":"Growth, retention, and what we are cutting"},{"layout":"titleBody","title":"Where we landed","body":["Revenue up 18% quarter on quarter","  Driven almost entirely by renewals","Churn flat at 2.1%"],"notes":"Pause here — the renewals number is the one they will ask about."}]}}}},"responses":{"200":{"description":"The presentation as stored, with the id of every slide that was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Presentation"}}}},"400":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found on this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The presentation is in the trash, or it changed since the revisionId you sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The saved presentation would exceed its size limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit or daily budget exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/slides/presentations/{presentationId}":{"get":{"operationId":"getPresentation","tags":["Presentations"],"summary":"Retrieve a presentation","description":"One presentation, slide by slide: the layout, the title, the text on every object that has any, and the speaker notes. This is the read to make before any edit — it is where slide ids and element ids come from.\n\nRequires the `slides:read` scope.","security":[{"ApiKey":[]},{"ApiKeyHeader":[]}],"parameters":[{"name":"presentationId","in":"path","required":true,"schema":{"type":"string"},"description":"The presentation id, as returned by GET /v1/slides/presentations."},{"name":"view","in":"query","schema":{"type":"string","enum":["digest","full"],"default":"digest"},"description":"`digest` is the readable summary. `full` adds `deck`, the stored presentation verbatim — large, because pictures and charts are stored inline."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Presentation"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found on this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit or daily budget exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/slides/presentations/{presentationId}/slides/{slideId}":{"get":{"operationId":"getSlide","tags":["Presentations"],"summary":"Retrieve one slide","description":"One slide in full: every object on it with its id, its role, its geometry, its paragraph ids and its alt text. Read this when you need an element id, or when you want to know exactly where something sits.\n\nRequires the `slides:read` scope.","security":[{"ApiKey":[]},{"ApiKeyHeader":[]}],"parameters":[{"name":"presentationId","in":"path","required":true,"schema":{"type":"string"},"description":"The presentation id, as returned by GET /v1/slides/presentations."},{"name":"slideId","in":"path","required":true,"schema":{"type":"string"},"description":"The slide id, from the presentation's `slides[].slideId`."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Slide"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found on this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit or daily budget exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/slides/presentations/{presentationId}:batchUpdate":{"post":{"operationId":"batchUpdatePresentation","tags":["Editing"],"summary":"Edit a presentation","description":"Apply a list of edits — add, move, duplicate, skip or delete a slide, rewrite the text on one, replace a phrase everywhere, or rename the presentation. Edits run in the order you give them and are saved together or not at all.\n\nRequires the `slides:write` scope.","security":[{"ApiKey":[]},{"ApiKeyHeader":[]}],"parameters":[{"name":"presentationId","in":"path","required":true,"schema":{"type":"string"},"description":"The presentation id, as returned by GET /v1/slides/presentations."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["requests"],"properties":{"requests":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/BatchUpdateRequest"}},"writeControl":{"$ref":"#/components/schemas/WriteControl"}}},"example":{"requests":[{"replaceAllText":{"containsText":"DRAFT","replaceText":"Final"}},{"createSlide":{"layout":"sectionHeader","insertionIndex":2,"title":"What we are cutting"}},{"updateSlideContent":{"objectId":"b7f0c2a1-0e3d-4f1a-9c6b-2a1d3e4f5a6b","body":["Sunsetting the legacy importer","Freezing the on-prem agent"]}}],"writeControl":{"requiredRevisionId":"9f3a1c2e4b6d8a0f1e2c3b4d"}}}}},"responses":{"200":{"description":"Applied as ONE save. `replies` lines up with `requests`, one entry each, and `revisionId` is what the presentation is at now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchUpdateResponse"}}}},"400":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found on this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The presentation is in the trash, or it changed since the revisionId you sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The saved presentation would exceed its size limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit or daily budget exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"x-mcp-tools":[{"name":"list_presentations","title":"List the account’s presentations","description":"List the presentations this account owns, most recently edited first, with the\nid every other tool needs.\n\nStart here when the user names a deck in words (\"the Q3 board deck\", \"my\nonboarding slides\") and you need its id. If they described something WRITTEN ON\na slide — a figure, a customer name, a heading — use search_presentations\ninstead, which reads the text.\n\nReturns id, title, how many slides it has and when it was last edited, and\nnothing of the slides themselves: use get_presentation for that. Only\npresentations this account OWNS — anything shared with it, or opened from a\nshare link, is not reachable with an API key.","scopes":["slides:read"]},{"name":"get_presentation","title":"Read a presentation, slide by slide","description":"Read a presentation: its title, its size, and one block per slide giving the\nslide number, the slide id, the layout it was built from and the text in each\nof its slots.\n\nCall this before you change anything. Every write tool takes a slide id from\nhere, and set_slide_content REPLACES the slot it is given — so an edit made\nwithout reading first is a guess about which slot holds what.\n\nLong decks come back a page of slides at a time, never a slide cut in half: the\nreply ends by saying which slides it showed and which `start_slide` reads the\nnext ones. Each slide is summarised — at most eight lines of a slot, no element\nids — so use get_slide when you need every element on one slide, its ids, or its\nwritable slots.\n\nSpeaker notes are left out unless you ask for them, because they are often\nlonger than the slide.","scopes":["slides:read"]},{"name":"get_slide","title":"Read one slide in full","description":"Read everything on one slide: every element with its own object id, what kind\nit is, the placeholder role it fills, its text line by line, its alt text and\nthe slide’s speaker notes — plus the layout this slide was built from and the\ntext slots that layout has.\n\nThis is the call to make before rewriting a slide. set_slide_content writes by\nSLOT (title, subtitle, body, columns) and the slot table printed here says which\nof them this layout actually has — a field the layout has no slot for is refused,\nnot quietly dropped. set_element_text writes by OBJECT ID, which is the only way\nto change a text box that fills no placeholder role at all.\n\nThe lines returned are exactly the lines those two tools take: an outline level\nis two leading spaces, and there are no bullet characters — bullets come from the\nlayout, not from the text. Send a line back unchanged and nothing changes.\n\nOne exception, and it matters: a line too long for one reply is shown cut short\nwith an … at the end. Never send such a line back — writing it would replace the\nfull line with the shortened one, and there is no undo.\n\nPosition and size are left out unless you ask for them, because nothing in this\nserver can write them back.","scopes":["slides:read"]},{"name":"search_presentations","title":"Find presentations by what their slides say","description":"Find the account’s presentations whose TITLE or SLIDES contain a piece of text,\nand get back each one’s id, which slides matched and the words around the first\nmatch.\n\nUse it when the user describes content rather than a file: \"which deck mentions\nthe Riga office\", \"where did I put the pricing slide\", \"find the deck that quotes\n£4,200\". Matching is case-insensitive substring, so a shorter fragment finds\nmore. It is not a pattern: a regular expression is searched for literally.\n\nThe slide numbers beside a hit are where to look; get_presentation on that deck\ngives you their ids, which is what every write tool takes.\n\nOpens the 40 most recently edited presentations at most, and says so when it\nstopped early. Trashed presentations are never opened.","scopes":["slides:read"]},{"name":"create_presentation","title":"Create a presentation","description":"Create a new presentation, optionally with all of its slides already in it, and\nget back the id every other tool needs — plus the id and number of every slide\nit made, so a follow-up edit needs no read.\n\nUse it whenever the user asks for a new deck: \"put together a short pitch\", \"make\nslides from these notes\". Prefer creating a presentation over adding to an\nexisting one: slides appended to somebody’s deck are far harder for them to undo\nthan a deck they did not want is to delete.\n\nEach slide names a LAYOUT and fills its slots. Write the whole deck in one call\nwhen you can — it is one save, and it is the only way to get a deck whose slides\nwere laid out together rather than one at a time.\n\nText is plain text: one line per entry, two leading spaces per outline level, and\nno bullet characters — bullets come from the layout. A field the layout has no\nslot for is refused rather than dropped, so the deck you get is the deck you\nasked for.","scopes":["slides:write"]},{"name":"add_slide","title":"Add a slide to a presentation","description":"Add one slide to an existing presentation and get back its id.\n\nThe layout decides which slots the slide has — get_slide on a comparable slide\nprints the slot table, and a field the layout has no slot for is refused rather\nthan dropped. Omit `layout` for titleBody, which is a title and one body.\n\nBy default the slide is appended at the end. Pass `insertion_index` to put it\nsomewhere else: 0 makes it the first slide, and the index counts the deck as it\nis NOW, so adding two slides at index 0 leaves them in the reverse of the order\nyou sent them.\n\nOne line per entry, two leading spaces per outline level, no bullet characters.\nTo change a slide that already exists, use set_slide_content instead — this\nalways makes a new one.","scopes":["slides:write"]},{"name":"set_slide_content","title":"Rewrite the text of one slide","description":"Replace the text in one or more of a slide’s slots. The slide keeps its layout,\nits geometry and its element ids; only the words change.\n\nTHIS REPLACES WHAT WAS THERE AND THERE IS NO UNDO. Read the slide first —\nget_slide prints every slot this layout has and what is currently in it — and\nsend only the slots you mean to change: a slot you leave out is untouched.\n\nAn empty string, or an empty list, CLEARS that slot rather than leaving it — that\nis how \"take the subtitle off this slide\" is said.\n\nOne line per entry, two leading spaces per outline level, no bullet characters.\nA field the layout has no slot for is refused rather than dropped, so a slide\nnever half-changes.","scopes":["slides:write"]},{"name":"set_element_text","title":"Replace the text of one element","description":"Replace all of the text in ONE element, named by its object id, with the lines\nyou give.\n\nThis is the tool for a text box that fills no placeholder role — the ones\nset_slide_content cannot reach, because they belong to no slot of the layout.\nget_slide lists every element on a slide with its object id and what it\ncurrently says.\n\nTHIS REPLACES THE WHOLE ELEMENT’S TEXT AND THERE IS NO UNDO. Send the lines you\nwant it to end up with, not just the new ones.\n\nOne line per entry. Two leading spaces per outline level, and no bullet\ncharacters — bullets come from the layout, so typing \"- \" puts a literal dash on\nthe slide. Lines that came out of get_slide can be sent straight back — except a\nline get_slide showed cut short with an …, which would write the shortened text\nover the full line.","scopes":["slides:write"]},{"name":"replace_all_text","title":"Replace every occurrence of some text","description":"Replace every occurrence of a piece of text across a presentation — on every\nslide, or only the slides you name — and get back how many there were.\n\nThis is the tool for a correction that appears in more than one place: a renamed\ncompany, a wrong date, a placeholder left in a template. The count is the point —\n\"0 occurrences changed\" means the deck did not say what you thought, and\nreporting the change as done would be wrong.\n\nTHIS REPLACES TEXT AND THERE IS NO UNDO. Match on something long enough to be\nunambiguous: replacing \"Ltd\" hits the \"Ltd\" inside every other company name too.\nRead the deck first if you are not certain what the match will catch.\n\nIt reaches shapes, table cells and speaker notes on the slides themselves. It\nnever touches layouts or masters, so boilerplate that lives on the master is\nleft alone. Matching ignores case unless you ask it not to.","scopes":["slides:read","slides:write"]},{"name":"move_slides","title":"Reorder slides","description":"Move one or more slides to a new position. The slides keep their ids and all of\ntheir content; only the running order changes.\n\nName the slides in the order you want them to end up in — they are placed\ntogether, in the order given, whatever order they were in before.\n\nThe index counts the deck as it stands BEFORE the move: 0 puts them first, and\nthe number of slides in the deck puts them last. That is the one thing to get\nright — an index chosen against the deck as it will look afterwards lands a\nslide beside the wrong one.\n\nSlide numbers everywhere else are stale after this. The ids are not, which is\nwhy every tool takes them.","scopes":["slides:write"]},{"name":"duplicate_slide","title":"Duplicate a slide","description":"Copy one slide, placing the copy directly after the original, and get back the\nnew slide’s id.\n\nThis is how you make a variant without retyping it: duplicate the slide, then\nrewrite the copy with set_slide_content. Everything comes across — layout, text,\ngeometry, images, speaker notes — with new ids for every part of it, so editing\nthe copy never touches the original.\n\nThe copy is a new slide, so every slide number after the original moves along by\none. Use the id this returns rather than a number you remember.","scopes":["slides:write"]},{"name":"delete_slides","title":"Delete slides","description":"Delete one or more slides from a presentation.\n\nTHIS CANNOT BE UNDONE. There is no version history here and no trash for a\nsingle slide: what the slides said is gone the moment this returns. Read them\nfirst, say what you are about to remove, and prefer set_slide_skipped when the\nuser only wants a slide left out of the next run-through.\n\nEither every slide named is deleted or none of them is — a bad id refuses the\nwhole call rather than deleting the others. A presentation cannot be emptied:\ndeleting every slide is refused, because a deck with no slides is not something\nthe editor can open.\n\nSlide numbers after the deleted ones move back. The ids of the survivors do not\nchange.","scopes":["slides:write"]},{"name":"set_slide_skipped","title":"Skip a slide, or stop skipping it","description":"Mark one slide as skipped, or un-skip it. A skipped slide stays in the\npresentation and keeps everything on it; it is simply passed over when the deck\nis presented.\n\nThis is the safe answer to \"take this slide out for now\": nothing is lost, and\nit is one call to put it back. Use delete_slides only when the user means the\nslide to be gone.\n\nNothing else about the slide changes, and the slide numbers stay as they were.","scopes":["slides:write"]},{"name":"rename_presentation","title":"Rename a presentation","description":"Change a presentation’s title. Not one word on its slides is touched.\n\nUse it when a deck has outgrown the name it was started with, or when the user\nasks for one to be called something else. The title is what they see in their\nlibrary and what list_presentations and search_presentations match on, so a good\none is how the deck gets found again.\n\nThe title is the presentation’s NAME, not the heading on its first slide:\nrenaming writes nothing onto a slide. The old title is replaced rather than kept\nalongside, so pass the id from list_presentations rather than one you remember —\na deck renamed since you last looked is no longer the one you think it is.","scopes":["slides:write"]}]}