# Lamina > Agentic creative API for generating videos, movies, images, and audio for products, brands, social, and ads. High-level briefs use one frozen plan/approval/execution lifecycle across browser, REST, SDK, and MCP; direct app and model APIs remain available for explicit low-level control. ## Base URL https://app.uselamina.ai ## Authentication All requests require an API key in the header: ``` x-api-key: lma_your_key_here ``` MCP uses hosted OAuth instead of an API key. Recommended endpoint: https://app.uselamina.ai/mcp/agent/v2 The v2 endpoint exposes the task-level lifecycle tools: lamina_plan, lamina_choose, lamina_execute, lamina_status, lamina_cancel, lamina_steer, lamina_upload_asset, lamina_brand_context, lamina_credits. Advanced endpoint: https://app.uselamina.ai/mcp/agent (30 lower-level tools). Use it only for direct app/model/compose control, brand or app mutations, refinement, or checkout. ## The Agent Creation Loop The canonical interactive MCP loop is: credits / brand context → plan → clarify → show frozen plan + cost → explicit approval → execute within maxCredits → status → deliver - Call lamina_plan with the brief and known inputs. It may use a paid router, but it NEVER dispatches generation. - Source assets (editing, transforming, or animating an image/video/audio the user provides): a file attached in the chat is NOT visible to the MCP tools — only your JSON arguments are. FIRST call lamina_upload_asset, PUT the bytes to the signed uploadUrl, then pass the returned assetUrl by type: inputs.imageUrl (source image), inputs.videoUrl (source video to edit/restyle/animate), inputs.audioUrl (voice track to lip-sync), inputs.presenterImage (face to animate). NEVER plan an edit/transform brief without the source asset, or Lamina generates from scratch and ignores the user's file. (To edit an existing video on a timeline, use lamina_editor_open with sourceVideoUrl.) - If status is needs_clarification, ask questions[] and plan again. If an awaiting_approval plan has requiredInputs[], collect those answers for execution without re-planning. - Show steps, outputs, warnings, and estimatedCredits. null means unknown, not free. - After approval, call lamina_execute with the exact planId and planFingerprint, inputs, a finite positive maxCredits, and an idempotencyKey. - Call lamina_status with the returned runId. wait=true waits at most 25 seconds; if timedOut is true or the run remains non-terminal, call status again. - Deliver completed outputs[].url values. v2 status normalizes pipeline, workflow, atomic, and compose runs. - Call lamina_cancel when asked. cancel_requested and not_cancellable are not confirmed cancelled. Advanced-only facts: - lamina_create is a compatibility name over the same frozen planner. Read canonicalStatus and plan; legacy mode=app callers may still dispatch with lamina_run. Prefer v2 lamina_execute for route-choice, compose, dynamic-workflow, and bounded execution. - lamina_brand_score supports completed image-producing workflow, atomic, or pipeline runs. It does not support video-only, compose-only, or output-less runs. - lamina_refine_to_brand supports bounded refinement of rerunnable workflow image outputs, not arbitrary atomic, compose, or pipeline runs. - lamina_topup returns a Stripe-hosted checkout link. NEVER collect card details in chat. Starter recipes: the v2 MCP server ships prompt templates (prompts/list + prompts/get) for common outcomes — narrated_video, talking_head, edit_image, animate_still, image_from_text — each a filled-in brief that drives the loop above. The server also returns top-level `instructions` summarizing this lifecycle on connect (server/discover + initialize). Full guide: /guides/agent-creation-loop ## Endpoints ### Plan and Create High-level REST uses the same immutable AgentPlan as browser Director and MCP. #### Plan Content POST /v1/content/plan Body: { "brief": "", "platform": "", "modality": "image|video|audio", "brandProfileId": "", "appId": "", "inputs": {} } Returns the compatibility fields plus the exact frozen plan under data.plan. Read planId, planFingerprint, canonicalStatus, questions, alternatives, estimatedCredits, and warnings. Choose an awaiting_choice route with POST /v1/content/plans/{planId}/choose and { "routeId": "..." }. Execute an awaiting_approval plan with POST /v1/content/plans/{planId}/execute, the exact planFingerprint, inputs, a positive maxCredits, and an Idempotency-Key header. Poll the returned pipeline run at GET /v1/content/runs/{runId}. #### Create Content POST /v1/content/create Body: { "brief": "", "platform": "", "modality": "image|video|audio", "brandProfileId": "", "appId": "", "inputs": {}, "headless": false } Compatibility convenience endpoint. Interactive calls freeze and return the plan without generation spend. For unattended execution, set headless=true, supply positive maxCredits, send Idempotency-Key, and set allowUnknownCost=true only when explicitly accepting an unknown-cost step. Questions and route choices never auto-dispatch. Returns a frozen plan, or { data: { status: "ran", runId, pipelineStatus, plan } } after a bounded headless dispatch. #### Create Brief POST /v1/content/brief Body: { "goal": "", "platform": "", "modality": "", "count": , "brandProfileId": "", "metadata": "" } Generates a structured content brief grounded in brand context, trends, and performance data. Use this when you want to review or modify the brief before creating. modality is one of: image, video, text, audio, mixed. - metadata: document context (e.g. { "documentType": "homepage", "fieldPurpose": "hero banner" }) to generate contextually relevant briefs. Returns: { data: } #### Batch Create POST /v1/content/batch Body: { "items": [{ "brief": "", "platform": "", "modality": "", "appId": "", "brandProfileId": "", "campaignId": "", "inputs": {} }], "brandProfileId": "", "campaignId": "" } Creates multiple pieces of content in parallel. Each item follows the same logic as create-content. Maximum 10 items per batch. Returns: { data: { batchId, total, queued, failed, items: [{ index, status, runId, workflowId, workflowName, error }] } } ### Discover Browse and inspect available apps. Use these when you want full control over which app runs and what inputs it receives. #### List Apps GET /v1/apps?search=&limit= Returns: { data: [{ appId, name, description, icon, isPublic, modality, outputFormats, capabilities, inputSummary }] } Top-level fields for quick filtering without parsing capabilities: - icon: emoji icon for the app (nullable) - modality: primary output type — "image", "video", "audio", or null - outputFormats: output MIME types (e.g. ["image/png", "video/mp4"]) - inputSummary: { required: [{ name, type }], optional: [{ name, type }], total } — what the app needs to run capabilities (nullable) describes what the app does semantically: - produces: what it generates (e.g. "photo", "video-clip", "voiceover") - strengths: what it's good at (e.g. "AI image generation", "background removal") - bestFor: best use cases (e.g. "e-commerce product photography") - limitations: known limitations - outputFormats: output MIME types (e.g. "image/png", "video/mp4") - hasQualityControl: whether automated quality checks are included - hasHumanApproval: whether human approval is required (not fully automated) - generationStepCount: number of AI generation steps Use modality and inputSummary for quick app picker UIs. Use capabilities for deeper reasoning about what an app produces and whether it fits your use case. #### Get App Details GET /v1/apps/{appId} Returns: { data: { appId, name, description, parameters: [{ id, name, type, required, options, default }], capabilities } } Parameter types: - text: free-form string - options: one of the values from the options array - url: publicly accessible URL to image/video #### Get App Workflow GET /v1/apps/{appId}/workflow Returns: { data: { appId, name, nodes: [{ id, type, label }], edges: [{ source, target }] } } #### Estimate Run Cost POST /v1/apps/{appId}/estimate Estimate the credit cost of running an app before committing to execution. Use this to make cost/quality tradeoff decisions. Returns: { data: { appId, name, estimatedCredits: { expected, min, max }, breakdown: [{ nodeId, nodeType, credits }], currentBalance, affordable } } - expected: typical cost for one run - min/max: range accounting for quality check retries - affordable: true if currentBalance >= expected cost #### Discover Apps POST /v1/apps/discover Semantic discovery: describe what you want to create and get ranked app matches with capabilities, estimated cost, and relevance scores. Use this to find the right app before running it. Body: { "intent": "", "constraints": { "maxCredits": , "outputFormat": "" }, "limit": } Returns: { data: { matches: [{ appId, name, description, relevanceScore, capabilities, estimatedCredits, whyMatch }], intent: { medium, summary } } } - intent: natural language description of what you want to create - constraints.maxCredits: only return apps costing at most this many credits - constraints.outputFormat: filter by output MIME type (e.g. "image/png", "video/mp4") - whyMatch: explanation of why this app matches the intent #### Generate App POST /v1/workflows/generate When no existing app fits (check Discover Apps first), generate a brand-new app from a plain-language instruction. A headless LLM planner assembles a validated workflow graph from the node catalog and auto-publishes it as a private, immediately-runnable app. Body: { "instruction": "", "name": "", "visibility": "private|shared|public (default private)", "provider": "claude|openai (optional; defaults to WFGEN_PROVIDER, then OpenAI)" } Returns: { data: { appId, name, description, visibility, runUrl, parameters: [...], outputs: [...] } } - instruction: describe what the app creates, including the inputs the user supplies and the desired outputs - The returned app is a normal app: run it via POST /v1/apps/{appId}/runs using the returned parameter keys, and it is discoverable once shared/public - On failure returns { error, details: [...] } where details are the validation problems the planner could not resolve #### Set App Visibility POST /v1/apps/{appId}/visibility Change an app's reach. Only the app creator or a workspace owner may change it. Body: { "visibility": "private|shared|public" } Returns: { data: { appId, name, visibility, parameters, outputs } } - private: creator / workspace owner only - shared: any workspace member (discoverable via Discover Apps) - public: anyone #### Run App POST /v1/apps/{appId}/runs?webhook= Body: { "inputs": { "": "" } } Input keys are parameter **names** (from Get App), not IDs. For options parameters, use the **label** string. webhook query parameter is optional — if provided, results POST to that URL on completion. Returns: { data: { runId, workflowId, status: "queued", webhookUrl, outputs: [...] } } #### List Templates GET /v1/templates?category=&limit= Returns: { data: [{ templateId, name, description, category, format, appId }] } Content creation templates available to your workspace. #### Get Template GET /v1/templates/{id} Returns: { data: { templateId, name, description, category, format, appId, defaults: { "": "" }, customizable: [""] } } Full template details including recommended app, pre-filled inputs, and customizable parameters. ### Track Monitor executions and retrieve results. #### Wait For Run (recommended for agents) GET /v1/runs/{runId}/wait?timeout= Blocks until the execution reaches a terminal state (completed, failed, or cancelled) or the timeout expires. Simplest way to get results — no polling loop, no SSE. timeout: default 60, max 120. Use 60 for image workflows, 120 for video. If the timeout expires before the execution finishes, the response includes `timeout: true` alongside the current state. This is NOT an error — call again or switch to polling. Returns: { data: { runId, workflowId, status, outputs: [{ id, label, type, value, status, error, mimeType, contentDescription, compatibleInputTypes, suggestedNextSteps }], errorMessage, startedAt, completedAt, createdAt }, timeout: } Completed outputs include composability metadata for chaining: - mimeType: canonical MIME type (e.g. "image/png", "video/mp4") - contentDescription: what was produced (e.g. "AI-generated photograph") - compatibleInputTypes: downstream tool categories (e.g. ["image-to-video", "upscale", "background-removal"]) - suggestedNextSteps: actionable hints (e.g. ["upscale for print quality", "remove background for compositing"]) #### Get Run Status (polling) GET /v1/runs/{runId} Returns: { data: { runId, workflowId, status, progress: { totalOutputs, completedOutputs, failedOutputs, percentComplete }, outputs: [{ id, label, type, value, status, error, mimeType, contentDescription, compatibleInputTypes, suggestedNextSteps }], errorMessage, startedAt, completedAt, createdAt } } Status: queued → running → completed | failed Poll every 3-5 seconds until status is completed or failed. progress provides granular completion tracking: - totalOutputs: total number of output nodes in the workflow - completedOutputs: how many have finished successfully - failedOutputs: how many have failed - percentComplete: 0-100 (null if status is failed) #### Stream Run (SSE) GET /v1/runs/{runId}/stream Opens a Server-Sent Events stream. Each event contains the current execution state with per-node output status. The stream closes when the execution reaches completed or failed. Use this instead of polling for real-time progress. Event format: { runId, status, outputs: [{ id, label, type, value, status, error }] } #### List Runs GET /v1/runs?status=&appId=&limit=&offset=&since= Returns past runs, newest first. Supports filtering by app, status, and date. Uses offset/limit pagination. Returns: { data: [{ runId, workflowId, status, source, errorMessage, startedAt, completedAt, createdAt }], pagination: { total, limit, offset } } #### Refine Run (feedback) POST /v1/runs/{runId}/feedback Body: { "feedback": "" } Iterative refinement: provide natural-language feedback on a completed run's outputs. An LLM identifies which workflow nodes need prompt changes, re-executes them, and returns updated outputs. Only works on completed runs. Returns: { data: { runId, status, summary, refinedNodes: [{ nodeId, reason }], outputs: [{ id, label, type, value }], errorMessage } } - summary: LLM-generated description of what was changed - refinedNodes: which nodes were re-executed and why - outputs: the full updated output set after refinement #### List Assets GET /v1/assets?runId=&outputType=&limit=&offset= Returns generated assets from past executions. Filterable by run and output type. Uses offset/limit pagination. Returns: { data: [{ id, runId, nodeLabel, outputType, sourceUrl, cdnUrl, textContent, status, createdAt }], pagination: { total, limit, offset } } ### Intelligence Brand context, performance prediction, recommendations, and trend signals. #### Get Brand Context GET /v1/intelligence/brand-context?brandProfileId=&campaignId=&workflowId=&platform=&objective=&modality=&topK= Returns: { data: { brandDna: { voiceAttributes, visualIdentity, contentPillars, audienceSignals, guardrails, toneSpectrum, performanceProfile }, guidance: { promptDirectives, negativePrompts, recommendedMoves, testIdeas, winningPatterns, weakPatterns, supportingMetrics, creativeStructure }, topPatterns: { topItems, topPatterns, weakPatterns } } } Brand DNA, creative guidance, and top-performing patterns the Intelligence engine uses to ground recommendations. All query parameters are optional filters. #### Predict Performance POST /v1/intelligence/predict Body: { "concept": "", "platform": "", "modality": "", "brandProfileId": "", "campaignId": "" } Predicts how content will perform before publishing. concept, platform, and modality are required. Returns: { data: } #### Get Recommendations GET /v1/intelligence/recommendations Returns: { data: [{ type, title, description, suggestedFormat, priority }] } Actionable content recommendations grounded in brand context and recent performance data. #### Get Trends GET /v1/intelligence/trends Returns: { data: [{ topic, momentum, relevance, sources, detectedAt }] } Current trend signals relevant to your workspace's content categories and audience. ### Distribute Publish content to connected channels and transfer assets to external storage. #### List Channels GET /v1/publishing/channels Returns: { data: [{ id, platform, accountName, accountType, username, hasInstagram, createdAt }] } Connected social and distribution channels for your workspace. #### Publish Content POST /v1/publishing/publish Body: { "accountIds": [""], "imageUrl": "", "videoUrl": "", "caption": "" } Publishes content to one or more connected accounts. Provide at least one of imageUrl, videoUrl, or caption. Returns: { data: } #### Transfer Asset POST /v1/publishing/transfer-asset Body: { "sourceUrl": "", "mediaType": "", "filename": "" } Transfers a generated asset to CDN storage. sourceUrl and mediaType are required. Returns: { data: } #### Get Publish History GET /v1/publishing/history?limit=&status=&platform= Returns: { data: [{ id, platform, contentType, contentUrl, caption, postUrl, status, error, publishedAt, createdAt }] } Publish history for your workspace, newest first. ### Score Evaluate content quality before publishing. #### Score Content POST /v1/content/score Body: { "contentItemIds": [""], "platform": "", "modality": "", "limit": } Scores content against brand context, audience fit, and predicted performance. Pass contentItemIds to score specific items, or use filter params. Returns: { data: } ### Compose Turn a script into a narrated, multi-shot video. Pick a FORMAT (never a model), preview cost, then render. Steer via the script — `direction` + `sections` — not by naming models. #### List Compose Formats GET /v1/compose/formats Returns: { data: { formats: [{ id, label, description, available, costTier, estimatedCredits, trending, tags, inputs }], capabilities: { maxScriptDurationSec, aspectRatios, clipSeconds } } } The format menu + fixed capabilities. Pick a format id; supply a format's required `inputs` when it lists them. `available: false` = roadmap. Optional `durationSec` scales the cost estimate. #### Plan Compose Video (dry run) POST /v1/compose/plan Body: { "script": "", "format": "", "direction": "", "sections": [], "inputs": {}, "aspectRatio": "" } Preview the format, cost, and per-beat storyboard WITHOUT generating anything or spending a credit (one LLM call). `ready: false` + `missingInputs`/`blockers` say what to supply. Returns: { data: { ready, format, cost, estimatedDurationSec, beatCount, beats: [...], missingInputs } } #### Compose Narrated Video POST /v1/compose/video Body: { "script": "", "title", "generationBrief", "direction", "sections", "format", "inputs", "aspectRatio", "voiceId", "idempotencyKey" } Async — synthesizes narration, storyboards into beats, generates a continuity-chained shot per beat, renders with transitions + Ken-Burns + captions. Invalid input → 400 with `details.reason` (SCRIPT_TOO_LONG, MISSING_REQUIRED_INPUT, FORMAT_UNAVAILABLE, …). Returns: { data: { runId, status } } (202) #### Get Compose Run GET /v1/compose/video/{runId} Poll a compose run. While running, `progress` reports the stage (narrating → storyboarding → generating_shots N/total → assembling → done). On completion, `result` carries videoUrl, durationSeconds, shotCount. Returns: { data: { runId, status, progress?, result? } } ### Account #### Get Usage GET /v1/account/usage Returns: { data: { credits: { balance, totalEarned, totalSpent, manageUrl }, rateLimit: { limit, windowMs } } } Credit balance, rate limit status, and link to credit management for your workspace. - manageUrl: URL to the billing/credits page where users can add credits. ### Webhooks #### Get Webhook Signing Key GET /v1/webhooks/signing-key Returns: { keys: [{ kty: "OKP", crv: "Ed25519", x: "", kid, use, alg }] } ## Webhooks Pass ?webhook=https://your-server.com/callback when running an app. On completion, we POST to your URL: - Same structure as polling response: { data: { runId, status, outputs, ... } } - Headers: X-Lamina-Webhook-Signature (ED25519 hex), X-Lamina-Webhook-Timestamp (unix seconds), X-Lamina-Webhook-Request-Id (run ID), X-Lamina-Webhook-User-Id (user ID that triggered it) - Signature message: "." - Retries: 3 attempts with backoff (5s, 30s, 2min) - Polling still works — you can use both webhooks and polling side by side ## Quick Integration (Python) ```python import requests API_KEY = "lma_your_key" BASE = "https://app.uselamina.ai/v1" H = {"x-api-key": API_KEY, "Content-Type": "application/json"} # --- Path 1: One-call creation (recommended) --- # Create content from a brief r = requests.post(f"{BASE}/content/create", headers=H, json={"brief": "Product hero shot of white sneakers, premium lighting"}) run_id = r.json()["data"]["runId"] # Wait for results (blocks up to 60s) result = requests.get(f"{BASE}/runs/{run_id}/wait?timeout=60", headers=H).json() for output in result["data"]["outputs"]: print(f"{output['label']}: {output['value']}") # --- Path 2: Full control (choose the app yourself) --- # Find app apps = requests.get(f"{BASE}/apps?search=catalog", headers=H).json()["data"] app_id = apps[0]["appId"] # Get parameters app = requests.get(f"{BASE}/apps/{app_id}", headers=H).json()["data"] print(app["parameters"]) # shows what inputs to provide # Run with webhook r = requests.post(f"{BASE}/apps/{app_id}/runs?webhook=https://your-server.com/cb", headers=H, json={"inputs": {"Upload": "https://example.com/photo.jpg", "Style": "Ghibli"}}) run_id = r.json()["data"]["runId"] # Wait for results result = requests.get(f"{BASE}/runs/{run_id}/wait?timeout=60", headers=H).json() for o in result["data"]["outputs"]: print(f"{o['label']}: {o['value']}") # Score the output score = requests.post(f"{BASE}/content/score", headers=H, json={"contentItemIds": [run_id]}).json()["data"] print(f"Score: {score}") # Publish to connected channels channels = requests.get(f"{BASE}/publishing/channels", headers=H).json()["data"] if channels: requests.post(f"{BASE}/publishing/publish", headers=H, json={ "imageUrl": result["data"]["outputs"][0]["value"], "caption": "New product shot", "accountIds": [channels[0]["id"]] }) ``` ## Quick Integration (Node.js) ```javascript const API_KEY = 'lma_your_key'; const BASE = 'https://app.uselamina.ai/v1'; const headers = { 'x-api-key': API_KEY, 'Content-Type': 'application/json' }; // --- Path 1: One-call creation (recommended) --- const res = await fetch(`${BASE}/content/create`, { method: 'POST', headers, body: JSON.stringify({ brief: 'Product hero shot of white sneakers, premium lighting' }), }); const { data } = await res.json(); // Wait for results (blocks up to 60s) const result = await fetch( `${BASE}/runs/${data.runId}/wait?timeout=60`, { headers } ).then(r => r.json()); for (const output of result.data.outputs) { console.log(`${output.label}: ${output.value}`); } // --- Path 2: Full control (choose the app yourself) --- const appsRes = await fetch(`${BASE}/apps/${appId}/runs?webhook=${encodeURIComponent(webhookUrl)}`, { method: 'POST', headers, body: JSON.stringify({ inputs: { "Upload": "https://example.com/photo.jpg" } }), }); const exec = await appsRes.json(); console.log('Run started:', exec.data.runId); // Wait for results const fullResult = await fetch( `${BASE}/runs/${exec.data.runId}/wait?timeout=60`, { headers } ).then(r => r.json()); // Or use SSE stream for real-time progress const stream = new EventSource(`${BASE}/runs/${exec.data.runId}/stream`, { headers: { 'x-api-key': API_KEY } }); stream.onmessage = (event) => { const execution = JSON.parse(event.data); console.log('Status:', execution.status); if (execution.status === 'completed' || execution.status === 'failed') { stream.close(); } }; // Score content const scoreRes = await fetch(`${BASE}/content/score`, { method: 'POST', headers, body: JSON.stringify({ contentItemIds: [exec.data.runId] }), }); const score = await scoreRes.json(); console.log('Score:', score.data); ``` ## Webhook Verification (Node.js) ```javascript const crypto = require('crypto'); // Fetch the signing key once at startup (or cache it): // const { keys } = await fetch('https://app.uselamina.ai/v1/webhooks/signing-key').then(r => r.json()); // const PUBLIC_JWK = keys[0]; // { kty: 'OKP', crv: 'Ed25519', x: '...', kid, use, alg } function verifyLaminaWebhook(rawBody, signatureHex, timestamp, jwk) { // Reject old timestamps (replay protection) if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false; const publicKey = crypto.createPublicKey({ key: jwk, format: 'jwk' }); const message = Buffer.from(`${timestamp}.${rawBody}`); return crypto.verify(null, message, publicKey, Buffer.from(signatureHex, 'hex')); } // In your webhook handler: app.post('/lamina-callback', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-lamina-webhook-signature']; const ts = req.headers['x-lamina-webhook-timestamp']; if (!verifyLaminaWebhook(req.body.toString(), sig, ts, PUBLIC_JWK)) { return res.status(401).send('Invalid signature'); } const { data } = JSON.parse(req.body); console.log('Run', data.runId, data.status); data.outputs.forEach(o => console.log(o.label, o.value)); res.status(200).send('ok'); }); ``` ## Errors Every error response includes a machine-readable `code` and a `retryable` flag: ```json { "error": "Human-readable message", "code": "AUTH_INVALID_KEY", "retryable": false, "requestId": "trace-id" } ``` Rate limit errors also include `retryAfter` (seconds). Validation errors include a `details` array with per-field problems. ### Error Codes | Code | HTTP | Retryable | When | |------|------|-----------|------| | AUTH_MISSING_KEY | 401 | No | No API key in header | | AUTH_INVALID_KEY | 401 | No | Key not found or revoked | | AUTH_INVALID_CONTEXT | 401 | No | Key missing workspace/user | | AUTH_FAILED | 500 | Yes | DB error during key validation | | FORBIDDEN | 403 | No | Workspace mismatch, app access denied | | NOT_FOUND | 404 | No | App, run, or template not found | | VALIDATION_ERROR | 400 | No | Invalid inputs, missing fields | | RATE_LIMITED | 429 | Yes | 100 req/min/IP; check retryAfter | | WEBHOOK_INVALID_URL | 400 | No | Malformed webhook URL | | WEBHOOK_INVALID_SECRET | 401 | No | Bad webhook secret | | RESOURCE_UNAVAILABLE | 503 | Yes | Service not configured | | INTERNAL_ERROR | 500 | Yes | Unhandled server error | Branch on `code` for error handling. Use `retryable` to decide whether to retry. Include `requestId` when reporting issues. Compose endpoints add a machine-readable `details.reason` sub-code under `VALIDATION_ERROR`: `SCRIPT_REQUIRED`, `SCRIPT_TOO_LONG` (with `maxScriptDurationSec`/`estimatedDurationSec`), `FORMAT_UNAVAILABLE`, `MISSING_REQUIRED_INPUT` (with `missingInputs`). Branch on `details.reason` to self-correct (e.g. supply the missing input, then retry). ## Machine-Readable API Spec Full OpenAPI 3.1 specification: GET /v1/openapi.json Interactive docs: GET /v1/docs ## Endpoint Summary (all 31) | Method | Path | Group | Purpose | |--------|------|-------|---------| | POST | /v1/content/create | Create | Create content end-to-end from a brief | | POST | /v1/content/brief | Create | Generate a structured content brief | | POST | /v1/content/batch | Create | Batch content creation | | POST | /v1/apps/{appId}/runs | Create | Run an app with explicit inputs | | GET | /v1/apps | Discover | List available apps | | GET | /v1/apps/{appId} | Discover | Get app details and parameters | | GET | /v1/apps/{appId}/workflow | Discover | Get app workflow graph | | GET | /v1/templates | Discover | List content templates | | GET | /v1/templates/{id} | Discover | Get template details | | GET | /v1/runs/{runId}/wait | Track | Wait for run to finish (recommended) | | GET | /v1/runs/{runId} | Track | Get run status and outputs | | GET | /v1/runs/{runId}/stream | Track | SSE stream for real-time progress | | POST | /v1/runs/{runId}/feedback | Track | Refine run with natural-language feedback | | GET | /v1/runs | Track | List past runs | | GET | /v1/assets | Track | List generated assets | | GET | /v1/intelligence/brand-context | Intelligence | Get brand DNA and guidance | | POST | /v1/intelligence/predict | Intelligence | Predict content performance | | GET | /v1/intelligence/recommendations | Intelligence | Get content recommendations | | GET | /v1/intelligence/trends | Intelligence | Get trend signals | | GET | /v1/publishing/channels | Distribute | List connected channels | | POST | /v1/publishing/publish | Distribute | Publish to social channels | | POST | /v1/publishing/transfer-asset | Distribute | Transfer asset to external CDN | | GET | /v1/publishing/history | Distribute | Get publish history | | POST | /v1/content/score | Score | Score content quality | | GET | /v1/compose/formats | Compose | List video formats (+ cost, capabilities) | | POST | /v1/compose/plan | Compose | Dry-run: format + cost + storyboard, no charge | | POST | /v1/compose/video | Compose | Compose a narrated multi-shot video (async) | | GET | /v1/compose/video/{runId} | Compose | Poll a compose run (+ progress) | | GET | /v1/account/usage | Account | Get credit balance and rate limits | | GET | /v1/webhooks/signing-key | Account | Get webhook verification key | | GET | /v1/health | Account | Health check |