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

Create a job, then poll until it's done. The same call in three languages:

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/sku-1.jpg",
      "https://cdn.example.com/sku-2.jpg"
    ],
    "subject": {
      "title": "Aluminum Field Chair",
      "brand": "Field Co.",
      "category": "Outdoor Seating",
      "price": 189,
      "material": "anodized aluminum",
      "color": "slate",
      "keyFeatures": ["folds flat", "weather-sealed"]
    },
    "render_quality": "standard",
    "duration": 16,
    "post_level": 2,
    "narration": { "enabled": true, "auto_generate": true, "tone": "tiktok" },
    "camera_style": "ultra-luxury",
    "end_card_cta": "Shop now",
    "listing_url": "https://store.example.com/products/aluminum-field-chair"
  }'

# → 202 { "id": "<uuid>", "status": "queued" }

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

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 — note captions needs narration.enabled and post_level ≥ 2.

# A vertical listing tour with an auto-written voiceover and burned captions.
# captions requires narration.enabled + post_level >= 2.
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,
    "post_level": 2,
    "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.

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. standard is the premium cost lever (higher-fidelity render). Alias: quality_tier (deprecated, still accepted).
post_levelinteger 0–3optional1Post-production complexity: 0 static · 1 animated (ffmpeg) · 2 lottie · 3 remotion motion graphics. Alias: render_tier (deprecated, still accepted).
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.
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, superbowl, tiktok, storyteller, minimal (legacy professional, casual, luxury, energetic, friendly are accepted and mapped). 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.
brand_overlay.cta_textstringoptionalCTA text, ≤ 120 chars.
captionsbooleanoptionalBurned-in captions from the narration script. Requires narration.enabled: true and post_level ≥ 2.

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"optionalauto from priceCamera, lighting and pacing treatment baked into the prompt (Direction axis). Legacy values (budget, mid-range, premium) are normalized to luxury. When omitted, ultra-luxury is auto-selected for a subject priced ≥ $100,000, otherwise luxury. Alias: cinematography_tier (deprecated, still accepted).
direction"default" | "auto" | style idoptionaldefaultAI vision director (vehicle/property). default keeps the pack template — byte-identical to today; auto lets the director pick one of the vertical's curated styles from your photos; a specific style id forces one (unknown → 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. ai_director: true is an accepted alias for auto.
end_card_ctastringoptionalOn-screen closing call-to-action card, 1–60 chars — distinct from the spoken narration.call_to_action. Requires post_level ≥ 2 (returns 400 otherwise).
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. standard (default) applies a domain-appropriate cinematic finish; raw opts out (bare segments); full produces a complete ad — auto-enables AI voiceover, burned captions, an end-card CTA and beat-synced music (only filling fields you omit) and forces post_level ≥ 2. Credit-neutral: post_level is unpriced and the voiceover incurs only the normal (free) narration. Alias: production_style with none/standard/commercial (deprecated, still accepted).
cinematicobjectoptionalFull studio recipe for granular control. Overrides finish when present. All sub-fields optional — the worker fills the rest. See the recipe table below.
objective"conversion" | "showcase" | "explainer" | "lifestyle"optionalEcommerce only. What the video is FOR — drives beat structure, transitions, pacing and narration framing + CTA. Unset → smart default: showcase, auto-upgraded to conversion when the product is on sale.

Cinematic recipe (overrides finish)

FieldTypeReqDefaultDescription
cinematic.enabledbooleanoptionaltrueMaster switch for the recipe.
cinematic.lookenumoptionalStudio look bundle: social-first, dealer-spotlight, luxury, real-estate-tour. Alias: cinematic.style (deprecated, still accepted).
cinematic.lutenumoptionalColor grade: none, teal-orange, film-noir, vivid, warm-doc, cool-modern, plus the film-stock emulations kodak-2383, fuji-eterna, bleach-bypass.
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"optionalHow tightly cuts snap to the beat. snap forces post_level ≥ 2.
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.

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.
quality_tier"fast" | "standard"optionalfastDeprecated pricing name still accepted by estimate. Equivalent to create render_quality.
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.
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.
descriptionstringoptionalFree-text listing notes (sanitized).

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.
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" },
    "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.

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.
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. Alias: production_style (none/standard/commercial, deprecated).
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; overrides finish.
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.

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 / post_level ≥ 2, or end_card_cta with post_level < 2). 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, captions or end_card_cta without post_level ≥ 2), 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