Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.recoupable.com/llms.txt

Use this file to discover all available pages before exploring further.

This is the canonical recipe used internally by Recoup’s create-content background task. Two paths are documented below: the async pipeline that an LLM agent should use, and the manual recipe for humans (or for cases where you want to swap a single step).

Hand it to your LLM

The fastest way to use this guide is to point an LLM at it directly — every page on this site is also served as raw markdown at the same path with a .md suffix, so models can fetch and follow it without parsing HTML:
Generate a music video for The Weeknd – "Blinding Lights" using
https://developers.recoupable.com/workflows/generate-music-video.md
Swap in your own artist and song. The LLM will resolve the artist, kick off the async pipeline, poll until done, and pull back the final video URL.

Running as an agent? Use the async pipeline

POST /api/content/video is synchronous and routinely takes 60–180s. Most agent shells (Claude Cowork, OpenAI tool calls, etc.) cap a single command at 30–60s and kill background processes when the shell exits — the manual recipe below is effectively un-runnable from those environments. Use the async path instead — same five steps, run server-side:
# Trigger
RUN_IDS=$(curl -sS -X POST "https://api.recoupable.com/api/content/create" \
  -H "x-api-key: $RECOUP_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg artist "$ARTIST_ACCOUNT_ID" --arg template "$TEMPLATE" \
        '{artist_account_id: $artist, template: $template}')" \
  | jq -r '.runIds[]')

# Poll (every ~10s) until COMPLETED / FAILED / CANCELED / CRASHED
RUN_ID=$(echo "$RUN_IDS" | head -1)
until STATUS=$(curl -sS "https://api.recoupable.com/api/tasks/runs?runId=$RUN_ID" \
                 -H "x-api-key: $RECOUP_API_KEY" \
               | jq -r '.runs[0].status') && \
      [[ "$STATUS" =~ ^(COMPLETED|FAILED|CANCELED|CRASHED)$ ]]; do
  sleep 10
done

# Read the output
curl -sS "https://api.recoupable.com/api/tasks/runs?runId=$RUN_ID" \
  -H "x-api-key: $RECOUP_API_KEY" \
  | jq '.runs[0].output'
# -> { videoSourceUrl, imageUrl, captionText, template, lipsync, audio: {...} }
Polling fits inside short shell timeouts and survives session restarts. See Tasks Runs for the full status enum (QUEUED, EXECUTING, COMPLETED, FAILED, CANCELED, CRASHED, etc.) and the CreateContentRunOutput schema.

Resolving $ARTIST_ACCOUNT_ID

POST /api/content/create needs the artist’s account_id. Three calls:
ORG_ID=$(curl -sS "https://api.recoupable.com/api/organizations" \
  -H "x-api-key: $RECOUP_API_KEY" | jq -r '.organizations[0].id')

ARTIST_ACCOUNT_ID=$(curl -sS "https://api.recoupable.com/api/artists?org_id=$ORG_ID" \
  -H "x-api-key: $RECOUP_API_KEY" \
  | jq -r --arg name "$ARTIST_NAME" '.artists[] | select(.name == $name) | .account_id')
The artist record exposes both id and account_id (both UUIDs). Use account_idid is the artist row’s primary key, account_id is the underlying account that owns it. The two are easy to swap; you’ll get a 404 from /api/content/create if you pass the wrong one.

Where the song lives

Step 5 (and the async pipeline’s lipsync mode) need a song.mp3. Don’t assume one exists, and don’t assume the user has one locally. Walk the agent through this fallback chain:
  1. Check the artist’s sandbox repo first. Each Recoup account has a backing GitHub repo. If the user has imported songs through Recoup, they live at predictable paths:
    .openclaw/workspace/orgs/{org-slug}/artists/{artist-slug}/songs/{song-slug}/{song-slug}.mp3
                                                                                /lyrics.json
                                                                                /clips.json
    
    Discover the repo with GET /api/sandboxes (returns github_repo and a filetree); fetch a file with GET /api/sandboxes/file?path=…. Binary files (.mp3, .png, .mp4) come back base64-encoded in the content field — decode before writing to disk.
  2. If no song is in the sandbox, ask the user how to proceed. Two options to offer:
    • “Want me to fetch the audio from YouTube?” — agent downloads via yt-dlp (or equivalent), saves locally; user is responsible for any rights / DSP-licensing implications.
    • “Want to supply the song yourself?” — user uploads / drops a path; agent reads from there.
    Don’t pick a path silently. The cost of fetching the wrong song from YouTube (or fetching one at all) is enough that the user should make the call.
  3. Don’t fall back to “use a placeholder track.” A music video without the song is not a deliverable.

Manual recipe (humans + targeted overrides)

The rest of this page walks the same steps you can run by hand or call individually if you want to swap a single stage (different prompt for image, different motion, different caption length).

Prerequisites

  • An auth credential for api.recoupable.com. Two options — pick one and use it for every call below:
    • API key (recoup_sk_…, recommended for sandbox / agent use): pass as -H "x-api-key: $RECOUP_API_KEY".
      • One-shot agent: POST /api/agents/signup with an agent+{unique}@recoupable.com email returns the key immediately.
      • Real-email signup: same endpoint with a real email mails a 6-digit code; complete with POST /api/agents/verify. See Agents.
    • Privy access token (for end-user flows in chat/UI): pass as -H "Authorization: Bearer $RECOUP_ACCESS_TOKEN".
    • The examples below use x-api-key. Substitute Authorization: Bearer … if you’re using a Privy token.
  • $ARTIST_NAME, $SONG_TITLE, $SONG_LYRICS_CLIP (a 1–2 sentence mood snippet)
  • $REFERENCE_IMAGE_URL (optional) — an artist photo or album cover to seed the image; if your template’s purpose is “show this exact image” (e.g. album-record-store), set this and skip image generation in step 2
  • A song.mp3 for step 5. Don’t ask the user for a local file — fetch from the artist’s repo via /api/sandboxes/file.
  • ffmpeg installed locally for step 5

Step 0: Scaffold the workspace BEFORE any API call

The VIDEO.md checklist is the workflow state — tick boxes and persist values back to the frontmatter as you go. To resume later, find the first unchecked box.
VIDEO_SLUG=$(echo "$SONG_TITLE" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-//; s/-$//')
VIDEO_DIR="videos/$VIDEO_SLUG"
mkdir -p "$VIDEO_DIR"

cat > "$VIDEO_DIR/VIDEO.md" <<EOF
---
artistName: $ARTIST_NAME
songTitle: $SONG_TITLE
template:
imageUrl:
videoUrl:
captionText:
finalVideoPath:
---

# $SONG_TITLE$ARTIST_NAME

## Pipeline checklist

- [ ] 1. Pick template (\`GET /api/content/templates\` + detail) — capture \`template\`
- [ ] 2. Generate the base image (\`POST /api/content/image\`) — capture \`imageUrl\`
  - [ ] 2a. (Optional) Upscale image (\`POST /api/content/upscale\` with \`type: "image"\`)
- [ ] 3. Generate the video (\`POST /api/content/video\`) — capture \`videoUrl\`
  - [ ] 3a. (Optional) Upscale video (\`POST /api/content/upscale\` with \`type: "video"\`)
- [ ] 4. Generate the caption (\`POST /api/content/caption\`) — capture \`captionText\`
- [ ] 5. Compose 9:16 final with audio + caption (local \`ffmpeg\`) — capture \`finalVideoPath\`

## Notes
EOF

Step 1: Pick a template (required: list and detail)

Templates carry the prompt, reference images, and styling that drive the rest of the chain — you can’t write a good Step 2 prompt without them. List, pick, then fetch detail:
curl -sS "https://api.recoupable.com/api/content/templates" \
  -H "x-api-key: $RECOUP_API_KEY" \
  | jq -r '.templates[] | "\(.id) — \(.description)"'

TEMPLATE="album-record-store"   # or artist-caption-{bedroom,outside,stage}

TEMPLATE_DETAIL=$(curl -sS "https://api.recoupable.com/api/content/templates/$TEMPLATE" \
  -H "x-api-key: $RECOUP_API_KEY")
Templates that list “Requires: face image” (e.g. artist-caption-bedroom) will fall back to their bundled reference images and produce a generic-likeness subject if you don’t supply one — they don’t 400. Pass $REFERENCE_IMAGE_URL if you want the model to preserve a specific artist’s likeness, or omit it for a stock-feeling result. See List Templates and Template Detail. After: write template into frontmatter, tick the box.

Step 2: Generate the base image

Use the template’s own prompt and reference images — don’t write your own from scratch. The template encodes the visual style; freeform prompts almost always drift off-brand.
PROMPT=$(echo "$TEMPLATE_DETAIL" | jq -r '.image.prompt')
REFS=$(echo "$TEMPLATE_DETAIL" | jq -c '[.image.reference_images[]?]')
# $REFS is 5–15 URLs depending on the template — pass all of them via images[]

IMAGE_URL=$(curl -sS -X POST "https://api.recoupable.com/api/content/image" \
  -H "x-api-key: $RECOUP_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg prompt "$PROMPT" --argjson refs "$REFS" \
        --arg ref "$REFERENCE_IMAGE_URL" \
        '{prompt: $prompt, images: $refs} + (if $ref == "" then {} else {reference_image_url: $ref} end)')" \
  | jq -r '.imageUrl')
Shortcut: if $REFERENCE_IMAGE_URL is the exact image you want (e.g. an album cover for album-record-store, or a final editorial photo), set IMAGE_URL=$REFERENCE_IMAGE_URL and skip this call entirely. Avoid: prompts that aim for a real artist’s likeness. Veo (used in step 3) rejects celebrity-likeness images with a 422. Use the template’s prompt as-is and let the reference images carry the style. See Generate Image. After: write imageUrl, tick the box.

Optional: Upscale (image or video)

Same endpoint for both, swap type. Skip if you don’t need the resolution bump.
# Image (after step 2)
IMAGE_URL=$(curl -sS -X POST "https://api.recoupable.com/api/content/upscale" \
  -H "x-api-key: $RECOUP_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg url "$IMAGE_URL" '{url: $url, type: "image"}')" \
  | jq -r '.url')

# Video (after step 3) — same call, type: "video", reassign $VIDEO_URL
See Upscale.

Step 3: Generate the video

Pass the image and a short motion prompt. For lipsync, also pass a presigned audio_url to a song clip — the model will animate the artist’s mouth.
MOTION=$(echo "$TEMPLATE_DETAIL" | jq -r '.video.movements[0] // "Slow camera drift, subtle subject motion"')

VIDEO_URL=$(curl -sS --max-time 360 -X POST "https://api.recoupable.com/api/content/video" \
  -H "x-api-key: $RECOUP_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg image "$IMAGE_URL" --arg prompt "$MOTION" \
        '{image_url: $image, prompt: $prompt, aspect_ratio: "9:16"}')" \
  | jq -r '.videoUrl')
This call routinely takes 60–180s. From a short-shell agent, use the async pipeline at the top of this page instead. From a long-lived shell, output is an 8s clip. Step 5 loops it to song length. See Generate Video. After: write videoUrl, tick the box.

Step 4: Generate the caption

TOPIC="Song: \"$SONG_TITLE\". Lyrics: \"$SONG_LYRICS_CLIP\". Artist: $ARTIST_NAME."

CAPTION_RESPONSE=$(curl -sS -X POST "https://api.recoupable.com/api/content/caption" \
  -H "x-api-key: $RECOUP_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg topic "$TOPIC" --arg template "$TEMPLATE" \
        '{topic: $topic, template: $template, length: "short"}')")

CAPTION_TEXT=$(echo "$CAPTION_RESPONSE"     | jq -r '.content')
CAPTION_FONT=$(echo "$CAPTION_RESPONSE"     | jq -r '.font          // "/System/Library/Fonts/Helvetica.ttc"')
CAPTION_COLOR=$(echo "$CAPTION_RESPONSE"    | jq -r '.color         // "white"')
CAPTION_STROKE=$(echo "$CAPTION_RESPONSE"   | jq -r '.borderColor   // "black"')
CAPTION_FONT_SIZE=$(echo "$CAPTION_RESPONSE" | jq -r '.maxFontSize  // 48')
length accepts "short" (default), "medium", "long", or "none" to skip. The four styling fields (font, color, borderColor, maxFontSize) are template-driven hints — Step 5 wires them into ffmpeg’s drawtext so the burned-in caption matches the template’s brand. See Generate Caption. After: write captionText, tick the box.

Step 5: Compose the final 9:16 video (local)

The API returns a 16:9 motion clip. Compose locally: crop to 9:16, overlay the song audio for the full duration, and burn in the caption with the styling from step 4.
curl -sS -o "$VIDEO_DIR/clip.mp4" "$VIDEO_URL"
SONG_PATH="$VIDEO_DIR/song.mp3"           # see "Where the song lives" above
FINAL_PATH="$VIDEO_DIR/final.mp4"

# Write caption to a file — drawtext reads it via textfile=, sidesteps shell-escaping (apostrophes, etc.)
echo -n "$CAPTION_TEXT" > "$VIDEO_DIR/caption.txt"

# Single-line filter graph — newlines inside -filter_complex are literal characters and break the [v] label
ffmpeg -y \
  -stream_loop -1 -i "$VIDEO_DIR/clip.mp4" \
  -i "$SONG_PATH" \
  -filter_complex "[0:v]crop=ih*9/16:ih,scale=1080:1920,drawtext=fontfile=$CAPTION_FONT:textfile=$VIDEO_DIR/caption.txt:fontcolor=$CAPTION_COLOR:fontsize=$CAPTION_FONT_SIZE:x=(w-text_w)/2:y=h-text_h-120:box=1:boxcolor=$CAPTION_STROKE@0.5:boxborderw=20[v]" \
  -map "[v]" -map "1:a" \
  -shortest -c:v libx264 -c:a aac -pix_fmt yuv420p \
  "$FINAL_PATH"
After: write finalVideoPath, tick the box. With every box ticked, the music video is complete.

Step 6: Publish (optional)

Once the MP4 is rendered, push it to the artist’s socials with the Connectors API. One heads-up worth knowing:
  • TikTok URL ownership. TIKTOK_PUBLISH_VIDEO (pull-from-URL mode) requires the source domain be verified in the TikTok dev portal. fal.media URLs will fail with url_ownership_unverified — use TIKTOK_UPLOAD_VIDEO instead, which accepts the same URL and uploads server-side.

The checklist is the source of truth — if a box isn’t ticked, treat the step as not run.