@uselamina/sdk
v0.8.0
Published
TypeScript SDK for Lamina — the agentic creative API for generating videos, movies, and images
Readme
@uselamina/sdk
TypeScript SDK for the Lamina public /v1 API.
Lamina is a headless creative engine for AI agents — it generates videos, movies, and images for products, brands, social, and ads. The SDK provides a fully typed Node.js client for discovering apps, running executions, polling or waiting for results, verifying webhooks, calling brand-aware intelligence endpoints, generating content from natural-language briefs, and publishing to connected social channels.
Use the SDK when: you need programmatic Node.js/TypeScript integration with the Lamina API.
See also: @uselamina/mcp for MCP server (agent tool access), @uselamina/cli for the terminal interface.
Install
npm install @uselamina/sdkRequires Node.js >=20.19.0. Pure ESM ("type": "module"). Ships full TypeScript definitions — no @types package needed.
Authentication
The SDK authenticates with an x-api-key header. API keys start with the lma_ prefix and are issued from the Lamina dashboard.
Three ways to construct a client:
import { LaminaClient } from '@uselamina/sdk';
// 1. Explicit API key
const client = new LaminaClient({ apiKey: 'lma_your_api_key' });
// 2. From the LAMINA_API_KEY environment variable
const client = LaminaClient.fromEnv();
// 3. From credentials saved by the lamina-cli login flow (~/.lamina/config.json)
const client = await LaminaClient.fromStoredCredentials();LaminaClientOptions also accepts baseUrl (defaults to the Lamina production API) and a custom fetch implementation for testing or proxying.
Quick start
import { LaminaClient } from '@uselamina/sdk';
const client = new LaminaClient({ apiKey: 'lma_your_api_key' });
// 1. Find an app
const apps = await client.apps.list({ search: 'product photo' });
// 2. Start a run
const run = await client.runs.run(apps.data[0].appId, {
inputs: { Front: 'https://example.com/photo.jpg' },
});
// 3. Wait for completion (long-poll; resolves when status is completed/failed)
const result = await client.runs.wait(run.data.runId);
console.log(result.data.outputs);Resources
The client exposes one namespace per resource. Every method returns an ApiEnvelope<T> of the form { data: T } (a few list endpoints also include pagination).
| Namespace | Purpose | Key methods |
|-----------|---------|-------------|
| client.apps | Discover and inspect generative apps (workflows) | list, get, workflow, estimate, discover |
| client.runs | Start runs, poll status, wait for completion, send refinement feedback | run, get, wait, serverWait, feedback, list |
| client.webhooks | Fetch the JWKS signing key set and verify incoming webhook payloads | signingKey, verify |
| client.intelligence | Brand-aware guidance, performance prediction, recommendations, trends | getBrandContext, predict, recommendations, trends |
| client.content | High-level brief-to-run pipeline; concept brainstorming and scoring | create, brief, score |
| client.publishing | Connected social channels, publish content, asset transfer, history | channels, publish, transferAsset, history |
client.executions and client.compound are kept as deprecated aliases for client.runs and client.content.
Apps
// List apps (paginated, optional text search)
const { data: apps } = await client.apps.list({ search: 'video', limit: 20 });
// Inspect parameters and capabilities of a single app
const { data: app } = await client.apps.get('app_abc123');
// Estimate credit cost before running
const { data: estimate } = await client.apps.estimate('app_abc123');
if (!estimate.affordable) throw new Error('insufficient credits');
// Natural-language app discovery
const { data: matches } = await client.apps.discover({
intent: 'I want a 9:16 product video for Instagram Reels',
constraints: { maxCredits: 50, outputFormat: 'video' },
limit: 3,
});
// Change an app's reach: private / shared / public
const { data: updated } = await client.apps.setVisibility('app_abc123', 'shared');Generate an app
When no existing app fits, generate a brand-new one from a plain-language instruction. A planner assembles a validated workflow and auto-publishes it as a private app — run it immediately like any other app.
const { data: app } = await client.workflows.generate({
instruction:
'Product photo: user uploads a product image and a background prompt; output one 1:1 image.',
// name?: 'My Product Photo App'
// visibility?: 'private' | 'shared' | 'public' (default: private)
// provider?: 'claude' | 'openai'
});
// app.appId is a normal app — run it with the returned parameter keys
const run = await client.runs.run(app.appId, {
inputs: { product_image: 'https://…/shoe.png', background: 'sunlit marble table' },
});Brand — make output on-brand
Configure the workspace brand, then have generation and runs respect it. A reject verdict on a run turns its reason into a guardrail the next generation avoids.
// Create (or update) a brand + a starter Brand DNA. Owner/admin key.
const { data: brand } = await client.brand.save({
brandName: 'Acme',
brandVoice: 'warm, confident, never salesy',
visualStyle: 'clean studio lighting, muted palette',
guardrails: ['no competitor logos'],
});
// Generate an on-brand app…
const { data: app } = await client.workflows.generate({
instruction: 'Instagram product photo from an uploaded image + a background prompt.',
brandProfileId: brand.brandProfileId,
});
// …and/or apply brand at run time on any existing app.
const run = await client.runs.run(app.appId, {
inputs: { product_image: 'https://…/shoe.png', background: 'sunlit marble' },
applyBrand: true,
});
// Close the loop: reject → the reason becomes a brand guardrail.
await client.runs.brandFeedback(run.data.runId, { verdict: 'reject', note: 'too neon' });
await client.brand.listProfiles(); // discover brandProfileIds
await client.brand.synthesize(brand.brandProfileId); // LLM re-synthesis from performance dataRuns
// Start a run; returns immediately with status: 'queued'
const { data: started } = await client.runs.run('app_abc123', {
inputs: { Front: 'https://example.com/photo.jpg' },
webhook: 'https://your-app.example.com/lamina-webhook', // optional
// applyBrand?: true // fold the workspace brand onto visual nodes; your inputs still win
// test?: true // sandbox: validate + stub, no dispatch, no credits
});
// Single status snapshot (no polling)
const { data: status } = await client.runs.get(started.runId);
// Refine a completed run with natural-language feedback
const { data: refined } = await client.runs.feedback(started.runId, {
feedback: 'Make the background warmer and crop tighter on the product.',
});
// List recent runs with filters
const { data: runs, pagination } = await client.runs.list({
status: 'completed',
appId: 'app_abc123',
limit: 50,
});Polling vs. waiting
Two ways to block until a run finishes:
runs.wait(runId, options?)— client-side polling loop. The SDK callsGET /v1/runs/:ideveryintervalMs(default5000) until the status iscompletedorfailed, ortimeoutMs(default30 * 60 * 1000) elapses. Pass anonPollcallback to observe progress.runs.serverWait(runId, { timeout })— single long-poll request. The server holds the connection open for up totimeoutseconds and returns as soon as the run reaches a terminal state. The envelope includestimeout: trueif the wait window expired before completion, in which case you can call it again.
// Client-side polling with progress
await client.runs.wait(runId, {
intervalMs: 3000,
timeoutMs: 10 * 60 * 1000,
onPoll: ({ status, progress }) => {
console.log(status, progress?.percentComplete);
},
});
// Server-side long-poll loop
let envelope = await client.runs.serverWait(runId, { timeout: 30 });
while (envelope.timeout) {
envelope = await client.runs.serverWait(runId, { timeout: 30 });
}For long-running runs in production, prefer webhooks over either polling strategy.
Reliability, streaming, and pagination
// Auto-retry on 429 / 503 (honors Retry-After, else exponential backoff).
// On by default (maxRetries: 2); tune or disable per client.
const client = new LaminaClient({
apiKey: 'lma_your_api_key',
maxRetries: 3, // default 2; 0 disables
retryBaseDelayMs: 500 // backoff base when no Retry-After header
});
// Idempotency-Key on any mutating request — a retried call runs at most once.
await client.request('/v1/apps/app_abc123/runs', {
method: 'POST',
body: { inputs: { Front: 'https://example.com/photo.jpg' } },
idempotencyKey: crypto.randomUUID(),
});
// Live progress via Server-Sent Events — yields each event payload.
for await (const event of client.runs.stream(runId)) {
console.log(event);
}
// Auto-paginating iterator — no manual offset bookkeeping.
for await (const run of client.runs.listAll({ status: 'completed' })) {
console.log(run.runId);
}Webhooks
Lamina signs webhook deliveries with an Ed25519 key published as JWKS. The SDK fetches the key set and verifies the signature, timestamp freshness (default ±5 minutes), and request-id/payload consistency in one call.
import express from 'express';
import { LaminaClient } from '@uselamina/sdk';
const client = new LaminaClient({ apiKey: process.env.LAMINA_API_KEY! });
const app = express();
app.post(
'/lamina-webhook',
express.raw({ type: 'application/json' }), // raw body required for signature verification
async (req, res) => {
const result = await client.webhooks.verify({
body: req.body, // Buffer or string
headers: req.headers,
});
if (!result.valid) {
console.warn('rejected webhook:', result.error);
return res.status(400).send(result.error);
}
const { runId, status, outputs } = result.payload.data;
// …handoff to your queue / persist / notify
res.sendStatus(200);
},
);For ad-hoc verification without an HTTP framework — or to cache the JWKS yourself — use the lower-level helpers:
import { verifyWebhookSignature, extractWebhookHeaders } from '@uselamina/sdk';
const keys = await client.webhooks.signingKey(); // cache this (rotation is rare)
const result = verifyWebhookSignature({ body, headers, keys });The LaminaWebhookListener class also provides a local development listener that exposes received events as an async iterator — useful for tunneling tools and CLI workflows.
Intelligence
Brand-aware endpoints that surface guidance, predictions, and trend patterns from a workspace's content history.
const { data: ctx } = await client.intelligence.getBrandContext({
brandProfileId: 'brand_123',
platform: 'instagram',
modality: 'video',
});
// ctx.brandDna, ctx.guidance, ctx.topPatterns
const { data: pred } = await client.intelligence.predict({
concept: 'minimalist studio shot of the product on a peach gradient',
platform: 'instagram',
modality: 'image',
brandProfileId: 'brand_123',
});
const { data: recs } = await client.intelligence.recommendations({ campaignId: 'camp_42' });
const { data: trends } = await client.intelligence.trends({ platform: 'tiktok', windowDays: 30 });Content
Higher-level brief-to-run pipeline. create selects an app, applies brand context and guidance, and either starts a run or returns needsInput when required parameters are missing. brief brainstorms scored content concepts; score ranks existing items.
const { data } = await client.content.create({
brief: 'Hero image for our new running shoe, energetic, 1:1 for Instagram',
platform: 'instagram',
modality: 'image',
brandProfileId: 'brand_123',
autoQuality: { enabled: true, minScore: 0.75, maxRetries: 2 },
});
if (data.needsInput) {
console.log('still need:', data.needsInput.missing);
} else {
const final = await client.runs.wait(data.runId!);
}Publishing
const { data: channels } = await client.publishing.channels();
const { data: result } = await client.publishing.publish({
accountIds: channels.map((c) => c.id),
imageUrl: 'https://cdn.uselamina.com/...',
caption: 'New drop. Out now.',
});
console.log(result.summary); // { total, success, failed }Error handling
Non-2xx responses throw a typed error. LaminaError is the base class; LaminaAuthError (401/403) and LaminaRateLimitError (429) are subclasses. Every error carries status, body, and the original Headers.
import { LaminaAuthError, LaminaError, LaminaRateLimitError } from '@uselamina/sdk';
try {
await client.runs.run('app_abc123', { inputs: {} });
} catch (err) {
if (err instanceof LaminaAuthError) {
// 401/403 — bad API key, revoked key, or insufficient permission
} else if (err instanceof LaminaRateLimitError) {
const wait = err.retryAfterSeconds ?? 5;
await new Promise((r) => setTimeout(r, wait * 1000));
} else if (err instanceof LaminaError) {
console.error(err.status, err.message, err.body);
} else {
throw err; // network, timeout, JSON parse, etc.
}
}runs.wait throws a plain Error if timeoutMs elapses before the run reaches a terminal state — catch it separately if you want to fall back to a webhook flow.
Types
All request and response types are exported from the package root, including AppDetail, ExecutionStarted, ExecutionStatus, ExecutionOutput, ListRunsResult, LaminaWebhookEnvelope, LaminaWebhookVerificationResult, BrandContextResponse, PerformancePrediction, LaminaCreateResult, PublishResult, and the option/param types for every method. Import them directly:
import type {
ExecutionStatus,
LaminaWebhookEnvelope,
RunExecutionParams,
} from '@uselamina/sdk';LaminaExecutionStatusState is the enum of run states: 'queued' | 'running' | 'completed' | 'failed'.
Docs
- API & guides: https://docs.uselamina.ai
- SDK + CLI guide: https://docs.uselamina.ai/guides/use-the-cli-and-sdk
- Issues: https://github.com/uselamina/lamina-sdk/issues
