# Lamina > Agentic creative API for generating videos, movies, and images for products, brands, social, and ads. Send a brief, get back generated images, videos, and text. Chain generation, scoring, and publishing in a single integration. Two paths: one-call creation (recommended) or full control with app selection and parameter mapping. ## Base URL https://app.uselamina.ai ## Authentication All requests require an API key in the header: ``` x-api-key: lma_your_key_here ``` ## Endpoints ### Create One-call creation endpoints. Start here if you want the engine to handle app selection and input mapping. #### Create Content POST /v1/content/create Body: { "brief": "", "platform": "", "modality": "", "brandProfileId": "", "campaignId": "", "appId": "", "inputs": {}, "templateId": "", "aspectRatio": "", "metadata": "" } End-to-end content creation: the engine generates a brief, selects the best app, resolves brand context, maps inputs, and executes the workflow. Async -- returns a run ID. modality is one of: image, video, text, audio, mixed. - aspectRatio: output aspect ratio (e.g. "1:1", "16:9", "9:16", "4:3", "4:5", "auto"). Auto-mapped to the matching workflow parameter. - metadata: freeform context object injected into prompt engineering. Useful for passing structured context from the calling application (e.g. { "documentTitle": "Summer Collection", "fieldName": "heroImage", "fieldPurpose": "Main banner for landing page" }). Returns: { data: { runId, workflowId, workflowName, status: "queued" } } #### 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 #### 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: } ### 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. ## Machine-Readable API Spec Full OpenAPI 3.1 specification: GET /v1/openapi.json Interactive docs: GET /v1/docs ## Endpoint Summary (all 27) | 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/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 |