Authentication
Create a key on the Team page (owners only). It acts for that workspace and spends the credits of whoever created it. Send it on every request:
Authorization: Bearer sk_live_…Connecting quickly
Every route below answers Access-Control-Allow-Origin: *, so it can be called directly from a browser — your own frontend, a low-code tool's live tester, a Zapier or Make HTTP step — not only from a server. A tool that connects by importing a schema rather than by hand-written requests (a ChatGPT Custom GPT's Actions, Zapier's OpenAPI import) can point at the spec directly:
GET https://your-domain/api/openapi.jsonA first call worth trying before anything else — proves the key and the workspace in one line:
curl https://your-domain/api/v1/me -H "Authorization: Bearer sk_live_…"Setup prompt for an external AI
Copy one of these into the AI that should be producing decks on your behalf — a coding agent, ChatGPT, or anything else you can hand instructions to. It explains the API well enough to use on its own.
These are generic — replace YOUR_API_KEY with a key from the Team page. Minting a key there gives you this same guide with the key already filled in.
A coding agent, or any AI that can call HTTP
Claude Code, Cursor's agent, a script, or any tool you can hand a system prompt to.
ChatGPT — Custom GPT Actions
Paste the OpenAPI URL as the Action's schema, and this as its instructions.
Plan a deck
The plan is written and returned at once (202). Images are generated in the background over the next minutes; read progress with GET, or give a webhookUrl. Repeat a request safely with an Idempotency-Key header — the same key returns the same deck instead of planning twice.
POST /api/v1/decks
Idempotency-Key: 4f1c-… (optional)
{
"generationMode": "fast", // "fast" for end-to-end, "advanced" for full control
"topic": "Why mid-market CFOs stall on AI",
"elements": "3 causes, 3 fixes, a practical CTA",
"content": "Why mid-market CFOs stall on AI — 3 causes, 3 fixes", // use this in advanced mode
"mode": "slide", // "carousel" | "slide" | "newsletter" | "report"
"slides": 8, // 3–40 (newsletter 1–6, default 1; report 2–24, default 8)
"ratio": "16:9", // defaults by mode: 1:1, 16:9, 1:2, 210:297 (A4)
"quality": "low", // "low" (1 credit/image) | "medium" (5x) | "high" (50x)
"structure": "Cover stating the value, then a summary, then one schema per idea.",
"styleInstructions": "Navy palette, no stock photography.",
"stylePresetId": "…", // or: a saved preset fills tone, instructions, structure
"referenceImageUrls": ["https://…/brand-ref.png"],
"referenceBankId": "…", // or: a saved bank supplies the references (first 3)
"textAmount": "detailed", // "short" | "detailed" | "hyper"
"webhookUrl": "https://your-app.example/hooks/studio",
"planOnly": false
}
→ 202 { "deck": { "id": "…", "status": "planning", "slides": [ … ],
"files": { "status", "pdf", "zip" }, "links": { "self", "pdf", "zip", "studio" } } }Two kinds of file URL — pick the right one
Every deck response carries both, and they are not interchangeable.
| Field | What it is | Use it for |
|---|---|---|
files.pdffiles.zip | A permanent, public URL on the CDN. No Authorization header, no expiry, no rendering on fetch. The same store the slide images already live on. | Anything that stores the URL and downloads it later — a publishing tool, a CMS, a scheduler, a spreadsheet, a webhook consumer. |
links.pdflinks.zip | This app's own route, rendering on demand, with a signed ?token= good for 30 minutes. | A person clicking it now, from a chat reply or an email. |
files.status is ready when the URLs are current for the slides the deck shows now, pending while they are being built, and unavailable when nothing has been generated yet. A deck publishes its files by itself as soon as it finishes, so a caller that polls or takes the webhook usually finds them already there. To not wait:
POST /api/v1/decks/{id}/files builds the PDF + ZIP if needed, answers with the URLs
GET /api/v1/decks/{id}/files what is published right now, builds nothing
→ 200 { "files": { "status": "ready",
"pdf": "https://<cdn>/decks/<id>/<signature>/deck.pdf",
"zip": "https://<cdn>/decks/<id>/<signature>/deck.zip",
"updatedAt": "…" } }The <signature> in the path is of the exact images that went into the file, which is what makes the URL safe to cache forever and safe to hand to something that keeps it: that path is always those pages. Regenerate a slide and the next call publishes a new URL for the new pages — the old one keeps serving the old ones, and files.status goes back to pending until the new pair exists, so a stale URL is never reported as current.
A deployment with no blob store configured answers 501 file_store_unconfigured and files.status: "unavailable"; the signed links still work there.
The four formats
carousel — a run of square social panels, one idea each. slide — a 16:9 presentation. newsletter — one tall page carrying a whole argument: a header band with an eyebrow, a headline and a standfirst, then four to six numbered sections that each show their point as a diagram, then a checklist band and a call to action. A newsletter is normally one page; ask for more only when there is a genuine second chapter.
report — a run of A4 pages that reads as a printed document: a cover, a contents page from six pages up, one numbered section per page with prose and at most one table, chart or callout, and a closing page of conclusions. Its PDF is laid on true 210 × 297 mm sheets, so it prints without scaling. Ask for quality: "medium" or "high" on a report — a page of prose is the hardest thing for an image model to render legibly.
Style presets, reference banks and reference images work identically in all three: the same house style, the same reference images sent with every page.
Quality and what it costs
quality: "low" is the default and costs 1 credit per image, plus 1 per reference image per page. "medium" costs 5x the base price and "high" costs 50x for maximum detail. A 10-page deck with 2 references is 30 credits at low, 150 at medium, or 1,500 at high. The tier is fixed when the deck is planned and every slide of it, including a later regeneration, is drawn and charged at that tier. Accounts marked unlimited are not charged either way.
Read, regenerate, export
GET /api/v1/me who the key acts for + limits (slides per mode, references, ratios, qualities)
GET /api/v1/style-presets saved presets → stylePresetId
GET /api/v1/reference-banks saved banks → referenceBankId
POST /api/v1/extract a PDF / .docx / text / link → the content to plan from
GET /api/v1/decks the workspace's recent decks
GET /api/v1/decks/{id} status + every slide (caption, prompt, imageUrl, error) + visualSystem
POST /api/v1/decks/{id} start or resume generating pending / failed slides
POST /api/v1/decks/{id}/slides/{n}/regenerate optional body { "note": "what was wrong" }; the old image stays in the page's history
POST /api/v1/decks/{id}/slides/{n}/revert put the page's previous image back
POST /api/v1/decks/{id}/proofread read the finished pages back: what differs from each caption (no credits)
POST /api/v1/decks/{id}/remake same plan, other ratio / quality / style — a low draft becomes the medium final
POST /api/v1/decks/{id}/wait block until every page has settled (or up to 240 s)
GET /api/v1/decks/{id}/export.pptx the deck as a PowerPoint file (captions become speaker notes)
POST /api/v1/decks/{id}/files publish the PDF + ZIP to permanent public URLs
GET /api/v1/decks/{id}/files the permanent URLs as they stand
GET /api/v1/decks/{id}/export.pdf one page per finished slide (bytes)
GET /api/v1/decks/{id}/export.zip the finished slides as numbered image files (bytes)
POST /api/v1/decks/{id}/share a public, revocable share page (open or email-gated)
POST /api/v1/estimate what a deck would cost, against the balance
POST /api/v1/images one to three standalone images
Editing the plan — after POST /api/v1/decks with "planOnly": true, or on any deck:
PATCH /api/v1/decks/{id}/slides/{n} caption and/or imagePrompt; "regenerate": true redraws it
POST /api/v1/decks/{id}/slides add a page (afterPosition, copyFromPosition, caption, imagePrompt)
DELETE /api/v1/decks/{id}/slides/{n} remove a page; the rest are renumbered
PATCH /api/v1/decks/{id}/order { "order": [3, 1, 2] } — today's positions in their new order
POST /api/v1/decks/{id}/refine { "feedback": "…" } — the planner rewrites the planSlides are drawn a few at a time on the server (workers passing the baton), so a forty-slide deck is never forty requests at once. Poll GET /api/v1/decks/{id} or take the webhook.
links.pdf and links.zip in every deck response already carry a signed ?token= — the exact URL works on its own, no Authorization header, which is what makes it safe to hand to a person or paste into a chat reply. It is scoped to that one deck and that one file, and good for 30 minutes; GET the deck again for a fresh pair. The header still works too, on a URL with no token.
Errors are { "error": "…", "code": "…" }. 402 means the wallet behind the key is empty.
Webhooks
Events: deck.planned, deck.completed, deck.failed. A deck.completed payload carries the finished deck including its permanent files.pdf and files.zip — the files are published before the delivery goes out, so a consumer can hand the URL straight on. Each POST carries the deck as JSON and a signature over <timestamp>.<body> with the key's webhook secret. Verify it and reject anything older than five minutes:
X-Studio-Event: deck.completed
X-Studio-Signature: t=1725350400,v1=<hex hmac-sha256>
// Node
const [t, v1] = header.split(",").map((kv) => kv.split("=")[1])
const expected = crypto.createHmac("sha256", WEBHOOK_SECRET).update(`${t}.${rawBody}`).digest("hex")
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
const valid = fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))Deliveries are retried twice with backoff; answer 2xx quickly and do the work afterwards.
MCP — connect Claude, ChatGPT, or your own agent
One MCP server, two faces. Connect to the one that matches what its user does; the same account and the same credits sit behind both. Two ways in: OAuth — the client discovers the authorization server next to the endpoint, registers itself, sends the workspace owner to a consent page, and holds a token that renews itself (what the hosted assistants need); or a key in the header, for tools that let you set one.
| Endpoint | For | Tools |
|---|---|---|
/api/mcpsimple | Asking. "Make me a carousel about X" and getting the file back. Nothing to explain. | make_deck (plan, draw, wait, publish — one call), create_deck, get_deck, get_deck_files, list_decks, list_style_presets, list_reference_banks, estimate_cost, regenerate_slide, remake_deck (the same plan at another quality or ratio) |
/api/mcp/advancedadvanced | Working. Plan without drawing, read every caption, change a line, ask for a rewrite, add or drop a page, reorder, then draw — and share, read documents, make single images. | All of the above, plus get_workspace, plan_deck, edit_slide, refine_deck, add_slide, delete_slide, reorder_slides, generate_deck, wait_for_deck, proofread_deck, revert_slide, share_deck, read_document, generate_image |
Both endpoints also serve prompts — ready-made ways of asking that a client shows as a menu (carousel_from_notes, slides_from_document, newsletter_from_article, report_from_findings, fix_a_page, proofread_and_fix, final_from_draft), each a brief that names the tools to call in order — and resources the assistant can read: studio://guide (the workflow, the formats, the prices), studio://workspace (credits, limits, and which providers are configured), studio://style-presets, studio://reference-banks. Every tool that spends is checked against the connection's scope per call; estimate_cost is there so an assistant can say the figure before spending it.
The whole thing in one paste. GET /llms.txt returns this API written out in full — every field, every error code, a worked example — as plain text meant to be handed to another AI tool or dropped into a system prompt. Point an assistant at that URL and it can drive the API with no other briefing. Decks also come out as PowerPoint: files.pptx for a permanent URL, links.pptx for a click now, or GET /api/v1/decks/{id}/export.pptx for the bytes. Each page becomes one slide, and its caption becomes that slide's speaker notes.
When the words must be exact. Pass proofread: true to make_deck or create_deck (or POST /api/v1/decks) and the engine reads every page back after drawing it — the image beside its caption — and draws it once more if a word or a number differs. On any deck, proofread_deck does the same read on demand and lists the findings per page; the verdict then sits under each page's proofread until the page is drawn again, never against an image it was not made for. regenerate_slide takes a note quoting the finding; the previous image stays in the page's history, and revert_slide brings it back. Proofreading spends no credits (it is the planner model, on its image quota); a redraw costs a page.
Simple: https://your-domain/api/mcp
Advanced: https://your-domain/api/mcp/advanced
Claude (claude.ai, Claude Desktop) — Settings › Connectors › Add custom connector
paste the server URL → Connect → sign in to the Studio as the workspace owner → Connect
ChatGPT — Settings › Connectors › Create (needs a plan that allows custom connectors)
paste the server URL, authentication: OAuth → sign in → Connect
Claude Code
claude mcp add --transport http studio https://your-domain/api/mcp/advanced
(a browser window opens for the consent page on first use)
or, with a key and no browser:
claude mcp add --transport http studio https://your-domain/api/mcp/advanced --header "Authorization: Bearer sk_live_…"
Cursor / Windsurf / VS Code — mcp.json
{ "mcpServers": { "studio": { "url": "https://your-domain/api/mcp/advanced",
"headers": { "Authorization": "Bearer sk_live_…" } } } }
A client that only speaks stdio — bridge it with mcp-remote
{ "mcpServers": { "studio": { "command": "npx",
"args": ["-y", "mcp-remote", "https://your-domain/api/mcp/advanced",
"--header", "Authorization: Bearer sk_live_…"] } } }Discovery documents: /.well-known/oauth-protected-resource (and, for the advanced endpoint, /.well-known/oauth-protected-resource/api/mcp/advanced) and /.well-known/oauth-authorization-server; registration at /api/oauth/register, consent at /oauth/authorize, tokens at /api/oauth/token (PKCE S256, refresh with rotation, access 30 days). Connected applications are listed on the Team page, where the owner can disconnect them.
Production links — a hosted form in your own application
Turn a key into a link that opens a production page: no sign-in, the workspace's style presets and reference banks ready to pick, a document to import if you like, and a deck produced end to end — watched on the page, or in the background with the id handed back at once. Frame it, or open it in a tab.
POST /api/v1/production-links (with the API key; a link cannot mint one)
{ "name": "CRM form", "ttlDays": 90 } // expiry: 90 days by default, 365 at most
→ 200 { "token": "pl_…", "url": "https://your-domain/produce#t=pl_…",
"embed": "<iframe src="…/produce#t=pl_…" width="100%" height="900" style="border:0" allow="clipboard-write"></iframe>",
"link": { "id": "…", "kind": "link", "expiresAt": "…" } }
// The same page opens on an existing deck:
https://your-domain/produce#t=pl_…&deck=<deck id>
// When framed, the page reports to its host:
window.addEventListener("message", (e) => {
if (e.data?.source !== "studio") return
// e.data.type: "studio:deck.created" → { deck: { id, status, links } }
// "studio:deck.settled" → { deck: { id, status, slides: [{ position, status, imageUrl }], links } }
})A link carries its key's scopes, spends the same credits and signs with the same webhook secret; it dies with the key and can be revoked on its own from the Team page. Anyone holding it can produce decks until it expires — hand it out like a key with a shelf life. The pl_… token also works as a bearer on every route above.
Embedding a shared production
Any share link renders chromeless with ?embed=1, answers oEmbed at /api/oembed?url=…, and exposes each slide of an open link at /s/{token}/slides/{n}. The share dialog in the library builds the snippet for you.