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.
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
baseUrl | string | required | https://api.catalogreel.com | REST API origin used by every request and by SDK/Postman imports. |
apiKey | string | required | — | Server-side bearer token created in /console/keys. Store it as a secret; never expose it in browser code. |
subTenant | string | optional | — | Your stable customer id for agency/platform integrations. Send it as X-Sub-Tenant with an umbrella key, or mint scoped keys per customer. |
webhookUrl | string (url) | optional | — | Public HTTPS endpoint to receive video.completed / video.failed events. Send as webhook_url on create requests. |
webhookSecret | string | optional | — | Org signing secret from /console/keys or GET /v1/webhook-secret. Use it to verify X-Video-Signature. |
jobId | string (uuid) | optional | — | Returned by POST /v1/videos. Poll GET /v1/videos/{id}, reprocess, cancel, or store it for idempotent reconciliation. |
idempotencyKey | string | optional | — | Caller-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:
- AI photo selection — every uploaded photo is scored and the strongest set is chosen, so you can send your whole shoot.
- Cinematography planning — a per-scene plan decides which shot anchors each segment and how the camera moves.
- 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
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
vertical | string | required | — | Tuned pack id. One of: property, ecommerce, vehicle. |
subject | object | required | — | Pack-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_urls | string[] | required | — | 1–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. |
intent | string | optional | — | Free-text generation context, ≤ 120 chars. |
brand | object | optional | — | Freeform brand metadata stored with the job (distinct from brand_overlay). |
render_quality | "fast" | "standard" | optional | fast | Render fidelity. standard is the premium cost lever (higher-fidelity render). Alias: quality_tier (deprecated, still accepted). |
post_level | integer 0–3 | optional | 1 | Post-production complexity: 0 static · 1 animated (ffmpeg) · 2 lottie · 3 remotion motion graphics. Alias: render_tier (deprecated, still accepted). |
webhook_url | string (url) | optional | — | Public HTTPS endpoint notified when the job finishes (video.completed / video.failed). HMAC-signed — see Webhooks. Optional; polling works without it. |
idempotency_key | string | optional | — | 1–255 chars. Replays return the existing job (HTTP 200) instead of creating a duplicate. |
vin | string | optional | — | 17-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
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
aspect_ratio | "16:9" | "9:16" | optional | 16:9 | Output orientation; defaults to 16:9 (landscape). Set "9:16" for vertical/social. 1:1 is intentionally unsupported by the generator and is rejected. |
duration | integer 8–90 | optional | 16 | Requested 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" | optional | 720p | Explicit output override. Omit it for 720p; set 1080p for HD output. HD output carries the higher finished-video price. |
Audio & narration
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
music | enum | optional | — | Background music preset: trending-tiktok, cinematic-epic, luxury-elegance, energetic-electronic, ambient-dreamy, modern-corporate, real-estate-warmth, or off. |
narration | object | optional | — | Voice-over. See sub-fields below. |
narration.enabled | boolean | required | — | Required within narration. Turns the voice track on. |
narration.script | string | optional | — | Custom narration text, 1–5000 chars. Mutually exclusive with auto_generate. |
narration.auto_generate | boolean | optional | — | Generate the script from subject data. Mutually exclusive with script. |
narration.voice | enum | optional | premium-warm | Curated commercial TTS voice (the spoken timbre). Male: premium-warm, smooth-baritone, bold-authority, friendly-pro. Female: conversational, confident-female, bright-upbeat, elegant-luxe. |
narration.tone | enum | optional | — | Creative 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_action | string | optional | — | Closing spoken CTA line, ≤ 200 chars. |
audio_mix | object | optional | — | Finer audio control applied during the final stitch. Requires at least one sub-field. |
audio_mix.custom_music_prompt | string | optional | — | 3–500 chars. Steers the AI music direction. A non-off music preset takes precedence over this. |
audio_mix.audio_mix_priority | "music" | "voice" | "balanced" | optional | — | Which track wins the mix. Also shapes music-only ducking, so it is accepted without narration. |
audio_mix.maintain_music_continuity | boolean | optional | — | Keep one continuous music bed across segments instead of restarting it each segment. |
Branding & overlays
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
brand_overlay | object | optional | — | Burned-in brand overlay. Requires at least one sub-field. |
brand_overlay.logo_url | string (url) | optional | — | Logo image URL. |
brand_overlay.brand_name | string | optional | — | Brand name text, ≤ 100 chars. |
brand_overlay.cta_text | string | optional | — | CTA text, ≤ 120 chars. |
captions | boolean | optional | — | Burned-in captions from the narration script. Requires narration.enabled: true and post_level ≥ 2. |
Background replacement
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
background | object | optional | — | AI 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_url | string (url) | optional | — | Reference background image URL. When enabled, the worker can composite selected hero photos against this background. |
background.prompt | string | optional | — | 3–500 chars. Generated background direction, or a refinement prompt when image_url is also supplied. |
Cinematic & end card
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
camera_style | "luxury" | "ultra-luxury" | optional | auto from price | Camera, 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 id | optional | default | AI 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_cta | string | optional | — | On-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_url | string (url) | optional | — | Optional detail-page URL for the end-card QR code. vdp_url and product_url are accepted aliases; provide only one. |
Finish
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
finish | "raw" | "standard" | "full" | optional | standard | Post-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). |
cinematic | object | optional | — | Full 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" | optional | — | Ecommerce 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)
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
cinematic.enabled | boolean | optional | true | Master switch for the recipe. |
cinematic.look | enum | optional | — | Studio look bundle: social-first, dealer-spotlight, luxury, real-estate-tour. Alias: cinematic.style (deprecated, still accepted). |
cinematic.lut | enum | optional | — | Color grade: none, teal-orange, film-noir, vivid, warm-doc, cool-modern, plus the film-stock emulations kodak-2383, fuji-eterna, bleach-bypass. |
cinematic.caption_style | enum | optional | — | Burned-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.transitions | enum | optional | — | cut, xfade-fade, xfade-wipeleft, xfade-fadeblack, auto. |
cinematic.spec_callout_style | enum | optional | — | Spec-panel look: commercial-panel, showroom-3d, showroom-3d-real, blueprint-scan, luxury-minimal. |
cinematic.music_sync | "none" | "soft" | "snap" | optional | — | How tightly cuts snap to the beat. snap forces post_level ≥ 2. |
cinematic.letterbox | "none" | "cinematic" | optional | — | Cinematic 2.39:1 letterbox bars. |
cinematic.motion | "none" | "subtle" | "dynamic" | optional | — | Ken 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" | optional | — | Warm film-burn light leak — adds warm light, distinct from the vignette (which darkens the edges). |
cinematic.logo_bug | enum | optional | — | Persistent corner logo watermark (uses the brand logo): none, top-left, top-right, bottom-left, bottom-right. |
cinematic.<toggles> | boolean | optional | — | Per-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
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
seed | integer | optional | — | 0 – 4294967295. Shared deterministic seed across segments for reproducible / consistent output. |
negative_prompt | object | optional | — | Exclusion 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" | optional | — | Forward-compat field. Only 'disallow' is accepted for property/ecommerce — any other value returns 400 (people are excluded at the pack level). |
scene_preset | enum | optional | — | Environment: 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
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
duration | integer 8–90 | optional | 16 | Requested length. Snaps to the same 8-second segment boundary and plan cap used by create. |
quality_tier | "fast" | "standard" | optional | fast | Deprecated pricing name still accepted by estimate. Equivalent to create render_quality. |
resolution | "720p" | "1080p" | optional | 720p | Output resolution to price. 1080p returns 400 if the current plan does not allow HD. |
narration | boolean | optional | false | Price-neutral today, accepted so estimates stay compatible if narration pricing changes later. |
background | boolean | optional | false | Whether to include background replacement credits. Returns 0 background_credits unless VIDEO_API_BACKGROUND_REPLACEMENT_ENABLED is on. |
Response
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
credits | integer | optional | — | Exact internal credits a matching create would reserve. |
usd | number | optional | — | Estimated dollar value at the standard prepaid rate. |
duration_seconds | integer | optional | — | Snapped deliverable length the estimate priced. |
segments | integer | optional | — | Number of 8-second source segments. |
resolution | "720p" | "1080p" | optional | — | Priced output resolution. |
background_credits | integer | optional | — | Additional background replacement credits included in credits; 0 while the feature flag is disabled. |
spendable / sufficient | integer / boolean | optional | — | Current 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")
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
title | string | optional | — | Product name (name is accepted as a fallback). Anchors the prompt, narration and end card. |
brand | string | optional | — | Manufacturer / brand. Used in narration and the brand highlight. |
category | string | optional | — | Product category, e.g. "Office Chairs". |
price | number | optional | — | Current price. Spoken in narration, shown as the price highlight, and feeds the auto cinematography tier. |
originalPrice / salePrice / onSale | number / number / boolean | optional | — | Deal signals — when set, narration calls out the markdown ("on sale now, was …"). |
material / materials | string / string[] | optional | — | Material(s); rendered into the subject phrase and the material highlight. |
color | string | optional | — | Primary color/finish. |
dimensions | string | optional | — | Free-text size, e.g. '27"W x 27"D'. |
keyFeature / keyFeatures | string / string[] | optional | — | Top selling points (up to ~5 used in narration). |
condition | string | optional | — | e.g. "new", "refurbished". |
rating | number | optional | — | 0–5 customer rating; narrated as social proof. |
description | string | optional | — | Free-text listing notes (sanitized, ≤ ~900 chars used). |
Vehicle (vertical: "vehicle")
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
year / make / model / trim | string|number / string | optional | — | Vehicle identity headline — drives the prompt, narration and identity-lock. |
price | number | optional | — | Listing price. Narrated, shown as the price highlight, feeds the auto cinematography tier. |
mileage | number | optional | — | Odometer; narrated and shown as a highlight. |
condition / newUsed | string / "new" | "used" | optional | — | Listing status; influences framing (showroom-fresh vs. dealer value). |
engine / transmission / drivetrain | string | optional | — | Powertrain spec; woven into narration and highlights. |
fuelType | string | optional | — | Fuel type; narrated and shown in the spec callout. |
cityMpg / highwayMpg | number | optional | — | EPA 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 / horsepower | number | optional | — | Combined MPG (fallback when city/highway are absent) and horsepower; used in narration. |
exteriorColor / interiorColor / bodyStyle | string | optional | — | Appearance details for the subject phrase and highlights. |
warranty / isCertified | string / boolean | optional | — | Assurance signals; isCertified adds a "Certified Pre-Owned" callout. |
keyFeature / keyFeatures | string / string[] | optional | — | Top selling points beyond the structured specs (e.g. "one owner, new tires"); up to ~5 used in narration. |
description | string | optional | — | Free-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")
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
propertyType | string | optional | — | e.g. "single-family home", "condo". Defaults to "home". |
bedrooms / bathrooms / squareFeet | number | optional | — | Core size signals — narration opening + highlights. |
price | number | optional | — | List price. Property prices ≥ $1M are spoken abbreviated ("one point two five million"). Feeds the auto cinematography tier. |
realEstateType | "sale" | "rent" | optional | — | Tenure; shifts framing between investment vs. livability. |
lotSize / lotSizeUnit | number / string | optional | — | Lot size and its unit (default "sq ft"). |
yearBuilt | number | optional | — | Construction year; narrated and shown as a highlight. |
city / state | string | optional | — | Location context for narration. |
garageSpaces / hasPool / hoaFee | number / boolean / number | optional | — | Feature signals surfaced as highlights and narration. |
keyFeature / keyFeatures | string / string[] | optional | — | Top selling points beyond the structured specs (e.g. "remodeled kitchen, walk to greenbelt"); up to ~5 used in narration. |
description | string | optional | — | Free-text listing notes (sanitized). |
GET /v1/videos/{id}
Returns the job. Poll until status is completed or failed.
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
id | string (uuid) | optional | — | Job id — use it to poll. |
status | enum | optional | — | pending → queued → processing → completed | failed | cancelled. |
listing_url | string | null | optional | — | Resolved detail-page URL used for the end-card QR code. |
video_url | string | null | optional | — | Final MP4 URL once completed. |
thumbnail_url | string | null | optional | — | Poster frame. |
source_urls | string[] | null | optional | — | The photos actually used after photo selection. |
progress | object | null | optional | — | Live { stage, label, percent } while processing. |
captions | object | null | optional | — | Caption track when captions were requested. |
error_code / error | string | null | optional | — | Set when status is failed. |
created_at / completed_at | timestamp | optional | — | Lifecycle 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)
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
narration | object | optional | — | { 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_options | object | optional | — | Advanced narration tuning (pacing, tone). |
audio_settings | object | optional | — | Music / voice preset overrides applied during the re-stitch. |
brand_overlay | object | optional | — | Same shape as the create endpoint — re-burns the overlay. |
end_card_cta | string | optional | — | Closing CTA card text, 1–60 chars. |
listing_url | string (url) | optional | — | Optional replacement detail-page URL for the end-card QR code. vdp_url and product_url aliases are accepted. |
finish | "raw" | "standard" | "full" | optional | — | Re-finalize into this post-production finish — no new render. Alias: production_style (none/standard/commercial, deprecated). |
cinematic | object | optional | — | Full 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_settings | object | optional | — | Advanced 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.
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
GET /v1/account/balance | umbrella | optional | — | Returns { balance, reserved, spendable, plan_tier } for the shared prepaid wallet. |
GET /v1/account/usage | umbrella | optional | — | Paginated ledger. Send X-Sub-Tenant to narrow usage to one customer. |
POST /v1/credits/checkout | umbrella | optional | — | Starts a Stripe Checkout top-up for { pack_id } and returns { checkout_url }. |
GET /v1/keys | umbrella | optional | — | Lists API keys without secrets. |
POST /v1/keys | umbrella | optional | — | Creates a key and returns the plaintext secret once. Include sub_tenant to create a scoped key. |
DELETE /v1/keys/{id} | umbrella | optional | — | Revokes an API key. |
GET /v1/sub-tenants | umbrella | optional | — | Lists customer scopes under the organization. |
POST /v1/sub-tenants | umbrella | optional | — | Creates or upserts a sub-tenant from { external_id, name }. |
GET /v1/sub-tenants/{externalId} | umbrella | optional | — | Fetches one sub-tenant by your external id. |
DELETE /v1/sub-tenants/{externalId} | umbrella | optional | — | Deletes a sub-tenant only after active scoped keys are revoked. |
GET /v1/webhook-secret | umbrella | optional | — | Reveals the org webhook signing secret (whsec_...). |
POST /v1/webhook-secret | umbrella | optional | — | Rotates 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.
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
standard | fast / 720p | optional | estimated | ~15s ≈ $6, ~30s ≈ $12, ~45s ≈ $18, ~64s ≈ $24 at the standard prepaid rate. |
hd | standard / 1080p | optional | estimated | ~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.
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
starter | default | optional | 10 rpm · 3 concurrent | Daily cap 1,000 jobs, up to 90s / 1080p, 30-day retention. New accounts start here with 0 balance. |
growth | on request | optional | 30 rpm · 8 concurrent | Daily cap 5,000 jobs, up to 90s / 1080p, 90-day retention. |
scale | on request | optional | 60 rpm · 20 concurrent | Daily 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
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
X-Video-Event | string | optional | — | video.completed or video.failed (also in the body as type). |
X-Video-Timestamp | string | optional | — | Unix milliseconds when the delivery was signed. Reject if older than ~5 min. |
X-Video-Signature | string | optional | — | Hex HMAC-SHA256 of `${timestamp}.${rawBody}` using your org signing secret. |
User-Agent | string | optional | — | drivereach-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.tsErrors & 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.
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
400 | validation | optional | — | A 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. |
401 | auth | optional | — | Missing or invalid bearer key. |
402 | balance | optional | — | Insufficient prepaid balance. Body: { error, required, available }. Top up in the console or via POST /v1/credits/checkout. |
403 | scope | optional | — | Key not permitted — account-management and sub-tenant routes require an umbrella key, or X-Sub-Tenant conflicts with a scoped key. |
404 | not found | optional | — | Job id does not exist or is outside your key / sub-tenant scope. |
409 | conflict | optional | — | Job is already terminal (completed/failed/cancelled) or has no source to act on — cancel / reprocess no longer applies. |
429 | rate / limit | optional | — | Rate limit, concurrency cap, or daily job cap reached. Honor the Retry-After header — see Billing & limits. |