heydecks

API

Decks

List, create, update, publish, export, preview, and delete decks.

Base URL: https://heydecks.com

All endpoints require Authorization: Bearer hd_live_….

#GET /v1/decks

List all decks in your workspace.

Shell
curl https://heydecks.com/v1/decks \
  -H "Authorization: Bearer hd_live_…"

200 Response — array of deck summaries:

JSON
[
  {
    "id": "deck_abc123",
    "title": "Q3 board update",
    "slug": "q3-board-update",
    "status": "published",
    "visibility": "public",
    "url": "https://heydecks.com/d/q3-board-update",
    "updated_at": "2026-06-12T10:00:00.000Z",
    "created_at": "2026-06-01T09:00:00.000Z"
  }
]

url is null for draft decks.

#POST /v1/decks

Create an empty deck. Add and edit its slides with the Slides and editing endpoints, the MCP server, or the dashboard editor.

NameTypeRequiredDescription
titlestringYesDeck title.
Shell
curl -X POST https://heydecks.com/v1/decks \
  -H "Authorization: Bearer hd_live_…" \
  -H "Content-Type: application/json" \
  -d '{"title": "New deck"}'

201 Response:

JSON
{
  "id": "deck_xyz789",
  "title": "New deck",
  "slug": "new-deck",
  "status": "draft",
  "visibility": "public",
  "created_at": "2026-06-12T12:00:00.000Z"
}

#GET /v1/decks/{id}

Fetch a single deck with all slide instances and their content.

Shell
curl https://heydecks.com/v1/decks/deck_abc123 \
  -H "Authorization: Bearer hd_live_…"

200 Response:

JSON
{
  "id": "deck_abc123",
  "title": "Q3 board update",
  "slug": "q3-board-update",
  "status": "published",
  "visibility": "public",
  "brand_id": "brand_xyz",
  "locale": "en",
  "url": "https://heydecks.com/d/q3-board-update",
  "slides": [
    {
      "id": "sec_001",
      "slide_type": "title",
      "order": 0,
      "content": {
        "title": "Q3 board update",
        "subtitle": "June 2026"
      },
      "notes": null
    }
  ],
  "created_at": "2026-06-01T09:00:00.000Z",
  "updated_at": "2026-06-12T10:00:00.000Z"
}

#PATCH /v1/decks/{id}

Update deck metadata. Pass only the fields you want to change.

NameTypeRequiredDescription
titlestringNoNew title.
slugstringNoNew URL slug. Must be unique in your workspace. Auto-suffixed (-2, -3 …) on conflict.
visibilitystringNo"public" | "password" | "expiring"
brand_idstringNoAttach a different brand by id.
Shell
curl -X PATCH https://heydecks.com/v1/decks/deck_abc123 \
  -H "Authorization: Bearer hd_live_…" \
  -H "Content-Type: application/json" \
  -d '{"title": "Q3 Board Update (final)", "visibility": "public"}'

200 Response — updated deck metadata (same shape as the GET, without slides).

#DELETE /v1/decks/{id}

Permanently delete a deck and all its slides. Returns 204 No Content.

Shell
curl -X DELETE https://heydecks.com/v1/decks/deck_abc123 \
  -H "Authorization: Bearer hd_live_…"

Warning — Deletion is permanent. There is no undo via the API. Use the dashboard to restore from version history.

#POST /v1/decks/{id}/publish

Make a deck publicly accessible at its slug URL.

Shell
curl -X POST https://heydecks.com/v1/decks/deck_abc123/publish \
  -H "Authorization: Bearer hd_live_…"

200 Response:

JSON
{
  "id": "deck_abc123",
  "status": "published",
  "url": "https://heydecks.com/d/q3-board-update",
  "published_at": "2026-06-12T10:01:00.000Z"
}

#POST /v1/decks/{id}/unpublish

Revert a deck to draft. The public URL stops resolving immediately.

Shell
curl -X POST https://heydecks.com/v1/decks/deck_abc123/unpublish \
  -H "Authorization: Bearer hd_live_…"

200 Response:

JSON
{
  "id": "deck_abc123",
  "status": "draft"
}

#POST /v1/decks/{id}/exports

Mint a short-lived download link for the deck as PDF or PPTX. The call is synchronous and cheap: it returns a signed /s/{code} URL immediately, and the file itself renders the first time that URL is opened. There is no job to poll.

The link is a capability. Anyone holding it can download the deck until it expires, so treat it like a secret and keep the TTL short.

Exports are free on every plan and work on drafts, so you can QA a deck before publishing it.

NameTypeRequiredDescription
formatstringNo"pdf" (default, any plan) or "pptx" (native editable PowerPoint, Solo plan or higher).
editablebooleanNoPPTX only. true (default) exports native shapes and text boxes; false exports picture-fidelity flat images. Ignored for PDF.
logostringNo"color" (default) or "mono" for a black-and-white brand logo.
ttl_minutesnumberNoHow long the link stays valid. Default 60, maximum 1440 (24 hours).

The body is optional — POST with no body exports a colour-logo PDF valid for 60 minutes.

Shell
curl -X POST https://heydecks.com/v1/decks/deck_abc123/exports \
  -H "Authorization: Bearer hd_live_…" \
  -H "Content-Type: application/json" \
  -d '{"format": "pptx", "editable": true, "ttl_minutes": 120}'

201 Response — the link is also returned in the Location header:

JSON
{
  "deck_id": "deck_abc123",
  "format": "pptx",
  "editable": true,
  "url": "https://heydecks.com/s/k3mq7rbn2xa",
  "expires_at": "2026-06-12T12:00:00.000Z"
}
NameTypeDescription
deck_idstringThe deck this link is scoped to.
formatstring"pdf" or "pptx". A link is bound to one format; it can't be re-pointed at another.
editablebooleanWhether the PPTX carries native shapes. Always true for PDF, where it has no meaning.
urlstringThe download link. Follow it with curl -L or hand it to a browser.
expires_atstringISO 8601 expiry. After this the link returns 410 Gone.

Minting a link does not render anything. No browser starts until someone opens the URL, so the first fetch is the one that does the work, and it is the slow one.

GET /s/{code} needs no Authorization header — the code is the capability. It responds with:

HTTPMeaning
200The file, with Content-Type and Content-Disposition set.
202A render is in flight. Wait Retry-After seconds (5) and request the same URL again.
410The link has expired. Mint a new one.
404Unknown code.
500The render failed.

A 202 is not a download. Its body is a self-refreshing HTML holding page, there for browsers. A client that writes any 2xx body to disk will save that HTML and call it a PDF. Branch on the status code, never on the body.

Which of those you actually see depends on the format, because only one of them renders progressively:

  • PDF over 25 slides renders in chunks, one chunk per request. Expect a run of 202s before the 200. This is the common case for a long deck, and the poll loop below is written for it.
  • PDF at 25 slides or fewer, PPTX, and previews render in a single pass. The request that starts the render holds the connection until the file is ready, so it returns 200 — after tens of seconds on a heavy deck. Set a generous client timeout rather than a short one plus a retry.
  • Those single-pass formats return 202 only to a second caller who opens the same link while the first render is still running. Polling is the right response there too: it picks up the artifact the first caller produced.
  • A large PPTX can 500 instead. There is no chunked PPTX renderer yet, so one browser session has to do every slide — the exact limit the chunked PDF path exists to dodge. On a 40-slide deck this is an expected outcome, not a freak error. Handle it: fall back to PDF, or split the deck.
Shell
# Follow an export link to completion. --max-time is generous because a
# single-pass render (any PPTX, a short PDF) blocks until the file is ready.
url=$(curl -sX POST https://heydecks.com/v1/decks/deck_abc123/exports \
  -H "Authorization: Bearer hd_live_…" | jq -r .url)

while :; do
  code=$(curl -s --max-time 180 -o deck.pdf -w '%{http_code}' -L "$url")
  case "$code" in
    200) echo "saved deck.pdf"; break ;;
    202) sleep 5 ;;
    *)   echo "failed: HTTP $code"; rm -f deck.pdf; break ;;
  esac
done

Poll the same link rather than minting a new one. A fresh link resumes from the chunks already finished (render progress is keyed on the deck, not on the code), so re-minting buys nothing and costs a round trip. Once rendered, the file is cached against the deck's last-edited timestamp: a second open of an unchanged deck is an immediate 200, and any edit to the deck invalidates it and re-renders on the next fetch.

That caching has one consequence worth knowing: the link is not a snapshot. It resolves the deck at fetch time, so if the deck is edited between minting and downloading, the download is the edited deck.

Errors:

HTTPCodeWhen
404not_foundNo such deck in your workspace.
403pptx_not_on_planformat: "pptx" on the free plan. Export PDF, or upgrade.
402subscription_past_dueThe account has a billing failure. Update payment to resume API writes.
422invalid_inputUnknown format or logo value, non-boolean editable, or ttl_minutes outside 1–1440.
429rate_limitOver your plan's request rate. See Rate limits.

The PPTX plan check happens when the link is minted, not when it is opened, so a link minted on a free plan can never be upgraded into a PPTX later.

#POST /v1/decks/{id}/previews

Mint a short-lived PNG link for a single slide. Decks built over the API are otherwise built blind: this is how you see what you actually wrote. Free on every plan, works on drafts.

NameTypeRequiredDescription
slidenumberNo0-based slide index. Defaults to 0 (the cover). Out-of-range clamps to the last slide.
ttl_minutesnumberNoHow long the link stays valid. Default 60, maximum 1440 (24 hours).
Shell
curl -X POST https://heydecks.com/v1/decks/deck_abc123/previews \
  -H "Authorization: Bearer hd_live_…" \
  -H "Content-Type: application/json" \
  -d '{"slide": 3}'

201 Response — the link is also returned in the Location header:

JSON
{
  "deck_id": "deck_abc123",
  "slide": 3,
  "total_slides": 12,
  "url": "https://heydecks.com/s/w8dhq2pkr4m",
  "expires_at": "2026-06-12T11:00:00.000Z"
}

slide is the index that was actually rendered after clamping, so compare it against what you asked for if the deck may be shorter than you think. The URL serves a PNG inline, so it can be embedded directly in an <img> tag or a chat message.

The link behaves like an export link — see Following the link. A preview renders in a single pass, so the first fetch blocks briefly and returns 200; it answers 202 only if a second caller opens the same link mid-render.

Errors: 404 not_found, 402 subscription_past_due, 422 invalid_input (negative or non-numeric slide, ttl_minutes outside 1–1440), and 429 rate_limit.