API reference

CatalogReel

Send catalog data and photos. Get back a polished, narrated, branded video. REST, async: create a job, then poll until the video is ready. Get a key in the console or try it live in the playground.

Authentication

Every request requires a bearer token in the Authorization header: Authorization: Bearer vid_live_…. Keys are created and revoked in the console. A missing or invalid key returns 401. A machine-readable OpenAPI 3.1 spec drives SDK generation and Postman import — or explore it interactively in the API reference.

Integration checklist

Keep these values in your vendor integration environment. The only required server secret is the API key; the rest are stable request variables or values returned by CatalogReel.

FieldTypeReqDefaultDescription
baseUrlstringrequiredhttps://api.catalogreel.comREST API origin used by every request and by SDK/Postman imports.
apiKeystringrequiredServer-side bearer token created in /console/keys. Store it as a secret; never expose it in browser code.
subTenantstringoptionalYour stable customer id for agency/platform integrations. Send it as X-Sub-Tenant with an umbrella key, or mint scoped keys per customer.
webhookUrlstring (url)optionalPublic HTTPS endpoint to receive video.completed / video.failed events. Send as webhook_url on create requests.
webhookSecretstringoptionalOrg signing secret from /console/keys or GET /v1/webhook-secret. Use it to verify X-Video-Signature.
jobIdstring (uuid)optionalReturned by POST /v1/videos. Poll GET /v1/videos/{id}, reprocess, cancel, or store it for idempotent reconciliation.
idempotencyKeystringoptionalCaller-generated key for safe retries. Replays return HTTP 200 with the original job and are never double-charged.

Quickstart

A complete commercial takes four fields: vertical, subject, image_urls, and finish: "full" — which auto-fills the voiceover, burned captions, end card and music. Create the job, then poll until it's done. Everything else on this page is optional fine-tuning.

curl -X POST https://api.catalogreel.com/v1/videos \
  -H "Authorization: Bearer vid_live_••••" \
  -H "Content-Type: application/json" \
  -d '{
    "vertical": "vehicle",
    "image_urls": ["https://cdn.example.com/lot-1.jpg", "https://cdn.example.com/lot-2.jpg"],
    "subject": { "year": 2022, "make": "Toyota", "model": "RAV4", "price": 28990 },
    "finish": "full"
  }'
# → 202 { "id": "<uuid>", "status": "queued",
#         "resolved": { "credits": 30, "duration_seconds": 16, "narration": true, … } }

curl https://api.catalogreel.com/v1/videos/<uuid> \
  -H "Authorization: Bearer vid_live_••••"
# → { "status": "completed", "video_url": "https://.../final.mp4" }

A 16-second video like the above is 30 credits (~$6); the 202 response echoes the exact price and every resolved setting under resolved. Two cheap ways to experiment before spending more: estimate prices any request for free, and an 8-second render on the default fast quality is the cheapest real video (15 credits ≈ $3). Once a render is done, iterate on the finish for ~$1 with reprocess instead of re-rendering. vehicle and property are generally available; ecommerce is in beta — render quality is still being tuned.

The three axes

Every creative knob belongs to one of three independent axes: Direction (how it's shot, before the render), Finish (how it's polished, after the render), and Intent (why the video exists — per-vertical). Pick what you want on each — they compose orthogonally, and the rest of the schema (subject, photos, branding, audio, format) is additive. When in doubt, set none of them: every axis has a smart default.

1 — Pre-render (HOW it's shot, before the render engine runs)

These fields change what the camera captures. They compose independently: a mountain-run direction at ultra-luxury pace in a mountains scene is a different beat from mountain-run at luxury pace on a dealership-lot.

FieldTypeReqDefaultDescription
directionshot templateoptionalAI vision director (default: auto). Auto (or omitted) lets the director pick from the vertical's curated styles based on your reference photos (vehicle: dealer-showcase, spec-hero, walkaround-360, street-energy, mountain-run, urban-night, open-highway, rolling-drive; property: listing-tour, architectural, neighborhood-lifestyle). Force a specific style by id. To opt back into the pre-director pack template (byte-identical to the old default), pass "default". Ecommerce always auto-directs.
camera_stylepacing tieroptionalluxury (default — slow, sustained shots) or ultra-luxury (auto-selected at price ≥ $100k; even slower / more refined). Pick ultra-luxury when the listing should feel exotic-tier regardless of price.
scene_presetenvironmentoptionalSoft environment hint that respects your reference photos. Vehicle: dealership-lot, modern-showroom, studio-hero, urban-drive, golden-hour-lot, test-drive-road. Property: mountains, desert, beach, city, countryside. Defaults to auto (match the photos).

2 — Post-render (HOW it's finished, after the render engine)

These re-apply during the stitch and can also be re-run via reprocess for ~$1 — no new render.

FieldTypeReqDefaultDescription
finishintensityoptionalOne-knob post-production preset. raw (bare segments — pick it when you composite downstream) · standard (default — domain-appropriate grade, transitions and motion graphics) · full (complete ad — auto-fills voiceover, burned captions, end-card CTA, and music for any audio field you left blank; pick it when the output ships as-is). Cost-neutral.
cinematicstudio recipeoptionalGranular control over every effect: lut, caption_style, transitions, spec_callout_style, music_sync, letterbox, motion (Ken Burns), film_overlay (warm light leak), logo_bug, plus per-element toggles (title_card, end_card, selling_points, lower_third, feature_spotlight, map_intro, floor_plan, brand_stinger…). COMPOSES over finish: only the fields you set override the finish baseline, everything else keeps the finish's value. Send enabled: false to kill post-production wholesale.

3 — Intent (the WHY)

Per-vertical "what is the video for" axes. They shape narration, structure, music defaults, and the CTA. All have smart defaults — set them only when you want to override what the listing signals already imply.

FieldTypeReqDefaultDescription
objectiveecommerce onlyoptionalWhat the video is FOR — conversion, showcase (default; upgraded to conversion when the product is on sale), explainer, lifestyle. Drives structure (which beats run), narration framing, end-card CTA (hard for conversion/showcase, soft for explainer/lifestyle), and the auto-resolved music preset. Orthogonal to finish, direction, and tone.
shopper_intentvehicle onlyoptionalWHO the video is FOR — in_market_vdp, model_curious, lead_recapture, recent_arrival, price_drop, or default. Smart-resolves from listing signals (daysOnLot ≤ 7 → recent_arrival; recent price reduction → price_drop; listing_url present → in_market_vdp; else default). Drives the SHOPPER MINDSET narration block, close pattern, and end-card CTA fallback. The parallel to ecommerce objective.
placementvehicle onlyoptionalWHERE the video PLAYS — lounge (showroom TV: in-store handhold, captions default on since screens are muted), vdp (your own detail page: points at the page's buttons, wins the rival-tab comparison), direct_send (a rep texting one lead: reply-to-hold handhold), marketplace (3rd-party listing: differentiation vs lookalike listings + dealer trust), or default. Orthogonal to shopper_intent (who it's for) — both compose into the narration; the placement's next-step handhold wins the close and the end-card CTA fallback. Explicit-only, no inference.
listing_intentproperty onlyoptionalWHY the video exists — just_listed, open_house, price_reduced, luxury_showcase, or default. Smart-resolves from listing signals (openHouseAt within 14 days → open_house; daysOnMarket ≤ 7 or listedAt within 7 days → just_listed; recent price reduction → price_reduced; price ≥ $1M for sale → luxury_showcase; else default). Drives the BUYER MINDSET narration block, close pattern, the hero-title eyebrow ("Just listed" / "Open house" / "New price"; luxury drops it), and the end-card CTA fallback. Completes the per-vertical Intent trio.
narration.toneall verticalsoptionalCreative direction for the auto-generated script — mcconaughey, superbowl, tiktok, storyteller, minimal (commercial); informational (a factual feature-and-benefit walkthrough — the real features/accessories and what each does for the owner, no hype); conversational (warm, plain, like a real salesperson talking, no hype); or spec (just-the-facts spec rundown, no benefit framing). Independent of narration.voice (the spoken timbre — premium-warm, smooth-baritone, elegant-luxe, etc.). Ignored when a custom narration.script is supplied.

Worked example

A high-converting vehicle ad on a freshly arrived listing might combine all three buckets:

  • Pre-render: direction: "spec-hero" — a spec-led shot template.
  • Post-render: finish: "full" — complete ad, voiceover + captions + end-card + music auto-filled.
  • Intent: shopper_intent: "recent_arrival" — narration says "just landed on the lot", the end-card CTA falls back to "Be the first to see it".

Each is independent of the others — picking one doesn't lock the rest. The same shape composes across verticals: swap shopper_intent for objective (ecommerce) or leave both unset (property) and the rest of the request is unchanged.

How generation works

You send raw product photos; the API does the production work:

  1. AI photo selection — every uploaded photo is scored and the strongest set is chosen, so you can send your whole shoot.
  2. Cinematography planning — a per-scene plan decides which shot anchors each segment and how the camera moves.
  3. Render & finalize — the generated video segments, narration/music, captions, brand overlay, and cinematic grading are stitched into the final MP4.

Recipes

Complete, copy-pasteable requests for common goals — each shows how to compose the fields documented below for a specific outcome. Start from the closest one and adjust. vid_live_•••• stands in for your key.

Vehicle ad from a VIN

A dealer-ready ad from a VIN and lot photos: the API decodes the spec sheet and EPA MPG, and finish: "full" adds the voiceover, captions, end-card and music.

# Dealer: a finished vehicle ad from a VIN + lot photos. The API decodes the
# spec sheet and EPA MPG, and finish:"full" adds a voiceover,
# burned captions, an end-card CTA and beat-synced music.
curl -X POST https://api.catalogreel.com/v1/videos \
  -H "Authorization: Bearer vid_live_••••" \
  -H "Content-Type: application/json" \
  -d '{
    "vertical": "vehicle",
    "vin": "1FTFW1RG5NFB12345",
    "image_urls": [
      "https://cdn.example.com/truck-front.jpg",
      "https://cdn.example.com/truck-side.jpg",
      "https://cdn.example.com/truck-interior.jpg"
    ],
    "subject": { "price": 58995, "mileage": 12500 },
    "finish": "full",
    "duration": 24,
    "listing_url": "https://dealer.example.com/inventory/1FTFW1RG5NFB12345"
  }'

# → 202 { "id": "<uuid>", "status": "queued" } — poll GET /v1/videos/<uuid>.
# The on-screen spec callout shows the decoded specs and "28/36" MPG.

Commercial ad with your own script

Bring a hand-written voiceover and a logo overlay; full fills only what you omit and wraps your script with captions, an end-card and music.

# Your script, your logo: a full-finish ecommerce ad with a hand-written
# voiceover and a burned-in brand overlay. full fills only what you omit,
# so it keeps your script and adds captions, an end-card and music.
curl -X POST https://api.catalogreel.com/v1/videos \
  -H "Authorization: Bearer vid_live_••••" \
  -H "Content-Type: application/json" \
  -d '{
    "vertical": "ecommerce",
    "image_urls": ["https://cdn.example.com/chair-1.jpg", "https://cdn.example.com/chair-2.jpg"],
    "subject": { "title": "Aluminum Field Chair", "brand": "Field Co.", "price": 189 },
    "finish": "full",
    "narration": {
      "enabled": true,
      "script": "Folds flat, follows you anywhere. The Field Chair — weather-sealed aluminum, built for the long weekend."
    },
    "brand_overlay": {
      "logo_url": "https://cdn.example.com/logo.png",
      "brand_name": "Field Co.",
      "cta_text": "Shop the Field Chair"
    }
  }'

Real estate tour (9:16, captions)

A vertical listing tour with an auto-written narration and burned captions — captions just needs narration.enabled; the overlay pass that renders them switches on automatically.

# A vertical listing tour with an auto-written voiceover and burned captions.
# captions just needs narration.enabled — the overlay pass switches on automatically.
curl -X POST https://api.catalogreel.com/v1/videos \
  -H "Authorization: Bearer vid_live_••••" \
  -H "Content-Type: application/json" \
  -d '{
    "vertical": "property",
    "image_urls": [
      "https://cdn.example.com/home-exterior.jpg",
      "https://cdn.example.com/home-kitchen.jpg",
      "https://cdn.example.com/home-yard.jpg"
    ],
    "subject": {
      "propertyType": "single-family home",
      "bedrooms": 4, "bathrooms": 3, "squareFeet": 2650,
      "price": 825000, "city": "Austin", "state": "TX", "realEstateType": "sale"
    },
    "aspect_ratio": "9:16",
    "duration": 32,
    "narration": { "enabled": true, "auto_generate": true, "tone": "storyteller" },
    "captions": true,
    "music": "real-estate-warmth"
  }'

Agency / multi-tenant

Provision a customer once, then render on their behalf so each customer's jobs stay isolated. See Sub-tenants for the model.

# Agency / platform: provision a customer, then render on their behalf so each
# customer's jobs stay isolated. These calls use an umbrella key.

# 1) Create (or upsert) the sub-tenant under your own stable id.
curl -X POST https://api.catalogreel.com/v1/sub-tenants \
  -H "Authorization: Bearer vid_live_umbrella_••••" \
  -H "Content-Type: application/json" \
  -d '{ "external_id": "dealer_42", "name": "Westside Motors" }'

# 2) Mint a key scoped to that sub-tenant (it can only ever see dealer_42 jobs).
curl -X POST https://api.catalogreel.com/v1/keys \
  -H "Authorization: Bearer vid_live_umbrella_••••" \
  -H "Content-Type: application/json" \
  -d '{ "name": "dealer_42 server key", "sub_tenant": "dealer_42" }'
# → { "id": 7, "key": "vid_live_••••", "key_prefix": "vid_live_a1b2", ... }  (key shown once)

# 3) Render with the scoped key — no header needed, the scope is implicit.
curl -X POST https://api.catalogreel.com/v1/videos \
  -H "Authorization: Bearer vid_live_••••" \
  -H "Content-Type: application/json" \
  -d '{ "vertical": "vehicle", "vin": "1FTFW1RG5NFB12345", "image_urls": ["https://cdn.example.com/truck-1.jpg"] }'

POST /v1/videos

Creates a video job and returns 202 with { id, status: "queued" } and a Location header pointing at the job's status URL. Generation takes minutes — poll that URL or set a webhook. All option fields below are wired end-to-end into the render.

The 202 body also carries a resolved object — everything the API already knows for certain about how the job will run, so you never have to guess what a default or smart-resolution did:

FieldTypeReqDefaultDescription
finish / cinematic_recipestring / booleanoptionalEffective finish preset and whether a granular recipe was layered on it.
creditsintegeroptionalExact credits reserved — what this job costs.
duration_seconds / segmentsintegeroptionalSnapped deliverable length and its 8-second segment count (what the price is based on).
render_quality"fast" | "standard"optionalEffective render fidelity.
motion_graphicsbooleanoptionalWhether the overlay pass (captions, cards, end card) will run.
narration / captions / end_card_cta / musicmixedoptionalThe audio kit the job will actually render — including everything finish: "full" auto-filled. music is a concrete preset id, or "auto" when it resolves at render time.
direction / camera_stylestringoptionalDirection-axis values: the director mode and the price-derived camera pacing.
objective / shopper_intent / listing_intentstringoptionalThe smart-resolved Intent-axis value for the vertical (ecommerce / vehicle / property respectively).

Core

FieldTypeReqDefaultDescription
verticalstringrequiredTuned pack id. One of: property, ecommerce, vehicle.
subjectobjectrequiredPack-specific structured data (e.g. title, price for ecommerce; bedrooms, bathrooms for property; year, make, model for vehicle). Drives the prompt and on-screen highlights.
image_urlsstring[]required1–30 HTTPS image URLs. You do not need to pre-curate: AI scores every photo, selects the strongest, and plans which shot anchors each scene — send your best 8–30. More photos help longer videos avoid repeating shots.
intentstringoptionalFree-text generation context, ≤ 120 chars.
brandobjectoptionalFreeform brand metadata stored with the job (distinct from brand_overlay).
render_quality"fast" | "standard"optionalfastRender fidelity. fast covers most social/inventory use; standard is the premium cost lever (higher-fidelity render).
webhook_urlstring (url)optionalPublic HTTPS endpoint notified when the job finishes (video.completed / video.failed). HMAC-signed — see Webhooks. Optional; polling works without it.
idempotency_keystringoptional1–255 chars. Replays return the existing job (HTTP 200) instead of creating a duplicate.
vinstringoptional17-char VIN, vehicle vertical only. The server decodes the spec sheet and looks up EPA city/highway/combined MPG, filling any subject field you did not send (your values always win). Free and best-effort — an unknown or malformed VIN is ignored, never an error. See Subject fields → Vehicle.

Format & length

FieldTypeReqDefaultDescription
aspect_ratio"16:9" | "9:16"optional16:9Output orientation; defaults to 16:9 (landscape). Set "9:16" for vertical/social. 1:1 is intentionally unsupported by the generator and is rejected.
durationinteger 8–90optional16Requested length in seconds. Each segment is a full 8-second clip, so this snaps to the nearest 8s multiple and is billed per segment; the real length is returned as duration_seconds. e.g. 30 → 32s, 50 → 48s, 90 → 88s.
resolution"720p" | "1080p"optional720pExplicit output override. Omit it for 720p; set 1080p for HD output. HD output carries the higher finished-video price.

Audio & narration

FieldTypeReqDefaultDescription
musicenumoptionalBackground music preset: trending-tiktok, cinematic-epic, luxury-elegance, energetic-electronic, ambient-dreamy, modern-corporate, real-estate-warmth, or off. When unset and finish is full, the default depends on vertical: vehicle → energetic-electronic, property → real-estate-warmth, ecommerce auto-resolves by objective (lifestyle/showcase → luxury-elegance, conversion → energetic-electronic, explainer → ambient-dreamy). modern-corporate is still callable explicitly but is no longer any vertical's default.
narrationobjectoptionalVoice-over. See sub-fields below.
narration.enabledbooleanrequiredRequired within narration. Turns the voice track on.
narration.scriptstringoptionalCustom narration text, 1–5000 chars. Mutually exclusive with auto_generate.
narration.auto_generatebooleanoptionalGenerate the script from subject data. Mutually exclusive with script.
narration.voiceenumoptionalpremium-warmCurated commercial TTS voice (the spoken timbre). Male: premium-warm, smooth-baritone, bold-authority, friendly-pro. Female: conversational, confident-female, bright-upbeat, elegant-luxe.
narration.toneenumoptionalCreative direction for the auto-generated script: mcconaughey (warm, unhurried), superbowl (big-event punch), tiktok (fast, casual hook), storyteller (narrative arc), minimal (terse commercial), informational (factual feature-and-benefit walkthrough — no hype, plain next-step close), conversational (warm, plain, real-salesperson read, no hype), spec (just-the-facts spec rundown, no benefit framing). Ignored when a custom script is supplied.
narration.call_to_actionstringoptionalClosing spoken CTA line, ≤ 200 chars.
audio_mixobjectoptionalFiner audio control applied during the final stitch. Requires at least one sub-field.
audio_mix.custom_music_promptstringoptional3–500 chars. Steers the AI music direction. A non-off music preset takes precedence over this.
audio_mix.audio_mix_priority"music" | "voice" | "balanced"optionalWhich track wins the mix. Also shapes music-only ducking, so it is accepted without narration.
audio_mix.maintain_music_continuitybooleanoptionalKeep one continuous music bed across segments instead of restarting it each segment.

Branding & overlays

FieldTypeReqDefaultDescription
brand_overlayobjectoptionalBurned-in brand overlay. Requires at least one sub-field.
brand_overlay.logo_urlstring (url)optionalLogo image URL.
brand_overlay.brand_namestringoptionalBrand name text, ≤ 100 chars. Also threaded into the narration's BUSINESS INFO block as the brand name.
brand_overlay.cta_textstringoptionalCTA text, ≤ 120 chars.
contact_infoobjectoptionalDealership / brand contact info reaching the narration's BUSINESS INFO block alongside brand_overlay.brand_name. All sub-fields optional; the playground "Scrape" tool auto-populates these from the listing page's schema.org seller block when present.
contact_info.phonestringoptionalPhone number, ≤ 40 chars.
contact_info.websitestring (url)optionalDealership website URL.
contact_info.citystringoptionalCity, ≤ 80 chars.
contact_info.statestringoptionalState/region, ≤ 40 chars.
captionsbooleanoptionalBurned-in captions from the narration script. Requires narration.enabled: true; the motion-graphics pass that renders them is enabled automatically.

Background replacement

FieldTypeReqDefaultDescription
backgroundobjectoptionalAI background replacement request. Feature-flagged for operators: when VIDEO_API_BACKGROUND_REPLACEMENT_ENABLED is false (the default), the API accepts requests but strips/ignores this field before rendering.
background.image_urlstring (url)optionalReference background image URL. When enabled, the worker can composite selected hero photos against this background.
background.promptstringoptional3–500 chars. Generated background direction, or a refinement prompt when image_url is also supplied.

Cinematic & end card

FieldTypeReqDefaultDescription
camera_style"luxury" | "ultra-luxury" | "authentic"optionalauto from priceCamera look baked into the prompt (Direction axis). luxury/ultra-luxury are cinematic; when omitted, ultra-luxury is auto-selected for a subject priced ≥ $100,000, otherwise luxury. "authentic" is an explicit-only, shot-on-a-phone capture aesthetic (vehicle) — handheld, available daylight, no color grade; changes only the footage. Pair with finish:"raw" for a fully bare clip.
direction"default" | "auto" | style idoptionalautoAI vision director (vehicle/property, Direction axis). auto (default) — the director picks one of the vertical's curated styles from your reference photos. Pass "default" to opt back into the fixed pack template. A specific style id forces it (unknown → vertical default). Vehicle: dealer-showcase, spec-hero, walkaround-360, street-energy, mountain-run, urban-night, open-highway, rolling-drive. Property: listing-tour, architectural, neighborhood-lifestyle. Ecommerce always auto-directs.
cta_textstring (1–60)optionalCTA easy-mode. When set, fans into any of these three slots the caller left unset: narration.call_to_action (the spoken close), end_card_cta (the on-screen end-card slab; only when the job renders an end card — finish full, captions, a listing URL, or any cinematic overlay), brand_overlay.cta_text (the corner overlay; only if brand_overlay is also set). Each precise field always wins when explicitly set. Capped at 60 so it can fan to the tightest slot (end_card_cta).
cta_mode"auto" | "hard" | "soft"optionalautoOne-knob hard/soft close. auto (default) keeps today's smart resolution from objective / shopper_intent. hard defaults narration.wants_cta to true; soft defaults it to false. An explicit narration.wants_cta still wins. Independent of cta_text.
end_card_ctastringoptionalOn-screen closing call-to-action card, 1–60 chars — distinct from the spoken narration.call_to_action. Setting it automatically enables the motion-graphics pass that renders the card. Caller always wins: an explicit value forces the CTA slab on even when the resolved ecommerce objective is lifestyle or explainer (which otherwise soft-close without a slab). For the easy path, set cta_text once at the top level and let it fan to all three CTA slots.
listing_urlstring (url)optionalOptional detail-page URL for the end-card QR code. vdp_url and product_url are accepted aliases; provide only one.

Finish

FieldTypeReqDefaultDescription
finish"raw" | "standard" | "full"optionalstandardPost-production finish intensity (Finish axis). standard (default) applies a domain-appropriate cinematic finish — which INCLUDES the per-vertical overlay beats automatically (for vehicles: title card, price/mileage reveal, spec callout, end card). raw opts out entirely (bare segments, no overlays). full produces a complete ad — auto-enables AI voiceover, burned captions, an end-card CTA and beat-synced music (only filling fields you omit). To keep standard but drop specific overlays, send a cinematic recipe with explicit false (e.g. cinematic: { spec_callout: false }). Credit-neutral: post-production and narration are unpriced.
cinematicobjectoptionalFull studio recipe for granular control (Finish axis). Composes OVER finish — it adds to / overrides the baseline, it does NOT gate it: an unset field keeps the finish's value, so leaving the recipe empty does not strip the finish's built-in overlays. Set a field to false to turn that overlay off at standard finish. All sub-fields optional. See the recipe table below.
language"en" | "es"optionalenNarration + caption + overlay language (Spanish support beta). "es" writes the spoken script in Spanish, times captions with the multilingual model, and localizes the hero eyebrow + end-card CTA (e.g. "Recién publicado", "Agenda una visita"). Secondary spec-tile labels (Miles/Beds/MPG…) stay English for now. All verticals.
objective"conversion" | "showcase" | "explainer" | "lifestyle"optionalEcommerce only — the per-vertical "why" axis (vehicle uses shopper_intent for the same role). What the video is FOR — drives beat structure, transitions, pacing, narration framing, AND the rendered cards (hero eyebrow + end-card CTA + per-playbook tonal palette). conversion / showcase keep a hard end-card CTA ("Shop now" / "Explore the collection"); lifestyle / explainer soft-close — visual end card holds on the product + QR + brand wordmark with no CTA slab and no hero eyebrow. When finish is full, ecommerce music also resolves by objective (lifestyle/showcase → luxury-elegance, conversion → energetic-electronic, explainer → ambient-dreamy) — the old hardcoded modern-corporate default is gone. Ecommerce end cards always drop the dealership-style phone | email | website + address footer regardless of objective (D2C buyers scan the QR). Unset → smart default: showcase, auto-upgraded to conversion when the product is on sale. Sending it on a non-ecommerce request returns 400.
shopper_intent"default" | "in_market_vdp" | "model_curious" | "lead_recapture" | "recent_arrival" | "price_drop"optionalVehicle only — the per-vertical "why" axis (ecommerce uses objective / property uses listing_intent for the same role). Who the video is FOR — drives the narration SHOPPER MINDSET block, close pattern, and end-card CTA fallback. default (or unset) keeps today's prompt + overlays byte-identical. Smart default resolves to recent_arrival when daysOnLot ≤ 7, price_drop on a recent reduction (originalPrice > price OR priceReducedAt < 30d), in_market_vdp when listing_url is set, else default. Sending it on a non-vehicle request returns 400.
listing_intent"default" | "just_listed" | "open_house" | "price_reduced" | "luxury_showcase"optionalProperty only — the per-vertical "why" axis (vehicle uses shopper_intent / ecommerce uses objective for the same role). Why the video exists — drives the narration BUYER MINDSET block, close pattern, the hero-title eyebrow ("Just listed" / "Open house" / "New price"; luxury_showcase drops the eyebrow for a mood-first open), and the end-card CTA fallback ("Be first to tour it" / "See open house times" / "Tour it at the new price" / "Request a private tour"). default (or unset) keeps today's prompt + overlays byte-identical. Smart default resolves to open_house when openHouseAt is within 14 days, just_listed when daysOnMarket ≤ 7 or listedAt within 7 days, price_reduced on a recent reduction (originalPrice > price OR priceReducedAt < 30d), luxury_showcase at price ≥ $1M for sale, else default. Sending it on a non-property request returns 400.
use_photo_brief_in_narrationbooleanoptionalfalseVehicle only. Opt-in: thread the photo-analysis-derived per-VIN brief (what the AI sees in the real listing photos — condition signals, real-world setting, distinguishing details) into the narration prompt as a CORE "this exact VIN" angle. Off by default → byte-identical to today's narration. Implicitly ON when shopper_intent resolves to in_market_vdp / recent_arrival / price_drop. Adds the photo-analysis stage to narration's critical path on the default vehicle path; the director-on path already has it free.

Cinematic recipe (composes over finish)

FieldTypeReqDefaultDescription
cinematic.enabledbooleanoptionaltrueMaster switch for the recipe.
cinematic.lookenumoptionalStudio look bundle: social-first (scroll-stopping product pop), dealer-spotlight (polished showroom), luxury (restrained premium), real-estate-tour (cool, airy walkthrough).
cinematic.lutenumoptionalColor grade. teal-orange = punchy commercial; vivid = social product pop; warm-doc = warm/editorial; cool-modern = airy interiors; film-noir = moody mono; kodak-2383 / fuji-eterna / bleach-bypass = film-stock emulation looks.
cinematic.caption_styleenumoptionalBurned-caption look — each distinct by shape: pop (blue chip), highlight (active word recolors white→blue, neighbors dim), premium (white serif fade), word-pop (kinetic blue overshoot + stroke), karaoke (white→blue fill + underline wipe), glow (blue neon). Fixed white→blue palette, not your brand colors.
cinematic.transitionsenumoptionalcut, xfade-fade, xfade-wipeleft, xfade-fadeblack, auto.
cinematic.spec_callout_styleenumoptionalSpec-panel look: commercial-panel, showroom-3d, showroom-3d-real, blueprint-scan, luxury-minimal.
cinematic.music_sync"none" | "soft" | "snap"optionalBeat-locks the edit to the music. soft: overlay graphics (hero, spec panel, feature cards) start on the nearest music beat. snap: additionally chooses the music in-point so a beat lands on the opening hook and the 8s scene seams sit near beats. Cut points themselves are not moved; narration-timed captions and callouts stay locked to the voiceover. snap implies the motion-graphics pass.
cinematic.letterbox"none" | "cinematic"optionalCinematic 2.39:1 letterbox bars.
cinematic.motion"none" | "subtle" | "dynamic"optionalKen Burns slow push-in on card beats (hero / spec / end card). On (subtle) by default for the commercial preset.
cinematic.film_overlay"none" | "subtle" | "medium"optionalWarm film-burn light leak — adds warm light, distinct from the vignette (which darkens the edges).
cinematic.logo_bugenumoptionalPersistent corner logo watermark (uses the brand logo): none, top-left, top-right, bottom-left, bottom-right.
cinematic.<toggles>booleanoptionalPer-element switches: captions, title_card, spec_callout, end_card, selling_points, lower_third, feature_spotlight, map_intro, floor_plan, film_finish, brand_stinger.

Generation controls

FieldTypeReqDefaultDescription
seedintegeroptional0 – 4294967295. Shared deterministic seed across segments for reproducible / consistent output.
negative_promptobjectoptionalExclusion guidance. { text: string(1–2000), mode: "append" | "replace" }. append (default) keeps the pack safety/identity guardrails; replace swaps the text (identity protection still applies).
person_generation"disallow"optionalForward-compat field. Only 'disallow' is accepted for property/ecommerce — any other value returns 400 (people are excluded at the pack level).
scene_presetenumoptionalEnvironment: auto, dealership-lot, modern-showroom, studio-hero, urban-drive, golden-hour-lot, test-drive-road, mountains, desert, beach, city, countryside. Precedence: when scene_preset is set AND direction is a scenic style (mountain-run, urban-night, open-highway, street-energy), scene_preset wins for the environment; the director still drives the shot template + camera moves.

Background replacement is currently operator-gated. When VIDEO_API_BACKGROUND_REPLACEMENT_ENABLED is unset or false, create requests that include background are accepted but the field is ignored before the job is priced or rendered. When enabled, background replacement adds per-segment credits and those credits are reflected in estimate responses.

POST /v1/videos/estimate

Dry-run the same duration, resolution, plan-cap, and background pricing used by POST /v1/videos without creating a job, holding balance, or touching rate-limit counters. Use this before presenting a price or disabling a submit button.

curl -X POST https://api.catalogreel.com/v1/videos/estimate \
  -H "Authorization: Bearer vid_live_••••" \
  -H "Content-Type: application/json" \
  -d '{
    "duration": 32,
    "resolution": "1080p",
    "background": true
  }'

# → {
#   "credits": 160,
#   "usd": 32,
#   "duration_seconds": 32,
#   "segments": 4,
#   "resolution": "1080p",
#   "background_credits": 0,
#   "spendable": 500,
#   "sufficient": true
# }

Body

FieldTypeReqDefaultDescription
durationinteger 8–90optional16Requested length. Snaps to the same 8-second segment boundary and plan cap used by create.
render_quality"fast" | "standard"optionalfastRender fidelity to price — same field as create.
resolution"720p" | "1080p"optional720pOutput resolution to price. 1080p returns 400 if the current plan does not allow HD.
narrationbooleanoptionalfalsePrice-neutral today, accepted so estimates stay compatible if narration pricing changes later.
backgroundbooleanoptionalfalseWhether to include background replacement credits. Returns 0 background_credits unless VIDEO_API_BACKGROUND_REPLACEMENT_ENABLED is on.

Response

FieldTypeReqDefaultDescription
creditsintegeroptionalExact internal credits a matching create would reserve.
usdnumberoptionalEstimated dollar value at the standard prepaid rate.
duration_secondsintegeroptionalSnapped deliverable length the estimate priced.
segmentsintegeroptionalNumber of 8-second source segments.
resolution"720p" | "1080p"optionalPriced output resolution.
background_creditsintegeroptionalAdditional background replacement credits included in credits; 0 while the feature flag is disabled.
spendable / sufficientinteger / booleanoptionalCurrent wallet spendable balance and whether it covers the estimated job.

Subject fields

subject is a freeform object — send whatever structured data you have for the listing. Every field is optional; the more you provide, the richer the narration, the more accurate the on-screen highlights, and the more specific the prompt. Unknown keys are ignored, and price additionally feeds the automatic camera_style. The accepted fields per vertical:

Ecommerce (vertical: "ecommerce")

FieldTypeReqDefaultDescription
titlestringoptionalProduct name (name is accepted as a fallback). Anchors the prompt, narration and end card.
brandstringoptionalManufacturer / brand. Used in narration and the brand highlight.
categorystringoptionalProduct category, e.g. "Office Chairs".
pricenumberoptionalCurrent price. Spoken in narration, shown as the price highlight, and feeds the auto cinematography tier.
originalPrice / salePrice / onSalenumber / number / booleanoptionalDeal signals — when set, narration calls out the markdown ("on sale now, was …").
material / materialsstring / string[]optionalMaterial(s); rendered into the subject phrase and the material highlight.
colorstringoptionalPrimary color/finish.
dimensionsstringoptionalFree-text size, e.g. '27"W x 27"D'.
keyFeature / keyFeaturesstring / string[]optionalTop selling points (up to ~5 used in narration).
conditionstringoptionale.g. "new", "refurbished".
ratingnumberoptional0–5 customer rating; narrated as social proof.
descriptionstringoptionalFree-text listing notes (sanitized, ≤ ~900 chars used).

Vehicle (vertical: "vehicle")

FieldTypeReqDefaultDescription
year / make / model / trimstring|number / stringoptionalVehicle identity headline — drives the prompt, narration and identity-lock.
pricenumberoptionalListing price. Narrated, shown as the price highlight, feeds the auto cinematography tier.
mileagenumberoptionalOdometer; narrated and shown as a highlight.
condition / newUsedstring / "new" | "used"optionalListing status; influences framing (showroom-fresh vs. dealer value).
engine / transmission / drivetrainstringoptionalPowertrain spec; woven into narration and highlights.
fuelTypestringoptionalFuel type; narrated and shown in the spec callout.
cityMpg / highwayMpgnumberoptionalEPA city / highway MPG. With both present, narration says "28 city / 36 highway MPG" and the spec callout shows "28/36"; otherwise mpgCombined is used. Auto-filled when you pass vin.
mpgCombined / horsepowernumberoptionalCombined MPG (fallback when city/highway are absent) and horsepower; used in narration.
exteriorColor / interiorColor / bodyStylestringoptionalAppearance details for the subject phrase and highlights.
warranty / isCertifiedstring / booleanoptionalAssurance signals; isCertified adds a "Certified Pre-Owned" callout.
daysOnLotnumberoptionalDays since the car landed on the dealership lot. Narration calls it out as "fresh arrival" when ≤ 14. Auto-resolves shopper_intent to recent_arrival when ≤ 7.
ownerCountnumberoptionalNumber of previous owners (1 = "one owner", 2 = "two owners"). Threaded into narration as an in-market trust signal.
recentServicestringoptionalShort free-text recent maintenance summary (e.g. "new tires, fresh oil change"). Surfaced in narration verbatim.
priceReducedAtstring (ISO date)optionalWhen the price was last reduced. Within the last 30 days → narration mentions "recently reduced" and auto-resolves shopper_intent to price_drop.
certifiedInspectionDatestring (ISO date)optionalCPO inspection date. Requires isCertified: true. Within the last year → narration mentions "Certified inspection: within the last year".
sellerNotesstringoptionalDealer-authored "why we love this one" (≤ 600 chars). FOREGROUNDED in narration as the "this exact VIN" angle, not buried alongside specs. The single highest-leverage field for an in-market shopper.
keyFeature / keyFeaturesstring / string[]optionalTop selling points beyond the structured specs (e.g. "one owner, new tires"); up to ~5 used in narration.
descriptionstringoptionalFree-text listing notes (sanitized).

Have a VIN? Send vin at the top level (not inside subject) on a vehicle request and the API decodes year, make, model, trim, body style, drivetrain, fuel type, engine and transmission, then looks up EPA cityMpg / highwayMpg / mpgCombined — filling any field you didn't send. Values you provide in subject always win. It's free and best-effort: an unknown or malformed VIN is simply ignored, never an error.

# Vehicle: send a VIN and the API fills the spec sheet for you.
curl -X POST https://api.catalogreel.com/v1/videos \
  -H "Authorization: Bearer vid_live_••••" \
  -H "Content-Type: application/json" \
  -d '{
    "vertical": "vehicle",
    "vin": "1FTFW1RG5NFB12345",
    "image_urls": ["https://cdn.example.com/truck-1.jpg"],
    "subject": { "price": 58995, "mileage": 12500 }
  }'

# The server decodes year/make/model/trim/engine/transmission/drivetrain/fuel
# and looks up EPA city/highway/combined MPG, merging them into subject. Values
# you send yourself (here price + mileage) are always kept.

Real estate (vertical: "property")

FieldTypeReqDefaultDescription
propertyTypestringoptionale.g. "single-family home", "condo". Defaults to "home".
bedrooms / bathrooms / squareFeetnumberoptionalCore size signals — narration opening + highlights.
pricenumberoptionalList price. Property prices ≥ $1M are spoken abbreviated ("one point two five million"). Feeds the auto cinematography tier.
realEstateType"sale" | "rent"optionalTenure; shifts framing between investment vs. livability.
lotSize / lotSizeUnitnumber / stringoptionalLot size and its unit (default "sq ft").
yearBuiltnumberoptionalConstruction year; narrated and shown as a highlight.
city / statestringoptionalLocation context for narration.
garageSpaces / hasPool / hoaFeenumber / boolean / numberoptionalFeature signals surfaced as highlights and narration.
keyFeature / keyFeaturesstring / string[]optionalTop selling points beyond the structured specs (e.g. "remodeled kitchen, walk to greenbelt"); up to ~5 used in narration AND rendered as the on-screen selling-points list.
descriptionstringoptionalFree-text listing notes (sanitized).
daysOnMarket / listedAtnumber / string (ISO date)optionalFreshness signals. daysOnMarket ≤ 7 (or listedAt within 7 days) auto-resolves listing_intent to just_listed.
openHouseAtstring (ISO datetime)optionalScheduled open house. Within the next 14 days → auto-resolves listing_intent to open_house (the strongest signal).
originalPrice / priceReducedAtnumber / string (ISO date)optionalReduction signals. originalPrice > price (or a cut within 30 days) auto-resolves listing_intent to price_reduced and powers the "was/now" framing.

GET /v1/videos/{id}

Returns the job. Poll until status is completed or failed.

FieldTypeReqDefaultDescription
idstring (uuid)optionalJob id — use it to poll.
statusenumoptionalpending → queued → processing → completed | failed | cancelled.
listing_urlstring | nulloptionalResolved detail-page URL used for the end-card QR code.
video_urlstring | nulloptionalFinal MP4 URL once completed.
thumbnail_urlstring | nulloptionalPoster frame.
source_urlsstring[] | nulloptionalThe photos actually used after photo selection.
progressobject | nulloptionalLive { stage, label, percent } while processing.
captionsobject | nulloptionalCaption track when captions were requested: { engine, start_offset_seconds, words: [{ text, start_seconds, duration_seconds }] }.
captions_srt_url / captions_vtt_urlstring | nulloptionalOn-demand SRT / WebVTT exports of the caption track (same API-key auth). Free. Null when the job has no captions. Tip: for an overlay-free master to pair with the SRT, reprocess the job with finish: "raw" (5 credits).
poster_urlsstring[] | nulloptionalPoster frames extracted from the final video (hook, middle, close) — ready-made thumbnails for players and listings.
variantsobject[]optionalOn GET: this job's post-only variants (children) — each { id, label, status, video_url }. Created via the request variants[] field or POST /v1/videos/{id}/variants.
variant_labelstring | nulloptionalSet on a variant child — the label for this recombination. Null on a base render.
qualityobject | nulloptionalAutomated quality report: { status, warnings[] }. Every segment is checked for black/frozen frames and duration; at finish: "full" a vision model also verifies the rendered subject against your photos and screens for artifacts, re-rendering flagged segments within a capped budget. status is "passed" / "passed_with_warnings" at full finish, "unchecked" otherwise.
error_code / errorstring | nulloptionalSet when status is failed.
created_at / completed_attimestampoptionalLifecycle timestamps.

POST /v1/videos/{id}/reprocess

Re-runs only the post-processing (audio stitch, narration, captions, brand overlay, color grade and the full cinematic effects recipe) on a completed video's stored source — no new video generation. It costs a flat 5 credits (about $1), far cheaper than regenerating. Returns 202 with a new job id; poll GET /v1/videos/{new-id} for the result. The original video is unchanged.

curl -X POST https://api.catalogreel.com/v1/videos/<uuid>/reprocess \
  -H "Authorization: Bearer vid_live_••••" \
  -H "Content-Type: application/json" \
  -d '{
    "narration": { "enabled": true, "mode": "regenerate" },
    "captions": true,
    "caption_style": "highlight",
    "brand_overlay": { "brand_name": "Field Co.", "cta_text": "Shop now" }
  }'

# → 202 { "id": "<new-uuid>", "status": "processing" }
# Poll GET /v1/videos/<new-uuid> for the reprocessed video_url.

Reprocess or re-render?

Rule of thumb: anything applied after the render engine (the Finish axis) reprocesses for ~$1; anything that changes what the camera captured (the Direction axis, photos, length, fidelity) needs a new render.

FieldTypeReqDefaultDescription
Reprocess (~$1, ~1 min)post-renderoptionalNarration voice/tone or a new script, music track or mix, captions on/off + style, color grade (lut), transitions, letterbox, Ken Burns motion, light leak, logo bug, brand overlay, end-card CTA, the finish preset itself, any cinematic recipe field.
Re-render (full price)pre-renderoptionalDifferent photos or photo emphasis, direction style, scene_preset, camera_style, duration or aspect ratio, render_quality, resolution, subject data changes that should be re-narrated AND re-shot.

Body (all optional)

FieldTypeReqDefaultDescription
narrationobjectoptional{ enabled, mode: "reuse" | "custom" | "regenerate", custom_script }. reuse keeps the existing voice track; custom synthesizes custom_script; regenerate writes a fresh script from the subject.
narration_optionsobjectoptionalAdvanced narration tuning (pacing, tone).
audio_settingsobjectoptionalMusic / voice preset overrides applied during the re-stitch.
brand_overlayobjectoptionalSame shape as the create endpoint — re-burns the overlay.
end_card_ctastringoptionalClosing CTA card text, 1–60 chars.
captionsbooleanoptionalTurns burned-in captions on/off during reprocess. Requires narration.enabled: true. If no finish override is provided, captions are toggled on the stored finish or a standard finish.
caption_styleenumoptionalCaption look: pop, highlight, premium, word-pop, karaoke, glow. Providing a style implies captions: true.
listing_urlstring (url)optionalOptional replacement detail-page URL for the end-card QR code. vdp_url and product_url aliases are accepted.
finish"raw" | "standard" | "full"optionalRe-finalize into this post-production finish — no new render.
cinematicobjectoptionalFull effects recipe — same shape as on create (see the Cinematic recipe table). Re-applies LUT, letterbox, Ken Burns motion, light leak, logo bug, captions and overlay beats to the finished video. On reprocess the recipe is resolved standalone (it wins over finish here).
cinematic_settingsobjectoptionalAdvanced post-processing tier overrides (legacy escape hatch; prefer cinematic).

Returns 404 if the job isn't yours, 409 if it isn't completed or has no stored source, and 402 if you don't have enough prepaid balance. The reprocess is billed as its own job and is refunded if it fails.

DELETE /v1/videos/{id}

Cancels a job that is still pending, queued or processing, and refunds the balance held for it. A job that has already finished returns 409.

Lounge slideshow URLs

POST /v1/lounge creates one stable unlisted showroom URL for the organization, or for one customer when you send X-Sub-Tenant. The public page at /lounge/{token} needs no API key, so it works on kiosk browsers and smart TVs. By default it shows all completed videos with a final URL, deduped by VIN, listing URL, then final video URL. Use per-video overrides to hide a video or force-show a dedupe loser.

Branding settings are intentionally small in v1: logo_url, logo_position, logo_opacity, slide_duration_seconds, transition, muted, autoplay, and show_title_overlay. Rotate the public token with { "rotate_token": true } on PATCH /v1/lounge.

# Create the default lounge URL for the whole organization.
curl -X POST https://api.catalogreel.com/v1/lounge \
  -H "Authorization: Bearer vid_live_••••" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": {
      "logo_url": "https://cdn.example.com/dealer-logo.png",
      "logo_position": "top-right",
      "slide_duration_seconds": 12,
      "muted": true,
      "autoplay": true
    }
  }'
# → { "data": { "public_url": "https://api.catalogreel.com/lounge/lounge_...", ... } }

# Create/read the lounge URL for one customer instead.
curl -X POST https://api.catalogreel.com/v1/lounge \
  -H "Authorization: Bearer vid_live_umbrella_••••" \
  -H "X-Sub-Tenant: dealer_42" \
  -H "Content-Type: application/json" \
  -d '{}'

# Preview the deduped effective playlist.
curl https://api.catalogreel.com/v1/lounge/videos \
  -H "Authorization: Bearer vid_live_••••"

# Hide or force-show one completed video in the lounge.
curl -X PATCH https://api.catalogreel.com/v1/lounge/videos/<video-uuid> \
  -H "Authorization: Bearer vid_live_••••" \
  -H "Content-Type: application/json" \
  -d '{ "visibility": "hidden" }'

Sub-tenants (X-Sub-Tenant)

If you operate on behalf of many businesses (a DMS, CRM, or agency), an umbrella key can act for any of your customers: send the X-Sub-Tenant header with your own stable id for that customer. The header tags every write and filters reads, so each customer's jobs stay isolated. Sub-tenants are created on first use (or explicitly via POST /v1/sub-tenants). Alternatively, mint a scoped key bound to a single sub-tenant — it ignores the header and can only ever see its own jobs. Account-management routes (keys, sub-tenants, balance, webhook secret) require an umbrella key; sending X-Sub-Tenant with a scoped key returns 403.

# Tag writes — and filter reads — to one of your customers (umbrella key).
curl -X POST https://api.catalogreel.com/v1/videos \
  -H "Authorization: Bearer vid_live_••••" \
  -H "X-Sub-Tenant: dealer_42" \
  -H "Content-Type: application/json" \
  -d '{ "vertical": "vehicle", "vin": "1FTFW1RG5NFB12345", "image_urls": ["https://cdn.example.com/truck-1.jpg"] }'

# List only that customer's jobs:
curl "https://api.catalogreel.com/v1/videos" \
  -H "Authorization: Bearer vid_live_••••" \
  -H "X-Sub-Tenant: dealer_42"

Account management routes

These routes are for server-to-server integrations using an umbrella key. Scoped keys can create, list, retrieve, cancel, and reprocess their own videos, but they cannot manage account balance, keys, sub-tenants, or webhook secrets.

FieldTypeReqDefaultDescription
GET /v1/account/balanceumbrellaoptionalReturns { balance, reserved, spendable, plan_tier } for the shared prepaid wallet.
GET /v1/account/usageumbrellaoptionalPaginated ledger. Send X-Sub-Tenant to narrow usage to one customer.
POST /v1/credits/checkoutumbrellaoptionalStarts a Stripe Checkout top-up for { pack_id } and returns { checkout_url }.
GET /v1/keysumbrellaoptionalLists API keys without secrets.
POST /v1/keysumbrellaoptionalCreates a key and returns the plaintext secret once. Include sub_tenant to create a scoped key.
DELETE /v1/keys/{id}umbrellaoptionalRevokes an API key.
GET /v1/sub-tenantsumbrellaoptionalLists customer scopes under the organization.
POST /v1/sub-tenantsumbrellaoptionalCreates or upserts a sub-tenant from { external_id, name }.
GET /v1/sub-tenants/{externalId}umbrellaoptionalFetches one sub-tenant by your external id.
DELETE /v1/sub-tenants/{externalId}umbrellaoptionalDeletes a sub-tenant only after active scoped keys are revoked.
GET /v1/webhook-secretumbrellaoptionalReveals the org webhook signing secret (whsec_...).
POST /v1/webhook-secretumbrellaoptionalRotates the webhook signing secret; update verifiers immediately.
# Balance + usage (umbrella key only)
curl https://api.catalogreel.com/v1/account/balance \
  -H "Authorization: Bearer vid_live_umbrella_••••"

curl "https://api.catalogreel.com/v1/account/usage?limit=20" \
  -H "Authorization: Bearer vid_live_umbrella_••••" \
  -H "X-Sub-Tenant: dealer_42"

# Top up prepaid balance through Stripe Checkout
curl -X POST https://api.catalogreel.com/v1/credits/checkout \
  -H "Authorization: Bearer vid_live_umbrella_••••" \
  -H "Content-Type: application/json" \
  -d '{ "pack_id": "pack_growth" }'

# Webhook signing secret
curl https://api.catalogreel.com/v1/webhook-secret \
  -H "Authorization: Bearer vid_live_umbrella_••••"

curl -X POST https://api.catalogreel.com/v1/webhook-secret \
  -H "Authorization: Bearer vid_live_umbrella_••••"

Billing & limits

Billing uses a prepaid balance. Each generation holds a reservation when the job is created and is charged only on success; a failed, cancelled, or partial job is fully refunded — you never pay for a video you didn't receive. Add balance in the console.

Public pricing is shown as an estimated dollar amount for the finished video. Internally, the API still returns the exact reservation for audit and idempotency. Each segment is a full 8s clip, so your requested duration snaps to the nearest 8s multiple — e.g. 16s → 2, 32s → 4, 48s → 6, 64s → 8 segments. Narration is included. Background replacement adds a small per-segment charge (one hero photo per segment). Reprocess is a low flat charge.

FieldTypeReqDefaultDescription
standardfast / 720poptionalestimated~15s ≈ $6, ~30s ≈ $12, ~45s ≈ $18, ~64s ≈ $24 at the standard prepaid rate.
hdstandard / 1080poptionalestimated~15s ≈ $8, ~30s ≈ $16, ~45s ≈ $24, ~64s ≈ $32 at the standard prepaid rate.

Plan limits

Plans are a limits bundle, not a subscription — they gate rate, concurrency, length and resolution. New organizations start on starter limits with 0 balance.

FieldTypeReqDefaultDescription
starterdefaultoptional10 rpm · 3 concurrentDaily cap 1,000 jobs, up to 90s / 1080p, 30-day retention. New accounts start here with 0 balance.
growthon requestoptional30 rpm · 8 concurrentDaily cap 5,000 jobs, up to 90s / 1080p, 90-day retention.
scaleon requestoptional60 rpm · 20 concurrentDaily cap 50,000 jobs, up to 90s / 1080p, 90-day retention.

When prepaid balance is insufficient the request is rejected with 402 and { error, required, available } — nothing is queued. Per-key and per-plan limits return 429 (rate limit, with a Retry-After header; or too many concurrent jobs; or the daily job cap), and a duration or resolution beyond your plan returns 400.

Account creation does not require a card, but generation requires prepaid balance. New organizations start with 0 balance, so an unfunded request returns 402 before a job is queued.

Webhooks

Set webhook_url on a create request and we POST a signed event to it the moment the job reaches a terminal state — so you don't have to poll. Two event types: video.completed and video.failed. The data.object is the same job shape returned by GET /v1/videos/{id}.

Payload

{
  "id": "evt_8s9d7f6g5h4j3k2l",
  "type": "video.completed",
  "created_at": "2026-05-29T18:04:11.123Z",
  "data": {
    "object": {
      "id": "b3f1c2a4-…-uuid",
      "status": "completed",
      "video_url": "https://…/final.mp4",
      "thumbnail_url": "https://…/thumb.jpg",
      "duration_seconds": 16
      // …same shape as GET /v1/videos/{id}
    }
  }
}

Headers

FieldTypeReqDefaultDescription
X-Video-Eventstringoptionalvideo.completed or video.failed (also in the body as type).
X-Video-TimestampstringoptionalUnix milliseconds when the delivery was signed. Reject if older than ~5 min.
X-Video-SignaturestringoptionalHex HMAC-SHA256 of `${timestamp}.${rawBody}` using your org signing secret.
User-Agentstringoptionaldrivereach-video-webhooks/1.

Verifying the signature

Every delivery is signed with HMAC-SHA256 over {timestamp}.{raw body} using your organization's signing secret — reveal it under Webhook signing secret in the console or call GET /v1/webhook-secret with an umbrella key. Recompute it over the raw request bytes, constant-time compare against X-Video-Signature, and reject deliveries whose X-Video-Timestamp is more than 5 minutes old.

import crypto from "crypto";
import express from "express";

const app = express();
const SECRET = process.env.VIDEO_WEBHOOK_SECRET; // from the console

// Use the RAW body — verify the exact bytes, never a re-serialized object.
app.post("/webhooks/video", express.raw({ type: "*/*" }), (req, res) => {
  const sig = req.header("X-Video-Signature");
  const ts = req.header("X-Video-Timestamp");
  const raw = req.body.toString("utf8");

  // Replay protection: reject deliveries older than 5 minutes.
  if (Math.abs(Date.now() - Number(ts)) > 5 * 60_000) return res.sendStatus(400);

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${ts}.${raw}`)
    .digest("hex");
  const ok =
    !!sig && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(raw);
  // event.type === "video.completed" | "video.failed"
  // event.data.object === the job (same shape as GET /v1/videos/{id})
  res.sendStatus(200); // 2xx = acknowledged; anything else is retried
});

Delivery & retries

Respond 2xx to acknowledge. A non-2xx response, timeout (~5s), or network error is retried with backoff up to 6 attempts; after that the delivery is marked failed. Delivery is at-least-once, so dedupe on the event id. Targets must be public HTTPS URLs (private/loopback addresses are rejected) and redirects are not followed. Webhooks are an optimization — GET /v1/videos/{id} remains the source of truth.

SDKs & OpenAPI

There's no bespoke SDK to wait on — the API is plain REST and the OpenAPI 3.1 spec is the source of truth. Generate a typed client in your language, or browse the spec interactively in the API reference.

# Typed TypeScript types straight from the live spec
npx openapi-typescript https://api.catalogreel.com/v1/openapi.json -o catalogreel.d.ts

Errors & idempotency

Validation failures return 400 with an error message listing every offending field (e.g. an unsupported aspect_ratio, an out-of-set duration, both narration.script and narration.auto_generate, captions without narration, a vertical-scoped field on the wrong vertical, or a pre-2026-06 field name — the message names the replacement). Auth failures return 401; unknown vertical or bad image URLs return 400. Reusing an idempotency_key returns the original job with HTTP 200 and is never double-charged. Credit and limit responses (402 / 429) are covered under Billing & limits.

FieldTypeReqDefaultDescription
400validationoptionalA field failed validation (e.g. bad aspect_ratio/duration, both narration.script and auto_generate, a vertical-scoped field on the wrong vertical like objective on a vehicle request, or a pre-2026-06 field name — the message names the replacement), an unknown vertical, or an unreachable image URL. error lists every offending field.
401authoptionalMissing or invalid bearer key.
402balanceoptionalInsufficient prepaid balance. Body: { error, required, available }. Top up in the console or via POST /v1/credits/checkout.
403scopeoptionalKey not permitted — account-management and sub-tenant routes require an umbrella key, or X-Sub-Tenant conflicts with a scoped key.
404not foundoptionalJob id does not exist or is outside your key / sub-tenant scope.
409conflictoptionalJob is already terminal (completed/failed/cancelled) or has no source to act on — cancel / reprocess no longer applies.
429rate / limitoptionalRate limit, concurrency cap, or daily job cap reached. Honor the Retry-After header — see Billing & limits.
API reference · CatalogReel