New here? Start with the step-by-step guide. This page is the technical API reference.
Headless API
Version v1 · Machine-readable spec: /api/v1/openapi.json
Drive Pitchstage headlessly — from AI agents (Claude / Cursor via the MCP server), scripts, and CI. Point at a folder of feature screenshots, get a demo video or carousel, iterating cheaply on a plan to find what fits, then committing one render.
All paths below are under /api (e.g. POST /api/v1/projects). The OpenAPI servers base is /api, so the spec lists them as /v1/*.
1. Auth
Workspace-scoped API keys. Mint one in the web app (Settings → API keys); the raw key pk_live_… is shown once — only its SHA-256 hash is stored, giving instant revoke, scope limits, and a per-key spend cap.
Authorization: Bearer pk_live_<32 lowercase-alnum>
Scopes: plan (read-only, no render cost) · render (billable — commits a plan). A plan-only key on a render endpoint returns LK_002.
2. The two-phase loop
POST /v1/projects → create a project (goal + screenshot folders) + presigned uploads PUT <presigned R2 url> → upload each screenshot POST /v1/projects/:id/plan → resolved config + outline + narration + preview frames + fit-score (CHEAP) … mutate any field, resubmit … → fit-score climbs; converge in 2–3 tries POST /v1/projects/:id/render → fork an isolated variant + enqueue the real render (BILLABLE) GET /v1/renders/:id → poll the machine-readable result (or register a webhook)
plan runs the director without ffmpeg/TTS — it returns the fully-resolved config plus per-scene preview frames (vision-inspectable stills, delivered async) and a fit-score against your goal. render commits a chosen plan: it forks a variant (so two variants never stomp each other), applies the config, and runs the existing render pipeline.
3. The resolved-plan object (the contract)
plan returns — and render accepts — one object. It is tiered and read-write symmetric: mutate any field and resubmit; unset fields get director defaults. Unknown fields are ignored (forward-compatible).
| Tier | Stability | Fields |
|---|---|---|
intent | stable (the 80%) | artifactType, goal, audience, tone, aspect, targetLengthS, captionStyle, brandKitId, voiceProfileId |
director | stable | outline, hook, cta, music, smartZoom, transitions, portraitLayout, narrativeTemplate, openingStyle, heroBeatIdx, brand, voice |
scenes[] | no stability promise | per-scene durationS, narrationText, emotion, importance, kineticText, motionPlan, layout, slideSpec, … |
The response also carries warnings[], estimate (durationS, costCents, sceneCount, ttsChars), fit (score 0..1, reasons[], gaps[]), previewFrames[], posterUrl, and generatedAt.
The director reconciles incoherent input — it never silently obeys. Each fix is a structured warning:
| Warning code | Meaning |
|---|---|
LAYOUT_IGNORED_LANDSCAPE | portraitLayout ignored for a non-9:16 aspect |
TARGET_LENGTH_CLAMPED | targetLengthS raised to the floor for the scene count |
MUSIC_DROPPED_NO_URL | music.source: upload with no uploadUrl → dropped to none |
HERO_BEAT_CLAMPED | heroBeatIdx clamped into range |
KINETIC_TRUNCATED | a kineticText over 6 words truncated |
KINETIC_KEPT_CAPTIONS_OFF | captions off, but kinetic pull-quotes still render (distinct channel) |
4. Endpoints
| Method · Path | Scope | Body | Returns |
|---|---|---|---|
GET /v1/health | plan | — | { version, ok } |
POST /v1/projects | plan | { name?, goal, intent?, features?[] } | { project, uploads[] } |
POST /v1/projects/:id/screenshots | plan | { features?[], screenshots?[] } | { uploads[] } |
POST /v1/projects/:id/plan | plan | PlanRequest? | ResolvedPlan |
GET /v1/projects/:id/plan | plan | — | ResolvedPlan (last) |
POST /v1/projects/:id/render | render | PlanRequest? (defaults to last plan) | { renderId, variantProjectId, status } |
POST /v1/artifacts/:id/regenerate | render | { scope, targetId?, videoConfig? } | { artifact } |
GET /v1/renders/:id | plan | — | RenderResult |
GET /v1/renders/:id/events | plan | — (SSE) | text/event-stream |
GET /v1/openapi.json | public | — | OpenAPI 3.1 |
features[] = { name, screenshots: { contentType, sizeBytes, width?, height? }[] } — one entry per feature directory; each seeds an outline beat. Caps: ≤ 50 screenshots, ≤ 20 features per request.
A RenderResult is { status, renderId, variantProjectId, parentProjectId, outputs[], posterUrl, transcript, durationS, scenes[], plan } — everything an agent needs to inspect a render without watching the MP4.
5. Results: poll or webhook
- Poll
GET /v1/renders/:iduntilstatusisready/failed. - Webhook (optional, behind
headlessWebhooks): registerwebhookUrl+webhookSecreton a key. On a headless render's completion you get a signedPOST(X-Pitchstage-Signature= HMAC-SHA256 of the body), eventrender.done/render.failed, 3 retry attempts on 5xx. Targets must be publichttps.
6. Errors + limits
Every error is { error: { code, message } } with a stable LK_* code and HTTP status:
| Code | Status | Meaning |
|---|---|---|
LK_001 | 401 | Auth required |
LK_002 | 403 | Scope / workspace forbidden |
LK_006 | 401 | Invalid / revoked API key |
LK_201 / LK_202 | 402 | Project / artifact quota |
LK_204 | 402 | Workspace cost ceiling |
LK_205 | 402 | Per-key monthly spend cap |
LK_404 | 404 | Not found |
LK_429 | 429 | Per-key rate limit (Retry-After) |
LK_422 | 422 | Validation |
Per-key defaults: 60 req/min, $5/mo spend cap. Responses carry X-RateLimit-Limit / -Remaining / -Reset. The two-phase design keeps the loop cheap — explore on plan, pay only on the render you commit.
7. Quotas + variants
A clone-on-write variant is a derived exploration: it does not consume the monthly project quota; its committed artifact consumes the artifact quota. Abandoned variants (no published artifact) are pruned after 7 days.
See also
- /api/v1/openapi.json — the machine-readable OpenAPI 3.1 spec (single source of truth).
- The CLI and the MCP server are both thin
/v1wrappers — drive the same plan→render loop from a terminal or an AI agent. - Manage your API keys in Settings.