PowerPoint generation API
The recurring deck: a title slide, an agenda, one slide per region / customer / product, and a closing slide. Somebody rebuilds it every quarter by duplicating a slide and retyping numbers. The template exists, the numbers are in a system, and the only missing piece is something that repeats a slide correctly. This page is about why "correctly" is the hard word. Every number below was measured against the live API from Germany on 6 September 2026 with a free key, using the quarterly deck in examples/.
The job: one slide per record
You mark a slide as the body of a loop, the same way you would mark a table row in Word:
{#regions} ... the slide, with {name}, {lead}, {revenue}, a table ... {/regions}
Send six regions, get six slides, each with that region's numbers, its own layout untouched, and the slides before and after it still in place. That is the whole feature, and it is the reason a deck is a different problem from a document.
Why a deck is harder than a document
A .docx is one body part. A .pptx is a part per slide, plus a
presentation part that lists them in order, plus a relationship file that names them, plus a
content-type override for each one. Duplicating a slide means touching all four in step. And here
is the asymmetry that makes it expensive to get wrong: LibreOffice will happily open the
wreckage, so a filler tested against LibreOffice looks fine — while PowerPoint refuses the
whole file if any one of the four disagrees, with the message "PowerPoint found a problem with
content" and no hint which of the four it was.
Three specific traps, all of which have to be handled at once:
- Slide ids are constrained. PowerPoint requires every
sldIdto be a distinct value in [256, 2147483648). Copy a slide and copy its id and the file is refused. - Order is not the file name. Slide order lives in the presentation part's
<p:sldIdLst>, not inslide1.xml,slide2.xml, … Inserting four copies in the middle must not move the closing slide to the front. - Every copy needs its own relationships. A slide that references an image, a layout or a
notes page has a
_relsfile of its own. Share one between copies and the copies fight over it.
What came back, measured today
The template has 4 slides: a title, a scorecard slide whose table loops over the regions, the per-region slide, and a closing slide. The data has six regions. Verify it the way we did — the
commands are ordinary unzip:
unzip -l deck.pptx | grep -c 'ppt/slides/slide' -> 9 (title + scorecard + 6 region slides + closing)
unzip -p deck.pptx ppt/presentation.xml | grep -o '<p:sldId id="[0-9]*" r:id="[^"]*"'
<p:sldId id="256" r:id="rId4"> <- title
<p:sldId id="257" r:id="rId5">
<p:sldId id="258" r:id="rId6">
<p:sldId id="259" r:id="rId9"> <- five new slide parts, spliced in
<p:sldId id="260" r:id="rId10">
<p:sldId id="261" r:id="rId11">
<p:sldId id="262" r:id="rId12">
<p:sldId id="263" r:id="rId13">
<p:sldId id="264" r:id="rId7"> <- the closing slide, still last
Nine distinct ids, counting up from 256, and the relationship ids tell the story: the closing
slide is still rId7 — the id it had in the template — and it is still at the end,
after five slide parts that did not exist when the file was written were created and placed in front of it.
| What | Measured, 6 September 2026 |
|---|---|
output: "document" | 62.7 ms server-side, 142 ms round trip from Germany. 210 placeholders found, 210 resolved, 9 sections, 1 image, 33 package parts rewritten. 37,275 B template → 58,030 B deck. 1 credit. |
output: "pdf" | 1,423.3 ms server-side, 1,496 ms round trip. 9 pages — one per slide — 57,345 B. 2 credits. |
Almost all of that second row is LibreOffice, not the fill; on the workbook measured on the
Excel page the split was 1,427 ms of conversion against 16 ms of
fill. Conversion runs one at a time, so a burst queues rather than slows. Ask for the
.pptx when you can — people usually want to edit the deck anyway.
Page 3 of that PDF reads back with pdftotext as:
REGIONAL REVIEW · Q2 2026
DACH
Regional lead Lukas Brandt · 26 people · 148 active accounts
REVENUE GROWTH YEAR ON YEAR UNITS SHIPPED
Q2 2025: €441,800.00
Group average 8.4%
The speaker notes travel with the slide
This is the detail that decides whether the generated deck is usable in the meeting. The template's notes page for the loop slide holds placeholders:
{name} — {lead}
{notes}
Ask the room: {question}
In the deck that came back, the notes part belonging to one of the generated region slides,
ppt/notesSlides/notesSlide7.xml, reads:
UK & Ireland — Owen Marsh
The only region down year on year. Be straight about the lost account and the recovery plan.
Ask the room: What is the realistic date for replacing the lost volume?
Each copy got its own notes part, its own relationship pointing at it
(ppt/slides/_rels/slide7.xml.rels →
../notesSlides/notesSlide7.xml) and its own content-type override. Tables inside a
slide repeat too: a section over a table produces one <a:tr> per array element,
which is the shape a reporting deck's table actually needs.
The self-check before the bytes leave
Because PowerPoint's failure mode is "the file is refused and you are not told why", the last
step of a render re-derives the package invariants from the finished bytes and throws if
any of them fails, rather than handing you a deck that opens on your laptop and not on the
customer's. That is also why the parts list comes back in the response: you can see
exactly which of the 33 parts were rewritten in your call.
The call
curl -X POST https://docmint.app.mintapis.com/v1/render \
-H "Authorization: Bearer $DOCMINT_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"template_base64": "<your .pptx, base64>",
"data": { "title": "Quarterly business review",
"regions": [ { "name": "DACH", "lead": "Lukas Brandt", "revenue": 478900 }, ... ] },
"output": "document"
}'
With Accept: application/json the deck comes back base64 in a JSON envelope
together with stats — tags found, tags resolved, sections, slides, and the list of
parts that were rewritten — which is what the numbers in the table above are read from. Leave the
header off and you get the .pptx bytes with a
Content-Disposition filename. A stored template works the same way: upload it once
with PUT /v1/templates/quarterly-deck and then send only "template":
"quarterly-deck" and the data. Storing, reading and listing templates cost no credits — measured today:
GET /v1/usage read {"used": 6, "remaining": 24} both before and after a
PUT of this deck.
A stored template also answers GET /v1/templates/{name}/fields with the form a
client should put on screen — each placeholder with its type, whether it repeats, and
where in the deck it sits. For this template that reads
{"name": "regions", "type": "array", "repeating": true, "used": 18, "locations": ["slide 3",
"slide 2, table \"Scorecard\"", ...]}. That is what the n8n node and the Make module build
their input fields from, instead of asking someone to retype field names from memory.
What this does not do
- No charts. A chart in a slide keeps the data it was saved with. If the deck needs a chart that changes, today the honest answer is to render the chart as an image somewhere else and place it with the image feature.
- It fills, it does not design. Every font, colour, position and master comes from your template. There is no browser deck editor and none is planned.
- No animations or transitions authored by us — whatever the template carries survives, nothing is added.
- Nothing is evaluated. No expression in your data is ever compiled or executed.
What we cannot claim
DocMint is new and small: no paying outside customer yet, no uptime history worth
quoting, no SLA, no SOC 2, one region. The free plan is 30 credits from a single
POST /v1/signup with no card, which is what produced every number on this page. The
PDF step is one LibreOffice process. And the honest scope note: this is a
template filler. If what you want is a deck generated from a prompt with no template, that
is a different product and we are not it.
What is checkable from outside without an account: the source is on GitHub, including the example deck and its data, so every command above can be run against your own render.
Next: the Excel report API, where the interesting problem is formulas rather than packages, or the Word template API for the comparison with Carbone, Docupilot and docxtemplater.