---
name: use-slides-api
url: https://vmax.ai/skill/slides
description: Write, store, present, download, and publish presentations through the internet.dev slides API from Vmax, or from any other tool holding a Vmax key. Covers the Markdown dialect the /slides editor compiles, the automatic layout and type fitting rules, the stored slide document, the client module, version and revision semantics, image assets, render jobs, and publication. Read before changing any Slides surface, the compiler, or the client, and when another tool wants to author a deck that reads the way Vmax draws it.
---

# use-slides-api

No comments in code. Constraints live beside their owning symbols in
`next-vmax-tools/AGENTS.md` under `## /slides`. Terms live in `/glossary`
(`presentation`, `slide`, `publication`). The slides surfaces carry no help
tips and `/help` lists none; the intro's `SLIDES_INTRO_PARAGRAPHS`
(`common/slides-copy.ts`) is the one in-product explanation.

## Find the authoritative source

The backend is the sibling `apis` repository, `../../../../apis` from
`public/www` (`/Users/…/Development/apis` beside `demiurge`), deployed at
`https://api.internet.dev`. Its own entry is
`public/skills/slides/SKILL.md`, served at
`https://api.internet.dev/skills/slides/SKILL.md`, with the document schema at
`references/schema.json` and the endpoint table at `references/api.md`. Read the
working tree first; executable source outranks prose.

| Contract | Backend source under `apis/` | Website counterpart |
|---|---|---|
| Routes, envelopes, auth, feature gates | `common/slides-api.ts` | `slides-api.ts` (`slidesRequest`, decoders) |
| Document shape, limits, normalization | `data/slides-validation.ts`, `data/model/slides.ts` | `SlideDocument` types, `SLIDE_LIMITS`, `decodeSlideDocument` |
| Create, read, update, delete, list, versions | `data/slides.ts` | `createSlideDeck`, `getSlideDeck`, `updateSlideDeck`, `deleteSlideDeck`, `listSlideDecks` |
| The server's own Markdown import and templates | `common/slides-markdown.ts`, `common/slides-templates.ts` | not used; the site sends a full `document` it compiled itself |
| Image assets and the upload window | `data/slides-assets.ts` | `createSlideAsset`, `completeSlideAsset`, `getSlideAsset`, `uploadSlideImage` |
| Render jobs, artifacts, retention | `data/slides-jobs.ts`, `workers/slides-renderer.ts` | `createSlideExport`, `getSlideJob`, `waitForSlideJob` |
| Export HTML and overflow rules | `common/slides-render.ts` | `FormSlideCanvas` mirrors its box, paragraph, bullet, and table rules |
| Publication and the public projection | `data/slides.ts` `publish`, `publicRevision`; `presentation()` in validation | `publishSlideDeck`, `unpublishSlideDeck`, `readPublicSlideDeck` |

## Transport

The browser calls `https://api.internet.dev/api/slides/*` directly with the
viewer's `vmax` cookie as `X-API-KEY`, the same host and credential
`next-slate-markdown/common/http.ts` uses for posts; the API answers CORS for
every origin. Every call goes through `retryFetch`: reads retry server faults
twice, writes and uploads send once, and a `429` is never retried. A non-JSON
answer is a `SlidesApiError` with status `0` naming the route. An error body is
`{ error, message }`; a `409` on a write carries `current_version`, surfaced as
`SlidesApiError.currentVersion`.

Every write carries a fresh `idempotency_key` from `slidesIdempotencyKey()`
(`crypto.randomUUID`). The site never replays a key with different input.

Personal decks need a verified account (level 10 or above) and no organization,
subscription, or credit account. The site never sends `domain`.

## Endpoints the site uses

| Method | Path under `/api/slides` | Client |
|---|---|---|
| GET | `/capabilities` | `readSlidesCapabilities` (feature flags, formats) |
| GET | `?limit=100&cursor=` | `listSlideDecks`, newest first, `next_cursor` paging |
| GET | `/:id` | `getSlideDeck` (document, private `data`, capabilities) |
| POST | `/create` | `createSlideDeck` with `document` and `data` |
| POST | `/update` | `updateSlideDeck` with `expected_version`, `document`, `data` |
| POST | `/delete` | `deleteSlideDeck` with `expected_version` |
| POST | `/assets/create`, `/assets/complete` | `uploadSlideImage` (SHA-256, presigned multipart POST, finalize job) |
| GET | `/assets/:id?deck_id=` | `getSlideAsset` (five minute signed URL) |
| POST | `/exports/create` | `createSlideExport` (`html`, `pdf`, `png` at scale two) |
| GET | `/jobs/:id?deck_id=` | `getSlideJob`, polled by `waitForSlideJob` on `poll_after_ms` |
| POST | `/publish`, `/unpublish` | `publishSlideDeck`, `unpublishSlideDeck` |
| GET | `/public/:publication` | `readPublicSlideDeck` (no key) |

Revisions, restore, asset delete, and job cancel exist on the API and have no
site caller. Presentation deletion uses `deleteSlideDeck` through
`SlidesFeed.remove` after `FormConfirmModal` confirmation in either editor
toolbar. It requires `capabilities.manage`, sends the confirmed
`expected_version` and a fresh idempotency key, and never retries a write
automatically. A version conflict requires reloading and confirming again.

The delete handler soft-deletes the deck, revokes its public links, and cancels
active jobs. Successful deletion removes it from the local history and clears
its editor selection. Example decks expose no Delete action, and the site has
no restore action. The fixture feed enforces the same permission and version
gates and accepts repeated deletion.

## Version, revision, and metadata

`version` is the deck's write counter: every save, publish, and unpublish
increments it, and every write states `expected_version`. `revision_number`
counts content snapshots and moves only when the normalized document changed.
The site compiles the same Markdown to byte-identical documents (slide and
element ids are position-keyed UUIDs from `deterministicSlideUuid`), so saving
an unchanged deck creates no revision.

Deck `data` is private, shallow-merged at the top level, and capped at 8 KiB.
The site owns one key, `vmax_slides` (`SLIDE_DECK_METADATA_KEY`), holding
`{ markdown, layouts, publication, watermarkAssetId }`: the Markdown source when it
fits `SLIDE_DECK_MARKDOWN_BYTES_LIMIT` (6 KiB), the pinned `{ id, revision_id }`
after a publish, and the uploaded watermark asset. A deck whose source does not fit, or that another tool wrote
as JSON, is edited from `slidesMarkdownFromDocument`, the decompiler that
writes the dialect back from positioned elements. When source exceeds the
Markdown cap, `layouts` stores up to one hundred slide-indexed layout names or
nulls; `slideDeckSource` restores those overrides after decompiling. Small
sources keep their directives in Markdown and omit the backup array. Send the whole object on
every write; a partial object would drop the other field.

## The Markdown dialect

The editor field uses the draft title as its label for personal and example
decks, without a separate title heading. It follows `slideDocumentTitle`: the
front-matter title, then the first non-empty slide heading, with `UNTITLED`
when neither is present. Each selected record opens with its own source and
assets before the editor renders; switching records resets Slate history, and
late source, save, or asset callbacks cannot update a different deck.

`parseSlidesMarkdown` (`slides-markdown.ts`) reads GFM through Marked's lexer.

- A level one or level two heading starts a slide, unless the current slide is
  still empty. A `---` rule starts a slide with no heading. Level three to six
  headings stay inside the slide.
- Front matter at the top, after an optional byte-order mark and blank lines:
  `title`, `description`, `label` (printed as written
  on every slide), `theme` (`light` or `dark`), `canvas` (`1280` or `960`).
  Unknown keys warn.
- Paragraphs, lists (nested lists indent, task items keep a box), blockquotes,
  fenced code, and tables become blocks. Bold, italic, inline code, and
  `http`, `https`, or `mailto` links become runs; any other link scheme is
  plain text.
- `![alt](asset:UUID)` is an uploaded image. An `http`, `https`, or `/` source
  is a pending image: it draws from its URL in the editor and uploads as an
  asset the moment the deck is saved, when the source line is rewritten to
  `asset:UUID`. Any other source renders as a labelled placeholder with a
  warning.
- Each preview has a template selector. Automatic removes its layout directive;
  an explicit choice writes it into that slide's source. Choices that cannot
  represent the current content are disabled, and an incompatible directive
  typed by hand produces a warning and uses automatic layout. Examples must
  be forked before their source or layout can change.
- Comments are directives on the slide they sit in: `<!-- notes: … -->` (or
  a paragraph starting with `Notes:`) is speaker notes, `<!-- hidden -->`
  hides the slide, `<!-- layout: name -->` forces a layout,
  `<!-- background: #hex -->` and `<!-- label: … -->` override per slide.
  A directive before a heading belongs to the previous slide.

## Layouts and fitting

`compileSlidesMarkdown` (`slides-compose.ts`) turns intents into a
`SlideDocument`. `chooseSlideLayout` picks, in order: `image`, `code`, `table`,
`columns` (two or three level three sections), `quote` (a headless
blockquote), `title` or `section` (heading alone; `title` on the first slide or
with one short line), `statement` (heading and prose), `bullets` (heading and a
list), else `stack`. Geometry comes from `slideGeometry(width)`: a five percent
margin, content from `y = 112` to `y = 624`, a headline column at 44 percent, a
6 percent gutter, and the body column in the rest.

Every text element is sized by `fitFontSize` (`slides-typeset.ts`), which
measures with Arial width tables as a conservative bound for the narrower
serif (`SLIDE_BODY_TYPEFACE` at scale 0.94; headlines through
`SLIDE_HEADLINE_TYPEFACE` at bold widths; code through
`SLIDE_MONO_TYPEFACE`) and steps the size down from the layout's maximum until
the wrapped height fits the box. Slide-level headings fit in one-pixel steps
within 20–48px on title slides, 20–44px on section slides, 15–36px beside
body text, 14–28px when stacked or beside an image, and 12–24px above code,
tables, or columns. Body text and column subheadings use their own ranges.
The compiled document carries the sizes into preview, presentation, and
saved exports. Headlines keep their text as written and
are marked bold in the document itself, so exports match the editor; nothing
is transformed to capitals. A statement or bullets slide whose
text cannot fit beside the headline at the minimum size falls back to `stack`,
which splits eight or more bullets into two columns. When even the minimum
overflows, the slide carries a warning naming it; the answer is to split the
slide, never to shrink further, because the API rejects overflowing text at
render time.

Body text is written with the theme's muted colour and bold runs are lifted to
the foreground by `FormSlideCanvas` (`.emphasis`), the reference deck's white on
grey contrast. Exports keep the bold weight and the muted colour. The document
carries the label and the `NN / NN` slide number as small text elements, so
exports carry them too.

The Vmax watermark is an image element on every slide: on the first save the
site rasterizes the logo (`slides-watermark.ts`, opaque black on a transparent
PNG, the same in both themes) and uploads it as a deck asset, storing
`watermarkAssetId` in `vmax_slides`;
`compileSlidesMarkdown(source, { watermarkAssetId })` then places it at
`slideWatermarkBox` (43 by 36 canvas pixels, 64 from the right, 44 from the
bottom) with `alt` equal to `SLIDE_WATERMARK_ALT`. The site's CSS mark covers a
deck only until that first save. A tool authoring documents directly gets the
mark by uploading its own PNG and adding the same element; the decompiler skips
any image whose alt is `SLIDE_WATERMARK_ALT`. The hidden badge stays site only.

Theme palettes (`SLIDE_THEME_PALETTES`): light is white, black, neon green;
dark is the light palette inverted through `invertSlideColor` (black, white,
the inverted muted grey) with the site's daybreak orange as its accent. The
document font is `times-new-roman`, so exports are a serif; the site renders EB
Garamond, the site's own serif, with the headline at the one shipped weight.

## Images, downloads, and publication

`uploadSlideImage` hashes the file with Web Crypto, creates the asset, POSTs the
returned fields plus the file to the presigned URL, completes the asset, waits
for the finalize job, and returns the ready asset with its signed URL. The editor
uses the same actions in a toolbar above the field and in its footer. Add
image accepts multiple files for the selected preview; dropping files into a
gold Markdown section targets that section. Local `blob:` image references
show immediately and become `asset:UUID` after upload. Deleting a reference
removes its image, including while upload is in flight. Save waits for active
uploads and can retry a failed local image. Asset URLs live five minutes; the
controller refreshes them every four. `uploadPendingSlideImages(source, upload)`
is the other way in: on save it fetches every URL or site path image the
compiler listed as pending, including local files and pictures outside the
current template's visible capacity, uploads each once, and rewrites the source lines to
the asset ids; a fetch that fails or returns a non-image fails the save with
the source named, so a deck never stores a picture it cannot render. The example
deck's figures live in `public/slides/intent-2026/`. `createSlideDeckFromSource`
uses the same upload path during creation and Fork: it first creates a deck
without image elements, uploads into that deck, then saves its complete
document. Existing asset references are resolved through their source deck
and copied, because the API requires every image to belong to its deck.

A download is a render job over the last saved revision: the site polls until a
terminal status and lists artifacts with five minute URLs; the job keeps them
seven days. Publishing requires a successful complete `html` render of the
revision, so `publishDeck` renders first, then publishes, then persists the
publication into `vmax_slides`. The public page is
`/slides/shared/{publication}` over `GET /api/slides/public/{publication}`,
which returns the audience projection (no notes, no hidden slides) and signed
asset URLs.

## Authoring from another tool

Send `document` JSON shaped by `compileSlidesMarkdown` when the deck should read
like Vmax's, or send `markdown` to the API for its plainer server layout. Keep
`vmax_slides.markdown` in `data` if the deck should reopen as Markdown on
`/slides`. Respect `expected_version` on every write and reconcile on `409`.

## Verify a change

Update `slides-api.ts`, the pure modules (`slides-markdown.ts`,
`slides-typeset.ts`, `slides-compose.ts`), the view model, the fixtures, the
surfaces, and their tests together. `slides-compose.test.ts` pins that every
fixture element stays inside the canvas and that every text element's measured
height fits its box; keep those invariants when changing a layout. Run
`node_modules/.bin/tsc --noEmit --pretty false` and `npm test`. Do not publish
or upload against the live API to test a reader; the fixtures feed on
`/-/vmax-tools-template/slides` simulates every action.
