Machine-readable discovery
Naming note
https://api.reelforger.com/v1/recipes/render with recipe_id.API Reference
For MCP callers, use the thin wrapper documented at /docs/mcp-remote-tools. MCP V1 mirrors these endpoints; it does not introduce a second planning/runtime layer.
For a dedicated guide to asynchronous music generation, polling, cost behavior, and reusing returned track URLs in a render, see /docs/music-api.
POST https://api.reelforger.com/v1/recipes/render (Recipes API, canonical)
Recommended default for AI-agent workflows. Send a strict recipe request and ReelForger compiles it into a full manifest, enqueues the job, and returns a job_id.
Caption input behavior for recipes:
- If
variables.transcript_wordsis supplied, ReelForger uses it directly. - If
variables.transcript_wordsis omitted, ReelForger may transcribe automatically. - If source selection is ambiguous, set
variables.transcription_source_asset_id.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
Authorization (header) | string | Required | Bearer <API_KEY> |
idempotency-key (header) | string | Optional | Request dedupe key (header takes precedence) |
recipe_id | enum | Required (canonical) | Recipe family identifier (for example voiceover_explainer) |
style_preset | enum | Required | Caption/style preset for generated overlays and captions |
variables | object | Required | Strict per-recipe variables object |
variables.transcript_words | array | Optional | If supplied, ReelForger uses these words directly for caption timing |
variables.transcription_source_asset_id | string | Optional | Explicit caption transcription source when transcript words are omitted (required if source is ambiguous) |
webhook_url | string(url) | Optional | Public endpoint that accepts webhook POST events |
webhook_headers | object | Optional | Optional custom webhook headers |
webhook_secret | string | Optional | Optional per-request webhook signing override |
idempotency_key | string | Optional | Payload-level dedupe key |
Request Example (cURL)
curl -X POST "https://api.reelforger.com/v1/recipes/render" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-d '{
"recipe_id": "voiceover_explainer",
"style_preset": "karaoke_yellow",
"idempotency_key": "recipe-001",
"variables": {
"voiceover_audio_url": "https://samplelib.com/lib/preview/mp3/sample-15s.mp3",
"background_assets": ["https://picsum.photos/id/1018/1080/1920"]
}
}'
Notes for caption-capable recipes:
- Add
variables.transcript_wordswhen you already have timestamps and want ReelForger to use them directly. - Omit
variables.transcript_wordsto let ReelForger transcribe automatically. - Use
variables.transcription_source_asset_idif source selection is ambiguous. - You can still provide
correct_textwhere supported to polish punctuation/casing even when ReelForger auto-transcribes words. - Current behavior: auto-transcription bills and processes the full selected source duration sent for transcription (not a smart-trimmed subsection).
- Transcription billing rule: 1 credit per 5 seconds transcribed (rounded up), charged only when ReelForger successfully performs transcription.
Response Example (202 Accepted)
{
"success": true,
"job_id": "8f1fd0fe-63a5-4fef-8f2c-8f1225f6d309",
"request_id": "req_abc123"
}
POST https://api.reelforger.com/v1/music/generate
Creates an asynchronous music generation job using the public API.
Important behavior:
- Every successful music generation costs exactly
20credits. - Every successful music generation returns exactly
2distinct tracks. - Those returned
audio_urlvalues can be used directly in later ReelForger render requests as normal audio assets.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
Authorization (header) | string | Required | Bearer <API_KEY> |
idempotency-key (header) | string | Optional | Request dedupe key |
prompt | string | Required in simple mode | Prompt for the generation request |
custom_mode | boolean | Optional | Defaults to false |
instrumental | boolean | Optional | Defaults to true |
model | enum | Optional | V4 | V4_5 | V4_5PLUS | V4_5ALL | V5 (defaults to V5) |
style | string | Required in custom mode | Genre / mood / production direction |
title | string | Required in custom mode | Base title for the returned tracks |
negative_tags | string | Optional | Exclusions for the generation |
webhook_url | string(url) | Optional | Public HTTPS endpoint that accepts music job webhook POSTs |
webhook_headers | object | Optional | Custom headers sent with outbound music webhook POSTs |
webhook_secret | string | Optional | Same request field shape as render webhook config |
idempotency_key | string | Optional | Payload-level dedupe key |
metadata | object | Optional | Flat key-value pairs (strings, numbers, booleans; max 10 keys, 500 chars each) echoed in webhook payloads |
Request Example (cURL)
curl -X POST "https://api.reelforger.com/v1/music/generate" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "idempotency-key: music-gen-001" \
-d '{
"prompt": "Cinematic lo-fi beat with a slow emotional build and no vocals",
"custom_mode": false,
"instrumental": true,
"model": "V5",
"metadata": {
"make_scenario_id": "12345",
"campaign": "spring_launch"
},
"webhook_url": "https://api.example.com/reelforger/webhooks",
"webhook_headers": {
"X-Environment": "production"
}
}'
Response Example (202 Accepted)
{
"success": true,
"data": {
"job_id": "9e1d1a87-1c7c-4518-a7d6-b65e24f9e6af",
"billing_policy": "flat_20_credits",
"credit_cost": 20
},
"request_id": "req_abc123"
}
GET https://api.reelforger.com/v1/music/jobs/{jobId}
Poll a previously created music generation job. Once complete, the response includes tracks[] with both generated songs.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
Authorization (header) | string | Required | Bearer <API_KEY> |
jobId | string(uuid) | Required | Music generation job ID |
Request Example (cURL)
curl -X GET "https://api.reelforger.com/v1/music/jobs/9e1d1a87-1c7c-4518-a7d6-b65e24f9e6af" \
-H "Authorization: Bearer <YOUR_API_KEY>"
Response Example (200 OK, completed)
{
"success": true,
"data": {
"id": "9e1d1a87-1c7c-4518-a7d6-b65e24f9e6af",
"status": "completed",
"billing_policy": "flat_20_credits",
"credit_cost": 20,
"tracks": [
{
"id": "track-1",
"audio_url": "https://assets.reelforger.com/music/night-drive-1.mp3",
"title": "Night Drive Bloom 1",
"duration": 87
},
{
"id": "track-2",
"audio_url": "https://assets.reelforger.com/music/night-drive-2.mp3",
"title": "Night Drive Bloom 2",
"duration": 91
}
]
},
"request_id": "req_abc124"
}
End-to-end Example: Generate music, poll, then use a returned track in a render
- Create a music generation job with
POST /v1/music/generate - Poll
GET /v1/music/jobs/{jobId}untilstatus === "completed" - Take one of the returned
tracks[].audio_urlvalues and use it as a standardaudioasset in a later render
Render Request Example Using Returned audio_url
curl -X POST "https://api.reelforger.com/v1/videos/render" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-d '{
"version": "v1",
"output": { "width": 1080, "height": 1920, "fps": 30 },
"assets": [
{ "id": "clip1", "type": "video", "url": "https://example.com/clip1.mp4" },
{ "id": "music1", "type": "audio", "url": "https://assets.reelforger.com/music/night-drive-1.mp3" }
],
"composition": {
"timeline": [
{
"id": "video-1",
"type": "video",
"asset_id": "clip1",
"time": { "start_seconds": 0, "duration_seconds": 8 }
},
{
"id": "audio-1",
"type": "audio",
"asset_id": "music1",
"time": { "start_seconds": 0, "duration_seconds": 8 },
"media_settings": { "volume": 0.35, "loop": true }
}
]
}
}'
That audio URL behaves exactly like any other public audio file in the manifest system.
POST https://api.reelforger.com/v1/videos/validate
Validates a render payload without queueing a job or charging credits. Use this as a preflight check before render (recipes or timeline).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
Authorization (header) | string | Required | Bearer <API_KEY> |
version | string | Required | Manifest version |
output.width | number | Required | Output width in pixels |
output.height | number | Required | Output height in pixels |
output.fps | number | Required | Output FPS (30 for launch) |
assets[] | array | Optional | Asset list |
composition.timeline[] | array | Optional | Media timeline layers |
composition.text_overlays[] | array | Optional | Text overlay layers |
composition.captions | object | Optional | Programmatic captions block |
composition.captions.words | array | Optional | If supplied, ReelForger uses these words directly for caption timing |
composition.captions.transcription_source_asset_id | string | Optional | Explicit caption transcription source when words are omitted (required if source is ambiguous) |
webhook_url | string(url) | Optional | Accepted but ignored during validate; included so you can reuse the exact same request body for /render |
webhook_headers | object | Optional | Accepted but ignored during validate |
webhook_secret | string | Optional | Accepted but ignored during validate |
Request Example (cURL)
curl -X POST "https://api.reelforger.com/v1/videos/validate" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-d '{
"version": "v1",
"output": { "width": 1080, "height": 1920, "fps": 30 },
"assets": [
{ "id": "clip1", "type": "video", "url": "https://example.com/clip1.mp4" }
],
"composition": {
"timeline": [
{
"id": "video-1",
"type": "video",
"asset_id": "clip1",
"time": { "start_seconds": 0, "duration_seconds": 6 }
}
],
"captions": {
"provider": "assemblyai",
"preset": "karaoke_yellow",
"mode": "phrase_karaoke",
"layout": { "y": "78%" },
"words": [
{ "text": "The", "start": 0, "end": 200 },
{ "text": "striker", "start": 200, "end": 600 }
]
}
}
}'
Response Example (200 OK)
{
"success": true,
"data": {
"valid": true,
"estimated_total_duration_seconds": 6,
"estimated_credit_cost": 6,
"warnings": [
{
"code": "captions_near_bottom",
"message": "Caption y is near the bottom edge. Consider 70%-80% for safer readability across presets.",
"path": "composition.captions.layout.y"
}
]
},
"request_id": "req_abc123"
}
Common Warning Codes
| Code | Meaning | Recommended Action |
|---|---|---|
duration_near_limit | Render duration is close to the 300-second cap | Trim timeline duration or shorten late-starting layers to preserve margin below 300s. |
captions_near_bottom | Caption layout may clip near lower edge | Move captions up (layout.y around 70%-80%) or reduce style.font_size. |
captions_without_correct_text | Captions may look less polished without punctuation alignment | Provide correct_text for punctuation/casing repair while preserving timestamps. |
karaoke_no_custom_highlight | Karaoke mode uses preset default highlight color | Set captions.style.highlight_color if brand color consistency is required. |
small_visual_layer_default_background | Small visual layer may stack unexpectedly without explicit z_index/transparent background | For PiP/split-screen, set layout.z_index explicitly and use background_mode: "transparent" where needed. |
low_vertical_position | Visual layer starts very close to canvas bottom | Raise layout.y or reduce layer height to prevent clipping. |
POST https://api.reelforger.com/v1/videos/render
Advanced custom Timeline API path. Submits a render manifest and enqueues an asynchronous video job.
Caption input behavior for timeline renders:
- If
composition.captions.wordsis supplied, ReelForger uses it directly. - If
composition.captions.wordsis omitted, ReelForger may transcribe automatically. - If source selection is ambiguous, set
composition.captions.transcription_source_asset_id.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
Authorization (header) | string | Required | Bearer <API_KEY> |
idempotency-key (header) | string | Optional | Request dedupe key (header takes precedence) |
version | string | Required | Manifest version |
output.width | number | Required | Output width in pixels (> 0) |
output.height | number | Required | Output height in pixels (> 0) |
output.fps | number | Required | Output FPS (> 0) |
assets[] | array | Optional | Asset list (defaults to empty array) |
assets[].id | string | Required (per asset) | Unique asset ID |
assets[].type | enum | Required (per asset) | video | audio | image |
assets[].url | string(url) | Required (per asset) | Public URL for media |
composition.auto_stitch | boolean | Optional | Defaults to false |
composition.duration_seconds | number | Optional | Explicit composition duration. Used first for image-duration inference when image time.duration_seconds is omitted. |
composition.timeline[] | array | Optional | Media layer list (video, audio, image only) |
composition.timeline[].id | string | Required | Unique layer ID |
composition.timeline[].type | enum | Required | video | audio | image |
composition.timeline[].asset_id | string | Required for video/audio/image | Must reference assets[].id |
composition.timeline[].time | object | Required for image (start_seconds required; duration_seconds may be inferred from composition duration). Required for video/audio unless composition.auto_stitch is true; untimed videos are stitched in order, while untimed audio in mixed timelines defaults to start_seconds: 0 and aligns to stitched video duration. | Layer timing |
composition.text_overlays[] | array | Optional | Text overlay list (see Working with Text) |
composition.timeline[].trim.start_seconds | number | Optional | Trim start (>= 0) |
composition.timeline[].style | object | Optional | CSS-like style overrides |
composition.timeline[].media_settings.* | object | Optional | Layer media controls (volume, fades, loop, etc.) |
composition.captions.words | array | Optional | If supplied, ReelForger uses these words directly for caption timing |
composition.captions.transcription_source_asset_id | string | Optional | Explicit caption transcription source when words are omitted (required if source is ambiguous) |
webhook_url | string(url) | Optional | Public HTTPS endpoint that accepts POST requests from ReelForger webhooks |
webhook_headers | object | Optional | Custom headers sent with outbound webhook POST |
webhook_secret | string | Optional | Optional per-request signing override. If omitted, ReelForger uses your dashboard-managed whsec_... signing secret |
idempotency_key | string | Optional | Payload-level dedupe key |
metadata | object | Optional | Flat key-value pairs (strings, numbers, booleans; max 10 keys, 500 chars each) echoed in webhook payload |
Image duration precedence:
- Explicit
composition.timeline[].time.duration_secondsalways wins. - If omitted for
image, ReelForger usescomposition.duration_secondswhen provided. - Otherwise, ReelForger infers from the max timed end across timeline/text overlays.
- If
composition.auto_stitchis enabled with untimed media clips, ReelForger can infer duration at render-time from probed media lengths.
Practical pattern for talking-head + logo without precomputed duration:
- Set
composition.auto_stitch: true - Leave base video
timeomitted - Set logo image
time.start_secondsonly (omit imageduration_seconds)
Request Example (cURL)
curl -X POST "https://api.reelforger.com/v1/videos/render" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-d '{
"version": "v1",
"output": { "width": 1080, "height": 1920, "fps": 30 },
"assets": [
{ "id": "clip1", "type": "video", "url": "https://example.com/clip1.mp4" }
],
"composition": {
"auto_stitch": true,
"timeline": [
{ "id": "video-1", "type": "video", "asset_id": "clip1" }
]
},
"webhook_url": "https://api.example.com/reelforger/webhooks",
"webhook_headers": {
"X-Environment": "production",
"X-Integration": "render-pipeline"
},
"metadata": {
"make_scenario_id": "12345",
"user_email": "test@test.com"
}
}'
Request Example (Node.js fetch with webhook config)
const response = await fetch("https://api.reelforger.com/v1/videos/render", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer <YOUR_API_KEY>",
},
body: JSON.stringify({
version: "v1",
output: { width: 1080, height: 1920, fps: 30 },
assets: [
{ id: "clip1", type: "video", url: "https://example.com/clip1.mp4" }
],
composition: {
auto_stitch: true,
timeline: [{ id: "video-1", type: "video", asset_id: "clip1" }]
},
webhook_url: "https://api.example.com/reelforger/webhooks",
webhook_headers: {
"X-Environment": "production"
},
metadata: { make_scenario_id: "12345", user_email: "test@test.com" }
// webhook_secret is optional:
// if omitted, ReelForger uses your dashboard-managed signing secret (whsec_...)
})
});
const data = await response.json();
console.log(data);
Response Example (202 Accepted)
{
"success": true,
"job_id": "8f1fd0fe-63a5-4fef-8f2c-8f1225f6d309",
"request_id": "req_abc123"
}
Response Example (200 OK, idempotent replay)
{
"success": true,
"data": {
"deduped": true,
"job": {
"id": "8f1fd0fe-63a5-4fef-8f2c-8f1225f6d309",
"status": "rendering",
"output_url": null,
"error_message": null,
"created_at": "2026-02-26T10:00:00.000Z",
"updated_at": "2026-02-26T10:00:05.000Z"
}
}
}
GET https://api.reelforger.com/v1/jobs/:jobId
Returns the current status for a previously submitted render job.
Status values include:
queuedtranscribingrenderingcompletedtranscription_failedfailed
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
Authorization (header) | string | Required | Bearer <API_KEY> |
jobId (path) | string | Required | Job identifier |
Request Example (cURL)
curl -X GET "https://api.reelforger.com/v1/jobs/<JOB_ID>" \
-H "Authorization: Bearer <YOUR_API_KEY>"
Response Example (200 OK)
{
"success": true,
"request_id": "req_abc123",
"data": {
"id": "8f1fd0fe-63a5-4fef-8f2c-8f1225f6d309",
"status": "completed",
"output_url": "https://assets.reelforger.com/user_xxx/job_yyy.mp4",
"error_message": null,
"created_at": "2026-02-26T10:00:00.000Z",
"updated_at": "2026-02-26T10:00:45.000Z",
"total_duration_seconds": 5.6,
"credit_cost": 9,
"billing": {
"total_credit_cost": 9,
"transcription_credit_cost": 3,
"transcription_duration_seconds_billed": 14.6,
"render_credit_cost": 6,
"render_credit_refunded": false
},
"diagnostics": {
"preflight_warning_count": 0,
"preflight_warnings": [],
"captions": {
"enabled": true,
"mode": "phrase_karaoke",
"preset": "karaoke_yellow",
"word_count": 15,
"has_correct_text": true
},
"status_snapshot": "completed"
}
}
}