Tutorial: turn a picture into a video
Start from one picture on your computer and finish with a downloaded video file. Every step shows the CLI, the TypeScript SDK and plain HTTP.
This tutorial takes one picture from your computer and turns it into a short video clip that you download and keep. It goes the whole way in order: choose a model, upload the picture, check the price, start the generation, wait for it, download the result, and deal with anything that goes wrong. You do not need to piece it together from the reference pages.
Every step shows three ways to do the same thing. Pick one and use it all the way through:
| Way | Choose it if | You need |
|---|---|---|
| CLI | You want to type commands and not write any code | Node.js 22.13 or newer |
| TypeScript SDK | You are building a Node.js program and want to see the calls it makes | Node.js 22.13 or newer |
| HTTP (curl) | You want to see the raw API, for example to use another programming language later | A bash terminal with curl and jq (macOS, Linux, or WSL on Windows) |
Would you rather just ask an AI assistant to do it? See Do it from an AI assistant.
What you will make
- The result: a short video file, usually MP4, that starts from your picture and moves the way your sentence describes.
- Your time: plan about 20 minutes the first time, most of it reading. The generation itself usually takes a few minutes; some models and settings take longer.
- Where it runs: on SpicyAPI. Your computer only sends the picture and the instructions, then downloads the finished file.
What it costs, and how to find out before paying
Prices are in US dollars. They depend on the model you pick and on your settings, such as the length and the resolution of the video, so this page does not quote a number. You can see the price in three places, and none of them costs anything:
- The live catalog (step 1) lists every model your account can use with your account's prices. Videos are usually priced
per_secondof output. When a model lists several prices, each one belongs to a setting such as a resolution. - A quote (step 3) gives the exact price of your exact request. It starts nothing and sets no money aside.
- The confirmation: the CLI shows the price again and waits for your
ybefore it starts anything.
What happens to your money:
- Only step 4 costs money, and so does running a failed task again. Listing models, uploading, quoting, waiting and downloading are free.
- When a generation starts, the quoted amount is set aside from your balance (a hold). That amount is also the most the video can cost.
- If the video succeeds, you are charged for what was produced, never more than the hold, and anything left over goes back to your balance.
- 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
settledandcostfor the final charge. - New accounts start with no free allowance, so you need a balance first. See Billing.
How it works
your picture ──upload──▶ spicy:// address (usable for one day)
│
your sentence + settings ──────────┴──▶ quote (free) ──▶ start the task (money set aside)
│
queued ──▶ running ──▶ succeeded / failed / expired
│
download link (about 20 minutes) ──▶ your fileA task is one generation job. SpicyAPI works on it in the background, so starting it and collecting the result are two separate actions. Every task has an ID that starts with job_; keep it, because it is how you find the task again. Other words used on this page are explained in the Glossary.
Before you start
Have an account, a balance and an API key
Follow Set up your account first. When you are done you should have:
- a SpicyAPI account with money on its balance;
- an API key, which starts with
sk-spicy-; - that key stored in an environment variable called
SPICY_API_KEY. An environment variable is a named value your terminal hands to the programs it runs, so the key never has to appear in a command or in your code.
Check that the key is set in the terminal window you are using, without printing the whole key:
echo "${SPICY_API_KEY:0:9}"You should see sk-spicy-. If you see nothing or an error, the key is not set in this window; go back to Set up your account.
Put your picture in a work folder
Create a folder for this tutorial and open your terminal in it. Every command on this page is run from this folder, and every file it creates lands here.
mkdir -p ~/spicy-video
cd ~/spicy-videoCopy your picture into that folder and name it start-frame.jpg (or start-frame.png if it is a PNG; then use that name wherever this page says start-frame.jpg).
What makes a good starting picture:
- Format and size: JPEG, PNG, WebP or GIF, up to 10 MiB (about 10 megabytes). A model can set stricter limits of its own, which you will see in step 1.
- The file must really be that format. SpicyAPI checks the file's contents, not just its name. Photos from phones are often HEIC; renaming one to
.jpgdoes not convert it. Open it and export or save it as JPEG first. - Shape: the picture becomes the first frame. Use a wide picture for a wide video and a tall one for a vertical video; some models also offer an
aspect_ratiosetting. - Rights: use a picture you are allowed to use.
Set up the tool you picked
The CLI is a program called spicyapi that you run in the terminal. Install it once (full instructions, including Node.js: CLI: before you start):
npm install -g @spicyapi/cli
spicyapi --versionIf the install fails with EACCES, skip it and type npx @spicyapi/cli wherever this page types spicyapi.
Then check that your key works and that you have a balance:
spicyapi balanceYou should see available, held and total in US dollars. available must be above zero.
The seven steps
Replace MODEL_ID_FROM_CATALOG with a real model ID
MODEL_ID_FROM_CATALOG is a placeholder, not a model. In step 1 you choose a real model ID from the live catalog (spicyapi models list, listModels() in the SDK, or GET /api/v1/models). Paste it wherever this page shows the placeholder. Sending the placeholder itself only returns a "not found" error.
Using Windows? Pick the CLI or SDK tab
The HTTP tab is written for bash. Windows PowerShell treats quotes, line breaks (\) and $(...) differently, so those commands will not run there as written. On Windows, use the CLI or SDK tab, or run the HTTP commands inside WSL (Windows Subsystem for Linux).
1. Choose a model and read its settings
Do this: list the image-to-video models your account can use, pick one, then read which settings it accepts.
List the models:
spicyapi models list --modality video --task image-to-video --jsonYou should see a JSON list. This one is shortened, and amounts are shown as …:
{
"total": 1,
"items": [
{
"model": "MODEL_ID_FROM_CATALOG",
"displayName": "Example video model",
"modality": "video",
"tasks": ["image-to-video"],
"enabled": true,
"available": true,
"quantityField": "duration_seconds",
"maxOutputDurationSeconds": 10,
"taskTimeoutSeconds": 1800,
"pricing": [
{ "variant": "720p", "unit": "per_second", "price": "…", "currency": "USD" },
{ "variant": "1080p", "unit": "per_second", "price": "…", "currency": "USD" }
]
}
]
}Choose a model whose enabled and available are both true and copy its model value. Then read its settings:
spicyapi models get MODEL_ID_FROM_CATALOG --jsonThe answer has the same fields plus inputSchema (the settings) and examples (inputs the server has already checked).
What to look at in the list:
| Field | What it tells you |
|---|---|
model | The model ID you will use in every later step |
enabled, available | Use a model only when both are true |
pricing | One entry per price level: variant (the setting it applies to, often a resolution), unit (usually per_second for video) and price in US dollars. regularPrice and offerLabel appear when a discount applies |
quantityField | The setting that decides how many units you pay for, usually duration_seconds. Empty means one unit per video |
maxOutputDurationSeconds | The longest video the model allows |
taskTimeoutSeconds | How long a generation may run before it stops as expired (and is not charged) |
How to read the settings. inputSchema is the model's list of settings, written as JSON Schema:
requiredlists the settings you must send.image_urlis always one of them for this kind of model, and most also requireprompt.propertieslists every setting the model accepts, each with atype, adescriptionin plain English, and oftenenum(the only allowed values),minimumandmaximum, anddefault(used when you leave the setting out).- Inside each setting,
x-uiholds display hints.x-ui.affects_price: truemeans changing that setting changes the price. - A few models add conditions at the top level, such as "if you send A you must also send B". Spicy Schema explains how to read them.
examplesholds complete inputs that the server has already checked, which are a safe starting point.
The settings you will meet most often. Names are the same across all models, but each model accepts only some of them, with its own allowed values:
| Setting | What it does | Changes the price? |
|---|---|---|
image_url | The starting picture, used as the first frame. Takes the spicy:// address from step 2 or a public https:// link | No |
prompt | What should happen: the movement, the camera, the mood. Describe motion rather than repeating what the picture already shows | Usually not |
duration_seconds | Video length in seconds. Allowed values are in enum or between minimum and maximum; some models accept a special value explained in the setting's description | Yes on most models: video is usually billed per second, so a longer clip costs more |
resolution | Output size, such as 720p or 1080p | Usually: higher resolutions tend to have their own, higher price |
aspect_ratio | The video's shape, such as 16:9, 9:16 or 1:1. Some models offer adaptive, which follows the picture | Check x-ui.affects_price |
generate_audio | Also generate a soundtrack that matches the picture | On some models |
last_image_url | A picture for the final frame; see First and last frame | Quote to check |
negative_prompt | What the video should avoid | Usually not |
seed | A number that makes results easier to compare between runs; it does not guarantee identical videos | No |
Send only the settings this model lists
Every model has its own list. A setting that is not in that model's properties, a misspelled name, or a value outside enum gets the whole request rejected with 400. Nothing is charged, but do not copy an input from another model without checking.
What just happened: you read the live catalog for your account, including your prices. Nothing was charged. Reading the catalog counts toward your account's request limit, so there is no need to repeat this before every video; do it again when you switch models or get a 400.
2. Upload your picture
Do this: upload start-frame.jpg and copy the spicy:// address you get back. Uploading is free.
spicyapi files upload start-frame.jpgYou should see:
fileId: fil_01k3m8x9q2z4v7n5p6r8s0t1w3
status: ready
bytes: 402118
contentType: image/jpeg
sha256: a91f4c2e5b7d16809e3f2a4c8d0b61e7c4a890e5d2f71b3c609ae8427d15f0c6
uri: spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3
expiresAt: 2026-09-18T09:12:11ZCopy the uri value exactly as printed.
What just happened: your picture was stored privately for your account, checked (its real format has to match its type), and given a spicy:// address. That address is not a web link: only your account can use it, and only inside a task's input. It can be used for one day (see expiresAt); after that, upload the picture again.
Picture already online?
If the picture is already at a public https:// address that opens without logging in, you can skip this step and put that address in image_url instead. It must stay reachable while the video is being generated.
If the upload fails:
| You see | What it means | What to do |
|---|---|---|
Unsupported file type, or the CLI or SDK says it cannot tell the type | The file is not JPEG, PNG, WebP or GIF | Convert the picture, or name it with the right extension |
File exceeds the 10 MB limit | The picture is larger than 10 MiB | Export a smaller version |
The uploaded file does not match the upload ticket | The file's contents are not the type its name or declaration says, for example a HEIC photo renamed to .jpg | Export it as a real JPEG or PNG and upload again |
The upload ticket has expired | More than about 20 minutes passed between the ticket and the commit | Start the upload again |
3. Describe the video and check the price
Do this: write your settings into a file called input.json, then ask for a quote.
Write input.json. Start with the two settings every image-to-video model accepts. Put your own sentence in prompt, and replace the image_url value with the uri you copied in step 2:
{
"prompt": "The camera slowly pushes in while rain runs down the window and neon light flickers",
"image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3"
}Settings you leave out use the model's default. To choose them yourself, add only settings that your model's properties list, with values it allows. For example, if your model lists duration_seconds and resolution and allows these values:
{
"prompt": "The camera slowly pushes in while rain runs down the window and neon light flickers",
"image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
"duration_seconds": 5,
"resolution": "720p"
}Save the file in your work folder:
Paste all the lines at once, after putting your own text and uri in them:
cat > input.json <<'EOF'
{
"prompt": "The camera slowly pushes in while rain runs down the window and neon light flickers",
"image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3"
}
EOFUse plain straight quotes ("), not curly ones, and no comma after the last setting.
Get the quote:
spicyapi tasks quote --model MODEL_ID_FROM_CATALOG --input-file input.jsonYou should see (amounts shown as …):
quoteId: eyJ2IjoxLCJtIjoi…
model: MODEL_ID_FROM_CATALOG
estimatedCost: …
maxCharge: …
currency: USD
quantity: 5
unit: per_second
expiresAt: 2026-09-17T08:05:00ZWhat just happened: SpicyAPI checked your input against the model's settings and priced this exact request. Nothing was started and nothing was charged. estimatedCost is the amount that will be set aside if you start now, and maxCharge is the most it can cost. quantity and unit show what you pay for, such as 5 seconds.
A quote is valid for five minutes and only for this exact input. Change the length or resolution and quote again to compare prices. The quote runs the same checks as starting the task, so problems show up here for free: a 400 means the input does not match the model's settings (fix input.json using step 1), and a 40201 means your balance would not cover it. Step 7 lists the other codes.
Want a test run without your own picture?
The model's examples from step 1 are complete inputs that the server has already checked. You can put one into input.json as it is, quote it, and generate it to see the whole flow work before you use your own picture.
4. Start the generation
This step costs money
Starting the generation sets the quoted amount aside from your balance. Once SpicyAPI accepts it, the task cannot be cancelled: it runs to the end and is charged if it succeeds. 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.
The idempotency key, in plain words. It is a label for one video, like an order number. If your connection drops and you are not sure whether the video started, send the same request again with the same label: SpicyAPI hands back the task it already started instead of starting, and charging for, a second one. A label works for 24 hours with the same API key. Use a new label for a different video: the same label with a different input is refused with 409. More in Idempotency.
Do this:
spicyapi tasks create --model MODEL_ID_FROM_CATALOG --input-file input.jsonThe CLI quotes again, shows the price and waits for you:
Estimated charge: USD …; maximum charge: USD …. Quote expires: 2026-09-17T08:05:00Z.
Idempotency-Key: 4f6755a5-0d72-4420-9f6b-9faf0e2b0241
Create a billable task with model MODEL_ID_FROM_CATALOG? … Continue? [y/N]Type y and press Enter. Anything else cancels, and nothing is charged. Copy the Idempotency-Key value somewhere before you answer; the CLI makes a new label on every run, and you need this one if the connection drops.
You should see:
idempotencyKey: 4f6755a5-0d72-4420-9f6b-9faf0e2b0241
taskId: job_01k3m8x9q2z4v7n5p6r8s0t1w3
state: queued
estimatedCost: …
deadlineAt: 2026-09-17T08:35:00ZCopy the taskId.
What just happened: SpicyAPI accepted the generation, which is now a task, and set estimatedCost aside from your balance. That is also the most this video can cost. state: queued means it is waiting for its turn. deadlineAt is the latest time it can finish; if it is not done by then it ends as expired and nothing is charged. deadlineAt is not when your files expire.
Passing the quote along means "only accept this at the price I was shown". If the price or the input changed in the meantime, the request is refused with 40901 instead of setting a different amount aside. The CLI does this for you.
If the connection dropped and you never saw a task ID, do not simply start again with a new label, which would make and charge for a second video. Repeat the request with the same label:
- CLI:
spicyapi tasks create --model MODEL_ID_FROM_CATALOG --input-file input.json --idempotency-key <the value you copied>. See Pick up an interrupted task. - SDK: run
node generate.mjs --yesagain; it reusesidempotency-key.txt. - HTTP: run the "Start the video" block again; it reuses
idempotency-key.txt.
If step 4 was refused, nothing was created and nothing was charged; the reason and the fix are in step 7.
5. Wait for the video
Do this: check the task until it finishes. Videos usually take a few minutes.
spicyapi tasks wait job_01k3m8x9q2z4v7n5p6r8s0t1w3 --wait-timeout-ms 1800000 --jsonUse your own task ID. --wait-timeout-ms 1800000 waits up to 30 minutes instead of the default 10.
You should see, once it is finished (shortened):
{
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "MODEL_ID_FROM_CATALOG",
"state": "succeeded",
"output": {
"assets": [
{
"key": "tasks/2026/09/17/job_01k3m8x9q2z4v7n5p6r8s0t1w3/result.mp4",
"url": "https://…",
"expiresAt": "2026-09-17T08:40:00Z",
"mime": "video/mp4",
"durationSeconds": 5,
"width": 1280,
"height": 720,
"bytes": 4839211
}
]
},
"cost": "…",
"settled": true
}Next time you can combine steps 4 and 5 by adding --wait to tasks create.
What just happened: your tool asked SpicyAPI about the task every few seconds. The states are queued (waiting), running (being generated), then one final state: succeeded, failed or expired. After success the file is copied into SpicyAPI's own storage, which takes a few more seconds; until then the result shows pending: true. cost is final once settled is true.
- Closing the terminal or pressing Ctrl + C does not cancel the task. It only stops the waiting. Keep the task ID and run this step again later.
- Do not check more often than every few seconds. Checking a task uses the account’s read allowance, independently of starting tasks; see Rate limits.
- For an app or a server, let SpicyAPI tell you when the task is done instead of asking: see Get notified instead of waiting.
6. Download and keep the video
Do this: get a fresh download link and save the file to your computer.
Get a fresh link:
spicyapi files download-url job_01k3m8x9q2z4v7n5p6r8s0t1w3key: tasks/2026/09/17/job_01k3m8x9q2z4v7n5p6r8s0t1w3/result.mp4
url: https://…
expiresAt: 2026-09-17T08:52:00ZPaste the url into a browser to download it, or save it from the terminal (keep the quotes, because the link contains & characters):
curl -L -o my-video.mp4 "PASTE_THE_URL_HERE"In Windows PowerShell, type curl.exe instead of curl. If a task produced several files, spicyapi tasks get <task-id> --json lists them, and --key picks one.
What just happened: every result comes with a direct download link. The link works for about 20 minutes and needs no API key; never add your key to it. When it expires, ask for the task again (or use files download-url) to get a new one.
Keep your own copy
SpicyAPI keeps generated files for about 14 days at most, less if your account's retention setting is shorter, and then deletes them for good. Your uploaded picture can be used for one day, and your prompt text is erased after 30 days by default. The record of the task and its cost is kept. To delete a finished task's files and prompt sooner, use spicyapi tasks purge, purgeTask() or POST /api/v1/jobs/purge; that deletes content only and is not a refund. See Retention and destruction.
7. When something goes wrong
Problems come in two kinds, and they are handled differently.
A request is refused. Your command or call gets an error straight away. Nothing was created and nothing was charged. The message says why, and the error carries a code and a request ID (req_…) to quote if you contact support.
| Code | What it means | What to do |
|---|---|---|
400, or a more specific code starting with 400 | The input does not match the model's settings: a missing required setting, an unknown or misspelled one, a value that is not allowed, an expired or foreign spicy:// address, a combination of settings the model cannot serve right now, or an invalid callback address | Compare input.json with the settings from step 1 and fix it. For an expired address, upload the picture again |
401 | The API key is missing, mistyped or revoked | Check SPICY_API_KEY as in Before you start |
40201 | Your balance does not cover the amount to set aside | Add funds under Billing in the console |
40202 | A spending limit on your key (daily, monthly or lifetime) was reached | Raise it on the API keys page, or wait for it to reset |
403, 40301 | This key may not use this model, or the request came from a place it is not allowed from | Check the key's model list and IP allowlist on the API keys page |
404 | No model with that ID for your account (or you sent the placeholder), or no task with that ID for this key | Copy the exact ID again; use the same key that created the task |
409 | This idempotency key was already used for a different request | Use a new label for a new video |
40901 | The quote expired (five minutes) or the price changed | Quote again, then start again |
429 | Too many requests in a short time | Wait for the number of seconds the error gives, then continue more slowly |
50301 | The model cannot take new work right now | Try again later, or choose another model |
The task fails. The task was accepted, but ends as failed or expired instead of succeeded. The task shows an errorCode that says why, and an errorMessage with more detail in words. 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.
Read it:
spicyapi tasks get job_01k3m8x9q2z4v7n5p6r8s0t1w3Look at state, errorCode and errorMessage.
Then decide what to do from errorCode, not from the wording of the message:
errorCode | What it means | What to do |
|---|---|---|
rate_limited, upstream_unavailable, upstream_failed | Generation was busy or temporarily unavailable | Run it again after a short pause |
timeout (or the state is expired) | The video was not finished in time | Try once more; if it happens again, choose a shorter length or a lower resolution and start a new task |
generation_failed | Generation ran but produced nothing | The same input usually fails the same way. Change the prompt or the picture and start a new task |
invalid_asset | The picture could not be used, for example because the upload expired or the file could not be read | Upload the picture again and start a new task with the new address |
invalid_request, unsupported_combination | The settings were not accepted | Change the settings and start a new task |
content_rejected | The selected model declined this request under its own policy | Change the prompt or the picture, or choose another model |
Any other value should be treated like upstream_failed. The full list is in Errors.
Run a failed task again. A retry creates a new task with the same model and input, at the model's current price. It costs money, like step 4, and it keeps the original input: if that input used an upload that has since expired, upload again and start a new task instead.
spicyapi tasks retry job_01k3m8x9q2z4v7n5p6r8s0t1w3 --wait --wait-timeout-ms 1800000It asks you to confirm, but unlike tasks create it does not show a price first. To see the price, run the tasks quote command from step 3.
Only failed and expired tasks can be retried. For a different version of a video that succeeded, go back to step 3 with a new label. There is no way to cancel a task once it has been accepted. More in Task commitment and retry.
Do it from an AI assistant instead
If you use an AI assistant that supports MCP, such as Claude Code, Cursor or Codex, the SpicyAPI MCP server lets it do all seven steps for you. Set it up once with MCP server: set up your client, then ask in your own words, for example:
Upload the picture at /Users/me/spicy-video/start-frame.jpg to SpicyAPI. Find image-to-video models my key can use and quote a 5-second clip on the two cheapest. Then generate the video with the cheaper one once I confirm, wait for it, and give me the download link.
On Windows, give the path as C:\Users\me\spicy-video\start-frame.jpg.
What happens:
spicyapi_upload_fileuploads the picture and gets itsspicy://address. It is free, but it does send the file, so name the exact file you mean. By default only files inside your home folder can be read; see Local files.spicyapi_models_listandspicyapi_model_getfind a model and its settings, andspicyapi_task_quoteprices the options. All free.spicyapi_task_createstops and asks you, showing the model, the estimated and maximum charge and when the quote expires. Nothing is charged until you confirm, and the assistant cannot confirm for you.spicyapi_task_waitwaits up to five minutes at a time, so for a longer video the assistant may wait more than once. It then gives you the download link, which works for about 20 minutes.
More examples and every tool: MCP server.
Going further
First and last frame
Some image-to-video models also accept last_image_url: a second picture for the video to end on. Check whether yours does by looking for last_image_url in the properties from step 1. If it is not there, the model does not support it and sending it is refused with 400.
Upload both pictures as in step 2, then put both addresses in input.json:
{
"prompt": "The flower slowly opens as morning light spreads across the table",
"image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
"last_image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w4"
}The setting's description explains how that model uses the final picture. Quote again before starting, to see whether the second picture changes the price.
Several videos
- Reuse the upload. One
spicy://address can be used in several tasks until itsexpiresAt, unless your account's upload retention is set to delete uploads as soon as a task finishes. - One label per video. Give each video its own idempotency key, such as
campaign-clip-1,campaign-clip-2. Running your script again then returns the videos already started instead of paying for them twice. - Do not start everything at once. Each running task has money set aside, a spending limit refuses tasks beyond it with
40202, and every status check counts toward your request limit. Start with two at a time.
Ready-made scripts: Run a batch with limited concurrency (SDK) and Run the CLI from a script.
Get notified instead of waiting
For a website or a background service, keeping a program running just to ask "is it done yet?" is wasteful. Instead, give the task a callback address: a public web address on your own server that SpicyAPI calls once, when the task finishes, with the same task details step 5 shows.
What you need:
- a server reachable from the internet at an
https://address, on the standard port (localhostis refused); - your webhook signing secret from the console, so your server can check that each call really came from SpicyAPI;
- code that checks the signature, answers quickly, and handles the same notification arriving twice.
Webhooks explains the format, the signature and the retry schedule, and the SDK has a minimal receiver. Keep a fallback: now and then, check tasks that are well past their deadlineAt without a notification.
Add the callback address when you quote and start the task:
spicyapi tasks create --model MODEL_ID_FROM_CATALOG --input-file input.json --callback-url https://your-app.example.com/hooks/spicyThe CLI includes the address in its own quote, so the price check still matches.
Next steps
Model catalog and schemas
How the live catalog and each model's settings work
Media
Uploads in detail, including uploads from a browser
Webhooks
Get told when a task finishes, and verify every call
Errors
Every error code and whether a retry can help
Billing
Holds, final charges, spending limits and your balance
Retention and destruction
How long files and prompts are kept, and deleting them sooner
Production integration
What to add before real users depend on it
FAQ
Short answers to the questions people ask most

