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
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.
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:
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.
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.
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 requestcurl -X GET "https://api.appstruct.cloud/v1/slides/presentations" \
-H "Authorization: Bearer ak_live_your_key_here"
Query parameters
Response200
Success
Errors
401Missing, invalid, expired or revoked API key403API key lacks the required scope404Not found on this account429Rate 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 requestcurl -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
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.
Errors
400Request validation failed401Missing, invalid, expired or revoked API key403API key lacks the required scope404Not found on this account409The presentation is in the trash, or it changed since the revisionId you sent413The saved presentation would exceed its size limit429Rate 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 requestcurl -X GET "https://api.appstruct.cloud/v1/slides/presentations/<presentationId>" \
-H "Authorization: Bearer ak_live_your_key_here"
Path parameters
Query parameters
Response200
Success
Errors
401Missing, invalid, expired or revoked API key403API key lacks the required scope404Not found on this account429Rate 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 requestcurl -X GET "https://api.appstruct.cloud/v1/slides/presentations/<presentationId>/slides/<slideId>" \
-H "Authorization: Bearer ak_live_your_key_here"
Path parameters
Response200
Success
Errors
401Missing, invalid, expired or revoked API key403API key lacks the required scope404Not found on this account429Rate 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 requestcurl -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
Request bodyapplication/json, required
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.
Errors
400Request validation failed401Missing, invalid, expired or revoked API key403API key lacks the required scope404Not found on this account409The presentation is in the trash, or it changed since the revisionId you sent413The saved presentation would exceed its size limit429Rate limit or daily budget exceeded
Ready to build?
Keys are created in the AppStruct dashboard and shown once, at creation.
Get an API key