← Back to Pitchstage

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

TierStabilityFields
intentstable (the 80%)artifactType, goal, audience, tone, aspect, targetLengthS, captionStyle, brandKitId, voiceProfileId
directorstableoutline, hook, cta, music, smartZoom, transitions, portraitLayout, narrativeTemplate, openingStyle, heroBeatIdx, brand, voice
scenes[]no stability promiseper-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 codeMeaning
LAYOUT_IGNORED_LANDSCAPEportraitLayout ignored for a non-9:16 aspect
TARGET_LENGTH_CLAMPEDtargetLengthS raised to the floor for the scene count
MUSIC_DROPPED_NO_URLmusic.source: upload with no uploadUrl → dropped to none
HERO_BEAT_CLAMPEDheroBeatIdx clamped into range
KINETIC_TRUNCATEDa kineticText over 6 words truncated
KINETIC_KEPT_CAPTIONS_OFFcaptions off, but kinetic pull-quotes still render (distinct channel)

4. Endpoints

Method · PathScopeBodyReturns
GET /v1/healthplan{ version, ok }
POST /v1/projectsplan{ name?, goal, intent?, features?[] }{ project, uploads[] }
POST /v1/projects/:id/screenshotsplan{ features?[], screenshots?[] }{ uploads[] }
POST /v1/projects/:id/planplanPlanRequest?ResolvedPlan
GET /v1/projects/:id/planplanResolvedPlan (last)
POST /v1/projects/:id/renderrenderPlanRequest? (defaults to last plan){ renderId, variantProjectId, status }
POST /v1/artifacts/:id/regeneraterender{ scope, targetId?, videoConfig? }{ artifact }
GET /v1/renders/:idplanRenderResult
GET /v1/renders/:id/eventsplan— (SSE)text/event-stream
GET /v1/openapi.jsonpublicOpenAPI 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

6. Errors + limits

Every error is { error: { code, message } } with a stable LK_* code and HTTP status:

CodeStatusMeaning
LK_001401Auth required
LK_002403Scope / workspace forbidden
LK_006401Invalid / revoked API key
LK_201 / LK_202402Project / artifact quota
LK_204402Workspace cost ceiling
LK_205402Per-key monthly spend cap
LK_404404Not found
LK_429429Per-key rate limit (Retry-After)
LK_422422Validation

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