Skip to content
AppStruct Slides API v1.0.0
OpenAPI Get an API key

AppStruct Slides API

Version 1.0.0 · openapi.json

Programmatic read and write access to your AppStruct Slides presentations — list them, read what is on every slide, build a deck from an outline, and edit the text on slides that already exist.

Authentication

Every request carries an API key, created from your AppStruct dashboard. Send it as either header:

Authorization: Bearer ak_live_xxxxxxxxxxxxxxxxxxxxxxxx
# or
x-api-key: ak_live_xxxxxxxxxxxxxxxxxxxxxxxx

Keys are shown once, at creation. A key reaches the presentations its own account owns and nothing else — decks shared with that account, and anything opened by a share link, are not visible through the API.

Scopes

  • *
  • slides:read
  • slides:write

AI assistants (MCP)

The same key opens a Model Context Protocol server at POST /v1/slides/mcp, so Claude, Claude Code, Cursor or any other MCP client can read and write these presentations directly. It speaks Streamable HTTP and takes the same Authorization: Bearer header as the endpoints below.

The tools an assistant is offered are derived from the key's scopes — a slides:read key never sees the write tools at all — and every tool runs through the same service and the same limits as the REST calls documented here. Copy-paste configuration for each client is in the dashboard: https://appstruct.ai/dashboard/slides/api/connect

Browser clients are refused: the endpoint rejects any request carrying an Origin header, which is what the MCP specification requires against DNS rebinding, and real MCP clients are not browsers and send none.

Work from ids, not from names

Every call after the first takes an id, and every id you will need comes back from the call before it. List presentations to find the one you mean; read it to get its slide ids and the ids of the objects on each slide; then edit.

Three ids and they do not mix. A presentation id is the path segment. A slide id is what deleteObject, duplicateObject, updateSlideContent, updateSpeakerNotes and updateSlideProperties take — and it is the same string as a slide's objectId, which is why both spellings come back. An element id is what insertText, deleteText and setText take, and you rarely need one: every placeholder on a slide is reachable by name through updateSlideContent, and element ids exist for the text boxes that have no name.

Reading a presentation is cheap and reading a stale id is not, so read before you write. There is no version history here and no undo: text this API replaces or deletes is gone.

The text you read is the text you write

A read gives every text object its content as an array of lines, one line per paragraph, and those lines are in exactly the form a write takes back. No list marker is ever stored or returned — bullets belong to the layout, not to the text — and a paragraph's outline level comes back as two spaces of indent per level. So setText of the lines a read just returned leaves the presentation byte-for-byte unchanged, which is what makes "read it, change one line, write it back" safe.

Writing goes the same way. A body is one paragraph per line, whether you send a string with newlines or an array of strings. A leading -, , •, 1. or 1) is a marker you typed and is stripped, so pasting a Markdown list does the obvious thing; two spaces of indent make one outline level, up to eight. The exceptions are worth knowing: speaker notes are stored exactly as sent, with no grammar applied, and insertText addresses the element's stored* text — a 0-based UTF-16 offset with paragraphs joined by newlines — which does not include the indent a read prints. A newline in insertText starts a new paragraph, inheriting the style of the one it split. There is no way to send formatting, and no way to send a deck: raw deck JSON cannot be uploaded as a create body, and ?view=full is a read-only door.

Where text lands on a slide

Text is placed by NAME, not by geometry: title, subtitle, body and columns[i] are matched against the placeholders the layout defines. body is columns[0], and sending both is refused. A field the layout has no slot for is refused too, naming the layout and the field — a quote layout has no title, and quietly dropping the title you sent would be worse than a 400.

LayoutNameText you can writeOther placeholders
titleTitle slidetitle, subtitle—
sectionHeaderSection headertitle, subtitle—
titleOnlyTitle onlytitle—
blankBlank——
titleBodyTitle and bodytitle, body—
twoColumnTwo columnstitle, columns[0], columns[1]—
comparisonComparisontitle, columns[0], columns[1], columns[2], columns[3]—
agendaAgendatitle, body—
appendixAppendixtitle, body—
captionImageImage and captiontitle, bodyimage
timelineTimelinetitle, columns[0], columns[1], columns[2], columns[3]—
chartLeftChart and takeawaytitle, bodychart
diagramFullDiagramtitle, body—
imageBleedFull-bleed imagetitle, subtitleimage
bigNumberBig numbertitle, columns[0], columns[1]—
quoteQuotecolumns[0], columns[1]—
statementStatementtitle—

columns[0] is the same slot as body, so a one-column layout can take either name. Read the numbered slots layout by layout: on comparison, columns[0] and columns[1] are the two headings and columns[2] and columns[3] the text under them; on bigNumber, columns[0] is the number and columns[1] is what it means; on quote, columns[0] is the quote and columns[1] the attribution; on timeline, the four columns are the four steps. Picture, chart and table placeholders stay empty — there is no way to fill them through the API yet.

Google's predefinedLayout names are accepted anywhere a layout key is:

Google nameAppStruct layout
BLANKblank
TITLEtitle
TITLE_AND_BODYtitleBody
TITLE_AND_TWO_COLUMNStwoColumn
TITLE_ONLYtitleOnly
SECTION_HEADERsectionHeader
SECTION_TITLE_AND_DESCRIPTIONsectionHeader
ONE_COLUMN_TEXTtitleBody
MAIN_POINTstatement
BIG_NUMBERbigNumber
CAPTION_ONLYcaptionImage

Editing

:batchUpdate applies a list of edits in order, in one save. Each edit sees the presentation as the edits before it left it, so a createSlide followed by an updateSlideContent naming the id from its reply is not a legal batch — you do not have the id yet. Put the content on the createSlide instead; that is what its title, body and columns fields are for.

A batch either applies whole or changes nothing. An edit the server cannot understand is refused with the position of the request that was wrong (Request 3: …), and nothing before it is written. Replies line up with requests one for one and use Google's envelope: {"createSlide": {"objectId": "…"}}, {"duplicateObject": {"objectId": "…"}}, {"replaceAllText": {"occurrencesChanged": 2}}, and {} for everything else.

Fields this API cannot honour are refused by name rather than ignored — createSlide.objectId, placeholderIdMappings, slideLayoutReference.layoutId, cellLocation, containsText.searchByRegex, a textRange narrower than ALL. A 200 that quietly did something else is the failure this API is built to avoid.

Limits

  • 300 presentations per account and 400 slides per presentation. Reaching either answers 400.
  • 100 edits per :batchUpdate call, and at most 2,000,000 characters of text in one request body.
  • 20,000 characters per text field and per line, 200 characters for a title or subtitle, and 1,000 characters for a search string.
  • 12 MiB per presentation, counting the pictures and charts stored inside it. A write that would exceed it answers 413.
  • List endpoints take ?limit= (1–200, default 50) and ?offset=, and return { data, meta }.
  • 120 requests per minute per key, plus a coarser 600 per minute per IP. Both answer 429 with Retry-After; every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds), and the per-IP tier reports the same three suffixed -ip.
  • 100,000 requests per rolling 24 hours per account, shared across every key. Each other AppStruct API meters its own separate allowance, so traffic there does not spend this one. Reported in X-AppStruct-RateLimit-Daily and X-AppStruct-RateLimit-Daily-Remaining.
  • 25 live keys per account. Revoke one you no longer use before minting another.

Errors

Standard HTTP status codes with a JSON body. Switch on statusCode, not on the human-readable message.

StatusMeaning
400The request, or one edit in it, could not be understood
401Missing, invalid, expired or revoked API key
403The key lacks the required scope
404No such presentation on this account, or no such slide on it
409The presentation is in the trash, or it changed since the revision you read
413The saved presentation would exceed its size limit
429A rate limit or the daily budget was exceeded

Concurrent editing

Writes to one presentation are serialised on the server, and writeControl.requiredRevisionId lets two API callers refuse to overwrite each other: read a presentation, compute your batch, send the revisionId you read, and a 409 tells you somebody got there first.

It does not protect you from the owner's editor. A presentation open in an editor tab is saved whole from that tab on its next edit or reconnect, and that save will overwrite changes made here. A library change made offline — a rename, a star, a move to the trash or into a folder — is re-applied over an API rename on the next sync. And changes made here appear in an open editor only after it is reloaded.

So: prefer writing to presentations nobody is editing. If somebody is, ask them to close it before you write and to reopen it afterwards.

Not available yet

Thumbnails, PDF and PPTX export — there is no renderer on the server. Inserting a picture by URL, or any picture at all. Shares and comments, including presentations shared with you. Anything about geometry, colour, fonts or animation. Uploading a deck as raw JSON. All of these are absent rather than partial: nothing in this version silently half-does one of them.

The tools an assistant is given

Derived from the key's scopes — a read-only key never sees the write tools at all.

ToolWhat it doesNeeds
list_presentations List the account’s presentationsList the presentations this account owns, most recently edited first, with the id every other tool needs. Start here when the user names a deck in words ("the Q3 board deck", "my onboarding slides") and you need its id. If they described something WRITTEN ON a slide — a figure, a customer name, a heading — use search_presentations instead, which reads the text. Returns id, title, how many slides it has and when it was last edited, and nothing of the slides themselves: use get_presentation for that. Only presentations this account OWNS — anything shared with it, or opened from a share link, is not reachable with an API key. slides:read
get_presentation Read a presentation, slide by slideRead a presentation: its title, its size, and one block per slide giving the slide number, the slide id, the layout it was built from and the text in each of its slots. Call this before you change anything. Every write tool takes a slide id from here, and set_slide_content REPLACES the slot it is given — so an edit made without reading first is a guess about which slot holds what. Long decks come back a page of slides at a time, never a slide cut in half: the reply ends by saying which slides it showed and which `start_slide` reads the next ones. Each slide is summarised — at most eight lines of a slot, no element ids — so use get_slide when you need every element on one slide, its ids, or its writable slots. Speaker notes are left out unless you ask for them, because they are often longer than the slide. slides:read
get_slide Read one slide in fullRead everything on one slide: every element with its own object id, what kind it is, the placeholder role it fills, its text line by line, its alt text and the slide’s speaker notes — plus the layout this slide was built from and the text slots that layout has. This is the call to make before rewriting a slide. set_slide_content writes by SLOT (title, subtitle, body, columns) and the slot table printed here says which of them this layout actually has — a field the layout has no slot for is refused, not quietly dropped. set_element_text writes by OBJECT ID, which is the only way to change a text box that fills no placeholder role at all. The lines returned are exactly the lines those two tools take: an outline level is two leading spaces, and there are no bullet characters — bullets come from the layout, not from the text. Send a line back unchanged and nothing changes. One exception, and it matters: a line too long for one reply is shown cut short with an … at the end. Never send such a line back — writing it would replace the full line with the shortened one, and there is no undo. Position and size are left out unless you ask for them, because nothing in this server can write them back. slides:read
search_presentations Find presentations by what their slides sayFind the account’s presentations whose TITLE or SLIDES contain a piece of text, and get back each one’s id, which slides matched and the words around the first match. Use it when the user describes content rather than a file: "which deck mentions the Riga office", "where did I put the pricing slide", "find the deck that quotes £4,200". Matching is case-insensitive substring, so a shorter fragment finds more. It is not a pattern: a regular expression is searched for literally. The slide numbers beside a hit are where to look; get_presentation on that deck gives you their ids, which is what every write tool takes. Opens the 40 most recently edited presentations at most, and says so when it stopped early. Trashed presentations are never opened. slides:read
create_presentation Create a presentationCreate a new presentation, optionally with all of its slides already in it, and get back the id every other tool needs — plus the id and number of every slide it made, so a follow-up edit needs no read. Use it whenever the user asks for a new deck: "put together a short pitch", "make slides from these notes". Prefer creating a presentation over adding to an existing one: slides appended to somebody’s deck are far harder for them to undo than a deck they did not want is to delete. Each slide names a LAYOUT and fills its slots. Write the whole deck in one call when you can — it is one save, and it is the only way to get a deck whose slides were laid out together rather than one at a time. Text is plain text: one line per entry, two leading spaces per outline level, and no bullet characters — bullets come from the layout. A field the layout has no slot for is refused rather than dropped, so the deck you get is the deck you asked for. slides:write
add_slide Add a slide to a presentationAdd one slide to an existing presentation and get back its id. The layout decides which slots the slide has — get_slide on a comparable slide prints the slot table, and a field the layout has no slot for is refused rather than dropped. Omit `layout` for titleBody, which is a title and one body. By default the slide is appended at the end. Pass `insertion_index` to put it somewhere else: 0 makes it the first slide, and the index counts the deck as it is NOW, so adding two slides at index 0 leaves them in the reverse of the order you sent them. One line per entry, two leading spaces per outline level, no bullet characters. To change a slide that already exists, use set_slide_content instead — this always makes a new one. slides:write
set_slide_content Rewrite the text of one slideReplace the text in one or more of a slide’s slots. The slide keeps its layout, its geometry and its element ids; only the words change. THIS REPLACES WHAT WAS THERE AND THERE IS NO UNDO. Read the slide first — get_slide prints every slot this layout has and what is currently in it — and send only the slots you mean to change: a slot you leave out is untouched. An empty string, or an empty list, CLEARS that slot rather than leaving it — that is how "take the subtitle off this slide" is said. One line per entry, two leading spaces per outline level, no bullet characters. A field the layout has no slot for is refused rather than dropped, so a slide never half-changes. slides:write
set_element_text Replace the text of one elementReplace all of the text in ONE element, named by its object id, with the lines you give. This is the tool for a text box that fills no placeholder role — the ones set_slide_content cannot reach, because they belong to no slot of the layout. get_slide lists every element on a slide with its object id and what it currently says. THIS REPLACES THE WHOLE ELEMENT’S TEXT AND THERE IS NO UNDO. Send the lines you want it to end up with, not just the new ones. One line per entry. Two leading spaces per outline level, and no bullet characters — bullets come from the layout, so typing "- " puts a literal dash on the slide. Lines that came out of get_slide can be sent straight back — except a line get_slide showed cut short with an …, which would write the shortened text over the full line. slides:write
replace_all_text Replace every occurrence of some textReplace every occurrence of a piece of text across a presentation — on every slide, or only the slides you name — and get back how many there were. This is the tool for a correction that appears in more than one place: a renamed company, a wrong date, a placeholder left in a template. The count is the point — "0 occurrences changed" means the deck did not say what you thought, and reporting the change as done would be wrong. THIS REPLACES TEXT AND THERE IS NO UNDO. Match on something long enough to be unambiguous: replacing "Ltd" hits the "Ltd" inside every other company name too. Read the deck first if you are not certain what the match will catch. It reaches shapes, table cells and speaker notes on the slides themselves. It never touches layouts or masters, so boilerplate that lives on the master is left alone. Matching ignores case unless you ask it not to. slides:read slides:write
move_slides Reorder slidesMove one or more slides to a new position. The slides keep their ids and all of their content; only the running order changes. Name the slides in the order you want them to end up in — they are placed together, in the order given, whatever order they were in before. The index counts the deck as it stands BEFORE the move: 0 puts them first, and the number of slides in the deck puts them last. That is the one thing to get right — an index chosen against the deck as it will look afterwards lands a slide beside the wrong one. Slide numbers everywhere else are stale after this. The ids are not, which is why every tool takes them. slides:write
duplicate_slide Duplicate a slideCopy one slide, placing the copy directly after the original, and get back the new slide’s id. This is how you make a variant without retyping it: duplicate the slide, then rewrite the copy with set_slide_content. Everything comes across — layout, text, geometry, images, speaker notes — with new ids for every part of it, so editing the copy never touches the original. The copy is a new slide, so every slide number after the original moves along by one. Use the id this returns rather than a number you remember. slides:write
delete_slides Delete slidesDelete one or more slides from a presentation. THIS CANNOT BE UNDONE. There is no version history here and no trash for a single slide: what the slides said is gone the moment this returns. Read them first, say what you are about to remove, and prefer set_slide_skipped when the user only wants a slide left out of the next run-through. Either every slide named is deleted or none of them is — a bad id refuses the whole call rather than deleting the others. A presentation cannot be emptied: deleting every slide is refused, because a deck with no slides is not something the editor can open. Slide numbers after the deleted ones move back. The ids of the survivors do not change. slides:write
set_slide_skipped Skip a slide, or stop skipping itMark one slide as skipped, or un-skip it. A skipped slide stays in the presentation and keeps everything on it; it is simply passed over when the deck is presented. This is the safe answer to "take this slide out for now": nothing is lost, and it is one call to put it back. Use delete_slides only when the user means the slide to be gone. Nothing else about the slide changes, and the slide numbers stay as they were. slides:write
rename_presentation Rename a presentationChange a presentation’s title. Not one word on its slides is touched. Use it when a deck has outgrown the name it was started with, or when the user asks for one to be called something else. The title is what they see in their library and what list_presentations and search_presentations match on, so a good one is how the deck gets found again. The title is the presentation’s NAME, not the heading on its first slide: renaming writes nothing onto a slide. The old title is replaced rather than kept alongside, so pass the id from list_presentations rather than one you remember — a deck renamed since you last looked is no longer the one you think it is. slides:write

Presentations

Finding presentations, reading what is on their slides, and creating new ones

GET /v1/slides/presentations

List presentations

Every presentation this account owns, most recently saved first. Slides are left out — fetch one presentation to read what is on it.

Requires the slides:read scope.

Example request
curl -X GET "https://api.appstruct.cloud/v1/slides/presentations" \
  -H "Authorization: Bearer ak_live_your_key_here"

Query parameters

NameTypeDescription
limit integerdefaults to 50 How many presentations to return.
offset integer How many to skip, for paging.
includeTrashed booleandefaults to false Include presentations in the trash. They are still owned and still readable; every write to one is refused.

Response200

Success

FieldTypeDescription
data object[]
data[].presentationId string
data[].title string
data[].slideCount integer
data[].starred boolean
data[].trashed boolean In the trash. Still owned and still readable; every write to it is refused with 409.
data[].folderId string The library folder the presentation sits in, or null at the root.
data[].updatedAt string (date-time) When the presentation was last SAVED, from the row itself rather than from anything a client wrote into the deck.
meta object
meta.total integer
meta.limit integer
meta.offset integer
meta.hasMore boolean

Errors

  • 401Missing, invalid, expired or revoked API key
  • 403API key lacks the required scope
  • 404Not found on this account
  • 429Rate limit or daily budget exceeded
POST /v1/slides/presentations

Create a presentation

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.

Requires the slides:write scope.

Example request
curl -X POST "https://api.appstruct.cloud/v1/slides/presentations" \
  -H "Authorization: Bearer ak_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"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."}]}'

Request bodyapplication/json, required

FieldTypeDescription
titlerequired string
size 16:9 | 4:3 | 16:10 | a4 The page shape.
slides object[] The slides to create. Omit for a presentation with one blank title slide.
slides[].layout string 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.)
slides[].title string
slides[].subtitle string
slides[].body string | string[] 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.
slides[].columns string[][] 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.
slides[].notes string Speaker notes. An empty string clears them. Notes are stored as you send them: the outline grammar above does not apply here.
slides[].skipped boolean Skip this slide when the deck is presented.
folderId string File the new presentation in this library folder. It must already exist. Omit to leave it at the root.
Example body
{
  "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."
    }
  ]
}

Response200

The presentation as stored, with the id of every slide that was created.

FieldTypeDescription
presentationId string
title string
slideCount integer
starred boolean
trashed boolean
folderId string
updatedAt string (date-time)
createdAt string (date-time)
size object The page size in points, and which preset it is.
size.width number
size.height number
size.preset 16:9 | 4:3 | 16:10 | a4 | custom
revisionId string 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 object[]
slides[].slideId string
slides[].objectId string 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.
slides[].index integer 0-based position in the deck.
slides[].slideNumber integer 1-based, the number a person sees.
slides[].layout string The layout key this slide was created with, as stored — not guessed from what is on it.
slides[].skipped boolean
slides[].title string The slide's title text, collapsed to one line for display. Write it back through title, not through this string.
slides[].elements object[]
slides[].elements[].objectId string The id every edit addressing this element takes — insertText, deleteText, setText.
slides[].elements[].type shape | table | chart | image | video | audio
slides[].elements[].role string 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.
slides[].elements[].index integer Which slot of that role this is. body index 1 is columns[1].
slides[].elements[].name string
slides[].elements[].text string[] 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.
slides[].notes string Speaker notes, when the slide has any.
deck object 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.

Errors

  • 400Request validation failed
  • 401Missing, invalid, expired or revoked API key
  • 403API key lacks the required scope
  • 404Not found on this account
  • 409The presentation is in the trash, or it changed since the revisionId you sent
  • 413The saved presentation would exceed its size limit
  • 429Rate limit or daily budget exceeded
GET /v1/slides/presentations/{presentationId}

Retrieve a presentation

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.

Requires the slides:read scope.

Example request
curl -X GET "https://api.appstruct.cloud/v1/slides/presentations/<presentationId>" \
  -H "Authorization: Bearer ak_live_your_key_here"

Path parameters

NameTypeDescription
presentationIdrequired string The presentation id, as returned by GET /v1/slides/presentations.

Query parameters

NameTypeDescription
view digest | fulldefaults to digest digest is the readable summary. full adds deck, the stored presentation verbatim — large, because pictures and charts are stored inline.

Response200

Success

FieldTypeDescription
presentationId string
title string
slideCount integer
starred boolean
trashed boolean
folderId string
updatedAt string (date-time)
createdAt string (date-time)
size object The page size in points, and which preset it is.
size.width number
size.height number
size.preset 16:9 | 4:3 | 16:10 | a4 | custom
revisionId string 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 object[]
slides[].slideId string
slides[].objectId string 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.
slides[].index integer 0-based position in the deck.
slides[].slideNumber integer 1-based, the number a person sees.
slides[].layout string The layout key this slide was created with, as stored — not guessed from what is on it.
slides[].skipped boolean
slides[].title string The slide's title text, collapsed to one line for display. Write it back through title, not through this string.
slides[].elements object[]
slides[].elements[].objectId string The id every edit addressing this element takes — insertText, deleteText, setText.
slides[].elements[].type shape | table | chart | image | video | audio
slides[].elements[].role string 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.
slides[].elements[].index integer Which slot of that role this is. body index 1 is columns[1].
slides[].elements[].name string
slides[].elements[].text string[] 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.
slides[].notes string Speaker notes, when the slide has any.
deck object 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.

Errors

  • 401Missing, invalid, expired or revoked API key
  • 403API key lacks the required scope
  • 404Not found on this account
  • 429Rate limit or daily budget exceeded
GET /v1/slides/presentations/{presentationId}/slides/{slideId}

Retrieve one slide

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.

Requires the slides:read scope.

Example request
curl -X GET "https://api.appstruct.cloud/v1/slides/presentations/<presentationId>/slides/<slideId>" \
  -H "Authorization: Bearer ak_live_your_key_here"

Path parameters

NameTypeDescription
presentationIdrequired string The presentation id, as returned by GET /v1/slides/presentations.
slideIdrequired string The slide id, from the presentation's slides[].slideId.

Response200

Success

FieldTypeDescription
slideId string
objectId string
index integer
slideNumber integer
layout string
skipped boolean
title string
elements object[]
elements[].objectId string
elements[].type string
elements[].role string
elements[].index integer
elements[].name string
elements[].text string[]
elements[].frame object Where the element sits on the page, in points, with the rotation in degrees. Read-only in this version.
elements[].frame.x number
elements[].frame.y number
elements[].frame.width number
elements[].frame.height number
elements[].frame.rotation number
elements[].paragraphs object[]
elements[].paragraphs[].paragraphId string Stable across edits that reword the line, so a comment anchored to it survives.
elements[].paragraphs[].text string
elements[].paragraphs[].level integer 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.
elements[].paragraphs[].bullet string The bullet the layout gives this paragraph, when it has one.
elements[].altText string
notes string

Errors

  • 401Missing, invalid, expired or revoked API key
  • 403API key lacks the required scope
  • 404Not found on this account
  • 429Rate limit or daily budget exceeded

Editing

Changing a presentation that already exists — slides, text and speaker notes

POST /v1/slides/presentations/{presentationId}:batchUpdate

Edit a presentation

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.

Requires the slides:write scope.

Example request
curl -X POST "https://api.appstruct.cloud/v1/slides/presentations/<presentationId>:batchUpdate" \
  -H "Authorization: Bearer ak_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"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"}}'

Path parameters

NameTypeDescription
presentationIdrequired string The presentation id, as returned by GET /v1/slides/presentations.

Request bodyapplication/json, required

FieldTypeDescription
requestsrequired object[]
requests[].createSlide object Add a slide, optionally with its text already written.
requests[].createSlide.layout string
requests[].createSlide.slideLayoutReference object Google's spelling of layout. Only predefinedLayout is read; layoutId names a layout object this API does not expose and is refused.
requests[].createSlide.insertionIndex integer Where the new slide goes, 0 for first. Omitted appends.
requests[].createSlide.title string
requests[].createSlide.subtitle string
requests[].createSlide.body string | string[]
requests[].createSlide.columns string[][]
requests[].createSlide.notes string
requests[].createSlide.skipped boolean
requests[].deleteObject object 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.
requests[].deleteObject.objectIdrequired string
requests[].duplicateObject object 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.
requests[].duplicateObject.objectIdrequired string
requests[].insertText object Insert a run of text into one element.
requests[].insertText.objectIdrequired string
requests[].insertText.textrequired string 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.
requests[].insertText.insertionIndex integer 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.
requests[].deleteText object 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.
requests[].deleteText.objectIdrequired string
requests[].deleteText.textRange object Only {"type": "ALL"} is supported. A narrower range would need an index space this version does not expose for deletes.
requests[].setText object 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.
requests[].setText.objectIdrequired string
requests[].setText.text string
requests[].setText.lines string[] Send exactly one of text or lines.
requests[].replaceAllText object 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.
requests[].replaceAllText.containsTextrequired string | object The text to look for, as a plain substring. Google's object form is accepted as well as the plain string.
requests[].replaceAllText.replaceTextrequired string 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.
requests[].replaceAllText.matchCase boolean Whether the search is case-sensitive. Defaults to false.
requests[].replaceAllText.pageObjectIds string[] Restrict the replacement to these slides. Omitted means every slide.
requests[].updateSlideContent object 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.
requests[].updateSlideContent.objectIdrequired string The SLIDE id.
requests[].updateSlideContent.title string
requests[].updateSlideContent.subtitle string
requests[].updateSlideContent.body string | string[]
requests[].updateSlideContent.columns string[][]
requests[].updateSlideContent.notes string
requests[].updateSlideProperties object Skip a slide, or stop skipping it.
requests[].updateSlideProperties.objectIdrequired string
requests[].updateSlideProperties.slidePropertiesrequired object
requests[].updateSlideProperties.fields string 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.
requests[].updateSlidesPosition object Move slides. They keep their order relative to each other.
requests[].updateSlidesPosition.slideObjectIdsrequired string[] The slides to move. No duplicates.
requests[].updateSlidesPosition.insertionIndexrequired integer 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.
requests[].updateSpeakerNotes object Replace a slide's speaker notes. An AppStruct extension. An empty string removes them entirely.
requests[].updateSpeakerNotes.objectIdrequired string The SLIDE id, not an element id.
requests[].updateSpeakerNotes.textrequired string
requests[].updateTitle object Rename the presentation.
requests[].updateTitle.titlerequired string The new name in the library. Nothing is written onto a slide.
writeControl object Refuse the whole batch if the presentation has changed since you read it.
writeControl.requiredRevisionId string The revisionId from the read you computed this batch against. A mismatch answers 409 and writes nothing.
Example body
{
  "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"
  }
}

Response200

Applied as ONE save. replies lines up with requests, one entry each, and revisionId is what the presentation is at now.

FieldTypeDescription
presentationId string
replies object[]
replies[].createSlide object
replies[].createSlide.objectId string The id of the slide that was created.
replies[].duplicateObject object
replies[].duplicateObject.objectId string The id of the copy.
replies[].replaceAllText object
replies[].replaceAllText.occurrencesChanged integer How many matches were replaced. Zero is a success, not an error.
revisionId string The revision the presentation is at now. Send it as the next batch's requiredRevisionId to chain writes safely.

Errors

  • 400Request validation failed
  • 401Missing, invalid, expired or revoked API key
  • 403API key lacks the required scope
  • 404Not found on this account
  • 409The presentation is in the trash, or it changed since the revisionId you sent
  • 413The saved presentation would exceed its size limit
  • 429Rate limit or daily budget exceeded

Ready to build?

Keys are created in the AppStruct dashboard and shown once, at creation.

Get an API key