Command Reference
Every ft subcommand, flag, environment variable, and exit code
A complete reference for the ft CLI. If you're new here, start with the CLI overview instead — this page is for looking things up.
Command summary
| Command | What it does |
|---|---|
ft auth login | Prompt for an API key and store it in the OS keychain |
ft auth logout | Forget the stored key |
ft auth whoami | Show the signed-in account |
ft me | Alias of ft auth whoami |
ft generate <tags> | Submit, poll, and download in one shot |
ft list | Show recent generations |
ft get <id> | Show one generation's detail |
ft download <id> | Download an already-completed track |
ft delete <id>... | Permanently delete tracks (confirms unless --yes) |
ft playlists | List your playlists |
ft playlist show <playlist> | Show a playlist and the tracks in it |
ft playlist add <playlist> <id>... | Add tracks to a playlist |
ft playlist remove <playlist> <id>... | Remove tracks from a playlist |
ft playlist move <src> <dst> <id>... | Move tracks between playlists |
ft doctor | Health check + resolved config dump |
ft update | Re-run the install script to upgrade |
ft help [command] | Print built-in help — overall or for a single command |
Built-in help
Every command has built-in help — useful when you forget a flag and don't want to leave the terminal.
ft help # overview of all commands
ft help generate # help for a specific subcommand
ft generate --help # same thing, flag form
ft -h # short formft help <command> prints the command's description, every flag with its default, and a short usage example. It's the fastest way to check whether a flag exists without opening the docs.
Global flags
These work on every command:
| Flag | Purpose |
|---|---|
--api-key <key> | Override the stored key for this invocation. Useful in CI. |
--base-url <url> | Override the API base URL. Defaults to https://pub.finetuning.ai. |
--config <path> | Use a custom config file path instead of the default. |
-v, --verbose | Log HTTP requests and responses to stderr. Keys are redacted to ft_live_ab3f...****. |
--no-color | Disable ANSI color output. |
Spinners, prompts, and progress always go to stderr; requested data goes to stdout. That's why ft list --json | jq always works.
ft auth
ft auth login
Prompts for your API key with echo off, validates the format (ft_live_ + 32 chars, 40 total), verifies it against /v1/me, and stores it in your OS keychain.
ft auth login
# Paste your API key: ft_live_********************************
# Stored in macOS Keychain. ✓
# Signed in as jane@example.com (pro tier)Flags:
-
--stdin— read the key from stdin instead of prompting. Use this in CI:echo "$FT_API_KEY" | ft auth login --stdin
ft auth logout
Removes the key from the keychain (and the plaintext fallback if present).
ft auth whoami
Calls GET /v1/me and prints the signed-in email, tier, and credit summary. ft me is an alias.
ft me --json | jq '.data.limits'ft generate
The flagship command. Submits a generation, polls until it's completed or failed, and downloads the MP3.
ft generate "<prompt>" [flags]Flags
| Flag | Default | Notes |
|---|---|---|
--duration <sec> | 60 | 5–210 for Pro/Lifetime. Server clamps anything out of range; the CLI prints note: duration clamped from X → Y on stderr. |
--bpm <n> | 120 | 60–200. Also clamped. |
--key <K> | C | C, C#, D, D#, E, F, F#, G, G#, A, A#, B |
--scale <s> | major | major or minor |
--time-sig <s> | 4 | 2, 3, 4, 5, 6, 7 |
--language <l> | en | en, ja, de, fr, es, zh, ko, pt, it, ru |
--lyrics "<text>" | empty | 0–2000 chars |
--seed <n> | random | Pass to reproduce a previous run |
-o, --output <path> | <slug>-<short-id>.mp3 in cwd | Where to save the MP3 |
--no-wait | off | Return immediately after the 202; CLI prints the id and exits |
--json | off | Emit raw API JSON to stdout instead of downloading |
Examples
# Defaults — 60s, 120 BPM, C major
ft generate "lofi chill piano, mellow, late night"
# Customised
ft generate "upbeat ad jingle, brass and snare" \
--duration 30 --bpm 128 --key D --scale major \
--output ./jingle.mp3
# Fire and forget
ft generate "deep ambient drone" --no-wait
# Queued. id=07e8d430-2310-4c57-87a8-cf1e6db376f7 status=processing
# Scripting mode — emits the final API record to stdout, skips download
ft generate "dark trap" --json | jq '.data.id'tags is the prompt, not a comma list of keywords. The field name is historical — pass free-form prose, quoted.
--json skips the download. If you want both the JSON and the file, run ft generate ... first, then ft get <id> --json separately.
Polling behaviour
Internally ft generate polls GET /v1/generations/:id every 3–15 seconds (exponential backoff) and honors Retry-After on 429 responses. Polling times out after 5 minutes with the message:
still processing, run `ft get <id>` laterIf you Ctrl-C mid-poll, the generation keeps running server-side and still costs a credit. Resume with ft get <id> or ft download <id> once it's done — never re-submit.
ft list
Shows recent generations as a table (or JSON).
ft list [flags]| Flag | Default | Notes |
|---|---|---|
--limit <n> | 20 | Max 99 |
--offset <n> | 0 | Pagination offset |
--status <s> | all | pending, processing, completed, failed |
--json | off | Raw API response on stdout |
Examples:
ft list --limit 10
ft list --status processing # what's still in-flight
ft list --status completed --limit 50
# Pipe-friendly
ft list --json | jq '.data.generations[].id'ft get
Show one generation's full detail (including audioUrl, fileSize, generationTime, and errorMessage if it failed).
ft get <id> # human-readable
ft get <id> --json # raw API recordid is the full UUID v4 (36 chars with hyphens) printed by ft list and ft generate.
While status is pending or processing, audioUrl is null.
ft download
Download a completed track without going through generation.
ft download <id> [-o <path>]- Streams the MP3 to disk (no in-memory buffering).
- Default filename:
<slug>-<short-id>.mp3in the current directory. - Shows a progress bar when stderr is a TTY.
audioUrl lives on media.finetuning.ai (a public R2 bucket). Don't curl it directly with X-API-Key — that host doesn't auth requests, and ft download handles streaming + naming + progress for you.
ft delete
Permanently deletes tracks from your library, in bulk.
ft delete <id>... [flags]| Flag | Default | Notes |
|---|---|---|
-y, --yes | off | Skip the confirmation prompt. Required in scripts / non-interactive shells. |
--json | off | Emit the merged API response on stdout |
ft delete 07e8d430-2310-4c57-87a8-cf1e6db376f7
# This permanently deletes the track from your library — everywhere, not just playlists.
# Delete 1 track(s)? [y/N] y
# Deleted 1 of 1 track(s)Deletion is permanent. The track disappears from your library, all playlists, and public pages immediately, and cannot be restored via the API.
Like all bulk commands, ft delete reports partial success: tracks that couldn't be deleted (bad ID, not yours) print as warning: lines on stderr, and the exit code is non-zero only when nothing was deleted. Pass more than 100 IDs freely — the CLI chunks at 100 per request automatically.
ft playlists
Lists your playlists.
ft playlists
# ID NAME TRACKS DURATION VISIBILITY UPDATED
# 5ab3ec64-6225-4965-acac-49a053609225 Focus Beats 14 42m private 3 min ago--json emits the raw API response. Playlists are created in the web app — the CLI only lists them and manages their tracks.
ft playlist
Inspects and manages the tracks inside playlists.
Everywhere a <playlist> argument appears, you can pass either the playlist ID or its name (case-insensitive exact match). If two playlists share a name, the CLI errors and lists the candidate IDs.
ft playlist show
Shows one playlist with every track in it. Works on your own playlists and on other users' public playlists (by ID).
ft playlist show "Focus Beats"
# ID 5ab3ec64-6225-4965-acac-49a053609225
# Name Focus Beats
# Visibility private
# Tracks 14
# Duration 42m
# Updated 3 min ago
#
# ID STATUS DURATION PROMPT CREATED
# a9b16641-... completed 3m lo-fi, chill, morn... 2 hours agoWith --json, the tracks array makes this the starting point for bulk pipes — see Bulk workflows.
ft playlist add
Adds tracks to a playlist.
ft playlist add "Focus Beats" <track-id>...
# Added 2 of 2 track(s)ft playlist remove
Removes tracks from a playlist — the tracks stay in your library.
ft playlist remove "Focus Beats" <track-id>...ft playlist move
Moves tracks from one playlist to another. A track that can't be added to the target (privacy rule, already there) stays in the source.
ft playlist move "Focus Beats" "Deep Work" <track-id>...Partial success and exit codes
All bulk commands share the same model as the API: each ID is checked individually, failures print as warning: <id>: <reason> on stderr, a one-line summary (Added 2 of 3 track(s)) goes to stdout, and the exit code is non-zero only when zero items succeeded. The CLI chunks at 100 IDs per request and merges results, so pass as many IDs as you like.
You'll never see the API's ADD_FAILED / MOVE_FAILED codes from the CLI — when a whole batch fails, the CLI absorbs that response into the same per-track warnings, so total failure and partial failure read the same way.
Privacy rule: public playlists may only contain public tracks, and private playlists only your own tracks. The API never changes a track's visibility — on Cannot add private track to public playlist, make the track public at finetuning.ai first.
Bulk workflows
The bulk commands accept any number of IDs, which makes them natural endpoints for pipes. Select tracks with ft list --json (your library) or ft playlist show --json (one playlist) + jq, then feed the IDs through xargs.
Add your recent completed tracks to a playlist:
ft list --status completed --limit 50 --json \
| jq -r '.data.generations[].id' \
| xargs ft playlist add "Focus Beats"
# Added 50 of 50 track(s)Clean up every failed generation:
ft list --status failed --json \
| jq -r '.data.generations[].id' \
| xargs ft delete --yes
# Deleted 4 of 4 track(s)(--yes is required here — pipes aren't interactive, so the confirmation prompt can't run. Make sure the jq filter selects exactly what you mean before adding it.)
Move a batch between playlists:
ft playlist move "Drafts" "Album" \
fea285f2-f8e4-4e60-98b0-c78f868ccc26 \
aa8ca15d-f5c0-4c7d-8e3c-c3ca332c0a26
# Moved 2 of 2 track(s)Move everything from one playlist to another — select from the source with ft playlist show:
ft playlist show "Drafts" --json \
| jq -r '.data.tracks[].id' \
| xargs ft playlist move "Drafts" "Album"
# Moved 12 of 12 track(s)What partial success looks like — one ID was already in the playlist, the rest went through, exit code 0:
ft playlist add "Focus Beats" <id-1> <id-2> <id-3>
# warning: 1c474c27-e97d-4c35-bb98-e588d06c32f0: Track already in playlist
# Added 2 of 3 track(s)In --json mode the merged response separates the two, so scripts can branch on exactly which IDs landed:
ft playlist add "Focus Beats" <id>... --json | jq '.data.added, .data.errors'Building your own integration instead of using the CLI? Everything on this page is available as plain HTTP endpoints — start at the API reference for playlists and bulk delete.
ft doctor
Runs a health check and dumps the resolved config. The first thing to try when something feels off.
ft doctor
# ✓ ft v0.1.0
# ✓ config: /Users/jane/Library/Application Support/finetuning/config.json
# ✓ base URL: https://pub.finetuning.ai
# ✓ /health → ok
# ✓ key: ft_live_ab3f...**** (jane@example.com, pro tier)ft update
Re-runs the install script for your OS to pull the latest release. Equivalent to running the install one-liner from the overview again.
Configuration
Config file
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/finetuning/config.json |
| Linux | ~/.config/finetuning/config.json |
| Windows | %APPDATA%\finetuning\config.json |
{
"baseUrl": "https://pub.finetuning.ai",
"defaultDuration": 60,
"defaultBpm": 120
}Override the location with FINETUNING_CONFIG_HOME=/some/path or --config <path>.
API key resolution order
When a command needs the key, the CLI checks these sources in order and uses the first hit:
--api-key <key>flagFINETUNING_API_KEYenvironment variable- OS keychain (service
finetuning, accountdefault) - Plaintext fallback at
~/.config/finetuning/credentials(0600) — only used on headless Linux without libsecret
Environment variables
| Variable | Purpose |
|---|---|
FINETUNING_API_KEY | API key to use. Overrides the keychain. |
FINETUNING_CONFIG_HOME | Custom config directory. |
NO_COLOR | Set to any value to disable ANSI color (same as --no-color). |
JSON mode and piping
Every read command supports --json. Spinners, prompts, and progress go to stderr; data goes to stdout — so this always works:
ft list --json | jq '.data.generations[] | select(.status == "completed") | .id'ft generate --json is the one exception worth knowing: it emits the final API record but does not download the MP3. That's the right shape for scripting; if you want both, run the download separately.
Exit codes
| Code | Meaning |
|---|---|
0 | Success |
1 | API error — read the stderr message; the CLI surfaces the server's message field verbatim |
2 | Validation error — bad flag, malformed argument, bad input |
3 | Network or transport error reaching the API |
Rate limits
The CLI shares limits with the public API:
POST /v1/generations(i.e.ft generate): 10 / minute / user- Every other read: 60 / minute / user
When polling completes a generation, the CLI honors Retry-After automatically. For back-to-back ft generate calls, you may need to slow down.
Common errors
| HTTP | Code | What to do |
|---|---|---|
401 | MISSING_API_KEY / INVALID_API_KEY | Run ft auth login — your stored key may have been revoked |
401 | ACCOUNT_DELETED | Contact support |
402 | MONTHLY_LIMIT_REACHED | Out of credits. Check ft me, upgrade or buy a pack |
403 | PRO_PLAN_REQUIRED | Free/Plus account — upgrade at finetuning.ai |
404 | NOT_FOUND | Bad generation ID |
429 | GENERATION_RATE_LIMITED | 10 creates/min cap — wait Retry-After |
429 | QUEUE_FULL | Too many concurrent in-flight generations. ft list --status processing to see them |
500 | GENERATION_FAILED | Usually transient. Retry once; if it persists, see errors |
See the full errors guide for the API-level reference.