Listing Photo → Video with Async Webhook Delivery
Turn listing photos into walkthrough-style videos via POST /v1/video/generations with image_url + webhook_url. Built for property platforms, listing-video services, and real-estate media teams.
Why property platforms need this
Listing photo → video in one call
Submit the MLS hero photo and a motion prompt. Get a 5-second walkthrough clip delivered by webhook — no polling loop required.
Webhook callbacks for production
Signed webhook delivery means your platform knows the instant a video is ready, without long-polling or cron jobs.
Per-listing traceability
Track listing_id + generation_id together so completed videos route to the correct property page, MLS upload, or social ad.
Clear status for agents & teams
Map render states to agent-friendly language so no one asks 'where is my video?' while it renders.
Cost math for a listing-video service
Simple per-listing economics agents and platform builders can quote immediately.
Prepare your listing manifest
One row per room or listing photo. Include the MLS image URL, a motion prompt describing the camera move, and the delivery target (MLS upload, website hero, social ad).
listing_id,image_url,video_prompt,property_type,delivery_target LST-4201,https://cdn.example.com/listings/4201-living-room.jpg,"5-second cinematic walkthrough, warm afternoon light, smooth camera dolly forward",residential,mls-upload LST-4202,https://cdn.example.com/listings/4202-kitchen.jpg,"5-second kitchen reveal, natural light from windows, slow pan left to right",residential,website-hero LST-4203,https://cdn.example.com/listings/4203-exterior.jpg,"5-second aerial approach, golden hour, gentle zoom toward front entrance",luxury,social-ad
Submit listing videos with bounded concurrency
This Node.js script reads the manifest, submits async video jobs with webhook_url, and writes a JSONL job log you reconcile when callbacks arrive.
import fs from "node:fs/promises";
const API_KEY = process.env.CREATIVEAI_API_KEY;
const WEBHOOK_URL = "https://your-platform.com/webhooks/listing-video";
const INPUT_CSV = "listing-videos.csv";
const OUTPUT_JSONL = "listing-video-jobs.jsonl";
const MAX_CONCURRENCY = 3;
function parseCsv(csv) {
const [headerLine, ...lines] = csv.trim().split("\n");
const headers = headerLine.split(",").map((v) => v.trim());
return lines
.filter(Boolean)
.map((line) => {
const values = line
.match(/("[^"]*"|[^,]+)/g)
?.map((v) => v.replace(/^"|"$/g, "").trim()) ?? [];
return Object.fromEntries(headers.map((h, i) => [h, values[i] ?? ""]));
});
}
async function submitListingVideo(row) {
const response = await fetch("https://api.creativeai.run/v1/video/generations", {
method: "POST",
headers: {
Authorization: "Bearer " + API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "kling-v3",
prompt: row.video_prompt,
image_url: row.image_url,
duration: "5s",
aspect_ratio: "16:9",
webhook_url: WEBHOOK_URL,
}),
});
const body = await response.json();
if (!response.ok) {
throw new Error("HTTP " + response.status + ": " + JSON.stringify(body));
}
return {
listing_id: row.listing_id,
property_type: row.property_type,
delivery_target: row.delivery_target,
generation_id: body.id,
status: body.status,
submitted_at: new Date().toISOString(),
};
}
async function runWithConcurrency(rows, worker, limit) {
const results = [];
let index = 0;
async function runner() {
while (index < rows.length) {
const current = index++;
try {
results[current] = await worker(rows[current]);
} catch (error) {
results[current] = {
listing_id: rows[current].listing_id,
status: "submit_failed",
error: String(error),
};
}
}
}
await Promise.all(Array.from({ length: Math.min(limit, rows.length) }, () => runner()));
return results;
}
async function main() {
if (!API_KEY) throw new Error("Set CREATIVEAI_API_KEY first");
const csv = await fs.readFile(INPUT_CSV, "utf8");
const rows = parseCsv(csv);
const results = await runWithConcurrency(rows, submitListingVideo, MAX_CONCURRENCY);
await fs.writeFile(
OUTPUT_JSONL,
results.map((r) => JSON.stringify(r)).join("\n") + "\n",
"utf8"
);
const submitted = results.filter((r) => r.status === "pending").length;
console.log("Submitted " + submitted + "/" + rows.length + " listing videos -> " + OUTPUT_JSONL);
}
main().catch((err) => { console.error(err); process.exit(1); });Receive the webhook and route the video
Verify the HMAC signature, match the generation_id to the listing, and push the completed video to your MLS, CMS, or property website.
import crypto from "node:crypto";
import fs from "node:fs/promises";
const API_KEY = process.env.CREATIVEAI_API_KEY;
const JOBS_PATH = "listing-video-jobs.jsonl";
function verifySignature(rawBody, signatureHeader) {
const expected = crypto
.createHmac("sha256", API_KEY)
.update(rawBody)
.digest("hex");
const received = (signatureHeader || "").replace("sha256=", "");
if (!received) return false;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(received)
);
}
export async function POST(request) {
const rawBody = await request.text();
const signature = request.headers.get("x-creativeai-signature");
if (!verifySignature(rawBody, signature)) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(rawBody);
const lines = (await fs.readFile(JOBS_PATH, "utf8")).trim().split("\n");
const jobs = lines.map((line) => JSON.parse(line));
const updated = jobs.map((job) =>
job.generation_id === event.id
? {
...job,
status: event.status,
output_url: event.output_url || null,
completed_at: event.completed_at || null,
error: event.error || null,
}
: job
);
await fs.writeFile(
JOBS_PATH,
updated.map((j) => JSON.stringify(j)).join("\n") + "\n",
"utf8"
);
if (event.status === "completed") {
// Push event.output_url to your MLS, website CMS, or listing platform here.
}
return Response.json({ ok: true });
}Track per-listing delivery state
Store listing ID, job ID, output URL, and error context together. Failed listings can be retried without re-submitting the full batch.
{"listing_id":"LST-4201","property_type":"residential","delivery_target":"mls-upload","generation_id":"vgen_abc123","status":"completed","output_url":"https://cdn.creativeai.run/output/video-abc123.mp4","completed_at":"2026-03-12T09:22:17Z","error":null}
{"listing_id":"LST-4202","property_type":"residential","delivery_target":"website-hero","generation_id":"vgen_def456","status":"pending","submitted_at":"2026-03-12T09:20:01Z"}
{"listing_id":"LST-4203","property_type":"luxury","delivery_target":"social-ad","generation_id":"vgen_ghi789","status":"failed","error":"provider timeout after retry budget"}Give agents clear status language
Real-estate agents and property managers should never see raw API states. Map each status to language they understand.
[
{
"status": "pending",
"agent_copy": "Queued — video render slot reserved, no action needed"
},
{
"status": "processing",
"agent_copy": "Rendering listing video — we'll notify you when it's ready"
},
{
"status": "completed",
"agent_copy": "Listing video ready — download or publish to MLS / website"
},
{
"status": "failed",
"agent_copy": "Render failed — retry or contact support with the job ID"
}
]Operational notes
MAX_CONCURRENCY = 3 and increase after verifying quota and webhook throughput on your hosting.listing_id + generation_id paired in your job store so retries don't duplicate renders./webhooks as the proof page for webhook reliability if buyers ask about delivery guarantees.Property marketing overview
Send /for-property-marketing first when the prospect wants pricing, staging, and video in one page.
Webhook reliability proof
Send /webhooks when the objection is retries, signing, failover, or polling fallback.
Multi-room consistency
Send /tutorials/multi-room-multi-angle-consistency when the prospect needs consistent style across rooms.
Also using DALL-E for listing photos?DALL-E 3 shuts down in 58 days
DALL-E 2 & 3 shut down May 12, 2026 — 58 days away. If you use DALL-E for virtual staging or listing photo generation, you're about to lose both your image and video backends. CreativeAI covers both — one API key for the full pipeline.
Use code DALLE1000 for bonus credits on your DALL-E migration.
Ready to add listing videos to your platform?
Sign up for free credits, copy the code above, and submit your first listing photo → video job in minutes. Use code SORAPROPERTY at signup for 200 bonus credits — enough for 40+ listing videos.