Finetuning.aiFinetuning.ai

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

CommandWhat it does
ft auth loginPrompt for an API key and store it in the OS keychain
ft auth logoutForget the stored key
ft auth whoamiShow the signed-in account
ft meAlias of ft auth whoami
ft generate <tags>Submit, poll, and download in one shot
ft listShow 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 playlistsList 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 doctorHealth check + resolved config dump
ft updateRe-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 form

ft 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:

FlagPurpose
--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, --verboseLog HTTP requests and responses to stderr. Keys are redacted to ft_live_ab3f...****.
--no-colorDisable 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

FlagDefaultNotes
--duration <sec>605–210 for Pro/Lifetime. Server clamps anything out of range; the CLI prints note: duration clamped from X → Y on stderr.
--bpm <n>12060–200. Also clamped.
--key <K>CC, C#, D, D#, E, F, F#, G, G#, A, A#, B
--scale <s>majormajor or minor
--time-sig <s>42, 3, 4, 5, 6, 7
--language <l>enen, ja, de, fr, es, zh, ko, pt, it, ru
--lyrics "<text>"empty0–2000 chars
--seed <n>randomPass to reproduce a previous run
-o, --output <path><slug>-<short-id>.mp3 in cwdWhere to save the MP3
--no-waitoffReturn immediately after the 202; CLI prints the id and exits
--jsonoffEmit 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>` later

If 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]
FlagDefaultNotes
--limit <n>20Max 99
--offset <n>0Pagination offset
--status <s>allpending, processing, completed, failed
--jsonoffRaw 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 record

id 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>.mp3 in 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]
FlagDefaultNotes
-y, --yesoffSkip the confirmation prompt. Required in scripts / non-interactive shells.
--jsonoffEmit 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 ago

With --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

OSPath
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:

  1. --api-key <key> flag
  2. FINETUNING_API_KEY environment variable
  3. OS keychain (service finetuning, account default)
  4. Plaintext fallback at ~/.config/finetuning/credentials (0600) — only used on headless Linux without libsecret

Environment variables

VariablePurpose
FINETUNING_API_KEYAPI key to use. Overrides the keychain.
FINETUNING_CONFIG_HOMECustom config directory.
NO_COLORSet 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

CodeMeaning
0Success
1API error — read the stderr message; the CLI surfaces the server's message field verbatim
2Validation error — bad flag, malformed argument, bad input
3Network 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

HTTPCodeWhat to do
401MISSING_API_KEY / INVALID_API_KEYRun ft auth login — your stored key may have been revoked
401ACCOUNT_DELETEDContact support
402MONTHLY_LIMIT_REACHEDOut of credits. Check ft me, upgrade or buy a pack
403PRO_PLAN_REQUIREDFree/Plus account — upgrade at finetuning.ai
404NOT_FOUNDBad generation ID
429GENERATION_RATE_LIMITED10 creates/min cap — wait Retry-After
429QUEUE_FULLToo many concurrent in-flight generations. ft list --status processing to see them
500GENERATION_FAILEDUsually transient. Retry once; if it persists, see errors

See the full errors guide for the API-level reference.

On this page