What this endpoint does
POST /api/v1/generate-and-post queues one paid video render and returns a job handle. It does not post or schedule anything — the name is kept for compatibility. When the job completes, your system decides what to do with the video: a blog embed, a CMS draft, an approval queue, or a post via POST /api/v1/post.
Endpoint
POST https://api.reelsbuilder.ai/api/v1/generate-and-postIdempotency-Key is required (see Idempotency). Requests without it return 400 IDEMPOTENCY_KEY_REQUIRED. Dry runs are the exception: add "dry_run": true to validate the request and get its price for free, without a key and without creating a job (see Dry run).
Quickstart
curl -X POST https://api.reelsbuilder.ai/api/v1/generate-and-post \
-H "Authorization: Bearer $REELSBUILDER_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A 30 second explainer on how Acme cuts onboarding time in half.",
"content_purpose": "education",
"platforms": ["youtube_shorts", "tiktok", "instagram"],
"duration_sec": 30,
"aspect_ratio": "9:16",
"content_style": "professional",
"agent_run_id": "agent-run-2026-10-06-001"
}'Accepted response (202)
{
"success": true,
"data": {
"job_id": "6a0f...",
"generation_id": "6a0f...",
"video_id": "6a0f...",
"status": "queued",
"poll_url": "/api/v1/jobs/6a0f...",
"distribution": {
"mode": "external_agent_distribution",
"requested_platforms": ["youtube_shorts", "tiktok", "instagram"],
"schedule_at": null,
"webhook_url": null
},
"output_contract": {
"video_url": "available after completion on GET /api/v1/jobs/{job_id}.data.job.artifacts.videoUrl",
"social_metadata": { "title": "...", "description": "...", "platforms": ["..."] }
},
"price_usd_cents": 60
},
"jobId": "6a0f...",
"videoId": "6a0f...",
"meta": {
"request_id": "8f0c...",
"agent_run_id": "agent-run-2026-10-06-001",
"key_mode": "live",
"retryable": false,
"estimated_completion_seconds": 180
}
}Treat data.job_id, data.video_id, data.status and data.poll_url as the stable contract; every Generate endpoint returns them the same way (see Jobs). The job id is also in the X-Job-Id header. The top-level jobId/videoId are deprecated aliases. Other fields are informational and may change.
Dry run (free)
curl -X POST https://api.reelsbuilder.ai/api/v1/generate-and-post -H "Authorization: Bearer $REELSBUILDER_API_KEY" -H "Content-Type: application/json" -d '{ "prompt": "How Acme halves onboarding time", "duration_sec": 30, "dry_run": true }'
# -> 200
{
"success": true,
"data": {
"dry_run": true,
"valid": true,
"mode": "stock",
"normalized_request": { "mode": "personalized", "options": { "clipType": "business", "includeCaptions": true, "includeMusic": true, "duration": 30, "aspectRatio": "9:16", ... } },
"price": { "usd_cents": 60, "display": "$0.60", "breakdown": [{ "item": "generate_and_post", "usd_cents": 60 }] },
"estimated_duration_sec": 30,
"estimated_render_seconds": { "min": 90, "max": 300 },
"warnings": [],
"charged": false
},
"meta": { "request_id": "…" }
}A dry run runs the same validation as a real call, so invalid input gets the same 400 INPUT_INVALID. Nothing is queued or charged. normalized_request shows how your fields were resolved (for example the inferred clipType).
Footage controls
The API supports footage_keywords and exclude_keywords. Check the deployed generate_and_post schema at GET /api/v1/tools before using them; both were advertised on October 7. Each accepts up to 32 nonempty strings, at most 100 characters each, trimmed, lowercased and deduplicated.
footage_keywords guides footage search without changing the narration. exclude_keywords rejects matching clip metadata before ranking and library fallbacks; it is a literal metadata filter with normalized singular/plural matching, not a guarantee about everything visible in a clip. Plurals are matched (robots excludes robot), but words that only look plural are kept whole (news does not exclude new). Review the selected footage. These controls preserve caller-supplied assets. Send the end customer's niche and keywords explicitly rather than relying on the API account's brand. Preview first; review actual footage before publishing.
Request fields
One of prompt, topic or script is required; otherwise the request fails with 400 INPUT_INVALID. Field names are the canonical ones; the listed aliases still work, and a dry run's warnings tells you when you used one.
| Field | Type | Description |
|---|---|---|
prompt | string | Natural-language brief for the render. |
topic | string | Short topic. Defaults to the prompt. |
script | string | Your own narration, read word for word (AI script writing is skipped). Size it to the length: about 25 words for 15s, 65 for 30s, 100 for 45s, 135 for 60s. Your words are never cut: a longer script makes a longer video (up to 120s), at the same price. |
content_purpose | enum | education, sales, awareness, product_demo, case_study, news or entertainment. |
asset_urls | URL[] | Your own media to use in the render. |
platforms | string[] | Intended downstream targets, echoed back for your pipeline: youtube, youtube_shorts, instagram, tiktok, facebook, facebook_reels, x, x_twitter. Nothing is posted. |
duration_sec | number | 15–120. Values below 15 are rejected. Aliases: duration_target_sec, duration. |
aspect_ratio | enum | 9:16 (1080×1920) only for stock video today; other values return 400. Use AI video or clips for 16:9 and 1:1. Alias: aspectRatio. |
clip_type | string | Footage niche; the background music follows it. For B2B technology use tech; for office, teams and meetings use business. When omitted, the niche is inferred from brand.industry and brand.keywords, then from your brief (cloud, security, DevOps, CRM, data and payments wording selects tech footage), then from your account brand's industry, and otherwise defaults to business. Tech sub-niches such as tech/ai or tech/cyber are accepted and currently draw from the same technology library; footage is matched to each line of the script within it. An unknown value returns 400 listing the valid niches. |
content_style | enum | professional (default), educational, inspirational, energetic, calm or entertaining. Sets the script tone and pacing. Other values return 400. |
captions | boolean | Burned-in captions. Default true. Same as caption_style.preset: "none" when false. Alias: include_captions. |
caption_style | object | Caption look; see Customize the render. preset: highlight (default; large 1–3 word chunks with the spoken word in colour), classic (bold uppercase phrases, no word colour) or none. text_color (#RRGGBB, default #FFFFFF), highlight_color (#RRGGBB, highlight preset only; default brand.accent_color, else your account brand, else #FFD60A), position (lower_third default, center, top), font_size (small, medium, large default). Unknown keys or bad values return 400. |
music | boolean or object | true/false switches the music bed on or off (default on). The object form { "enabled": true, "volume": 0.2 } also sets the level (0–1 under the voice, default 0.35). include_music and music_volume are accepted as aliases; music wins when both are sent. |
include_branding | boolean | ReelsBuilder watermark. Default true; set false on a paid plan to remove it. |
voice_id | string | ElevenLabs narration voice id. Wins over voice_gender. When omitted, chosen from voice_gender, else from the brand tone. Aliases: voice, voiceId. |
voice_gender | enum | female or male. Picks the default voice when voice_id is omitted. |
voice_speed | number | 0.7–1.3, default 1.0. Generated scripts are sized for the speed, so a slower voice gets fewer words. If narration still runs past the requested length it is sped up (at most 1.35×) so the video keeps its length. |
language | string | ISO 639-1 narration and script language (en, es, de, pt, ar, …). Default: detected from your script or brief. A language we have no voice for returns 400 instead of rendering a wrong-language voice. |
caption_language | string | ISO 639-1 caption language when it should differ from the narration; captions are translated. Default: same as language. |
cta | object | { "text": "Book a demo at acme.com" }, up to 120 characters. Appended word for word as the last line of a generated script. Ignored when you send script (end your script with it instead). Without cta, a generated script closes by naming brand.website_url (or your account website) when one is set. |
include_hook | boolean | Generated scripts only. true opens with a researched hook line (English narration); false forbids any hook or intro line. Default: off, with a natural opening. |
delivery | object | Optional. { "visibility": "private" } keeps the finished files in a private bucket instead of our public CDN; the job then returns links that expire after 24 hours. See Private delivery. |
webhook_url | URL | Optional https endpoint that receives one terminal callback. See Webhooks. Alias: webhookUrl. |
dry_run | boolean | Free. Validate and price the request without creating a job. No Idempotency-Key needed. See Dry run. |
schedule_at | date-time | Echoed back in distribution.schedule_at for your pipeline. It does not delay the render or schedule a post. |
agent_run_id | string | Your trace id. Echoed in meta and in the webhook body. |
brand | object | Optional per-video brand. Omit it to use your account's brand settings. When sent, it replaces the account brand for that video (nothing is filled in from the account), so you can render for each of your customers from one account. Fields, all optional (send at least one): name, website_url (https), logo_url (public https PNG/JPG, shown as a bottom-right watermark), primary_color and accent_color (#RRGGBB; the spoken-word caption color, accent first), tone (professional, casual, energetic, bold, calm, analytical), industry, keywords (up to 10). Invalid values return 400. See Brand per video. |
brand_id | — | Not supported. Returns 422 CUSTOMER_CONTEXT_UNAVAILABLE; use brand instead. |
Customize the render
Every setting has a default, so you only send what you want to change. Request values beat brand values: caption_style > brand colors > your account brand > defaults. Invalid values return 400 INPUT_INVALID before anything is charged.
{
"prompt": "How Acme cuts onboarding time in half for IT teams.",
"clip_type": "tech",
"caption_style": {
"preset": "highlight",
"highlight_color": "#00C2FF",
"position": "center",
"font_size": "medium"
},
"music": { "volume": 0.15 },
"voice_gender": "female",
"voice_speed": 1.1,
"language": "en",
"cta": { "text": "Book a demo at acme.com" }
}Brand per video
By default every video uses the brand saved on your ReelsBuilder account. If you render for many end customers, send a brand object with each request instead. The script mentions the brand name and website, captions use the colors, and the logo appears as a watermark.
"brand": {
"name": "Acme Cloud",
"website_url": "https://acme.com",
"logo_url": "https://acme.com/logo.png",
"primary_color": "#0A66C2",
"industry": "Cloud security",
"tone": "professional"
}Combine it with "include_branding": false on a paid plan so only your customer's logo appears. Host logos on a public https URL (IP addresses and internal hosts are rejected); a square or wide PNG with a transparent background works best.
Polling
curl https://api.reelsbuilder.ai/api/v1/jobs/6a0f... \
-H "Authorization: Bearer $REELSBUILDER_API_KEY"A live job looks like this (fields abbreviated):
{
"success": true,
"data": {
"job": {
"id": "6a0f...",
"status": "completed",
"progress": [{ "stage": "...", "message": "...", "percent": 100 }],
"artifacts": {
"videoId": "6a0f...",
"videoUrl": "https://...mp4",
"videoUrls": ["https://...mp4"],
"thumbnailUrl": "https://...jpg"
},
"error": null
}
}
}Keep polling while status is queued, leased, running or uploading. Stop on completed, failed, cancelled or dead_lettered. On failure, read data.job.error (code, message, retryable) before retrying. Polling is free and counts toward the 60 requests/minute read limit; every 10–15 seconds is plenty.
Sandbox keys (rb_test_) return a different, simplified poll shape: data.status and data.video_url at the top level, with no data.job object. Handle both if you test in sandbox.
Cancel a job
curl -X POST https://api.reelsbuilder.ai/api/v1/jobs/6a0f.../cancel \
-H "Authorization: Bearer $REELSBUILDER_API_KEY"Returns the updated job. A job that is already terminal returns 409 JOBS_CANCEL_NOT_ALLOWED. Cancelled jobs do not send a webhook.
Pricing and limits
- Live keys are charged $0.60 per accepted request from your prepaid balance. The charge appears in the
X-Api-Credits-Cost-Usd-Centsresponse header. - If the job fails, the charge is refunded.
- Insufficient balance returns
402 INSUFFICIENT_API_CREDITSwithcost_usd_cents,balance_usd_centsandtopup_min_usd_cents. Top up before retrying. - At most 5 heavy render jobs can be active per account. The sixth returns
429 RENDER_CONCURRENCY_LIMIT_EXCEEDED; it is not queued. - Sandbox keys return a fixture with
sandbox: true, create no job and charge nothing.
Agent pattern (TypeScript)
const apiKey = process.env.REELSBUILDER_API_KEY;
if (!apiKey) throw new Error("Set REELSBUILDER_API_KEY first");
const runId = crypto.randomUUID();
const apiBase = "https://api.reelsbuilder.ai";
async function readResponse(response: Response) {
const body = await response.json();
if (!response.ok || !body.success) {
throw new Error(body.error?.message ?? body.error?.code ?? `HTTP ${response.status}`);
}
return body.data;
}
const accepted = await readResponse(await fetch(`${apiBase}/api/v1/generate-and-post`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": runId,
},
body: JSON.stringify({
prompt: "A short update about Acme's new onboarding flow.",
content_purpose: "product_demo",
agent_run_id: runId,
}),
signal: AbortSignal.timeout(30000),
}));
if (!accepted?.job_id) throw new Error("Response is missing job_id");
let videoUrl: string | undefined;
for (let attempt = 0; attempt < 80; attempt++) {
const data = await readResponse(await fetch(`${apiBase}/api/v1/jobs/${accepted.job_id}`, {
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(30000),
}));
// Live uses data.job; sandbox fixtures expose status and video_url on data.
const job = data?.job ?? data;
if (!job?.status) throw new Error("Response is missing job status");
if (job.status === "completed") {
videoUrl = job.artifacts?.videoUrl ?? job.video_url;
if (!videoUrl) throw new Error("Completed job has no video URL");
break;
}
if (["failed", "cancelled", "dead_lettered"].includes(job.status)) {
throw new Error(job.error?.message ?? `Job ${job.status}`);
}
if (!["queued", "leased", "running", "uploading"].includes(job.status)) {
throw new Error(`Unexpected job status: ${job.status}`);
}
await new Promise((resolve) => setTimeout(resolve, 15000));
}
if (!videoUrl) throw new Error("Polling limit reached; resume polling the same job_id");
// Sandbox URLs are fixtures, not rendered assets.
// Your pipeline can publish a completed live video with POST /api/v1/post.
See also
- Autoblog Video API
- Webhooks — the optional terminal callback
- Multi-Platform Post API — publish the finished video
- Idempotency
- Rate Limits
- Errors