AI Builder API
The AI Builder API turns a natural-language prompt into a Prezent-quality deck, lets you revise it by describing the change you want, and hands you the finished file.
All three operations stream. You start the work, then read Server-Sent Events for progress and the result — there is no status endpoint to poll.
Full schemas and per-field documentation live in the interactive API Reference.
The AI Builder API changed substantially in 2.0 — polling became streaming, and the many editing endpoints collapsed into one. See What changed from 1.0 below. The 1.0 API is unchanged and still served; its docs are under version 1.0.
Authentication
All endpoints require a Bearer token:
Authorization: Bearer YOUR_API_KEY
See Getting Started → Authentication for details.
Endpoint summary
| Method | Path | Description |
|---|---|---|
POST | /api/v2/generate | Generate a presentation |
POST | /api/v2/autogenerator/edit | Revise a presentation |
POST | /api/v2/autogenerator/download | Download the file |
POST | /api/v2/streams/sessions | Get a stream URL for a callback_id |
GET | /api/v1/autogenerator/templates | List available templates |
GET | /api/v2/autogenerator/languages | List supported languages |
GET | /api/v2/integrations/auth | Connect a third-party app |
GET | /api/v2/integrations/auth/status | Check an app's connection |
GET /api/v1/autogenerator/templates keeps its v1 path — it is
unchanged in 2.0 and there is no v2 equivalent.
How the streaming flow works
Generate and edit both follow the same three steps:
1. POST /api/v2/generate → { resData: { callback_id } }
2. POST /api/v2/streams/sessions → { data: { stream_url } }
with that callback_id
3. open stream_url (SSE) → progress events…
terminal event + download_url
Download is two steps, not three —
POST /api/v2/autogenerator/download returns a stream_url directly,
so it skips step 2.
The event contract
Every event on a v2 stream has the same shape, so one parser handles the whole stream:
event: status
data: {"status":"inprogress","message":"Analyzing the key elements","callback_id":"3f8e7d61-…"}
| Field | Notes |
|---|---|
status | inprogress while working; then one of success, failed, cancelled |
message | Human-readable step description — safe to show a user verbatim |
callback_id | The generation or edit turn this event belongs to |
download_url | Only on a success event. May be null on a generation stream if the file has not been exported yet |
The stream closes after the terminal event. Branch on status rather
than on the SSE event: name — it carries the same information and is
simpler.
Progress messages are drawn from a fixed set, in order:
- Understanding your prompt
- Checking for app references — only when the prompt references a connected app
- Analyzing the key elements
- Adjusting structure for clarity
- Optimizing spacing and alignment
- Crafting your speaker notes — only when notes were requested
- Finalizing your slides
Steps that do not apply are skipped, so do not treat this as a fixed-length progress bar.
Reconnection, timeouts, and full client code are in Streaming (SSE).
Response envelopes
Errors use the canonical envelope on every endpoint:
{ "success": false, "data": null, "error": { "code": "...", "message": "..." } }
Success shapes are not uniform — each endpoint documents its own:
| Endpoint | Success shape |
|---|---|
POST /api/v2/generate | { message, resData, error } |
POST /api/v2/autogenerator/edit | { message, resData, error } |
POST /api/v2/autogenerator/download | flat top-level fields |
POST /api/v2/streams/sessions | { success, data } |
GET /api/v2/autogenerator/languages | { status, message, data } |
GET /api/v1/autogenerator/templates | { success, data } |
GET /api/v2/integrations/auth | one of two shapes — see the endpoint |
See Developer Guide → Error codes for the full code catalog.
POST /api/v2/generate — Generate a presentation
Starts a generation and returns a callback_id. Nothing else — the deck
arrives over the stream.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
prompt | string | yes | What to generate, in plain language. |
template_id | string | no | Brand template to apply. Takes precedence over settings.template_id; falls back to your key's configured template. |
audience_id | string | no | Audience profile to tailor tone and detail to. See Audiences. |
audience | object | no | Object form, { "id": "…" }. audience_id takes precedence. |
files | array | no | [{ "file_id": "…" }] — files to ground the deck in. Upload first via POST /api/v1/preprocess. |
web_links | array | no | URLs for the generator to read. |
text | string | no | A block of text as source material. |
texts | array | no | Several blocks of text. Takes precedence over text. |
websearch | boolean | no | Let the generator search the web. Takes precedence over settings.websearch. Default false. |
externalContext | boolean | no | Alias for websearch. |
settings | object | no | Generation settings — see below. |
Be specific in the prompt. "A 10-slide deck on renewable-energy market trends for a board audience" produces a better result than "renewable energy".
Settings
Every key in settings is optional. An omitted key falls back to
the default configured on your API key, then to the documented default —
so a fully configured key needs no settings object at all.
| Key | Type | Accepted values / default |
|---|---|---|
template_id | string | Any template you can access. Root template_id wins. |
language | string | A locale from /api/v2/autogenerator/languages. Default en. |
websearch | boolean | Default false. Also settable at the root of the request, which takes precedence. |
add_sources_to_footer | boolean | Default false. |
add_sources_to_slides_note | boolean | Default false. Takes precedence over add_speaker_notes_to_slides_note. |
generate_speaker_notes | boolean | Default false. |
add_speaker_notes_to_slides_note | boolean | Default false. Ignored when add_sources_to_slides_note is enabled. |
image_library | object | { brand_library, prezent_library, ai_images, my_workspace, additional_context }, each a boolean. Omitted keys default to false, except ai_images, which defaults to true when image_library itself is omitted. company_library and brand_library are the same library — set brand_library. |
voice_settings | object | { "type": "standard" } — brand-voice profile. |
deck_default_size | integer | 1–200, default 7. The prompt can override it. |
deck_max_size | integer | 1–200, default 100. Must be ≥ deck_default_size. |
export_type | string | pptx or pdf. Default pptx. |
enable_mcp_context | boolean | Default false. Pull context from connected apps. |
Example
- cURL
- Python
- Node.js
curl -X POST https://api.prezent.ai/api/v2/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A 10-slide deck on renewable-energy market trends for 2026",
"template_id": "your-template-id",
"settings": {
"language": "en",
"websearch": true,
"generate_speaker_notes": true
}
}'
import requests
resp = requests.post(
"https://api.prezent.ai/api/v2/generate",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"prompt": "A 10-slide deck on renewable-energy market trends for 2026",
"template_id": "your-template-id",
"settings": {
"language": "en",
"websearch": True,
"generate_speaker_notes": True,
},
},
)
resp.raise_for_status()
callback_id = resp.json()["resData"]["callback_id"]
const res = await fetch('https://api.prezent.ai/api/v2/generate', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: 'A 10-slide deck on renewable-energy market trends for 2026',
template_id: 'your-template-id',
settings: { language: 'en', websearch: true, generate_speaker_notes: true },
}),
});
const { resData } = await res.json();
const callbackId = resData.callback_id;
Success response (200)
{
"message": "Job submitted successfully",
"resData": {
"callback_id": "3f8e7d61-9a2b-4c5e-b013-7d4a6f2c8e91"
},
"error": null
}
The payload is under resData, not data.
callback_id is valid for about 3 hours and is what you pass to
streams, edit, and download.
Validation errors (422)
Field-level failures return INVALID_INPUT with error.details as an
array of messages, one per failed field:
{
"success": false,
"data": null,
"error": {
"code": "INVALID_INPUT",
"message": "The request contains invalid input.",
"details": [
"prompt is required",
"settings.language: 'xx' is not a supported language locale"
]
}
}
POST /api/v2/autogenerator/edit — Revise a presentation
Applies a natural-language edit. One endpoint replaces 1.0's
regenerate, node-change, and slide-actions — describe the change
instead of choosing an operation.
Edits are conversational: each builds on the last. Pass the
callback_id of the most recent turn, and use the new id you get
back for the next edit.
A slide can only be addressed once the deck size is known, so editing a
callback_id that is still generating returns 400. Wait for the
stream's terminal event first.
Only one turn runs at a time — if a previous edit is still in flight,
the request is rejected with that reason in error.message.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
callback_id | string | yes | The presentation to edit — the id of the original generation, or of the most recent edit. |
prompt | string | yes | The change to make, in plain language. |
slideIndex | integer | no | Which slide to edit, counting from 0. Omit to edit the whole deck. |
files | array | no | [{ "file_id": "…" }] — context for this edit. |
web_links | array | no | URLs to read as context. |
text | string | no | A block of text as context. |
slideIndex is validated against the actual deck — a value past the
last slide returns 400 reporting the real slide count.
Example
# Edit the whole deck
curl -X POST https://api.prezent.ai/api/v2/autogenerator/edit \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"callback_id": "3f8e7d61-9a2b-4c5e-b013-7d4a6f2c8e91",
"prompt": "Make the tone more formal and shorten every bullet"
}'
# Edit slide 5 (index 4)
curl -X POST https://api.prezent.ai/api/v2/autogenerator/edit \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"callback_id": "3f8e7d61-9a2b-4c5e-b013-7d4a6f2c8e91",
"prompt": "Replace the chart with a comparison table",
"slideIndex": 4
}'
Success response (200)
{
"message": "Edit submitted successfully",
"resData": {
"callback_id": "8c1d4b92-6f37-4e2a-a95c-1b0e7d3f6a48"
},
"error": null
}
This callback_id is new — not the one you sent. Stream it to watch
the edit, and pass it as the callback_id of your next edit to keep
building on this version.
POST /api/v2/autogenerator/download — Download the file
Returns a stream_url directly — no /streams/sessions step. Open
it and read the terminal event for the download_url.
A stream rather than an inline URL because the file may not exist yet:
| Case | What you see |
|---|---|
| Already exported in this format | terminal event immediately, with the download_url |
| Needs exporting | progress events, then the terminal event with the new URL |
An export can take several minutes — far longer than an HTTP response allows — which is why both cases are delivered the same way.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
callback_id | string | yes | The presentation to download. |
export_type | string | no | pptx or pdf. Defaults to your key's configured format, then pptx. |
Example
curl -X POST https://api.prezent.ai/api/v2/autogenerator/download \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"callback_id": "3f8e7d61-9a2b-4c5e-b013-7d4a6f2c8e91",
"export_type": "pptx"
}'
Success response (200)
{
"message": "Connect to stream_url to receive the download URL on the done event",
"status": "success",
"callback_id": "3f8e7d61-9a2b-4c5e-b013-7d4a6f2c8e91",
"export_type": "pptx",
"stream_url": "https://stream-api-prod.myprezent.com/v2/streams/a1b2c3?token=eyJhbGciOi...&turn_id=3f8e7d61&stream_type=export&export_type=pptx",
"expires_at": "2026-08-05T12:34:56.000Z"
}
Fields are at the top level, not under data.
status: "success" confirms the stream was minted — it does not
mean the file is ready. Read the stream's terminal event for that.
stream_url expires at expires_at (about 5 minutes). Call the
endpoint again for a fresh one.
Quota (429)
Download quota is checked before the stream is minted, so a key without room never receives a URL it cannot redeem:
{
"success": false,
"data": null,
"error": {
"code": "USAGE_LIMIT_EXCEEDED",
"message": "Usage limit exceeded.",
"details": "This presentation has 12 slide(s), but only 5 download slide(s) remain on your plan (limit 50000)."
}
}
POST /api/v2/streams/sessions — Get a stream URL
Exchanges a callback_id from generate or edit for an authenticated SSE
URL.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
callback_id | string | yes | The id from generate or edit. |
Example
curl -X POST https://api.prezent.ai/api/v2/streams/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "callback_id": "3f8e7d61-9a2b-4c5e-b013-7d4a6f2c8e91" }'
Success response (201)
{
"success": true,
"data": {
"callback_id": "3f8e7d61-9a2b-4c5e-b013-7d4a6f2c8e91",
"stream_url": "https://stream-api-prod.myprezent.com/v2/streams/a1b2c3?token=eyJhbGciOi...&turn_id=3f8e7d61",
"expires_at": "2026-08-05T12:34:56.000Z"
}
}
The token is in the URL, so send no Authorization header when
opening it — browsers' EventSource cannot set headers anyway.
stream_url expires in about 5 minutes. Calling this endpoint again
mints a fresh URL, and the stream replays from the start of the
turn — so a late reconnect loses nothing.
For an edit, the URL also replays from the point the edit was queued, so you see the whole edit even if you connect a moment late.
Errors
| Status | error.code | Meaning |
|---|---|---|
400 | MISSING_REQUIRED_FIELD | callback_id absent or not a string. |
404 | STREAM_CALLBACK_NOT_FOUND | No generation found for this id under your key, or its session expired (~3h). |
GET /api/v1/autogenerator/templates — List templates
Returns the brand templates available for generation — the values you
pass as template_id. Unchanged from 1.0, and still on the v1 path.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | no | Case-insensitive name filter. |
sort | string | no | Sort expression (for example name:asc). |
source | string | no | Source filter (brand vs prezent). |
enabledFeature | string | no | Optional feature flag filter. |
limit | integer | no | Items per page (1–200, default 50). Enables pagination. |
cursor | string | no | Opaque cursor from a previous response's next_cursor. |
Success response (200)
{
"success": true,
"data": {
"items": [
{ "id": "tmpl_corp_2024", "name": "Corporate 2024", "code": "CORP24", "source": "brand" }
],
"next_cursor": null
}
}
Results are always scoped to the auto_generator feature server-side;
the caller cannot override the scoping. See
Developer Guide → Pagination.
GET /api/v2/autogenerator/languages — List supported languages
The languages a deck can be generated in. Pass a locale from this list
as settings.language; an unsupported value is rejected with 422.
The list is static — cache it rather than calling this before every generation.
Example
curl -s https://api.prezent.ai/api/v2/autogenerator/languages \
-H "Authorization: Bearer YOUR_API_KEY"
Success response (200)
{
"status": "success",
"message": "Supported languages retrieved successfully",
"data": [
{ "language": "Arabic", "locale": "ar" },
{ "language": "Chinese (Simplified)", "locale": "zh" },
{ "language": "English (UK)", "locale": "en-uk" },
{ "language": "English (US)", "locale": "en" },
{ "language": "French", "locale": "fr" },
{ "language": "German", "locale": "de" },
{ "language": "Hindi", "locale": "hi" },
{ "language": "Indonesian", "locale": "id" },
{ "language": "Italian", "locale": "it" },
{ "language": "Japanese", "locale": "ja" },
{ "language": "Korean", "locale": "ko" },
{ "language": "Portuguese (Brazil)", "locale": "pt" },
{ "language": "Spanish", "locale": "es" }
]
}
GET /api/v2/integrations/auth — Connect an app
Connects a third-party app (Google Drive, Notion, …) to the caller's
Prezent account. Once connected, a generation can pull context from it
by setting settings.enable_mcp_context to true.
The connection is made for the user who owns the API key — there is no parameter to connect an app on someone else's behalf.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
mcp_id | string | yes | Identifier of the app to connect. |
Two response shapes
Which you get is a property of the app, not something you choose. Branch
on whether status is present.
Authorization required — send the user to the URL:
{ "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=..." }
The URL is single-use and time-limited; request a fresh one rather than
storing it. After the user authorizes, confirm with
/auth/status.
No authorization needed — already done:
{ "status": "connected", "message": "MCP connection successful" }
Errors
| Status | error.code | Meaning |
|---|---|---|
400 | MISSING_REQUIRED_FIELD | mcp_id not supplied. |
404 | MCP_NOT_FOUND | No app registered under this mcp_id. |
503 | SERVICE_UNAVAILABLE | The integrations service is temporarily unreachable. |
GET /api/v2/integrations/auth/status — Check an app's connection
Whether an app is currently connected for the caller. Use it after sending a user through an authorization URL, and before a generation that depends on the app.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
mcp_id | string | yes | Identifier of the app to check. |
Example
curl -s "https://api.prezent.ai/api/v2/integrations/auth/status?mcp_id=google-drive" \
-H "Authorization: Bearer YOUR_API_KEY"
Success response (200)
{ "connected": false }
End-to-end example
Generate a deck, stream it to completion, then download it.
- Python
- Node.js
import json
import httpx # pip install httpx httpx-sse
from httpx_sse import connect_sse
BASE = "https://api.prezent.ai"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
def read_stream(stream_url):
"""Read a v2 stream to its terminal event; return the download_url."""
# The token is in the URL — send no auth header here.
with httpx.Client(timeout=None) as sc:
with connect_sse(sc, "GET", stream_url) as events:
for sse in events.iter_sse():
event = json.loads(sse.data)
print(event["status"], "-", event["message"])
if event["status"] == "inprogress":
continue
if event["status"] != "success":
raise RuntimeError(f"{event['status']}: {event['message']}")
return event.get("download_url")
raise RuntimeError("stream closed without a terminal event")
with httpx.Client(base_url=BASE, headers=HEADERS, timeout=30) as c:
# 1. start the generation
gen = c.post("/api/v2/generate", json={
"prompt": "A 10-slide deck on renewable-energy market trends for 2026",
"template_id": "your-template-id",
})
gen.raise_for_status()
callback_id = gen.json()["resData"]["callback_id"]
# 2. mint a stream URL, 3. read it to completion
sess = c.post("/api/v2/streams/sessions", json={"callback_id": callback_id})
sess.raise_for_status()
download_url = read_stream(sess.json()["data"]["stream_url"])
# 4. the generation stream may finish before the file is exported.
# A null download_url means "ask for the export", not "failed".
if not download_url:
dl = c.post("/api/v2/autogenerator/download", json={
"callback_id": callback_id,
"export_type": "pptx",
})
dl.raise_for_status()
download_url = read_stream(dl.json()["stream_url"])
print("deck ready:", download_url)
import { EventSource } from 'eventsource'; // npm install eventsource
const BASE = 'https://api.prezent.ai';
const HEADERS = {
Authorization: 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
};
const post = async (path, body) => {
const res = await fetch(`${BASE}${path}`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path} → ${res.status} ${await res.text()}`);
return res.json();
};
// Read a v2 stream to its terminal event and resolve the download_url.
const readStream = (streamUrl) =>
new Promise((resolve, reject) => {
// The token is in the URL — no Authorization header here.
const es = new EventSource(streamUrl);
es.onmessage = (e) => {
const event = JSON.parse(e.data);
console.log(event.status, '-', event.message);
if (event.status === 'inprogress') return;
es.close();
if (event.status === 'success') resolve(event.download_url ?? null);
else reject(new Error(`${event.status}: ${event.message}`));
};
es.onerror = () => { es.close(); reject(new Error('stream failed')); };
});
// 1. start
const { resData } = await post('/api/v2/generate', {
prompt: 'A 10-slide deck on renewable-energy market trends for 2026',
template_id: 'your-template-id',
});
const callbackId = resData.callback_id;
// 2 + 3. stream to completion
const { data } = await post('/api/v2/streams/sessions', { callback_id: callbackId });
let downloadUrl = await readStream(data.stream_url);
// 4. a null download_url means "ask for the export", not "failed"
if (!downloadUrl) {
const dl = await post('/api/v2/autogenerator/download', {
callback_id: callbackId,
export_type: 'pptx',
});
downloadUrl = await readStream(dl.stream_url);
}
console.log('deck ready:', downloadUrl);
On a generation stream, download_url can be null — the deck is built
but not yet exported to a file. Treat null as "call download", not as
an error. On a download stream it is always populated.
What changed from 1.0
| 1.0 | 2.0 | |
|---|---|---|
| Generate | POST /api/v1/autogenerator | POST /api/v2/generate |
| Track progress | poll GET /api/v1/autogenerator/status | open an SSE stream |
| Revise | regenerate, node-change, slide-actions | POST /api/v2/autogenerator/edit |
| Download | POST /api/v1/autogenerator/download | POST /api/v2/autogenerator/download |
| Templates | GET /api/v1/autogenerator/templates | unchanged |
There is no status endpoint in 2.0. Progress arrives as events, so there is no polling loop to write and no interval to tune — and the progress messages are written for display, so you can show them to a user directly.
Editing is one endpoint. Rather than choosing between regenerate, node-change, and slide-actions, describe the change you want. Edits chain, so a sequence of revisions is a conversation.
These 1.0 endpoints have no 2.0 equivalent: status-bulk, meta,
slide-data, extract-images, brand-image-search,
library-image-search, replace-image, and reaction-feedback. If you
depend on any of them, stay on
1.0 — both versions run side by side and
there is no forced migration.
Errors
Every error uses the canonical envelope. The codes you are most likely to see on this surface:
| Status | error.code | Cause |
|---|---|---|
400 | BAD_REQUEST, INVALID_JSON, MISSING_CALLBACK_ID, MISSING_REQUIRED_FIELD, INVALID_DATA_TYPE | Malformed request, unknown callback_id, deck not ready to edit, slideIndex out of range. |
401 | UNAUTHORIZED, INVALID_API_KEY, EXPIRED_API_KEY | Missing, invalid, or expired key. |
404 | ENDPOINT_NOT_FOUND, STREAM_CALLBACK_NOT_FOUND, MCP_NOT_FOUND | Key not scoped to this path, expired session, or unknown app. |
422 | INVALID_INPUT | Field validation failed — error.details is an array of messages. |
429 | TOO_MANY_REQUESTS, RATE_LIMIT_EXCEEDED, USAGE_LIMIT_EXCEEDED | Gateway throttle, per-category limit, or annual quota. |
500 | INTERNAL_SERVER_ERROR | Unexpected server error. |
503 | SERVICE_UNAVAILABLE | A downstream service is temporarily unreachable. |
Full catalog: Error Reference.
Where to go next
- Streaming (SSE) — reconnection, timeouts, and production-ready client patterns.
- API Reference — every field, interactively.
- Rate Limits — the three limit tiers and how to back off.
- Template Converter — apply a brand template to an existing deck (unchanged in 2.0).