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.
curl https://heydecks.com/v1/decks \
-H "Authorization: Bearer hd_live_…"200 Response — array of deck summaries:
[
{
"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.
| Name | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Deck title. |
curl -X POST https://heydecks.com/v1/decks \
-H "Authorization: Bearer hd_live_…" \
-H "Content-Type: application/json" \
-d '{"title": "New deck"}'201 Response:
{
"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.
curl https://heydecks.com/v1/decks/deck_abc123 \
-H "Authorization: Bearer hd_live_…"200 Response:
{
"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.
| Name | Type | Required | Description |
|---|---|---|---|
title | string | No | New title. |
slug | string | No | New URL slug. Must be unique in your workspace. Auto-suffixed (-2, -3 …) on conflict. |
visibility | string | No | "public" | "password" | "expiring" |
brand_id | string | No | Attach a different brand by id. |
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.
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.
curl -X POST https://heydecks.com/v1/decks/deck_abc123/publish \
-H "Authorization: Bearer hd_live_…"200 Response:
{
"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.
curl -X POST https://heydecks.com/v1/decks/deck_abc123/unpublish \
-H "Authorization: Bearer hd_live_…"200 Response:
{
"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.
| Name | Type | Required | Description |
|---|---|---|---|
format | string | No | "pdf" (default, any plan) or "pptx" (native editable PowerPoint, Solo plan or higher). |
editable | boolean | No | PPTX only. true (default) exports native shapes and text boxes; false exports picture-fidelity flat images. Ignored for PDF. |
logo | string | No | "color" (default) or "mono" for a black-and-white brand logo. |
ttl_minutes | number | No | How 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.
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:
{
"deck_id": "deck_abc123",
"format": "pptx",
"editable": true,
"url": "https://heydecks.com/s/k3mq7rbn2xa",
"expires_at": "2026-06-12T12:00:00.000Z"
}| Name | Type | Description |
|---|---|---|
deck_id | string | The deck this link is scoped to. |
format | string | "pdf" or "pptx". A link is bound to one format; it can't be re-pointed at another. |
editable | boolean | Whether the PPTX carries native shapes. Always true for PDF, where it has no meaning. |
url | string | The download link. Follow it with curl -L or hand it to a browser. |
expires_at | string | ISO 8601 expiry. After this the link returns 410 Gone. |
#Following the link
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:
| HTTP | Meaning |
|---|---|
| 200 | The file, with Content-Type and Content-Disposition set. |
| 202 | A render is in flight. Wait Retry-After seconds (5) and request the same URL again. |
| 410 | The link has expired. Mint a new one. |
| 404 | Unknown code. |
| 500 | The render failed. |
A
202is 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 the200. 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
202only 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
500instead. 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.
# 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
donePoll 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:
| HTTP | Code | When |
|---|---|---|
| 404 | not_found | No such deck in your workspace. |
| 403 | pptx_not_on_plan | format: "pptx" on the free plan. Export PDF, or upgrade. |
| 402 | subscription_past_due | The account has a billing failure. Update payment to resume API writes. |
| 422 | invalid_input | Unknown format or logo value, non-boolean editable, or ttl_minutes outside 1–1440. |
| 429 | rate_limit | Over 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.
| Name | Type | Required | Description |
|---|---|---|---|
slide | number | No | 0-based slide index. Defaults to 0 (the cover). Out-of-range clamps to the last slide. |
ttl_minutes | number | No | How long the link stays valid. Default 60, maximum 1440 (24 hours). |
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:
{
"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.