Developers

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.json

A 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.

OpenAPI spec (for Zapier, Make, or a Custom GPT's Actions)

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.

FieldWhat it isUse it for
files.pdf
files.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.pdf
links.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 plan

Slides 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.

EndpointForTools
/api/mcp
simple
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/advanced
advanced
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.