Command-line tool (CLI)
Use the spicyapi command to browse models, check prices, generate images and videos and download the results from a terminal, without writing code.
What the CLI is
spicyapi is a small program you run in a terminal, the window where you type commands instead of clicking buttons. Each command does one job: list the models your account can use, tell you what a generation will cost, start an image or video generation, wait for it to finish and give you a link to download the result.
You do not write any code. You type a command, press Enter and read the answer.
It is a good fit when you want to:
- try models and compare prices before anyone writes an integration;
- generate images or videos from your own computer and save the files;
- repeat the same kind of generation without clicking through a page each time;
- check a model's exact settings before you build something on top of it.
The CLI covers image and video generation. Text and chat models are called through the chat-compatible endpoints described in Text and streaming.
There are three other official tools. Pick the CLI if you want to type commands yourself:
| Tool | Choose it when |
|---|---|
| SDK | You are writing a JavaScript or TypeScript program that calls SpicyAPI |
| MCP server | You want an AI coding assistant to run these same steps for you |
| Agent Skill | You want to teach an AI agent the safe way to use SpicyAPI |
Writing in another language? There are official SDKs for Python, Go, PHP and Java too.
Nothing on this page spends money by accident
Only two commands can cost money: tasks create and tasks retry. Both stop and ask you before anything is charged. Everything else is free to run as often as you like.
What you can do
| I want to… | Command | Costs money? | Needs an API key? |
|---|---|---|---|
| Check that SpicyAPI is reachable | spicyapi status | No | No |
| Search this documentation | spicyapi docs search <words> | No | No |
| See which models I can use, and their prices | spicyapi models list | No | Yes |
| See which settings one model accepts | spicyapi models get <model> | No | Yes |
| Check my balance | spicyapi balance | No | Yes |
| See how much a key has spent | spicyapi usage | No | Yes |
| Get the exact price of a generation without starting it | spicyapi tasks quote | No | Yes |
| Start an image or video generation | spicyapi tasks create | Yes, after asking you | Yes |
| Wait for a generation to finish | spicyapi tasks wait <task-id> | No | Yes |
| Look at one generation and its result links | spicyapi tasks get <task-id> | No | Yes |
| List my recent generations | spicyapi tasks list | No | Yes |
| Run a failed generation again | spicyapi tasks retry <task-id> | Yes, after asking you | Yes |
| Permanently delete a generation's files and prompt | spicyapi tasks purge <task-id> | No, and nothing is refunded | Yes |
| Upload a picture, video or audio file to use as input | spicyapi files upload <path> | No | Yes |
| Get a fresh download link for a result | spicyapi files download-url <task-id> | No | Yes |
| Check that a callback really came from SpicyAPI | spicyapi webhooks verify | No | No, it uses your webhook signing secret |
In the commands above, a word in angle brackets such as <task-id> means "put your own value here", without the brackets.
Before you start
You need four things: a terminal, Node.js, an API key and a way to run the CLI. This takes about ten minutes the first time.
Open a terminal
A terminal is a text window for running commands. Everything on this page is typed or pasted there, then run with Enter.
Press Cmd + Space, type Terminal and press Enter. A window opens with a line ending in % or $: that is where you type. Paste with Cmd + V.
Install Node.js 22.13 or newer
The CLI runs on Node.js, a free program. Check whether you already have it:
node -vIf it prints v22.13.0 or a higher number, such as v22.14.0 or v24.4.1, you are ready. If it prints a lower number, or says command not found or is not recognized:
- Go to nodejs.org, open the download page and pick the LTS version for your system.
- Run the installer and accept the default options.
- Close the terminal and open a new one, then run
node -vagain.
Node.js comes with npm and npx, the two helpers used in the last step.
Create an API key
The API key is how SpicyAPI knows the request comes from your account.
- Create an account at spicyapi.ai/register. Accounts may be opened in batches; if sign-up is not open for you yet, join the waitlist.
- Add funds under Billing in the console. Generations are paid from your balance, in US dollars.
- Open API keys and click Create key. Give it a name you will recognize later, such as
my laptop. Only the name is required. - Copy the key immediately. It starts with
sk-spicy-and is shown only once. You can also use Save as a file.
New keys have no spending limit unless you set one. A daily limit means a mistake or a leaked key cannot run up a large bill, and you can change it later on the same page.
Treat the key like a password
Anyone who has your key can spend your balance. Do not send it in chat or email, do not show it in a screenshot or a support ticket, and never save it in a project folder that goes into Git or GitHub. If it leaks, revoke it on the API keys page and create a new one.
Store the key in SPICY_API_KEY
The CLI has no login command and never takes the key as a command option. It reads an environment variable called SPICY_API_KEY: a named value that your terminal passes to the programs it runs.
For the current window only (it is forgotten when you close the window):
export SPICY_API_KEY="sk-spicy-paste-your-key-here"Permanently, add that same export line to the end of your shell's startup file, which runs every time you open a terminal. On macOS the file is ~/.zshrc; on most Linux systems it is ~/.bashrc.
nano ~/.zshrcPaste the export line at the bottom, press Ctrl + O then Enter to save, and Ctrl + X to exit. On Linux, use nano ~/.bashrc instead. Open a new terminal window for the change to take effect.
Check that it is set, without printing the whole key:
echo "${SPICY_API_KEY:0:9}"It should print sk-spicy-.
Where the key may and may not live
Your shell startup file and your Windows account settings stay on this computer, which is fine for personal use. Never put the key in a file inside a project, never commit it, and never paste it into a shared document. Terminals also remember the commands you type, so on a shared computer prefer the startup file or the Windows settings dialog over typing the key into a command.
Choose how to run the CLI
There are two ways, and the commands are the same.
npx downloads the CLI when needed and runs it:
npx @spicyapi/cli statusThe first time, npm asks Need to install the following packages: @spicyapi/cli@… Ok to proceed? (y). Press y and Enter.
With this method, write npx @spicyapi/cli wherever this page writes spicyapi. For example, spicyapi models list becomes npx @spicyapi/cli models list.
Your first result
This walkthrough generates one image and downloads it. Each step shows the command, roughly what you will see, and what just happened. Your numbers, IDs and times will differ.
Replace MODEL_ID_FROM_CATALOG with a real model ID
MODEL_ID_FROM_CATALOG is a placeholder, not a model. Step 3 shows how to pick a real ID from the live catalog (spicyapi models list, the same list as GET /api/v1/models). Paste your ID wherever this page shows the placeholder. Sending the placeholder itself only returns a "not found" error.
Check the connection
spicyapi statusSpicyAPI is reachable — api.spicyapi.ai answered in 523 ms.What happened: the CLI asked SpicyAPI whether it is up. This needs no key and costs nothing. If you see "could not be reached", check your internet connection or proxy; see Troubleshooting.
Check your balance
spicyapi balanceavailable: 12.34
held: 0.06
total: 12.40What happened: this is the first command that uses your key. Amounts are US dollars. available is what you can spend now, held is set aside for generations that are still running, and total is the two added together. You may also see a funding line that breaks the balance down into prepaid funds and promotional grants.
If you see Error: SPICY_API_KEY is required for authenticated API operations, the key is not set in this terminal window. The Next: line below it shows how to set it; see also Before you start.
Pick a model
spicyapi models list --modality image --json{
"total": 1,
"items": [
{
"model": "MODEL_ID_FROM_CATALOG",
"displayName": "Example image model",
"modality": "image",
"tasks": ["text-to-image"],
"enabled": true,
"available": true,
"pricing": [
{ "variant": "", "unit": "per_image", "price": "0.03", "currency": "USD" }
]
}
]
}This example is shortened: the real list has more models and more fields.
What happened: the CLI fetched the models your account can call today, with your prices. Choose one whose enabled and available are both true, and copy its model value, for example by selecting it with the mouse. That value is the model ID. IDs read like maker/model/task.
priceis in US dollars perunit:per_image,per_secondof video,per_requestorper_1k_tokens.- Several
pricingentries mean the price depends on your settings, such as resolution. - To find a model by name, add
--searchand part of its name.
--json prints the list as readable, indented JSON. Without it, the whole list is squeezed onto one long line.
See which settings that model accepts
spicyapi models get MODEL_ID_FROM_CATALOG --json{
"model": "MODEL_ID_FROM_CATALOG",
"enabled": true,
"available": true,
"inputSchema": {
"type": "object",
"required": ["prompt"],
"properties": {
"prompt": { "type": "string" },
"aspect_ratio": { "type": "string", "enum": ["1:1", "16:9", "9:16"] }
},
"additionalProperties": false
}
}What happened: inputSchema is the model's list of settings, which the rest of this page calls the input.
requiredlists the settings you must send. Here, onlyprompt.propertieslists every setting the model accepts, and nothing else is allowed: one unknown or misspelled name gets the whole request rejected.enumlists the only values allowed for that setting.
Every model has its own settings, so always read them for the model you chose. For ready-made inputs that the server has already checked, run spicyapi models list --search <name> --include-examples --json and look at examples.
Write your input to a file
Put the input in a file called input.json, in the folder your terminal is in. A file avoids quoting problems, especially on Windows.
Paste all three lines at once:
cat > input.json <<'EOF'
{"prompt": "A paper lantern glowing on a rainy street at night, cinematic light"}
EOFWhat happened: you described what to generate, using the setting names from the previous step. The quotes must be plain straight quotes ("), not curly ones.
Check the price
spicyapi tasks quote --model MODEL_ID_FROM_CATALOG --input-file input.jsonquoteId: eyJ2IjoxLCJtIjoi…
model: MODEL_ID_FROM_CATALOG
estimatedCost: 0.03
maxCharge: 0.03
currency: USD
quantity: 1
unit: per_image
expiresAt: 2026-09-17T08:05:00ZWhat happened: SpicyAPI priced this exact request. Nothing was started and nothing was charged. estimatedCost is what it is expected to cost, and maxCharge is the most it can cost. This step is optional: the next command fetches the price again and shows it to you before it spends anything.
Start the generation and wait for it
spicyapi tasks create --model MODEL_ID_FROM_CATALOG --input-file input.json --waitThe CLI first shows the price and asks you to confirm:
Estimated charge: USD 0.03; maximum charge: USD 0.03. Quote expires: 2026-09-17T08:05:00Z.
Idempotency-Key: 4f6755a5-0d72-4420-9f6b-9faf0e2b0241
Create a billable task with model MODEL_ID_FROM_CATALOG? Estimated charge: USD 0.03; maximum charge: USD 0.03. Quote expires: 2026-09-17T08:05:00Z. Idempotency-Key: 4f6755a5-0d72-4420-9f6b-9faf0e2b0241. Continue? [y/N]Type y and press Enter. Anything else cancels, and nothing is charged. Answer within five minutes, before the quote expires.
As soon as SpicyAPI accepts the job, the CLI prints its task ID:
Accepted: job_01k3m8x9q2z4v7n5p6r8s0t1w3
If waiting is interrupted: spicyapi tasks get job_01k3m8x9q2z4v7n5p6r8s0t1w3Images usually take seconds and videos take minutes. When it finishes, the result appears:
idempotencyKey: 4f6755a5-0d72-4420-9f6b-9faf0e2b0241
accepted: {"taskId":"job_01k3m8x9q2z4v7n5p6r8s0t1w3","state":"queued","estimatedCost":"0.03","deadlineAt":"2026-09-17T08:30:00Z"}
task: {"taskId":"job_01k3m8x9q2z4v7n5p6r8s0t1w3","model":"MODEL_ID_FROM_CATALOG","state":"succeeded","output":{"assets":[{"key":"tasks/…/result.png","url":"https://…","expiresAt":"2026-09-17T08:20:00Z","mime":"image/png","width":1024,"height":1024}]},"cost":"0.03","settled":true,…}What happened: after your y, SpicyAPI accepted the generation, which from then on is called a task, and set the estimated amount aside from your balance. The CLI then checked on the task every few seconds until it finished. In the last line, look for "state":"succeeded" and the "url" inside output. cost is the final charge once settled is true. Add --json to the command if you prefer the result spread over several readable lines.
Closing the window does not cancel the task
Once accepted, a task always runs to the end and is charged normally. Pressing Ctrl + C or closing the terminal only stops the waiting. Keep the task ID and pick it up again with spicyapi tasks wait <task-id>; see Pick up a task after the wait was interrupted.
Download the result
The url is a direct download link that works for about 20 minutes and needs no key. Paste it into a browser and save the file, or download it from the terminal (keep the quotes, because the link contains & characters):
curl -L -o result.png "PASTE_THE_URL_HERE"Use the file extension that matches mime: .png for image/png, .jpg for image/jpeg, .mp4 for video/mp4.
If the link has expired, get a new one with spicyapi files download-url <task-id>, or run spicyapi tasks get <task-id>, which returns fresh links.
What happened: you copied the result to your own computer. Do this soon: SpicyAPI keeps generated files for about 14 days at most (less if your account is set shorter), then deletes them for good. See Retention and destruction.
How to read the output
- Normal output shows one
name: valueper line. Values that contain several parts, such asoutputorretention, are printed as JSON on a single line. --jsonprints the complete answer as indented JSON. It works with every command, before or after the command name:spicyapi --json balanceandspicyapi balance --jsonare the same.- Results go to the screen's normal output; messages go to the error output. Prices, the idempotency key, the
Accepted:lines and errors are messages. Sospicyapi --json tasks get <task-id> > task.jsonsaves only the result intask.json, while the messages still appear on screen. - Errors look like
Error: <what went wrong> (request req_…), usually followed by aNext:line that says what to do. With--json, an error is a one-line JSON object witherror,type,status,codeandrequestId, and noNext:line. When no key is set,typeisMissingApiKeyError. Quote thereq_…value if you contact support. - Money is a US dollar amount written as plain text without a
$sign, such as0.03. - Times are in UTC. The
Zat the end of2026-09-17T08:05:00Zmeans UTC. - Exit status is
0when a command succeeds and not0when it fails, which scripts can check.
Command reference
Every command also has built-in help: add --help, for example spicyapi tasks create --help.
Global options and environment variables
These options work with every command.
| Option | What it does | Default | Required |
|---|---|---|---|
--json | Print the complete result as JSON | Off | No |
--timeout-ms <milliseconds> | How long to wait for one answer from SpicyAPI before giving up, from 1 to 120000 | 30000 (30 seconds) | No |
--max-retries <count> | How many times to try again automatically after a network error, 429 or server error, for requests that are safe to repeat, from 0 to 5 | 2 | No |
--api-base-url <url> | Send API requests to a different address. You normally never need this | https://api.spicyapi.ai/api/v1 | No |
--service-base-url <url> | The address status checks. You normally never need this | https://api.spicyapi.ai | No |
-V, --version | Print the CLI version | — | No |
-h, --help | Show help for the command | — | No |
| Environment variable | What it is for |
|---|---|
SPICY_API_KEY | Your API key. Needed by every command except status, docs search and webhooks verify |
SPICY_WEBHOOK_SECRET | Your webhook signing secret, read by webhooks verify |
SPICY_API_BASE_URL, SPICY_SERVICE_BASE_URL | Same as the two address options above. Addresses must use https:// |
status
What it does: checks whether SpicyAPI is up and reachable from your computer.
spicyapi statusNo options. No key needed. Free.
Output: one sentence. SpicyAPI is reachable — … answered in … ms. means everything is fine. could not be reached means your computer got no answer: check your network or proxy. is not healthy or reports it is not ready means a problem on our side; current incidents are listed at status.spicyapi.ai.
With --json you see the raw checks. operational: true is the answer that matters; a readiness entry with status 404 is normal on the public address.
docs search
What it does: finds the documentation page for a topic. It searches a small index built into the CLI, so it works without a key and without calling SpicyAPI. Each result links to the page on this documentation site, docs.spicyapi.ai.
spicyapi docs search [words] [--limit <count>]| Argument or option | What it does | Default | Required |
|---|---|---|---|
words | Words to look for. Leave it out to list pages | Empty | No |
--limit <count> | Maximum number of results, from 1 to 25 | 10 | No |
spicyapi docs search retentionslug: retention
title: Retention and destruction
summary: What the platform caps are, how far down you can dial them, how to destroy a task's content, and what we cannot promise.
keywords: ["privacy","delete","storage","retention","per-request retention","purge","destroy"]
url: https://docs.spicyapi.ai/docs/retentionWhen there are several results, they are printed one after another, and each starts with slug:. If nothing matches, it says so and still exits with status 0:
No documentation matched "refnd". Try fewer or different words, or run `spicyapi docs search` without words to browse pages.With --json, no match prints an empty list, []. Free.
models list
What it does: lists the models your account can call today, with the prices your account pays.
spicyapi models list [options]| Option | What it does | Default | Required |
|---|---|---|---|
--modality <type> | Only image, video or text models. The option also accepts audio, but no audio models are offered today | All | No |
--task <task> | Only one kind of job, such as text-to-image, edit, image-to-video, text-to-video or reference-to-video | All | No |
--provider <name> | Only models from one model maker, the company or lab that built the model. Must match the provider value exactly | All | No |
--search <text> | Only models whose ID or display name contains this text, ignoring upper and lower case | None | No |
--include-schema | Add every model's full settings (inputSchema). Makes the output much larger | Off | No |
--include-examples | Add ready-made example inputs that the server has already checked (examples) | Off | No |
spicyapi models list --modality video --task image-to-video --jsonOutput: total and items. Use --json, because without it all items are printed on one line. The most useful fields of each item:
| Field | Meaning |
|---|---|
model | The model ID to use in other commands |
displayName, provider, modality, tasks | Name, model maker, type of output and kind of job |
enabled, available | Use models where both are true |
pricing | One entry per price level: variant (for example a resolution), unit, price in USD, plus regularPrice and offerLabel when a discount applies |
taskTimeoutSeconds | How long a generation may run before it ends as expired |
maxOutputDurationSeconds | The longest video length this model allows, when it has one |
Free. The model list counts toward your account's request rate, so there is no need to run it before every generation.
models get
What it does: shows one model in full, including the settings it accepts.
spicyapi models get <model>| Argument | What it does | Default | Required |
|---|---|---|---|
model | The exact model ID. The slashes in it are fine as they are | — | Yes |
spicyapi models get MODEL_ID_FROM_CATALOG --jsonOutput: the same fields as models list, plus inputSchema. In inputSchema, read required (settings you must send), properties (every allowed setting and its type), enum (the only allowed values), minimum and maximum (number limits) and default (used when you leave the setting out). Entries named x-ui are hints for building forms; you can ignore them. Free.
balance
What it does: shows how much money is available on your account.
spicyapi balanceNo options. Free.
Output: available (what you can spend now), held (set aside for tasks that are still running) and total, all in US dollars. It may also include funding, which splits the balance into prepaid funds and promotional grants, including when grants expire and which models they pay for. You do not need to check the balance before every generation: tasks create checks it for you.
usage
What it does: shows how many generations the current API key ran and how much they cost, grouped by day and by model.
spicyapi usage [--from <date>] [--to <date>]| Option | What it does | Default | Required |
|---|---|---|---|
--from <date> | First day to include, as YYYY-MM-DD in UTC | The last seven days, including today | No |
--to <date> | The day after the last day to include, as YYYY-MM-DD in UTC. The range can be at most 92 days | The last seven days, including today | No |
spicyapi usage --from 2026-09-01 --to 2026-10-01This covers all of September: --to is the first day that is not included. Leave out both options to see the last seven days, including today.
Output: from, to, currency, totalCalls, totalSpend, then days (per day: calls, succeeded, failed, spend) and models (the same numbers per model). totalSpend only counts final charges. days and models describe the same tasks in two ways, so do not add them together. For your whole account rather than one key, use the Usage page in the console.
Free. It is limited to 30 calls a minute, so use it to check totals, not in a loop.
tasks quote
What it does: tells you exactly what one generation would cost, without starting it and without setting any money aside.
spicyapi tasks quote --model <model> --input-file <path>| Option | What it does | Default | Required |
|---|---|---|---|
--model <model> | The exact model ID | — | Yes |
--input-file <path> | Read the input from a JSON file. Use - to read it from what is piped into the command | — | Use this or --input-json |
--input-json <json> | Give the input directly as JSON text. Quoting is easier with --input-file | — | Use this or --input-file |
--callback-url <url> | The callback address you plan to use, since it is part of the priced request | None | No |
Output: quoteId, model, estimatedCost, maxCharge, currency, quantity (for example the number of seconds being charged), unit and expiresAt. A quote is valid for five minutes.
Free, no confirmation. Use it to compare models or settings. You do not have to run it before tasks create, which gets its own quote.
tasks create
What it does: starts an image or video generation. This costs money. The CLI always shows the price and asks first.
spicyapi tasks create --model <model> --input-file <path> [options]| Option | What it does | Default | Required |
|---|---|---|---|
--model <model> | The exact model ID | — | Yes |
--input-file <path> | Read the input from a JSON file. - reads it from what is piped into the command, which also requires --yes | — | Use this or --input-json |
--input-json <json> | Give the input directly as JSON text | — | Use this or --input-file |
--wait | Stay until the task finishes, then print the result | Off: print the task ID and return | No |
--yes | Skip the confirmation question. Only use it once you know the price, typically in scripts | Off: ask | No |
--idempotency-key <key> | A name for "this one generation". Running the command again with the same key within 24 hours gives you the same task instead of a new one. Up to 128 characters | A new random key each run | No |
--retention <duration> | Delete this task's generated media, result payload, prompt and other input text sooner than usual: 30m, 1h, 7d or a number of seconds. It can only shorten. 0 deletes them once the task finishes. Billing records are always kept | Your account setting | No |
--callback-url <url> | A public address starting with https:// that SpicyAPI calls when the task finishes. Plain http://, localhost and private network addresses are refused. Only useful if you run a server; see Webhooks | None | No |
--poll-interval-ms <milliseconds> | With --wait: check every this many milliseconds, from 100 to 60000 | Starts at 2 seconds and slows down to 10 seconds | No |
--wait-timeout-ms <milliseconds> | With --wait: stop waiting after this long, from 1 to 86400000. The task keeps running | 600000 (10 minutes) | No |
What happens, in order:
- The CLI reads your input and checks that it is a JSON object. If you gave no input, it stops here.
- If nobody can answer a question, for example in a script, and you did not pass
--yes, it stops here. - It gets a quote and prints the estimated charge, the maximum charge, when the quote expires and the idempotency key.
- It asks
Continue? [y/N]. Onlyyoryescontinues. The default is no. - It creates the task at the quoted price. The estimated amount is set aside from your balance, and that amount is also the most the task can cost.
- Without
--wait, it printsidempotencyKey,taskId,state(queued),estimatedCostanddeadlineAt, then returns. With--wait, it printsAccepted: <task-id>and a recovery command straight away, waits, then printsidempotencyKey,acceptedand the finishedtask.
deadlineAt is the latest time the task can finish before it ends as expired. It is not when your download link or files expire.
spicyapi tasks create --model MODEL_ID_FROM_CATALOG --input-file input.json --waitOn macOS and Linux you can also give the input inline:
spicyapi tasks create --model MODEL_ID_FROM_CATALOG --input-json '{"prompt": "A paper lantern on a rainy street"}'If waiting fails, the task is still running
A wait can end early because of the time limit, a network problem or Ctrl + C. The task itself carries on and is charged normally. The Accepted: line printed at the start has its ID; continue with spicyapi tasks wait <task-id>. Do not simply run the same create command again without --idempotency-key: that starts, and charges for, a second generation.
When the wait fails with an error (rather than Ctrl + C), the last lines repeat everything you need, so you do not have to scroll back for the Accepted: line:
Error: task job_01k3m8x9q2z4v7n5p6r8s0t1w3 did not reach a terminal state within 600000ms
Task job_01k3m8x9q2z4v7n5p6r8s0t1w3 was accepted and may still be running; only the wait stopped.
Recover: spicyapi tasks get job_01k3m8x9q2z4v7n5p6r8s0t1w3
To replay the same request without creating a second task, reuse --idempotency-key 4f6755a5-0d72-4420-9f6b-9faf0e2b0241.If the error was one with a known fix, such as 429 or a server error, a Next: line follows. With --json, the same facts are in the error object as taskId, idempotencyKey and recover.
tasks wait
What it does: waits for a task you already started, until it finishes.
spicyapi tasks wait <task-id> [options]| Argument or option | What it does | Default | Required |
|---|---|---|---|
task-id | The task ID, such as job_01k3m8x9q2z4v7n5p6r8s0t1w3 | — | Yes |
--poll-interval-ms <milliseconds> | Check every this many milliseconds, from 100 to 60000 | Starts at 2 seconds and slows down to 10 seconds | No |
--wait-timeout-ms <milliseconds> | Stop waiting after this long, from 1 to 86400000 | 600000 (10 minutes) | No |
spicyapi tasks wait job_01k3m8x9q2z4v7n5p6r8s0t1w3 --wait-timeout-ms 1800000Output: the finished task, with the same fields as tasks get, once its state is succeeded, failed or expired. If the task succeeded but its files are still being copied, it keeps waiting a little longer.
If the time limit runs out, you see Error: task … did not reach a terminal state within 600000ms. That is not a failure: the task is still running. Wait again, with a longer --wait-timeout-ms if needed. Stopping a wait never cancels a task. Free.
tasks get
What it does: shows one task right now: its state, cost, result links and when its content will be deleted.
spicyapi tasks get <task-id>| Argument | What it does | Default | Required |
|---|---|---|---|
task-id | The task ID | — | Yes |
Use the same API key that created the task: another key gets "not found".
Output fields:
| Field | Meaning |
|---|---|
state | queued (waiting to start), running, succeeded, failed or expired (ran out of time). Older tasks can also show canceled |
output.assets | The results. Each has a url (a download link valid for about 20 minutes), expiresAt (when that link stops working), mime (file type), size details and a key. pending: true means the file is still being copied, so try again in a few seconds. unavailable: true means it cannot be recovered |
errorCode, errorMessage | Why the task failed. See Errors |
cost, settled | The charge in USD. It is final once settled is true |
input | The settings you sent. Removed when your prompt retention runs out |
createdAt, completedAt, deadlineAt | When it was created, when it finished, and the latest time it could finish |
contentState | present (content still stored), expired (removed by retention rules) or purged (you deleted it) |
retention | outputsExpireAt (when result files are deleted), promptsExpireAt (when your prompt text is erased) and source (which setting decided) |
sourceTaskId | Present when this task is a retry of another task |
Normal output ends with a plain-language summary, such as Outputs expire: … or Content: Destroyed — you removed the outputs …. Each call returns fresh download links. Free.
tasks list
What it does: lists tasks created with the current API key, a page at a time.
spicyapi tasks list [options]| Option | What it does | Default | Required |
|---|---|---|---|
--from <date> | First day to include, YYYY-MM-DD in UTC | A seven-day window ending today | No |
--to <date> | The day after the last day to include, YYYY-MM-DD in UTC. The range can be at most 92 days | See above | No |
--state <state> | Only tasks in one state: queued, running, succeeded, failed, canceled or expired | All | No |
--model <model> | Only tasks for one exact model ID | All | No |
--limit <count> | Tasks per page, from 1 to 100 | 20 | No |
--cursor <cursor> | Continue from the previous page, using its nextCursor value | First page | No |
spicyapi --json tasks list --from 2026-09-01 --to 2026-09-08 --state failedOutput: items (each with taskId, model, state, cost, settled, createdAt, deadlineAt and completedAt), hasMore and nextCursor. If hasMore is true, run the same command again with the same --from and --to, adding --cursor and the nextCursor value. The list has no download links; use tasks get for the task you want. Free.
tasks retry
What it does: runs a failed or expired task again, as a new task with a new ID. This costs money.
spicyapi tasks retry <task-id> [options]| Argument or option | What it does | Default | Required |
|---|---|---|---|
task-id | The failed or expired task to run again | — | Yes |
--wait | Stay until the new task finishes | Off | No |
--yes | Skip the confirmation question | Off: ask | No |
--idempotency-key <key> | Running the retry again with the same key gives you the same new task instead of another one | A new random key each run | No |
--poll-interval-ms <milliseconds> | With --wait: how often to check | Starts at 2 seconds and slows down to 10 seconds | No |
--wait-timeout-ms <milliseconds> | With --wait: stop waiting after this long | 600000 (10 minutes) | No |
It asks Retry source task … Continue? [y/N]. Unlike tasks create, it does not show a price before asking. The new task is charged at the model's current price, so check it first with spicyapi models get <model> if the price matters.
Output: idempotencyKey, the new taskId, sourceTaskId (the original), state, estimatedCost (the amount set aside) and deadlineAt.
- Only
failedandexpiredtasks can be retried, plus oldercanceledones. For another version of a successful result, usetasks create. - The original input is reused unchanged, so a retry only helps when the failure was temporary. Check
errorCodefirst; see Retry a failed task and Errors. - If the input pointed at an uploaded file that has since expired, the retry fails; upload the file again and use
tasks create. - The retry keeps the original task's retention setting.
- If
tasks createlost its connection before you saw a task ID, that is not a failed task. Use the recovery steps in Pick up a task after the wait was interrupted instead.
tasks purge
What it does: permanently deletes a finished task's generated media, result payload (the stored result data), prompt and other input text, right away instead of waiting for them to expire.
spicyapi tasks purge <task-id> [--yes]| Argument or option | What it does | Default | Required |
|---|---|---|---|
task-id | A finished task: succeeded, failed, expired or canceled | — | Yes |
--yes | Skip the confirmation question | Off: ask | No |
A task that is still queued or running cannot be purged. It cannot be cancelled either — there is no cancellation command or API — so wait for it to finish with spicyapi tasks wait <task-id>, then purge it.
It explains what will be deleted and asks you to confirm, because it cannot be undone. Download anything you want to keep first.
Output: taskId, contentState (purged), purgedAt, contentRemovedBy (user), billingRetained (always true) and mediaDeletionPending, followed by a line naming what was destroyed and saying the stored media is removed within about a minute. If contentState is still present, nothing was removed this time; try again later.
Free, and not a refund: the ledger entry, charged amount, model, state, timestamps and request ID stay on record. See Retention and destruction.
files upload
What it does: uploads a file from your computer, such as the starting picture for an image-to-video generation, and gives you a spicy:// address to put in your input.
spicyapi files upload <path> [--content-type <type>]| Argument or option | What it does | Default | Required |
|---|---|---|---|
path | The file to upload | — | Yes |
--content-type <type> | The file type, when it cannot be told from the file name: image/jpeg, image/png, image/webp, image/gif, video/mp4, video/webm, audio/mpeg or audio/wav | Detected from the extension | No |
Supported files are .jpg, .jpeg, .png, .webp and .gif images up to 10 MiB, .mp4 and .webm videos, and .mp3 and .wav audio up to 90 MiB and 600 seconds. A model can set lower limits of its own.
spicyapi files upload ./start-frame.jpgfileId: fil_01k3m8x9q2z4v7n5p6r8s0t1w3
status: ready
bytes: 402118
contentType: image/jpeg
sha256: a91f4c2e5b7d16809e3f2a4c8d0b61e7c4a890e5d2f71b3c609ae8427d15f0c6
uri: spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3
expiresAt: 2026-09-18T09:12:11Z
width: 1280
height: 720Output: copy the uri value exactly as printed into the model's file setting, often image_url. It can be used until expiresAt, normally one day. After that, upload the file again. Uploading is free.
files download-url
What it does: creates a new download link for a finished task's result. It prints the link; it does not save the file.
spicyapi files download-url <task-id> [--key <key>]| Argument or option | What it does | Default | Required |
|---|---|---|---|
task-id | A task that succeeded | — | Yes |
--key <key> | Which result to link to, when a task produced several. Use the key value from output.assets in tasks get | The first result | No |
Output: key, url and expiresAt. The link works for about 20 minutes; open it in a browser or download it with curl. A task that has not succeeded yet answers "not found". If the file is still being copied you get a conflict error; wait a few seconds and try again. Once the task's files have been deleted, no new link can be made. Free.
webhooks verify
What it does: if you run a server that receives SpicyAPI callbacks (see --callback-url), this checks that one saved callback is genuine and unmodified. It runs entirely on your computer and needs no API key.
spicyapi webhooks verify --body-file <path> --timestamp <seconds> --signature <base64> --payload-version <version>| Option | What it does | Default | Required |
|---|---|---|---|
--body-file <path> | A file with the callback body exactly as received, byte for byte | — | Yes |
--timestamp <seconds> | The X-Webhook-Timestamp header value | — | Yes |
--signature <base64> | The X-Webhook-Signature header value | — | Yes |
--payload-version <version> | The X-Webhook-Payload-Version header value: 1 or 2 | — | Yes |
--secret-env <name> | The name of the environment variable that holds your webhook signing secret. The secret itself is never typed on the command line | SPICY_WEBHOOK_SECRET | No |
--tolerance-seconds <seconds> | How far the timestamp may be from your computer's clock | 300 | No |
Output: on success, payload (the callback content), payloadVersion, taskId, deliveryId (version 2) and timestamp. If anything does not match, it prints an error and exits with a non-zero status. Free.
A signing secret is created automatically when you register, but its value is never shown. To get one you can copy, open Webhooks in the console and click Generate (or Rotate if one is configured). The new secret is shown only once. Rotating takes effect immediately, with no overlap: the old secret stops signing at once. See Webhooks.
Common recipes
Generate an image
spicyapi models list --modality image --json
spicyapi models get MODEL_ID_FROM_CATALOG --json
spicyapi tasks create --model MODEL_ID_FROM_CATALOG --input-file input.json --waitPick an ID from the first command, build input.json from the settings shown by the second, then run the third and confirm with y. The walkthrough in Your first result explains each step.
Turn a picture into a video
Find an image-to-video model and its settings
spicyapi models list --modality video --task image-to-video --json
spicyapi models get MODEL_ID_FROM_CATALOG --jsonIn inputSchema, find the setting for the starting picture (usually image_url), the length setting (usually duration_seconds) and anything in required.
Upload your picture
spicyapi files upload ./start-frame.jpgCopy the uri value, which starts with spicy://.
Write the input
Create input.json as in Your first result, using the setting names from the model and your own uri:
{
"prompt": "Slow push-in, rain running down the window",
"image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
"duration_seconds": 5
}Check the price, then generate
spicyapi tasks quote --model MODEL_ID_FROM_CATALOG --input-file input.json
spicyapi tasks create --model MODEL_ID_FROM_CATALOG --input-file input.json --wait --wait-timeout-ms 1800000Videos are usually charged per second, so the length you choose changes the price. Videos can take several minutes, so this example waits up to 30 minutes instead of the default 10. Download the .mp4 from the url as in Your first result.
The uploaded picture can be used for one day. After that, upload it again.
Pick up a task after the wait was interrupted
A task keeps running even if the terminal closes, the network drops, your computer sleeps or the wait times out. How you get back to it depends on what you saw:
-
You saw
Accepted: job_…, or aRecover:line under the error. You have the task ID. Wait again, or just look at it:spicyapi tasks wait job_01k3m8x9q2z4v7n5p6r8s0t1w3 --wait-timeout-ms 1800000 spicyapi tasks get job_01k3m8x9q2z4v7n5p6r8s0t1w3 -
You only saw the
Idempotency-Key:line, for example because the connection dropped while the task was being created. Run the same command again, with the same model and the same input, and add that key:spicyapi tasks create --model MODEL_ID_FROM_CATALOG --input-file input.json --idempotency-key 4f6755a5-0d72-4420-9f6b-9faf0e2b0241 --waitThe CLI shows the price and asks again, but if the first attempt was accepted, SpicyAPI hands back that same task instead of starting and charging for a second one. This works for 24 hours with the same API key. If the first attempt never arrived, the task is created now, once.
-
You have neither. List your recent tasks and find it by time and model:
spicyapi --json tasks list --limit 10
Retry a failed task
First find out why it failed:
spicyapi tasks get job_01k3m8x9q2z4v7n5p6r8s0t1w3Then read errorCode. Failed or expired tasks normally release the full hold. Some content refusals are charged at the accepted quote only when the model page explicitly discloses this exception. Check settled and cost for the final charge. tasks retry sends the same input again, so it only helps when the cause was temporary:
errorCode | What to do |
|---|---|
rate_limited, upstream_unavailable, timeout, upstream_failed | Wait a little, then retry once. If it fails again, stop retrying: for timeout, try a shorter duration or a lower resolution with tasks create |
generation_failed | Do not retry. Change the prompt or the image, then use tasks create: the same input usually fails the same way |
invalid_asset | Do not retry, because a retry reuses the old file. Upload the file again, then use tasks create with the new uri |
invalid_request, unsupported_combination | Do not retry unchanged. Change the settings, then use tasks create |
content_rejected | Do not retry unchanged. The model declined this request: change the prompt or choose another model, then use tasks create |
The full list is in Errors.
When a retry makes sense, run it. It creates, and charges for, a new task:
spicyapi tasks retry job_01k3m8x9q2z4v7n5p6r8s0t1w3 --waitExport your usage
Save a month of usage for the current key to a file:
spicyapi --json usage --from 2026-09-01 --to 2026-10-01 > usage-2026-09.jsonThe file holds daily and per-model totals. For a task-by-task list, save spicyapi --json tasks list --from 2026-09-01 --to 2026-09-08 --limit 100 in the same way, following nextCursor while hasMore is true. Account-wide figures and invoices are in the console.
Run the CLI from a script
In a script nobody can answer Continue? [y/N], so billable commands refuse to run unless you add --yes. When you do:
- quote first and check the price in the script;
- give every generation its own fixed
--idempotency-key, such asposter-2026-09-17-v1, so that running the script twice does not charge twice; - add
--jsonand readstatefrom the result: a command that exits with status0has not necessarily produced a successful task; - set spending limits on the key in the console.
spicyapi --json tasks create --model MODEL_ID_FROM_CATALOG --input-file input.json --idempotency-key poster-2026-09-17-v1 --yes --wait > result.jsonCosts, confirmation and safety
Which commands cost money. Only tasks create and tasks retry. Prices are in US dollars. Quoting, listing, checking, uploading, downloading and deleting are free.
Seeing the price first. tasks quote prices a request without starting anything. tasks create gets its own quote and shows Estimated charge and maximum charge before it asks. When the task is accepted, the estimated amount is set aside (held) and is also the most you can be charged. When the task succeeds you pay for what was actually produced, never more than that amount, and anything left over goes back to your balance.
The confirmation. Continue? [y/N] means the answer is no unless you type y. --yes answers for you, so only use it once you know the price. If the CLI runs somewhere that cannot ask, such as a script or with piped input, and there is no --yes, it refuses instead of assuming yes. tasks purge asks in the same way, because deleting cannot be undone. tasks retry asks too, but does not show a price first.
The idempotency key, in plain words. It is a label for one generation, like an order number. Every tasks create and tasks retry prints one. If your internet connection drops and you are not sure whether the generation started, run the same command again with --idempotency-key and the printed value: you get the original task back, and you are not charged twice. The label works for 24 hours with the same API key. Do not reuse it for a different generation: the same key with a different input is rejected.
No cancelling. Once accepted, a task cannot be cancelled. Ctrl + C, closing the terminal or revoking the key only stops the waiting; the task finishes and is charged normally.
Failed or expired tasks normally release the full hold. Some content refusals are charged at the accepted quote only when the model page explicitly discloses this exception. Check settled and cost for the final charge.
Download what you want to keep. Result files are kept for about 14 days at most, or less if your account or --retention says so, and then deleted for good. Download links last about 20 minutes, but you can always ask for a new one while the file exists. Uploaded files can be used for one day. Prompt text is erased after 30 days by default. Billing records are kept. See Retention and destruction.
Spending limits. Each key can have daily, monthly and lifetime limits, set on the API keys page. New keys have no limit until you set one. A task that would go over a limit is refused before it starts.
Your key. The CLI only reads the key from SPICY_API_KEY, never prints it, and in error hints shows at most its first nine characters (sk-spicy-).
More detail: Billing, Idempotency, Task commitment and retry.
Troubleshooting
Find the message you see. Errors from SpicyAPI end with (request req_…); include that value if you contact support, but never send your key.
Every error code is explained in Errors, and recovery steps are in Troubleshooting and recovery.
Source
The package is MIT-licensed and developed in the open at github.com/spicyapi-ai/spicy-cli. That is where to read the code behind any command on this page, report behaviour that differs from what is described here, or propose a change.
Next steps
Model catalog and schemas
How the live catalog and each model's settings work
Billing
Prices, what is set aside, spending limits and invoices
Retention and destruction
How long files and prompts are kept, and how to delete them sooner
Errors
Every error code and whether it is worth retrying
TypeScript SDK
Do the same things from your own JavaScript or TypeScript code
Python SDK
Do the same things from your own Python code
Go SDK
Do the same things from your own Go code
MCP server
Let an AI coding assistant run these steps, with confirmation before spending
Agent Skill
Teach an AI agent the safe SpicyAPI workflow
Quickstart
The same flow using the HTTP API directly

