# Madnion Video API — rich interfaces v3, storyboards v2 and custom composition v1

## Illustrations, depth and camera — depth-1

Available on the existing Video-Gen connection through **composition `version: 1`**, using the same `validate`, `render` and `status` actions. These are optional, reusable inputs, not a new endpoint, credential or npm SDK. Existing compositions and Director defaults are unchanged. `depth-1` is the feature-release label, **not** a value to put in `video.version` or an automatic Director style.

Start with this [complete six-second JSON example](/examples/video-depth-1.json). It needs no uploaded assets and produces a silent landscape proof with a home, animated voice orb, HVAC fan, shadows and a camera move. Download and edit it in your backend. For a service-team outcome, replace an illustration's `kind` with `service-van`; use ordinary position/opacity tracks to reveal it. All copy, colours, placement and timing remain editable.

### Validate, render and poll from your backend

Use your app's existing service-account key with `integrations` permission and access to this workspace connection. Never expose it in frontend code. Include the same `appId` on validation, rendering, asset and status calls. Existing request metering, prices, app/workspace isolation and retention apply; validation does not create a render, but is still an API request.

```js
// Node backend. Save /examples/video-depth-1.json beside this file first.
import { readFile } from 'node:fs/promises';
const video = JSON.parse(await readFile('./video-depth-1.json', 'utf8'));
const appId = process.env.MADNION_APP_ID;
const serviceKey = process.env.MADNION_SERVICE_ACCOUNT_KEY;
if (!appId || !serviceKey) throw new Error('Missing backend credentials');

async function call(action, fields = {}) {
  const response = await fetch('https://api.madnion.com/functions/v1/custom-proxy', {
    method: 'POST',
    headers: {'Content-Type': 'application/json', 'x-service-account-key': serviceKey},
    body: JSON.stringify({slug: 'series-x-video-api', appId, action, ...fields}),
    signal: AbortSignal.timeout(30000)
  });
  const result = await response.json();
  if (!response.ok) throw new Error(`${action}: HTTP ${response.status}`);
  return result;
}
const check = await call('validate', {data: {video}});
if (!check.valid) throw new Error('Fix validation before rendering');
const job = await call('render', {
  data: {video, idempotencyKey: 'my-depth-proof-001'}
});
// Persist job.jobId immediately. After a timeout, resume status polling; do not
// submit another render. Use a NEW idempotency key when changing the video.
const deadline = Date.now() + 10 * 60 * 1000;
let completed = false;
while (Date.now() < deadline) {
  const status = await call('status', {pathParams: {jobId: job.jobId}});
  const state = status.state ?? status.status;
  if (state === 'done') {
    // Save/download status.downloadUrl privately before expiry. See retention below.
    // Do not put signed URLs in public logs or pass secrets to that download URL.
    completed = true;
    break;
  }
  if (['error', 'expired', 'cancelled'].includes(state)) throw new Error(`Render ${state}`);
  await new Promise(resolve => setTimeout(resolve, 5000));
}
if (!completed) throw new Error(`Still pending: resume polling saved job ${job.jobId}`);
```

### Illustration layers

```json
{
  "type": "illustration", "x": 8, "y": 30, "width": 32, "height": 50,
  "enter": "none", "exit": "none", "depth": -0.6,
  "illustration": {
    "kind": "service-home", "accent": "#3979ba",
    "ink": "#153247", "surface": "#edf5fc", "progress": 0
  }
}
```

This is one layer inside `video.scenes[].layers`, not a complete request. Position and dimensions are percentages of the canvas. Four original vector illustrations are available:

| `illustration.kind` | Purpose | Animation |
| --- | --- | --- |
| `service-home` | Home, caller and service context | Layer position, scale, opacity and tilt |
| `hvac-unit` | Shaded equipment with fan and airflow | `keyframes.progress` drives the fan |
| `voice-orb` | Shaded listening/speaking focal point | `keyframes.progress` drives the waveform |
| `service-van` | Service-team handoff context | Layer position, scale, opacity and tilt |

`accent`, `ink` and `surface` are optional six-digit hex colours. Static `illustration.progress` defaults to 0, range 0–1. Animate with `keyframes.progress` using the same range. Progress is authored timing, **not** acoustic lip-sync or speech detection. Illustrations are mock visual context, not actual dispatch footage or verified outcomes. They are not a substitute for your logo: supply your real PNG/JPEG image alias with `type: "image"`, `fit: "contain"`, and `space: "screen"` to preserve branding while the camera moves. Uploaded media must belong to the calling app/workspace.

### Camera, parallax and fixed branding

Each scene can include `camera`, with 2–32 strictly increasing `{time,value,easing?}` points per track. Times are seconds relative to that scene, from 0 through its duration. Easing: `linear`, `ease-in`, `ease-out`, `ease-in-out` (default for camera tracks).

| Scene camera track | Meaning | Allowed values |
| --- | --- | --- |
| `x`, `y` | Translation in canvas percent | −15 to 15 |
| `zoom` | Scale | 0.85–1.25 |
| `tiltX`, `tiltY` | Perspective tilt in degrees | −18 to 18 |
| `roll` | Rotation in degrees | −8 to 8 |

```json
{
  "camera": {
    "zoom": [{"time":0,"value":0.96},{"time":3,"value":1.04},{"time":6,"value":0.98}],
    "tiltY": [{"time":0,"value":-5},{"time":6,"value":3}]
  }
}
```

Camera tracks move `space: "world"` layers (the default); `space: "screen"` layers render above them, keeping their relative order. The background stays fixed. Missing camera tracks default to zero translation/rotation and zoom 1. With `camera`, these tracks replace the legacy `cameraZoom` move; without it, existing `cameraZoom` behaviour is unchanged and `space` does not regroup layers.

Layer inputs: `depth` −1 to 1, `tiltX`/`tiltY` −25 to 25 degrees, and `shadow: "none" | "soft" | "elevated"`. Optional `keyframes.depth`, `keyframes.tiltX` and `keyframes.tiltY` animate within those bounds. Depth adds parallax from camera x/y: 8 pixels × camera percentage point × depth. Keep depth unset/zero on screen-anchored branding. Depth does not change stacking order or create automatic occlusion. Shadows are fixed presets, not custom CSS.

Existing optional geometry tracks `width`, `height` (1–100 canvas percent), `radius` (0–100 pixels), `clipRight`, `clipBottom` (0–100 percent) let a panel expand or a message reveal inside the same scene. All layer tracks use 2–32 ordered points within the scene. These controls are **2.5D perspective and parallax**, not a full 3D engine, ray tracing or motion blur.

### Compatibility and review

Unknown fields, injected SVG/HTML/CSS and out-of-range values are rejected. No arbitrary vector files or executable components are accepted. Existing limits still apply: 90 layers per scene, 1–30 seconds per scene, 20 scenes and 120 seconds total. Schema validation is not a visual-fit or readability guarantee: camera transforms may crop content. Review the widest zoom/tilt, layer overlap, real font, logo contrast and transitions before publishing.

The example is designed for landscape. The existing output contract also accepts `9:16` and `1:1`, but v1 layers do **not** automatically reflow; reposition and resize them for those canvases instead of simply changing the ratio. Add your owned narration/music using the audio contract below; no voice, sound effects or music are generated by these illustration inputs.

## AI Director — available by default

Director is available by default to authenticated apps with the existing **AI and Integrations** permissions. No AdvisorPro-only allowlist, manual activation or separate `video:director` grant is required. The additive Director uses **Madnion's AI Gateway**, not direct provider keys. Existing `render`, `creative_plan`, `scene_compose`, `validate`, speech actions and their prices remain unchanged. This is commercial composition from supplied assets, not unrestricted cinematic generation or autonomous browser recording.

AdvisorPro example: [download the complete one-file test handoff](/advisorpro-director-test-guide.md). It includes the implementation prompt, full validated test request, backend transport, polling, error handling and review gates. Other apps substitute their own app ID, backend credential and brand. No companion file is needed.

Director flow: brief → editable story/shot plan → per-scene narration → measured-duration pacing and captions → validated proof → review report. `director-1-preview` uses a 720p proof; `director-2-preview` preserves the explicitly requested resolution and aspect ratio. The job always ends in `needs_review`, never “commercially approved”. Human playback, listening and claim review are required before calling the existing `render` action with the approved `result.video` at delivery resolution.

### Director 2 — source-led, business-neutral planning

Opt in with `data.version: "director-2-preview"` on the **same** `director_create`/`director_revise` actions. Existing `director-1-preview`, renderer versions, SDK transport, Video/Voice actions, permissions, tenant isolation, retention and prices are unchanged. Director versions and renderer/Storyboard versions are different namespaces: Director 2 currently compiles an editable **composition v1**, not a Storyboard v2 request. Do not change renderer versions manually.

The AI plans the narrative; a deterministic compiler chooses bounded, size-aware layouts and motion. It does not generate renderer code or arbitrary transitions. Source type and creative treatment are independent: an article can become an explainer or social reel; a product or service is not forced into software screens.

| Input | Contract |
| --- | --- |
| `source.type` | `article`, `linkedin-post`, `product`, `video-transcript`, `marketing-brief` |
| `source.title`, `source.text` | Required title ≤160 characters and supplied content/transcript ≤16,000 characters |
| `source.rightsConfirmed` | Must be `true`; caller confirms permission to adapt the content and media |
| `treatment` | `explainer`, `service-promo`, `product-demo`, `social-reel` |
| `durationSeconds` | Required integer 15–120; a **maximum**, not an exact length promise |
| `brand.logo` | Required explicit choice: an existing inline PNG/JPEG asset alias, or `null` for intentionally unbranded output. Brand text is never substituted as a logo |
| `output` | Explicit `aspectRatio` (`16:9`, `9:16`, `1:1`) and `resolution` (`720p`, `1080p`); optional `fps` 24 or 30 |
| `design` | Optional Studio 1 direction; see controls below. Layouts remain editable in the returned composition |
| `mastering` | `web` (default for Director 2) or `none`; masters the final audio mix, not the source assets |
| `pacing` | Optional `{leadIn: 0.12, sceneGap: 0.35, trimSilence: true}`; seconds, defaults shown |

#### Studio 1 visual direction and sound

Director 2 now defaults to `design.style: "studio-1"`. It uses subject-led document, journey, product and network illustrations, staged actions, backed real logos, centered button labels and size-specific layouts. Choose the motif that explains your subject; the compiler has no branches keyed to a customer's business name. A schematic route is not a verified map, and a product illustration is not a photograph of your product. Supply owned imagery/footage for that specificity.

```json
{
  "design": {
    "style": "studio-1",
    "motif": "journey",
    "logoSurface": "#ffffff",
    "logoScale": 1.15,
    "motion": "standard"
  },
  "mastering": "web"
}
```

These are fields inside your Director 2 request, not a complete request. `motif` accepts `documents`, `journey`, `product`, `network`; omission selects documents for articles/transcripts, product for product sources, otherwise network. `logoSurface` is a six-digit hex color (default white); `logoScale` is 0.7–1.4 (default 1). `motion: "reduced"` removes positional/scale entrances and uses fades. `design.style: "classic"` retains the earlier Director 2 layout. Pin `studio-1` or `classic` explicitly for predictable creative treatment; do not rely on an unversioned future design. Existing Director 1 and previously saved compositions are not redesigned.

Studio transitions blend the previous scene's final frame into the new scene over 350 ms, without a fade-through-black gap or changing the audio timeline. This is **not** object morphing or continuous shared-object choreography. Captions retain the existing estimated phrase timing, not acoustic word alignment.

Director 2 detects boundary silence in managed PCM16 narration, preserving 80 ms before and 120 ms after detected activity, and removing at most two seconds at either edge. Internal pauses are retained; original audio assets are unchanged. `pacing.trimSilence: false` disables trimming. `leadIn` accepts 0.05–0.5 seconds; `sceneGap` accepts 0.15–1.5 seconds. These control the lead and trailing hold around each clip; the three-second minimum scene duration and frame rounding can create a longer hold for very short narration. This is conservative signal detection, not transcription, and cannot guarantee every quiet phoneme is identified perfectly. Review the voice. Director 1 retains its previous padding.

`mastering: "web"` measures the final mix and applies two-pass loudness normalization targeting −16 LUFS and a −1.5 dBTP ceiling, with encoding headroom. It then checks the encoded AAC output (loudness within 1 LU, peak within 0.05 dB measurement tolerance); failure fails the render rather than returning an unchecked master. The render's `status.audioMaster` and Director's `result.review.audioMaster` report `state`, target and measured levels. Director 2 also reports per-scene `result.review.narrationTrimming`. `silent`/`no_audio` are explicit states, not successful narration. This does not repair bad pronunciation, voice selection, background noise or a poor narration/music balance. Listen to the complete result. No music or effects are added unless supplied.

Landscape is 1280×720 or 1920×1080; portrait is 720×1280 or 1080×1920; square is 720×720 or 1080×1080. Unsupported ratios/resolutions are rejected, not silently changed. Layouts reflow per aspect; actual logos and content media use `contain` to preserve proportions and avoid cropping. Provide a transparent logo with modest built-in whitespace and contrast appropriate to `brand.background`. Convert SVG to a real PNG yourself; arbitrary SVG/remote logo URLs are not accepted. Inline raster limits and the total 2 MB Director request limit still apply.

Existing uploaded fonts, audio, video and `music` controls remain supported. Uploaded references must belong to the calling app/workspace. Sources are **supplied text**, not automatic URL scraping: a YouTube URL alone does not fetch a transcript, download footage or establish reuse rights. Supply an authorized transcript and separately uploaded, owned media. Referenced footage must cover the compiled scene at its selected `sourceStart`; short clips fail validation instead of freezing or looping silently.

`brief.evidence` remains the approved factual reference list. Source material is context, not independent verification. Only publish claims you can support; referencing an evidence ID does not prove the claim. Fictional data and illustrative social posts are not real campaign results. There is no automatic fact-checking, asset sourcing, music selection, acoustic caption alignment or commercial-quality approval.

Example request (replace the logo placeholder with actual PNG bytes before submitting):

```js
const data = {
  version: "director-2-preview",
  idempotencyKey: "advisory-article-reel-001",
  model: "core/or-glm-5.3-flash",
  reasoning: { effort: "low" },
  budget: { maxCalls: 2, maxOutputTokens: 10000, maxAiCostCents: 50, maxRepairs: 1 },
  source: {
    type: "article",
    title: "Turn expertise into a focused offer",
    text: "Package your expertise around a customer problem. Begin with a diagnostic, scope the offer, then establish a repeatable delivery rhythm.",
    rightsConfirmed: true
  },
  treatment: "explainer",
  durationSeconds: 60,
  brand: {
    name: "Your advisory business", logo: "company_logo",
    background: "#101522", foreground: "#ffffff", accent: "#e3c37b", font: "sans"
  },
  assets: { company_logo: "data:image/png;base64,REPLACE_WITH_REAL_BYTES" },
  fonts: {},
  output: { aspectRatio: "9:16", resolution: "1080p", fps: 30 },
  voice: { id: "am_michael", rate: 1 },
  brief: {
    audience: "Experienced executives exploring a fractional practice",
    objective: "Explain how expertise becomes a focused advisory offer",
    pain: "A job title does not explain the problem you solve.",
    benefits: ["Define a buyer problem.", "Build a scoped, repeatable offer."],
    evidence: [{ id: "method", kind: "demonstration", claim: "The supplied article describes diagnosis, scoping and repeatable delivery.", source: "Our authorized article" }],
    cta: { label: "Explore our approach", destination: "example.com" }
  }
};
// Existing backend transport and service-account authentication:
const envelope = { slug: "series-x-video-api", appId: "YOUR_APP_ID", action: "director_create", data };
```

The returned semantic `storyboard.beats` uses `scene: {kind,title,items,asset?,sourceStart?}` plus `narration`, `understand`, `action`, `consequence`, and `evidenceIds`. Available scene kinds: `hook`, `contrast` (two labels), `process` (2–4 steps), `editorial` (requires evidence), `media` (requires a content image/video, not the logo), `social` (illustrative LinkedIn post), `outcome`, `cta`. There are 4–8 beats; first hook and last CTA. Titles ≤60 characters and up to four item labels ≤46 characters. The final CTA is taken from your brief even if the model attempts to replace it. Supply this semantic plan as `storyboard` to override planning, or edit the returned `result.video` layers for advanced rendering through the unchanged `render` action.

Duration is checked twice: a conservative narration word ceiling before speech, then measured audio before rendering. `narration_exceeds_requested_duration` means speech was produced but no render was submitted; explicitly revise the script rather than stretching, truncating audio or silently exceeding the chosen cap. Phrase captions are estimated within measured clips—not word timestamps. Music ducking follows those narration windows. `invalid_director_plan` includes a field-specific `errorDetails` where available; repeated identical repair failures stop spending. AI and already-produced speech/request usage remain metered on failed jobs.

Preview status reports `commercialApproval: false`. Watch the output, review real logo contrast, custom-font readability, claims, pacing and sound before publishing. No promise that every arbitrary business brief will become an effective advertisement without editorial review.

| New action | Upstream route | Purpose |
| --- | --- | --- |
| `director_create` | `POST /director` | Queue a new, explicitly budgeted proof |
| `director_status` | `GET /director/{jobId}` | Read this app's status, editable composition, measured timings and AI charge ledger |
| `director_revise` | `POST /director/{jobId}/revise` | Submit a full revised request with a **new** idempotency key; original retained |
| `director_cancel` | `POST /director/{jobId}/cancel` | Stop future Director operations; already-submitted speech/render jobs may finish |

Access is backend service-account only: the **same app** needs an active service key with `integrations` and `ai`, its existing plan entitlement, an enabled AI grant, a permitted structured-output model and available credits. `video:director` is a legacy compatibility scope and is no longer required. The managed Video service is shared across apps, but its configuration/secrets remain private and job/asset ownership is derived from each caller's app and workspace. Private customer proxies are not shared. Workspace/admin browser tokens do not delegate spending. Credentials and permissions are rechecked between stages; this change does not grant new models, credits or plan entitlements.

The app credential is forwarded **only to the fixed Madnion-owned HTTPS Director route**, encrypted at rest for the job, unavailable in status/logs/model prompts, and removed on termination or expiry. No provider key or platform-wide AI credential is given to the worker. Jobs, uploaded media, narration and renders retain the originating workspace/app boundary. Deployments must configure a dedicated encryption key; do not reuse a provider key.

```json
{
  "slug": "series-x-video-api",
  "appId": "YOUR_APP_ID",
  "action": "director_create",
  "data": {
    "version": "director-1-preview",
    "idempotencyKey": "advisorpro-proof-001",
    "model": "core/or-glm-5.3-flash",
    "reasoning": {"effort": "low"},
    "budget": {"maxCalls": 3, "maxOutputTokens": 16000, "maxAiCostCents": 50, "maxRepairs": 2},
    "brand": {"name": "AdvisorPro", "background": "#faf9fb", "foreground": "#171717", "accent": "#e72587", "font": "sans"},
    "output": {"aspectRatio": "16:9", "resolution": "1080p", "fps": 30},
    "voice": {"id": "am_michael", "rate": 1},
    "assets": {},
    "fonts": {},
    "brief": {
      "audience": "Advisors, consultants and fractional executives",
      "objective": "Demonstrate why expertise-led content supports relevant conversations",
      "pain": "Potential clients rarely see the expertise you deliver.",
      "benefits": ["Turn expertise into useful content.", "Review outreach before it is sent."],
      "evidence": [{"id": "workflow", "kind": "demonstration", "claim": "Source knowledge informs reviewed drafts.", "source": "Customer-approved workflow; illustrative UI, not a recording"}],
      "cta": {"label": "Put your expertise to work.", "destination": "app.advisorpro.ai"}
    }
  }
}
```

Discover granted models using `/ai-proxy/models?app_id=...`; the example alias is not a grant. The planner requires a text model; Director never escalates to a stronger model on its own (the Gateway's existing provider-routing policy still applies). Optional `reviewModel` must be separately granted for structured text output and image inputs; image-only models are not reviewers. The review explicitly requests text output, not image generation. Eight sampled frames can identify some layout faults, **not** certify complete motion continuity or audio. The current Director automatically repairs invalid JSON/unsupported scene schemas only (up to `maxRepairs`); visual findings require an explicit revision. Default API availability is not a commercial-quality certification: review every output before publishing it.

Customization: `assets` and `fonts` use the existing owned-media contract below. Supply a full `storyboard` to override model planning: `{subjects, beats:[{scene,narration,understand,action,consequence,evidenceIds}]}`. Every beat must explain its visible action and consequence. Use 4–10 beats, beginning with `hero` and ending with the brief's exact CTA; existing interface scene props/timing/shared transitions apply. Keep evidence IDs grounded in the brief. `analytics` remains explicitly illustrative. The initial Director cannot run customer code, Blender scripts, arbitrary URLs or publish anything. Submitted sources are context, not browsing instructions or independently verified claims.

For a reproducible component example, [download the validated AdvisorPro storyboard](/advisorpro-director-storyboard-example.json). Assign that JSON object to `data.storyboard` in the [sample request](/advisorpro-director-test-request.json), persist a new explicitly approved idempotency key, and submit normally. This skips planning inference (optional review inference is separate) but still runs paid/metered narration and rendering. It contains illustrative interfaces, not captured product footage or verified commercial outcomes. Keep the matching brief/evidence/CTA, or update both the brief and storyboard together.

Optional music: `music: {asset: "bed", volume: 0.16, duckVolume: 0.035}` with `assets.bed` referencing your uploaded, licensed audio. It must cover the final measured timeline. Ducking follows known narration clip windows, not acoustic speech detection. Captions are **estimated phrase timing**, not word timestamps or transcription. Director 2 masters the final mix by default as described above; Director 1 remains unchanged. Listening and creative approval are still release-review tasks. Do not advertise music or sound effects as included when none were supplied.

AI budgeting: `maxAiCostCents` limits managed Gateway credit reservations across the job, **not all media/API-request charges or measured VPS compute**. `maxCalls` caps model attempts, `maxOutputTokens` bounds each response, and invalid-output repairs consume the same budget. The Gateway checks `max_cost_cents` before reserving credits; it rejects a call whose worst-case reservation would exceed the remaining ceiling. `result.ai.costCents` contains confirmed Gateway charges; `costComplete:false` means an interrupted call needs reconciliation. No prices or Stripe settings change.

Monitoring and billing: Director proxy requests are recorded with verified workspace/app, action, status and duration, using the existing API-request meter. AI calls use the existing Gateway credit reservation/settlement and return their charge ledger; speech/render requests use their existing metering. Polling is an API request, not a new render. Existing pricing, included allowances, spending caps and billing enforcement configuration apply; this release does not change prices or switch Stripe into live billing. There is no new Director surcharge. A logged failed request is not proof of a settled charge.

Reasoning and output controls: `reasoning: {effort: "low"}` configures the planner; optional `reviewReasoning` separately configures `reviewModel`. GLM 5.3 Flash supports `low`, `high`, `max`, requires thinking, and defaults to `low` in Director when omitted. Other models must support the requested effort; unsupported provider settings fail without paid retries. `maxOutputTokens` accepts 512–32,768, bounded by the granted model limit, and includes thinking **plus** visible JSON, not words. The recommended GLM starting point is low reasoning and 16,000 tokens under an unchanged 50-cent ceiling. Reasoning text is excluded from responses/storage but still billed. Existing callers' supplied limits are never silently raised.

Planning prompts now include an explicit JSON schema for required beat fields, per-component props/text limits, allowed asset aliases and the exact CTA. The current managed route uses `json_object` response mode, not guaranteed provider-enforced strict schema. Ownership, evidence, timing and shared-transition continuity are always validated locally before speech/rendering. `invalid_scene_schema` includes a bounded `errorDetails` validation hint; no credential or internal reasoning text is returned.

`result.ai.requests` records `finishReason`, usage, applied reasoning effort and output allowance alongside the request ID and settled charge. `model_output_limit` (finish reason `length`), `model_empty_output`, and `model_incomplete_response` stop immediately, without repeating the same paid attempt or switching models. A repeated JSON/schema error stops as `model_repair_repeated`; otherwise repairs receive the previous plan and validator feedback within the original call/cost limits. A new revision is explicit and separately budgeted. `needs_review` with `result.error` is not a successful video.

Separate single-host durable queue: one Director worker, 50 pending jobs globally, 10/workspace, 5/app. Existing video/voice workers retain their limits. More admitted requests do not mean more simultaneous rendering. Queue/state survives restart. If an external operation's outcome is unknown after a crash, stop in `needs_review` rather than automatically spending again. `429` means bounded admission; `503 director_not_enabled` means an operator has temporarily disabled the service. Cancellation cannot refund completed work or interrupt already-submitted downstream renders.

Director input, plans and step payloads expire after seven days; minimal audit/ownership history remains. Temporary frame-review files are removed after the review attempt. Existing media has its **own** seven-day clock, starting when each job/upload was created; entering Director review does not extend it. Download outputs promptly. Apps retain their Director and media job IDs and request `status` for fresh signed links rather than storing signed URLs in their database.

Backend helper: [video-director-sdk.mjs](/video-director-sdk.mjs). It exposes `create`, `status`, `revise`, `cancel` using your existing functions client. **Never pass a service-account key in a public/browser bundle.**

## Request usage and attribution

Requests forwarded through this service appear in workspace Analytics, its Connector usage view, and the selected app's Connector usage. Each recorded request counts once, including polling, downloads and upstream errors; action and status distinguish them from work submissions. A request count is not a completed-job count or a measured compute cost.

Use a verified app credential, or supply an app belonging to your workspace with your workspace credential, for app-level attribution. Calls without an app remain workspace-only; they are not assigned to an unrelated app. Requests rejected before upstream dispatch are not connector operations. Historical proxy logs are restored to reporting without creating retroactive charges. Existing prices and Stripe billing configuration are unchanged.

## Download deadlines and cleanup

**Download completed videos and narration immediately into your app's own storage. Madnion is temporary processing storage, not a permanent media library.**

| Data | Availability / retention |
| --- | --- |
| Generated video and Voice/Speech audio | 7 days from job submission, not completion |
| Signed preview/download link | 15 minutes from issuance, subject to the underlying file still being available |
| Ready uploaded footage, audio and fonts | 7 days from upload initialization; queued/running jobs already referencing them are protected |
| Pending/failed upload files and orphan/failed attempt files | Eligible for cleanup after 24 hours; active uploads and active job files are protected |
| Crawl/Search result bodies | 24 hours from job creation; save results in your own storage |
| Minimal job/asset metadata, ownership and audit history | Preserved; these records cannot reconstruct deleted media |

On a completed Video or Speech response, `expiresAt` is the **signed-link expiry**, not the seven-day storage deadline. Save the job ID, app/workspace context and submission time in your backend. Poll with the same authorized app until `done`, then download `downloadUrl`, verify the HTTP response and save the bytes durably. Do not store only the temporary URL or use it as a permanent website/video-player URL.

If a link expires, request `status` (or `speech_status`) again for a fresh link while the job remains available. Refreshing a link does **not** extend storage retention. After retention expires, status/media may return 404; resubmitting the original inputs with a new idempotency key is required to generate a new output. Do not interpret every 404 as expiry: wrong app/workspace or an unknown ID also returns not-found.

Cleanup is scheduled hourly, with up to two minutes of jitter and catch-up after downtime. Asset cleanup runs hourly inside the Video API and fails closed if active references cannot be checked. Expiry is an access deadline, not a promise that bytes remain downloadable until the next cleanup. Active jobs and their referenced assets are protected; expired assets cannot be selected for new jobs. Re-upload them before submitting new work.

Legacy generated files are also reviewed: expired terminal outputs/assets follow a seven-day policy, and orphan generated outputs/intermediate voiceovers become eligible after 24 hours. Open files and active references are skipped; unidentified assets are retained for review. Legacy job/asset records and deletion audit manifests are preserved. Deletion is permanent—there is no restore API. This cleanup does not delete app settings, credentials, integrations, customer business records or data in third-party vendor accounts.

This is an API-only composition renderer. Your platform owns the editor and user experience. No internal agent is required. Generated content has no automatic Madnion watermark.

Standalone [Voice API](/docs/voice-api) and [Crawl API](/docs/crawl-api) have separate references. Existing video and speech action names below remain supported; no migration is required. Crawl supports public HTML fetching and opt-in isolated JavaScript rendering.

Call the existing `custom-proxy` function with your authorized Madnion credentials and the `series-x-video-api` integration. Never send the upstream Video-Gen secret to a client.

## Vector motion layers — `interface-motion-1`

An opt-in extension to version 3, using pinned `lottie-web` 5.13.0 SVG-light rendering. Existing `interface-1`, v1 and v2 inputs remain unchanged; brief mode still defaults to `interface-1`. Discover support through `scene_catalog`: its `motion` object contains limits and a complete original, editable example. This adds no paid AI generation or new credential.

1. Call `scene_catalog` through the existing `series-x-video-api` custom proxy.
2. Take `motion.example` from the upstream response (unwrap the custom-proxy response envelope), or adapt your own v3 composition with `renderer: "interface-motion-1"`.
3. Set `motion.animations` to a map of inline, shape-only Lottie JSON objects. These are request data, not globally shared asset IDs. Reuse an animation alias across scenes.
4. Call `scene_compose` with the composition, then `validate` and `render` with `{ "video": composition }`. Existing status/download, audio, font and caption inputs remain supported.

Each scene can add motion layers:

```json
{
  "component": "showcase",
  "duration": 4,
  "title": "Carry the idea forward",
  "transition": "shared",
  "transitionDuration": 0.8,
  "props": {"features": ["Your brand", "Your timing"]},
  "motion": [{
    "id": "signal",
    "animation": "pulse",
    "x": 75, "y": 60, "width": 16, "height": 24,
    "start": 0, "end": 4,
    "speed": 1, "loop": true, "opacity": 1,
    "colors": {"#6633ff": "#7855d8"}
  }]
}
```

This is a scene fragment, not a standalone render request: define `motion.animations.pulse` and a preceding scene with matching motion `id`/`animation` for the shared transition. The catalog example includes both.

Positions/sizes are percentages of the output frame; the entire box must stay within 0–100%. Animations use contain framing above interface content and below captions. Start/end are scene-local seconds, speed is 0.1–4, opacity is 0–1. Colours replace exact **static fill/stroke** RGB values; animated colours, text and image substitutions are not supported. Playback is explicitly frame-seeked, not browser autoplay.

`transition: "shared"` requires an adjacent matching post subject (editor/social-post/calendar), or a matching motion-layer `id` and animation alias. Motion layers must end at the previous scene boundary and start at zero in the next. Their position/size/opacity interpolate over `transitionDuration` (0.2–2 seconds, at most half the scene). Their source clock continues when speed/loop remain unchanged. An opaque directional reveal changes the background while the shared element moves independently; this avoids ghosted text, but is not arbitrary shape morphing. Cut/wipe remain available. Colour changes between scenes are immediate, not interpolated.

**Accepted Lottie subset:** 2D shape layers, ellipses, rectangles, paths, groups, fills, strokes, transforms and trim paths, with static/keyframed numeric properties. Maximum six animations, four placements per scene, 24 shape layers and 160 shapes per animation, 250 KB per animation / 600 KB total, 2048px dimensions and 1800 source frames at 1–60 fps. The existing total HTTP request limit still applies. Unknown/unsupported fields fail validation rather than silently fetching dependencies.

**Not supported:** `.lottie` ZIP archives, expressions, scripts, external URLs, image/text/font layers, precomps, repeaters, effects, masks or 3D inside Lottie. Use existing owned media/font inputs outside the animation. Do not upload marketplace assets unless their licence permits the intended use; the included signal-ring example is original and reusable with this API. The media-queue-1 runtime enforces verified workspace/app ownership for jobs and uploaded assets.

Technical validation is not commercial approval. Review actual motion, copy, audio and each output ratio before publishing. A small animation layer should support the story, not cover the product UI.

## Rich interface components — `interface-1`

New `creative_plan` requests with `mode: "brief"` default to the **`interface-1` renderer** and return a version 3 component composition. This is a shared renderer, not an AdvisorPro-only demo. It provides consistent button geometry, interface typography, action timing, opaque directional reveals and continuous post handoffs. It is deterministic: no paid AI director is required.

**Compatibility:** existing v1 layers, Storyboard v2, old `scene_compose` recipes and named compositions retain their existing behavior. They are not silently upgraded. Pin `renderer: "classic-1"` on brief requests to keep the previous planner. Pin `video.renderer: "interface-1"` on v3 compositions; unknown versions fail rather than falling back. Save the returned JSON and your assets for reproducibility. This pin identifies the schema/motion family, not bit-identical encoder output across infrastructure upgrades.

### API flow with your existing connection

1. Call `creative_formats` or `scene_catalog`. Look for `defaultRenderer: "interface-1"` and `interfaceComponents`.
2. Send a brief to `creative_plan`, or send a v3 component document to `scene_compose`. Advanced callers may also send `{mode:"advanced",video:...}` to `creative_plan`.
3. Read `video`, `editorial.cueSheet` and `preflight` from the response envelope. Edit the returned component data as needed.
4. Call `validate` with `{video}`. Then call `render` with the same `{video}`, poll `status` and use the existing download action/URL.

These are the same workspace-authorized proxy actions and saved Video-Gen key; do not put the upstream secret in a browser. SDK/CLI clients that can invoke these existing actions can send the same JSON without a new credential. This release does not change billing or introduce a new SDK package.

Download the [complete editable AdvisorPro-style example](/video-api-interface-example.json). It contains illustrative content, no client assets or secrets. Change its brand, subject, copy and media before use. It renders silently unless you add audio.

```json
{
  "slug": "series-x-video-api",
  "action": "scene_compose",
  "data": {
    "version": 3,
    "renderer": "interface-1",
    "output": {"aspectRatio":"16:9","resolution":"1080p","fps":30},
    "brand": {"name":"Your consultancy","background":"#171526","foreground":"#ffffff","accent":"#783bea"},
    "subjects": {
      "article": {
        "title":"Focus on the work that matters",
        "body":"A useful perspective, grounded in your experience.",
        "author":"Your consultancy",
        "role":"Independent expertise",
        "platform":"linkedin",
        "destination":"your-website.example"
      }
    },
    "scenes": [
      {"component":"editor","duration":5,"title":"Shape your perspective","subject":"article","props":{"fieldLabel":"Post content","button":"Review post"},"action":{"at":3,"label":"Reviewed"}},
      {"component":"social-post","duration":5,"subject":"article","props":{"interaction":"comment","comment":"How would this work for our team?"},"action":{"at":2.5,"label":"Comment draft"}},
      {"component":"calendar","duration":4,"title":"Make consistency visible","subject":"article","props":{"weekStart":"2026-10-19","day":1,"hour":10},"action":{"at":2,"label":"Scheduled"}}
    ]
  }
}
```

The three scenes share `subject: "article"`: the same post moves between them, instead of being replaced with unrelated cards. This is a **visual demonstration**, not a LinkedIn API call or proof of publishing. Use authorized brand assets. No account names, comments, meetings or results from another customer are bundled.

### Component inputs

Every scene accepts `component`, `duration` (3–30 seconds), `title` (65 characters), `subtitle` (100), optional `subject`, `props`, `action: {at,label}`, `transition: "wipe" | "cut"` and `zoom` (1–1.06). `action.at` is **seconds within the scene**, from 0.8 to duration minus 0.65. Returned cue-sheet times are absolute seconds in the film. A maximum of 20 scenes and 120 seconds applies. Unknown fields are rejected.

| Component | `props` and behavior |
|---|---|
| `hero`, `cta` | `label` (28), `destination` (55), `logo` image alias. Brand-led opening/close with an aligned button. |
| `library` | `query` (50), `items` (up to 4 `{title,detail}`; 38/65 characters). Search typing, file rows and visible source selection. |
| `editor` | `fieldLabel` (30), `button` (24); requires `subject`. Toolbar, typed draft, review action and live post preview. |
| `social-post` | `interaction: "comment" | "like" | "none"`, `comment` (80); requires `subject`. Recognisable author, image, article and social-action hierarchy. Comment is a simulated draft. |
| `calendar` | `weekStart` ISO date, `day` integer 0–4 relative to that date, `hour` integer 9–13, optional up to 4 `entries: [{title,day,hour}]` (title 28). Desktop week grid; portrait focused day grid. Requires `subject`. |
| `analytics` | Up to 4 `metrics: [{label,value}]` (label 22; value 0–1 billion), `sample` defaults true, `source` (180). Animated bars. `sample:false` requires a source description; it does not independently verify the data. |
| `showcase` | `asset`, `kind: "image" | "video"`, `sourceStart`, up to 3 `features` (55 each). Contains—not crops—screenshots and footage; footage must cover the whole scene. |

`subjects` is a map of up to 12 reusable objects. Each supports `title` (60), `body` (180), `author` (36), `role` (45), `destination` (45), `image` and `avatar` image aliases, and `platform: "linkedin" | "generic"`. Supply your own image for actual visual evidence; the no-image fallback is neutral branded artwork. `assets`, `fonts` and `audio` retain the existing media contract below. Images/logos/avatars are PNG/JPEG data URIs, not remote URLs or raw SVG. Uploaded audio/video/fonts are owner-validated asset IDs.

### Narration, fonts, captions and music

Custom font example: `fonts: {brand: {assetId:"YOUR_FONT_ASSET_ID"}}`, then `brand.font: "brand"`. Font aliases and file kinds are validated before rendering. This release uses one font family with browser-synthesized regular/bold styles; it does not add separate uploaded weight variants.

Provide explicit audio tracks and trims. For example, after registering `assets.voice` and `assets.music` using uploaded asset IDs:

```json
{
  "audio": [
    {"asset":"voice","start":0,"sourceStart":0.15,"duration":10,"volume":1,"fadeIn":0.02,"fadeOut":0.05},
    {"asset":"music","start":0,"duration":14,"envelope":[{"time":0,"value":0.08},{"time":10,"value":0.08},{"time":10.4,"value":0.22},{"time":13.5,"value":0.22},{"time":14,"value":0}]}
  ],
  "captions": [
    {"text":"Start with your expertise.","start":0,"end":3},
    {"text":"Turn it into a useful perspective.","start":3,"end":6}
  ]
}
```

This example fits the 14-second composition above. Audio sources must cover their requested trims. Caption cues are ordered/non-overlapping, 90 characters maximum, at least 0.4 seconds, and within the film. Narration is not transcribed automatically, leading silence is not automatically removed, and ducking is **explicit envelope automation**, not acoustic detection. Put a supplied licensed/original click at a cue-sheet time to match the visible action; sounds are not synthesized by this renderer.

### Quality boundary

Landscape, portrait and square outputs are supported at 720p/1080p and 24/30 fps. Portrait uses stacked editing and day-calendar layouts rather than a shrunken desktop. Validation checks the schema, finite timings, text-length budgets, font aliases, media ownership/kind/duration and caption/audio coverage. It is **not** measured glyph-overflow detection or a guarantee of commercial quality. Review a render with your actual font, copy, images and audio before publishing. There is no new preview-frame endpoint, speech alignment service or pricing change in this component release. The current download and retention policy is documented above. Jobs and uploaded assets are now isolated by verified workspace/app scope.

## Render

## Reusable scenes and creative planning (preview)

Available on the existing workspace-scoped `series-x-video-api` connection, using the same saved Video-Gen key. These are deterministic composition tools, not paid generative AI. Access requires an authorized workspace integration key, integration-scoped app service key, or workspace administrator session. This is not a globally enabled connector for every workspace.

| Madnion action | Purpose |
|---|---|
| `scene_catalog` | Discover versioned scene IDs, inputs and examples |
| `scene_compose` | Assemble scenes into editable v1 layers |
| `creative_formats` | Discover brief-driven formats |
| `creative_plan` | Compile a brief or validate advanced composition input |
| `validate` | Validate `{ "video": ... }` without rendering |

POST these actions to `https://api.madnion.com/functions/v1/custom-proxy`. Discovery example: `{ "slug":"series-x-video-api", "action":"scene_catalog" }`.

```json
{
  "slug":"series-x-video-api",
  "action":"scene_compose",
  "data":{
    "brand":{"name":"Your brand","background":"#101521","foreground":"#ffffff","accent":"#6855cc"},
    "output":{"aspectRatio":"16:9","resolution":"1080p","fps":30},
    "scenes":[
      {"component":"pain-hook","version":1,"duration":5,"props":{"title":"Expertise deserves to be seen.","detail":"Start with what makes you different."}},
      {"component":"input-demo","version":1,"duration":6,"props":{"title":"Build around your knowledge","label":"Website or portfolio","value":"portfolio.example","button":"Build knowledge"}},
      {"component":"workflow","version":1,"duration":6,"props":{"title":"Connect the next steps","items":["Knowledge","Campaign","Conversation"]}},
      {"component":"cta","version":1,"duration":5,"props":{"title":"Make your next move.","detail":"Explore the product","destination":"example.com"}}
    ]
  }
}
```

The composition result contains `video`, `review` and `preflight` (inside the custom proxy's response envelope). Submit that `video` to `render` using `data: {video}`. You may edit all returned layers, keyframes, timing and audio before validating and rendering again. No immutable whole-film template is required.

Six components: `pain-hook`, `input-demo`, `workflow`, `device-demo`, `benefit-cards`, `cta`. Call `scene_catalog` for their exact required props. Each requires `version: 1`; duration is 3–15 seconds, maximum 20 scenes and 120 seconds total. Landscape, portrait and square are supported. Pass `assets`, `fonts` and `audio` using the existing v1 contract below. `device-demo` requires `{title,label,asset,kind}` with an owned image/video asset alias; it uses contain framing. Video footage must cover the scene duration.

These initial adaptations come from AdvisorPro and FleetGo projects; they do not yet cover every historic video. Client footage, logos, voices and claims are not bundled. Recipes remain `requires_review`: inspect typography, cropping, transitions, sound and claims before commercial use. Scene boundaries currently use cuts, not shared-element transitions. No automatic narration generation is included in composition; use the speech actions separately.

### Older layer-based connected story — experimental compatibility mode

The compatibility planner exposes `mode: "visual_story", version: 1` through the **existing `creative_plan` action**. For new productions prefer the richer `interface-1` components above. This older mode flattens UI into primitive layers and does not use the new component renderer. Check `creative_formats` before using any version; if absent, do not silently substitute a different format.

It illustrates search → research themes → strategy → content editor → social feed → publishing calendar → illustrative analytics. One selected thumbnail and topic move through the workflow. The compiler is deterministic, does not call paid AI, does not access social accounts and never actually publishes. UI and analytics are explicitly illustrative, not verified activity.

```json
{
  "slug": "series-x-video-api",
  "action": "creative_plan",
  "data": {
    "mode": "visual_story", "version": 1, "duration": 30,
    "brand": {"name":"Your brand","background":"#f4f6fb","foreground":"#172139","accent":"#6354db"},
    "output": {"aspectRatio":"16:9","resolution":"1080p","fps":30},
    "assets": {"hero":"data:image/png;base64,REPLACE_WITH_VALID_IMAGE"},
    "story": {
      "query":"Ideas for independent advisors",
      "topic":"Expertise that opens doors",
      "sources":["Industry research","Customer questions","Your portfolio"],
      "themes":["Expertise","Trust","Growth"],
      "audience":"Independent advisors",
      "post":"Turn expertise into useful conversations.",
      "platform":"linkedin",
      "thumbnail":"hero",
      "cta":"Build your next campaign",
      "destination":"your-product.example"
    }
  }
}
```

Replace the image placeholder with a valid owned PNG/JPEG data URI under the existing image limits. Optional `story.logo` and `story.platformLogo` name additional image aliases. Supply approved platform marks yourself; logos, customer imagery and copyrighted reference assets are not bundled. Without a platform-logo asset, a text platform label is used. Supported platform labels are `linkedin`, `instagram` and `x`; this first release shares a simplified feed treatment, not an exact reconstruction of each product.

Optional `story.sourceThumbnails` supplies exactly three image aliases for visual variety in research, the media selector and calendar. The main `story.thumbnail` remains the persistent selected content object. Without variants, the selected thumbnail is repeated; production callers should supply varied, relevant imagery. The feed includes a brand avatar, post preview, social actions and illustrative comment placeholders, not actual customer replies or engagement counts.

Version 1 supports landscape only, 20–30 seconds, 720p/1080p and 24/30 fps. `sources` and `themes` require exactly three labels of at most 24 characters. Limits: query 42, topic 32, audience 28, post 70, CTA 32, destination 40 characters. Unknown fields and unsupported versions are rejected. Duration scales choreography, but does **not** time-stretch caller audio: supply correctly timed tracks. Faster versions need extra readability review.

The result includes `video`, `editorial.continuity`, timed `editorial.beats`, `editorial.cueSheet`, `review` and `preflight`. All layers and keyframes remain editable; there is no locked rendered template. Media/font validation checks the upstream credential plus verified Madnion workspace/app owner.

Pass `fonts` and `audio` through the existing composition contract for uploaded fonts, narration, effects and explicit ducking envelopes. No automatic narration, alignment, licensed sound library or music selection is added. Rendered review remains mandatory: preflight is not a perceptual quality score. The six legacy scene recipes are unchanged; brief mode now defaults to `interface-1`, as described above.

### Rendering the composed video

POST `https://api.madnion.com/functions/v1/custom-proxy`

```json
{
  "slug": "series-x-video-api",
  "action": "render",
  "data": {
    "video": {
      "version": 1,
      "output": {"aspectRatio":"16:9","resolution":"720p","fps":30},
      "brand": {"background":"#090914","foreground":"#ffffff","accent":"#8855ff","font":"sans"},
      "scenes": [{
        "duration": 5,
        "gradient": "#302050",
        "cameraZoom": 1.04,
        "transition": "fade",
        "layers": [
          {"type":"text","text":"Your brand. Your story.","x":10,"y":30,"width":80,"height":35,"fontSize":6,"bold":true,"enter":"rise"},
          {"type":"rectangle","x":10,"y":73,"width":35,"height":1,"color":"#8855ff","enter":"scale","delay":0.3}
        ]
      }]
    }
  }
}
```

Response: `{ "jobId": "job_...", "status": "rendering" }`. Store the job ID and poll status every 3–5 seconds with a bounded client timeout. Do not repeatedly resubmit the render while polling.

The initial status may be `queued`. Include an optional `idempotencyKey` alongside `video` inside `data` (8–128 letters, numbers, underscores or hyphens). Retrying the same payload with the same key and upstream credential returns the original job ID, including after restart. Reusing the key with a different normalized payload returns HTTP 409. Payload fingerprints are order-sensitive; retry the same JSON structure. The key stays reserved for the lifetime of the stored job record.

## Status and output

```json
{"slug":"series-x-video-api","action":"status","pathParams":{"jobId":"job_..."}}
```

States: `queued`, `rendering`, `done`, `error`. Successful completion includes `previewUrl`, `downloadUrl`, and `expiresAt`. Links are bearer capabilities, valid for 15 minutes. Anyone holding a link can access that file until expiry. Do not put links in public logs. Request status again to refresh links. Preview supports byte ranges; download uses attachment disposition. Provider keys never appear in either URL.

## V1 composition fields

- `version`: must be `1`. Unknown fields are rejected, not silently ignored.
- `output.aspectRatio`: `16:9`, `9:16`, `1:1`.
- `output.resolution`: `720p` or `1080p` (short-side size); `fps`: 24 or 30.
- `brand`: `background`, `foreground`, `accent` as hex colors; `font`: `sans`, `serif`, `mono`, or an alias declared in `fonts`. Optional `name` is metadata, not an automatically rendered label. Use a text layer to display it.
- `assets`: map of names to inline PNG/JPEG data URIs or uploaded audio/video `{ "assetId": "asset_..." }` references. Inline images are limited to approximately 1 MB each, 4096 pixels per side, and 12 megapixels. Maximum 48 assets. The render request must fit the 2 MB JSON limit; binary audio/video uploads are separate. JPEG baseline/progressive headers are supported. SVG and arbitrary remote media URLs are not accepted.
- `scenes`: 1–20 entries, each 1–30 seconds; maximum combined duration 120 seconds.
- Scene fields: `duration`, `background`, optional `gradient` (second gradient color), `cameraZoom` (1–1.3), optional `camera` (depth-1 tracks above), `transition` (`cut`, fade-through-background `fade`, or `crossfade`), and `layers` (0–90, in back-to-front order; camera screen layers render above world layers). Composition v1 `crossfade` blends the previous final frame for 350 ms; scene/audio timings are unchanged.
- Composition v1 optional `audioMaster: {"targetLufs": -16, "truePeakDb": -1.5}` masters and verifies the encoded final mix. Allowed targets: −24 to −14 LUFS; peak −3 to −1 dBTP. Omit it to preserve existing audio behavior. It is not currently a top-level field of Storyboard v2/interface v3. Completed `status.audioMaster` returns measured values; assets and other jobs are never modified.

### Layers

Types: `text`, `image`, `video`, `rectangle`, `ellipse`, `illustration`. See the depth-1 section above for illustration kinds and optional `depth`, `tiltX`, `tiltY`, `space` and `shadow` fields.

| Field | Meaning |
|---|---|
| `x`, `y`, `width`, `height` | Percent of canvas; position 0–100, size 1–100 |
| `text` | Plain text, up to 1000 characters; newlines supported |
| `asset` | Asset ID for an image layer |
| `fontSize` | Percent of canvas width, 1–25 |
| `font`, `bold`, `align` | Font preset, boolean weight, left/center/right |
| `color` | `#RRGGBB` or `#RRGGBBAA` |
| `fit` | Image `contain` or `cover` |
| `opacity`, `rotation`, `radius` | 0–1, degrees -360–360, corner radius 0–100px |
| `enter`, `exit` | `none`, `fade`, `rise`, `slide-left`, `scale` |
| `delay` | Entrance delay in seconds |
| `motionDuration` | Motion duration in seconds, 0.1–2, at most half a scene |
| `travel` | Slide/rise distance in pixels, 0–500 |

Preset animations use deterministic cubic ease-out. Text remains within its layer box; the client must allocate enough space to avoid clipping. Camera zoom crops the scene edges, so keep important content inside safe margins. No HTML, CSS expressions, scripts or arbitrary remote resource fetching is accepted.

### Explicit motion keyframes

Each layer accepts `keyframes`, with optional tracks for `x`, `y`, `scale`, `rotation`, and `opacity`, plus the geometry/depth tracks documented above. Each track has 2–32 points with strictly increasing scene-local `time` values in seconds, bounded by the scene duration. Values are bounded: x/y 0–100 percent, scale 0.1–4, rotation -360–360 degrees, opacity 0–1. Before/after a track, its first/last value is held. Each destination point's `easing` controls the preceding segment: `linear` (default for layer tracks), `ease-in`, `ease-out`, `ease-in-out`.

```json
{
  "type": "image", "asset": "photo",
  "x": 10, "y": 10, "width": 80, "height": 80,
  "enter": "none", "exit": "none",
  "keyframes": {
    "scale": [{"time":0,"value":0.85},{"time":3,"value":1,"easing":"ease-in-out"}],
    "rotation": [{"time":0,"value":-4},{"time":3,"value":0,"easing":"ease-out"}],
    "opacity": [{"time":0,"value":0},{"time":0.5,"value":1}]
  }
}
```

Tracks replace the base x/y/rotation/opacity values and multiply the preset scale. Preset entrance/exit transforms and opacity still compose with tracks; use `enter: "none"` and `exit: "none"` for fully keyframed motion. The last sampled frame is one frame interval before scene duration, so place a final keyframe earlier when a visible hold is needed.

To add a logo: set `assets.logo` to `data:image/png;base64,...`, then add `{ "type":"image", "asset":"logo", "x":5, "y":5, "width":15, "height":15, "fit":"contain" }` to a scene.

## Validation and compatibility

Invalid composition input returns HTTP 400 with `error: "invalid_video"` and a field-specific message. The legacy `{composition, props, codec}` contract remains available for existing templates. It cannot be combined with `video`. Direct unvalidated invocation of `CustomVideo` is rejected. Custom compositions export H.264 MP4 with AAC when audio is present.

## Operational limits — media-queue-1

This remains a workspace custom proxy, not a general-availability integration. All Video actions now require verified Madnion service/workspace credentials and workspace/app scope. Include `appId` consistently on render, validation, asset and status calls. Service keys cannot select another app; workspace integration keys may intentionally operate on apps in their own workspace.

**Migration:** old shared-credential job/asset records are preserved but are not assigned to an app or exposed through the new API. Legacy generated bytes follow the cleanup policy above; they are not retained indefinitely. Re-upload assets under the intended app and regenerate outputs when needed. Never infer ownership from a supplied ID. Signed links are bearer capabilities, bounded by both link expiry and underlying file availability.

The API persists jobs in a local SQLite queue with FULL synchronous commits. A separate worker claims a renewable 60-second lease, renewed every 20 seconds. Process death allows recovery after lease expiry, up to three attempts. Random lease tokens and attempt-specific output filenames prevent stale attempts from overwriting results. API restarts do not discard waiting jobs. Explicit execution failures are terminal, not silently retried.

| Limit | Current value |
| --- | --- |
| Concurrent renders | 1 globally; 1 per workspace |
| Pending/running jobs | 100 globally / 50 per workspace / 25 per app-credential scope |
| Queue waiting deadline | 1 hour |
| Render execution deadline | 20 minutes |
| Output/result retention | 7 days from submission |
| Retained unexpired jobs | 10,000 |
| Disk admission reserve | At least 2 GiB free |

HTTP 429 means capacity is full; retry after at least 10 seconds using the same idempotency key. HTTP 503 means storage/queue availability or disk reserve is insufficient. This proxy may not forward upstream Retry-After, so clients must implement backoff. Expired outputs and input payloads are cleaned automatically; minimal job/audit metadata is retained. Expired status returns 404, and an idempotency key remains reserved in retained metadata.

Use the validated `video` contract (versions 1–3). Raw legacy `composition/props` submissions and the old synchronous `/tts` surface are not exposed by this runtime. Use the separate queued Speech/Voice actions instead. Existing composition customization remains supported.

Video, Voice, Crawl and Search share a VPS budget. Worker/API processes have CPU/RAM caps, queues are separate, and scheduling is workspace-fair. These are bounded single-host services, not unlimited throughput or high availability. No completion webhook, cancellation, distributed rendering or measured-compute pricing is promised. Review actual media before publishing.

Separate queued voice generation is available through the same configured Video-Gen connection; see **Speech generation** below. Rendering itself does not automatically synthesize narration. Font-variant families, user-defined easing functions and asset-retention management remain future work. Uploaded narration/music, footage and individual custom fonts are supported as described below.

## Speech generation — same Video-Gen connection

The `series-x-video-api` custom proxy now supports `speech_voices`, `speech_generate` and `speech_status`. They reuse its existing saved Video-Gen key: do not create another provider key or send that secret to a browser. This is enabled on the existing workspace connection, not a globally available integration.

Authentication: an active app service-account key with `integrations` scope, a workspace access key with `integrations:call`, or a verified workspace owner/admin dashboard session. App IDs must belong to the connection's workspace; a service key cannot name another app. End-user SSO tokens are not accepted for speech in this release. Speech jobs are scoped to the verified workspace and app; use the same app context when polling. Requests are recorded in custom-proxy usage logs with app/workspace identity. These logs record requests, not measured server-compute cost.

```json
{
  "slug": "series-x-video-api",
  "action": "speech_generate",
  "appId": "YOUR_APP_ID",
  "data": {
    "text": "A warm welcome. A better way there.",
    "voice": "bf_emma",
    "rate": 1,
    "idempotencyKey": "welcome-voice-v1"
  }
}
```

POST to `https://api.madnion.com/functions/v1/custom-proxy` with your existing Madnion authentication. The response is HTTP 202 with `{jobId,status,characters}`. Poll rather than resubmit:

```json
{"slug":"series-x-video-api","action":"speech_status","appId":"YOUR_APP_ID","pathParams":{"jobId":"job_..."}}
```

States are `queued`, `rendering` (speech synthesis), `done`, `error`. A completed response includes `downloadUrl`, `expiresAt`, `contentType: "audio/wav"`, `bytes` and `audioSeconds`. The download URL is a bearer capability valid for 15 minutes; fetch it without credentials, do not log/share it publicly, and poll again to refresh it. A different app or workspace cannot retrieve the job through status. Anyone holding an issued download URL can use that URL until expiry.

Use `speech_voices` with no data to discover voices and bounds. The self-hosted Kokoro engine currently offers English voices `af_heart`, `af_bella`, `bf_emma` (default), `am_michael`, `bm_george`; American and British options are available. There is no voice cloning or claimed South African accent. Text: 1–1500 characters; rate: numeric 0.8–1.2. Output: mono PCM WAV at 24 kHz. No paid voice provider is invoked; server-compute costs still exist.

Optional Kitten Mini voices and supplied-script word alignment are documented in the separate [Voice API](/docs/voice-api#optional-kitten-engine). Use `series-x-voice-api` for these options. Existing Video speech calls and Director narration remain Kokoro by default; inline render `vo` does not accept these new options. Import the resulting WAV as an audio asset and apply returned word times with your scene offset when building captions yourself.

Speech uses a separate durable queue and warm CPU worker: one active synthesis globally, 200 pending/running globally, 50 per workspace and 25 per app-credential scope. Jobs have a five-minute execution limit and one-hour queue deadline. Leases/recovery follow the media runtime above. Audio expires after seven days; cleanup removes expired files and input text while retaining minimal audit metadata. Use a bounded wait and retry with the same idempotency key (8–128 letters, digits, `_`, `-`); changed input returns 409. No completion webhooks are provided.

### JavaScript SDK usage

Use the existing Supabase/Madnion `functions.invoke` SDK. A dependency-free polling helper is also provided at [/video-speech-sdk.mjs](/video-speech-sdk.mjs); download it into your application's source, or import it as a browser ES module. It is not a separately published npm package.

```js
import { createSpeechClient } from './video-speech-sdk.mjs';

const speech = createSpeechClient({
  invoke: supabase.functions.invoke.bind(supabase.functions),
  appId: 'YOUR_APP_ID',
  // Backend only. For an owner/admin dashboard session, omit this header.
  headers: { 'x-service-account-key': process.env.MADNION_SERVICE_KEY }
});
const catalog = await speech.voices();
const job = await speech.generate({
  text: 'A warm welcome. A better way there.',
  voice: 'bf_emma', rate: 1, idempotencyKey: 'welcome-voice-v1'
});
const audio = await speech.wait(job.jobId, { timeoutMs: 360000 });
// Download on your backend, then upload via asset_init(kind: 'audio')
// if you want to include it in a video render. No provider key is needed here.
const wav = await (await fetch(audio.downloadUrl)).arrayBuffer();
```

For workspace-key callers, pass your existing `Authorization: Bearer wak_...` through the invoke headers. Never put service-account or workspace keys in browser code. Use Madnion's proxy for these actions: the upstream now requires server-injected workspace/app identity in addition to its API key. An upstream key alone is not sufficient. Never distribute the upstream key to app clients or let them supply identity headers.

## Upload audio, footage and fonts

1. Call Madnion's proxy with the same authorized connection used for rendering:

```json
{"slug":"series-x-video-api","action":"asset_init","data":{"kind":"video","bytes":1234567}}
```

`kind` is `video`, `audio` or `font`. `bytes` must exactly match the file size. The response includes `assetId`, a signed `uploadUrl`, `method: "PUT"`, and `expiresIn: 900`.

2. PUT the raw file bytes to the returned upload URL with `Content-Type: application/octet-stream`. Do not attach Madnion or provider credentials. Browser CORS is enabled for this signed upload. The URL is a bearer capability: keep it private. Upload once; replay returns 409. Expired/tampered URLs return 403. Validation occurs before success. A failed upload requires a new upload-init request; it is not resumable.

```js
const response = await fetch(uploadUrl, {
  method: "PUT",
  headers: {"Content-Type": "application/octet-stream"},
  body: file // Browser File/Blob or server-side bytes
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
```

3. Confirm readiness if needed:

```json
{"slug":"series-x-video-api","action":"asset_status","pathParams":{"assetId":"asset_..."}}
```

4. Reference the ready asset from the same verified workspace/app and upstream credential. Another app cannot inspect or use it, including in validation or creative planning. Uploaded content persists on local VPS disk for seven days; re-upload near expiry before starting a long queued render.

### Formats and bounds

- Video: MP4 containing H.264; optional AAC/MP3 audio. Maximum 50 MiB, 600 seconds, 3840px per side, 8.3 megapixels and 60 fps.
- Audio: PCM WAV or MP3, mono/stereo, up to 96 kHz, 50 MiB and 600 seconds.
- Fonts: TTF or OTF, at most 2 MiB each; font table bounds are checked and browser font loading must succeed. WOFF/WOFF2, collections and external font URLs are not supported. Upload only fonts/media you have permission to use.
- Storage admission caps: 200 MiB reserved per verified workspace, 2 GiB globally, 500 unexpired asset records. Pending/failed uploads become cleanup-eligible after 24 hours; ready assets after seven days, subject to active-job protection above. Cleanup removes the file and marks its metadata expired. Capacity returns 429. There is no caller-facing delete endpoint yet.

### Composition with footage, narration and a font

Send this object as `data` of the existing `render` action after uploading the three files:

```json
{
  "video": {
    "version": 1,
    "assets": {
      "footage": {"assetId":"asset_VIDEO_ID"},
      "narration": {"assetId":"asset_AUDIO_ID"}
    },
    "fonts": {"brandface":{"assetId":"asset_FONT_ID"}},
    "brand": {"font":"brandface"},
    "scenes": [{
      "duration": 5,
      "layers": [
        {"type":"video","asset":"footage","x":0,"y":0,"width":100,"height":100,"fit":"cover","sourceStart":1,"playbackRate":1,"volume":0,"enter":"none","exit":"none"},
        {"type":"text","text":"Your headline","x":10,"y":70,"width":80,"height":20,"fontSize":5}
      ]
    }],
    "audio": [{"asset":"narration","start":0,"duration":5,"sourceStart":0,"volume":0.8,"fadeIn":0.1,"fadeOut":0.3}]
  }
}
```

Video layers span their scene. `sourceStart` is seconds into the source; `playbackRate` is 0.25–4. The source must cover the entire scene after trimming/rate adjustment. Clip audio defaults to muted (`volume: 0`); set 0–1 explicitly to include it. Layer keyframes, fit, placement and transforms also apply to video. No automatic looping or source exhaustion behavior is assumed.

`audio` accepts up to 32 global tracks (including scene narration). Each needs `asset` and `duration`; `start` (timeline seconds) and `sourceStart` (source seconds) default to zero. `volume` is 0–1; `fadeIn`/`fadeOut` are seconds, each at most half the track duration. Source/timeline overruns are rejected. Multiple tracks mix together. V2 supports explicit narration-based ducking, described below. There is no automatic loudness normalization or limiter; callers must choose levels and review their mix.

`fonts` accepts up to four aliases, used by `brand.font` or layer `font`. Font files load before frames are rendered; loading failures fail the render instead of silently substituting. This is one font face per alias; use separate aliases for different faces. Bold may be synthesized by the browser. No TTS/voice-cloning service is invoked: narration must be uploaded audio.

## Storyboard v2

Use the same `render` action and set `data.video.version` to `2`. V1 requests remain supported. V2 compiles a data-only storyboard into validated layers; it does not accept executable code, browser credentials, arbitrary CSS or remote asset URLs. Your platform owns the authoring experience.

```json
{
  "video": {
    "version": 2,
    "output": {"aspectRatio":"16:9","resolution":"1080p","fps":30},
    "brand": {"name":"Your brand","background":"#faf9fb","foreground":"#171717","accent":"#FF3399","font":"sans"},
    "assets": {
      "demo":{"assetId":"asset_VIDEO_ID"},
      "voice":{"assetId":"asset_AUDIO_ID"},
      "music":{"assetId":"asset_MUSIC_ID"}
    },
    "scenes": [{
      "type":"product_demo",
      "title":"Start with your expertise",
      "duration":8,
      "media":{"asset":"demo","sourceStart":0,"playbackRate":1},
      "presentation":{"frame":"laptop","fit":"contain","x":10,"y":12,"width":80,"height":76,"frameColor":"#303039","screenColor":"#ffffff"},
      "narration":{"asset":"voice","duration":7,"volume":0.9},
      "callouts":[{"text":"Add your website","start":2,"end":5,"y":82}],
      "captions":[{"text":"Build on your own expertise.","start":0,"end":2,"y":91,"height":8,"fontSize":2}]
    }],
    "audio":[{"asset":"music","role":"music","duration":8,"volume":0.15,"duckTo":0.04,"fadeOut":1}]
  }
}
```

### Scene inputs

All scenes accept `duration` (1–30 seconds), `background`, optional `gradient`, `transition` (`cut` or `fade`), additional v1 `layers`, `callouts`, `captions`, and `narration`. Timing is rounded to the output frame rate. Position and size values are percentages of the canvas; font sizes are percentages of canvas width. The output supports landscape, portrait and square; compose readable layouts for each format rather than relying on automatic art direction.

| Type | Inputs |
| --- | --- |
| `title` | `title`, `subtitle`, optional `logo` asset alias. For completely bespoke typography/positions, use `custom` with layers. |
| `product_demo` | `media` with asset alias, source start, playback rate and volume; `presentation` with `laptop`, `desktop` or `none`, fit and frame placement/colours. `contain` is the default and preserves the full footage. |
| `workflow` | `workflow.nodes` and `workflow.edges`: explicit positions, labels, reveal times and colours. Connections animate into view; no business logic is executed. |
| `custom` | Arbitrary supported text, image, video and shape layers, including their existing motion/keyframe inputs. |

Example workflow:

```json
{
  "type":"workflow","duration":6,"title":"Respond to what happens",
  "workflow":{
    "nodeColor":"#252128","lineColor":"#FF3399",
    "nodes":[
      {"id":"a","label":"Invitation","x":10,"y":30,"width":25,"height":14},
      {"id":"b","label":"Accepted?","x":60,"y":30,"width":25,"height":14,"color":"#B3006B","textColor":"#ffffff","reveal":0.5}
    ],
    "edges":[{"from":"a","to":"b","reveal":0.5}]
  }
}
```

### Voice, music and captions

Scene `narration` references an uploaded audio alias and requires `duration`; optional `start` is relative to the scene, with `sourceStart`, `volume`, `fadeIn` and `fadeOut`. Generate speech separately with your chosen authorized voice provider, then upload it. This renderer does not synthesize or clone voices.

Global audio can declare `role: "narration"`, `"music"` or `"effect"`. A track with `duckTo` lowers its volume around explicit narration intervals (150ms attack / 200ms release). It does not detect speech acoustically. `duckTo` cannot exceed the track's base `volume`. V1 audio also accepts an explicit `envelope` of ordered `{time,value,easing}` points for precise mixing.

Caption/callout cues accept `text`, `start`, `end`, `x`, `y`, `width`, `height`, `fontSize`, `color` and `background`. Times are scene-relative. Captions must be ordered and non-overlapping; they are burned into frames, not automatically transcribed. Caller-supplied text must fit the chosen box; review a short render for layout and readability.

### Validation and limits

The upstream authenticated `POST /validate` accepts `{video: ...}` and returns dimensions, duration, scene/audio counts without queuing a render. This endpoint is not automatically a configured Madnion proxy action; use the existing `render` action for gateway requests, or have an administrator register validation explicitly. Do not expose the upstream key to client code.

Limits: 20 scenes, 120 seconds total, 48 assets, four fonts, 32 audio tracks, 90 compiled layers per scene. V2 allows up to 30 additional layers, eight callouts, 20 caption cues, ten workflow nodes and 15 edges per scene; the combined compiled-layer limit still applies. Media source coverage, asset ownership, cue bounds and workflow references are validated before queue admission. Unknown fields are rejected.

Jobs still use the persistent, bounded queue and signed download links. This release does not add automatic browser recording, an autonomous director, completion webhooks, cancellation or distributed rendering. Per-app ownership is enforced even when a provider credential is shared. Supply approved recordings, mask private data before upload, and poll status. A successful render is not a guarantee of editorial quality or factual accuracy.
