Developers
Salven API
The studio’s models at the studio’s prices, from Claude, from your own code or from a coding agent. Every run is paid from your credits and lands in your gallery.
Overview
The Salven API does what the Salven studio does: the same models, the same prices, the same checks. It makes images, video, audio (speech, music, sound effects) and 3D models, applies the ready-made effects, puts your saved characters into a picture or a clip by their @handle, and reads a text or makes a clip speak in your saved voices.
A run is charged in credits to the account the key belongs to, and its result lands in that account’s gallery on salven.ai. The price is known before anything is spent (a dry run), it is taken when the run starts, and a run that fails gives its credits back.
The API is open to every Salven account. There are three ways in:
| Way | For whom | What you need |
|---|---|---|
| Claude | Anyone who uses Claude: you ask in a chat, Claude runs the models. | The address https://salven.ai/mcp and your Salven sign-in. |
| REST | Your own scripts, services and automations. | An API key and any HTTP client. |
| An agent | Claude Code, Cursor and other coding agents. | An API key and the file SKILL.md. |
All three reach the same server code, so a run costs the same and is checked the same whichever way it starts.
Connect Claude
Salven is a remote MCP server at https://salven.ai/mcp. Add it to Claude once, and Claude can choose a model, quote a price, run it and hand you the result, all on your Salven account.
The quickest way to connect Claude is the button on the Salven MCP page: it opens Claude's new connector window with the name and address already filled in.
Claude on the web, desktop and mobile
- In Claude, open Customize → Connectors → Add custom connector. The mobile apps cannot add a connector: do this step on claude.ai, and the connector then works in the mobile apps too.
- Paste the address
https://salven.ai/mcp. - Claude sends you to Salven. Sign in if you are not signed in, read what the app will be able to do, and press Allow.
Who can add a connector depends on your Claude plan: the Free plan allows one custom connector, and on Team and Enterprise plans a connector is added for the whole organization by an owner (or a member whose role allows it), not by each member for themselves.
The same address, with a copy button, is in Settings under Claude & API.
Claude Code
# Add Salven to Claude Code as an MCP server. Then, inside Claude Code, run /mcp and pick salven to sign in.
claude mcp add --transport http salven https://salven.ai/mcpThe command only saves the server. To sign in, start Claude Code, run /mcp, pick salven and follow the sign-in: your browser opens on Salven, where you sign in and press Allow, as above.
Other MCP clients
Any client that speaks MCP over Streamable HTTP can use the same address. A client that supports OAuth signs you in the way Claude does. A client that sends a fixed header can use an API key instead:
Server URL: https://salven.ai/mcp
Transport: Streamable HTTP
Header: Authorization: Bearer <your Salven API key>In Claude Code the same header is set when the server is added, instead of the sign-in above; the shell takes the key from the SALVEN_API_KEY environment variable:
# Add Salven to Claude Code with an API key instead of the OAuth sign-in.
# The shell reads the key from SALVEN_API_KEY, so it is never typed into the command.
# Claude Code saves the header, with the key in it, together with the server.
claude mcp add --transport http salven https://salven.ai/mcp \
--header "Authorization: Bearer $SALVEN_API_KEY"What Claude can do
The connection gives Claude the tools below. They call the same code as the REST API, and runs made through them count toward the same limits.
| Tool | What it does |
|---|---|
account | The account the connection acts as, and its credit balance. |
list_models | The models available now, optionally of one kind: id, modes, inputs and base price. |
get_model | One model’s full spec: modes, params, inputs, aspect options, prompt limits and pricing. |
generate | Starts a generation with a model, or only quotes it with dryRun: true. Waits up to about 45 seconds and answers the finished files, or the ids of runs still going. |
get_generation | One generation’s status and, when it is done, its files. Can wait up to 40 seconds for it to finish. |
list_generations | The account’s works, newest first, made on the site or through the API. |
list_effects | The ready-made effects: the pictures each takes, its price and its quality options. |
run_effect | Applies an effect to your picture or pictures, or only quotes it with dryRun: true. |
list_characters | Your characters and AI influencers, each with its @handle. |
create_character | Saves a person as a character: a name, an @handle, one to four photos and, if you like, one of your saved voices. Free. |
list_saved_voices | Your saved voices: recordings of a voice, each with its id, name, language and length. |
create_voice | Saves a voice from a recording of 10–60 seconds of one person speaking. Free. |
design_voice | Makes up a voice from a description and records a text in it. Paid; create_voice then saves the recording as a voice. |
upload_from_url | Copies a file from a public web address into your Salven uploads. Salven downloads the file itself: images up to 10 MB, audio up to 20 MB, video and GLB models up to 60 MB. |
create_upload_link | Makes a one-time link for adding files from your device. |
get_upload_link | The files that arrived through an upload link. |
Giving Claude a file
A model’s inputs must be your own files on Salven, and a chat cannot pass a tool the picture you attached to a message. Claude has three ways to get a file:
- An output of one of your earlier works, which Claude finds with
list_generations. - A public web address, which Claude copies in with
upload_from_url. Salven downloads the file itself, so the 25 MB limit of a direct upload does not apply: an image may be up to 10 MB, an audio file up to 20 MB, a video or a GLB model up to 60 MB. This is the way to bring in a video or a 3D model larger than 25 MB. The download has to finish while Claude waits for the tool’s answer, so a large file needs an address that serves it quickly. - A file on your device: Claude makes an upload link with
create_upload_linkand gives you a page to open. You pick the files there, with no sign-in, and tell Claude they are in. A link works for 30 minutes and takes up to 10 files, in the formats and sizes of the upload rules: a file sent this way passes through the web server, so a video or a GLB model must be under 25 MB. An agent with a terminal, such as Claude Code, can send a file from the disk to the link itself.
Credits and disconnecting
Everything Claude makes is paid from your credits, at the studio’s prices. generate and run_effect can quote a run without starting it, and Salven’s instructions tell Claude to name the cost and wait for your yes before an expensive run (video, 3D, several pictures) or a spend you did not ask for. When exactly to ask is still Claude’s decision, so say so if you want a quote first.
To end a connection, open Settings, find the app under Claude & API and press Disconnect twice (the second press confirms). The app loses access at once.
Quick start
One image over REST, start to finish. You need a Salven account with some credits and a terminal with curl.
- Open Settings, find Claude & API, type a name for the key and press Create key.
- Copy the key now: it is shown once.
- Put it in the environment variable
SALVEN_API_KEY, for example withexport SALVEN_API_KEY=<your key>in the shell you will use. - Run the calls below one by one.
API=https://salven.ai/api/v1
AUTH="Authorization: Bearer $SALVEN_API_KEY"
# Who the key acts as, and the balance
curl -s "$API/me" -H "$AUTH"
# The image models, each with its params, inputs and pricing
curl -s "$API/models?kind=image" -H "$AUTH"
# Price first: dryRun answers the cost and starts nothing
curl -s "$API/generations" -H "$AUTH" -H "Content-Type: application/json" \
-d '{"model":"nano-banana-2","prompt":"a red fox in the snow, film photo","params":{"aspect_ratio":"16:9","resolution":"2K"},"dryRun":true}'
# Then run it. The Idempotency-Key names THIS generation: to retry it (a timeout, a 5xx, a 409),
# rerun the command with the same key; pick a new key only for a new generation.
curl -s "$API/generations" -H "$AUTH" -H "Content-Type: application/json" \
-H "Idempotency-Key: fox-001" \
-d '{"model":"nano-banana-2","prompt":"a red fox in the snow, film photo","params":{"aspect_ratio":"16:9","resolution":"2K"}}'
# Wait for the result: put generations[0].id from the answer above in place of GENERATION_ID,
# and repeat while "status" is "queued" or "processing"
curl -s "$API/generations/GENERATION_ID?wait=60" -H "$AUTH"The examples are written for bash: macOS, Linux, WSL, or Git Bash on Windows. In Windows PowerShell curl is an alias of another command, so use curl.exe and PowerShell’s own syntax for variables and quoting, or run the examples in Git Bash or WSL.
| Call | What it answers |
|---|---|
GET /me | The account and its balance. |
GET /models?kind=image | The image models, each with its params, inputs and pricing. |
POST /generations with "dryRun": true | The price: cost for one generation, total for the whole request. Nothing starts. |
POST /generations | 202 and generations[].id. The credits are taken now. |
GET /generations/{id}?wait=60 | The generation. Repeat while status is queued or processing; once it is done, outputs[].url are the files. |
The Idempotency-Key header names this one generation. If the answer is lost, send the same request with the same key: it returns the first answer instead of charging again. See Idempotency.
With an input file
A model that works from a picture, a video or an audio file needs the file on Salven first. This uploads a photo and turns it into a clip:
API=https://salven.ai/api/v1
AUTH="Authorization: Bearer $SALVEN_API_KEY"
# Upload the picture. The answer's "url" is the file's address on Salven:
# {"url":"https://cdn.salven.ai/uploads/...","kind":"image","mimeType":"image/jpeg","bytes":184233}
curl -s "$API/uploads" -H "$AUTH" -F "file=@photo.jpg"
# Put that url here
PHOTO_URL="PASTE_THE_URL_FROM_THE_ANSWER_ABOVE"
# Price first: the same body with dryRun
curl -s "$API/generations" -H "$AUTH" -H "Content-Type: application/json" -d @- <<EOF
{
"model": "kling-v3-pro",
"prompt": "she turns and smiles, slow dolly in",
"params": { "duration": 5 },
"inputs": { "startImage": "$PHOTO_URL" },
"dryRun": true
}
EOF
# Then run it, with a key that names this generation (reuse it only to retry this very request)
curl -s "$API/generations" -H "$AUTH" -H "Content-Type: application/json" \
-H "Idempotency-Key: anim-001" -d @- <<EOF
{
"model": "kling-v3-pro",
"prompt": "she turns and smiles, slow dolly in",
"params": { "duration": 5 },
"inputs": { "startImage": "$PHOTO_URL" }
}
EOF
# Wait for the clip: put generations[0].id from the answer above in place of GENERATION_ID,
# and repeat while "status" is "queued" or "processing" (video takes minutes)
curl -s "$API/generations/GENERATION_ID?wait=60" -H "$AUTH"Inputs must be your own files: uploaded with POST /uploads, or outputs[].url of your own generations. A link from anywhere else is refused with input_not_allowed: download the file and upload it.
Authentication
Every request carries an API key: Authorization: Bearer <key>. The header X-API-Key: <key> is accepted as well.
- A key is
slv_followed by 40 letters and digits. - It is shown once, when it is made. Salven stores only a hash of it, so a lost key cannot be shown again: revoke it and make a new one.
- An account may have up to 10 live keys. Make and revoke them in Settings under Claude & API. A revoked key stops working at once.
- A key acts as its account: the account’s credits pay and its gallery gets the results. Treat it like a password: keep it in an environment variable or a secret store, and never put it in code, a repository or a log.
- Nothing but the key is read. A browser signed in to salven.ai cannot call the API with its cookie.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | unauthorized | The key is missing, is not a Salven key, is unknown or was revoked. |
| 403 | api_not_available | The API is not open to this account. |
Conventions
- Base URL:
https://salven.ai/api/v1. Every path on this page is relative to it. - Bodies are JSON in both directions. Only
POST /uploadstakesmultipart/form-data. - Times are ISO 8601, in UTC.
- Every answer is sent with
Cache-Control: no-store. - Money is counted in credits. A credit costs 5 ₽, and every credit figure is a decimal number (
1.4,240.5): prices are rounded up to a tenth of a credit. - An error from the API is
{ "error": { "code": …, "message": … } }with a matching HTTP status. The web server in front of the API can answer without a JSON body: a413for a request body over 25 MB, and a bare502or504while the site restarts. See Errors.
How a generation goes
- Balance.
GET /meanswersbalance: the most a run may cost. - Model.
GET /models, or one kind with?kind=. Read the chosen model’s spec:params,inputs,aspect,modes,pricing. Send only the names and values the spec lists. - Uploads. Only when the model takes files:
POST /uploadsanswers aurlto use as an input. - Quote.
POST /generationswith"dryRun": trueanswerscost(one generation) andtotal(cost×count). - Start. The same body without
dryRun, with anIdempotency-Keyheader. The answer is202withgenerations[].id, and the credits are taken. - Wait.
GET /generations/{id}?wait=60, repeated whilestatusisqueuedorprocessing. - Deliver.
outputs[].urlare direct links to the files.
Statuses
| Status | Meaning |
|---|---|
queued | Accepted and paid for, waiting its turn. |
processing | Being made. |
done | Finished. outputs holds the files. |
failed | Could not be made. The credits were returned (refunded). |
A failed generation has error.code set to generation_failed, and error.reason says why in one word: content_prompt (the model’s content filter refused the prompt; it most often trips on names of brands, films, characters and real people), content_input (it refused an attached file), real_person (the model does not take photos of real people), no_output, input_too_small, tryon_no_fit, rig_pose, busy (the provider was overloaded: the same request may be sent again) or unknown. error.message says the same in a sentence. The provider’s own text is not shared. The credits are already back, so it is fine to try again, perhaps with other wording or another model. After two failures in a row, stop and look at the request.
How long it takes
| What | Typical time |
|---|---|
| Image | 10–60 seconds |
| Video | 1–5 minutes, longer for clips of 10–15 seconds |
| Speech | A few seconds to half a minute |
| Music | Up to a few minutes |
| 3D model | 2–8 minutes; some models can take over half an hour |
| 3D rigging | About 2 minutes |
Nothing on Salven’s side ends a run that hangs. A sensible client stops waiting after about 5 minutes for an image, 10 for audio, 20 for video and 60 for 3D, and does not start the same run again on its own: the run stays in the account’s gallery.
Endpoints
| Endpoint | What it does |
|---|---|
GET /me | The account behind the key and its balance. |
GET /models | Every model the API can run, optionally of one kind. |
GET /models/{id} | One model’s spec. |
POST /uploads | Stores a file as your own and answers its URL. |
POST /generations | Starts one or more generations, or quotes them. |
GET /generations/{id} | One generation: its status and, when done, its files. |
GET /generations | The account’s generations, newest first. |
GET /characters | The account’s characters and influencers. |
GET /characters/{handle} | One of them, with its photos. |
POST /characters | Makes a character. |
GET /voices | The account’s saved voices. |
POST /voices | Saves a voice from a recording. |
POST /voices/design | Makes a trial recording of a voice from a description (paid). |
GET /effects | The ready-made effects. |
GET /effects/{slug} | One effect’s spec. |
POST /effects/{slug}/run | Runs an effect, or quotes it. |
The figures in the examples on this page are examples. Live prices are in GET /models and in the table under Models; the exact price of a request is what its dry run answers.
GET /me
{
"id": "cmg1x2y3z0000abcd1234efgh",
"username": "anna",
"name": "Anna",
"credits": 240.5,
"dailyAllowance": 0,
"balance": 240.5,
"key": { "id": "cmg4k5m6n0001abcd5678ijkl", "name": "Claude Code", "prefix": "slv_Ab12Cd" }
}credits never expire. dailyAllowance is a partner’s free daily credits left for today, and they are spent first. balance is the two together: the most a run may cost. key names the key the request came with.
GET /models and GET /models/{id}
GET /models answers { "models": [ … ] } in the studio’s order. ?kind= narrows it to image, video, audio or 3d; any other value is refused with 400 invalid_kind. GET /models/{id} answers one model, or 404 model_not_found. A model looks like this:
{
"id": "kling-v3-pro",
"name": "Kling v3 Pro",
"kind": "video",
"description": "Top-tier video quality, start from a frame",
"pricing": { "credits": 2.6, "unit": "second", "secondsFrom": "params.duration" },
"modes": [
{ "slug": "generate", "name": "Generate", "promptRequired": true, "priceMultiplier": 1 }
],
"prompt": { "maxLength": 2500 },
"negativePrompt": { "maxLength": 2500 },
"aspect": null,
"params": [
{
"name": "duration",
"type": "number",
"min": 3,
"max": 15,
"step": 1,
"default": 5,
"priced": true
},
{
"name": "aspect_ratio",
"type": "enum",
"options": ["16:9", "9:16", "1:1"],
"default": "16:9"
},
{ "name": "audio", "type": "boolean", "default": false, "priceFactorWhenTrue": 1.5 }
],
"inputs": [
{ "name": "startImage", "kind": "image", "required": false, "multiple": false, "max": 1 },
{
"name": "endImage",
"kind": "image",
"required": false,
"multiple": false,
"max": 1,
"requires": "startImage"
}
],
"maxCount": 1,
"startImageSetsShape": true,
"seed": false,
"references": null,
"characters": { "supported": true, "maxCharacters": 3, "needsStartFrame": true, "onlyWith": null },
"voices": null,
"speechTags": null
}| Field | How to read it |
|---|---|
pricing | credits per unit. generation is a flat price. second multiplies by seconds, and secondsFrom says which: a duration param, or the length of an input file. 1000_characters is for speech: the prompt’s length. The mode’s priceMultiplier and the factors of the chosen options multiply the result; an input with pricePerFile adds that share of credits for each file sent. The total is rounded up to a tenth of a credit. |
modes | The studio modes the model runs in. Send mode only when there is more than one; the default is generate, else the first. promptRequired: false means the inputs say everything and no prompt is needed. |
prompt, negativePrompt | The longest text in characters. negativePrompt: null means the model takes none. |
params | Settings, sent by name. number: within min..max on step; maxWith lowers max while another param holds a value (Kling 4.0’s duration stops at 20 with variant flash; a longer one is refused). enum: exactly one of options; priceFactors gives the price factor of each option, optionNotes says what an option is (a voice’s gender and tone, for example), onlyWith names options offered only beside another param’s values (Seedance 2.0’s 1080p only with variant pro; another pair is refused). boolean: true or false, with priceFactorWhenTrue when the switch costs extra. text: a string of at most maxLength characters, empty for none; billed: true means its characters are billed with the prompt’s. A param left out takes its default; an unknown name is refused. |
aspect | Not null: the shape of the result is the top-level aspect field, one of options. Null: the model has its own aspect_ratio param, or its input decides the shape. |
inputs | Files, sent as inputs: { "<name>": "<url>" }, or as an array of URLs for a multiple input, at most max. kind is what the file must be. A required input must be there; one with requires is used only together with that other input. pricePerFile: each file of the list adds that share of the price. |
maxCount | How many separate generations one request may start with count. |
startImageSetsShape | true: a start frame decides the clip’s shape, and aspect_ratio is ignored once one is sent. |
seed, seedMax | Whether the model takes a seed, and its largest value. See Seeds. |
references | The limits for reference media on the models that take it, else null. |
characters | Whether an @handle in the prompt brings that person’s photos (supported), for how many people in one run (maxCharacters), whether a start frame must come with them (needsStartFrame), and the only values of another param they go with (onlyWith, e.g. Kling 4.0’s regular version, not Flash). See Characters. |
voices | Whether the model takes your saved voices, sent by id in voices: min 1 is a speech model that speaks only in one of them; promptTags: true is a video model that takes up to max, named @Voice1, @Voice2… in the prompt; onlyWith as for characters (Kling 4.0’s regular version). null: none. See Saved voices. |
speechTags | On a speech model, the marks it reads inside the text, written its own way: square (any instruction in square brackets, "any", or only the listed words), paren (round brackets), angle (<laugh>), wrap (around words, <whisper>…</whisper>), pause (<#0.5#>). readsBrackets: true: a [word] that is not one of its marks may be spoken aloud. null: none known. SKILL.md lists which model reads which. |
Two things are left to SKILL.md: the motions param type used by 3D rigging, and the rules of the references block.
POST /uploads
A multipart form with the field file and, optionally, kind (image, video, audio or model). Without kind it is told from the file’s first bytes. The answer is 201:
{
"url": "https://cdn.salven.ai/uploads/cmg1x2y3z0000abcd1234efgh/1790847000000-9f2c4e6a8b0d1f35.jpg",
"kind": "image",
"mimeType": "image/jpeg",
"bytes": 184233
}| Kind | Formats | Largest file |
|---|---|---|
image | JPEG, PNG, WebP | 10 MB |
video | MP4, MOV, WebM | 25 MB |
audio | MP3, WAV, M4A, OGG | 20 MB |
model | GLB (binary glTF 2.0) | 25 MB |
- The format is read from the bytes, not from the file name.
- HEIC photos are refused with
heic_not_supported: convert them to JPEG first. - A 3D model must be one GLB file with its textures inside; a
.gltfis not taken. - The 25 MB of a video or a 3D model is the web server’s limit on the whole request body, so the file itself must be a little smaller. A larger body is stopped before the API sees it: that answer is a
413whose body is not JSON. - A video or a GLB model over that, up to 60 MB, can come in only from a public web address, through the MCP tool
upload_from_url: see Giving Claude a file. - The file becomes your own upload. Only your own files can be inputs: uploads, and
outputs[].urlof your own generations.
Refusals: 400 missing_file, invalid_form_data, invalid_kind; 413 file_too_large (an image or an audio file over its limit, with max in bytes); 415 unsupported_format, heic_not_supported; 502 storage_error (retry once); 503 uploads_disabled.
POST /generations
{
"model": "nano-banana-2",
"mode": "generate",
"prompt": "a red fox in the snow, film photo",
"params": { "aspect_ratio": "16:9", "resolution": "2K" },
"count": 1,
"enhancePrompt": true,
"dryRun": false
}| Field | Meaning |
|---|---|
model | Required. A model id from GET /models. |
mode | Optional. One of the model’s modes[].slug. |
prompt | Required when the mode’s promptRequired is true. Any language; @handle names one of your characters. See Prompts. |
negativePrompt | Only where the model’s negativePrompt is not null, up to its maxLength. |
aspect | Only where the model’s aspect is not null: one of its options. |
params | Param name → value, as the model’s params list them. |
inputs | Input name → file URL, or an array of URLs for a multiple input. |
seed | Only where the model’s seed is true: a whole number from 0 to seedMax. |
voices | Only where the model’s voices is not null: ids of your saved voices. Exactly one on a speech model with voices.min 1; on a video model up to voices.max, named @Voice1, @Voice2… in the prompt in this order. See Saved voices. |
count | 1 to maxCount separate generations, each charged. Default 1. |
enhancePrompt | Default true. false sends the prompt exactly as written. |
dryRun | true: answer the price, start nothing, charge nothing. |
A field the list does not name is refused with invalid_request. Send an Idempotency-Key header with every real run; see Idempotency.
A dry run answers 200. cost is one generation, total is what the whole request will take:
{
"dryRun": true,
"model": "nano-banana-2",
"mode": "generate",
"cost": 1.9,
"count": 1,
"total": 1.9,
"balance": 240.5
}A real run answers 202:
{
"generations": [
{
"id": "cmg7q2w8e0003abcd9012mnop",
"status": "queued",
"kind": "image",
"model": "nano-banana-2",
"mode": "generate",
"effect": null,
"prompt": "a red fox in the snow, film photo",
"characters": [],
"seed": null,
"source": "api",
"cost": 1.9,
"refunded": 0,
"createdAt": "2026-10-01T09:30:00.000Z",
"startedAt": null,
"finishedAt": null,
"outputs": [],
"error": null
}
],
"charged": 1.9,
"balance": 238.6
}If something stops the request part-way through count (usually the balance), the generations already started stand, and the answer also has "partial": true and "stopped" with the code of what stopped it. To run the rest, send a new request with the smaller count under a new Idempotency-Key.
balance is null when it could not be read after the charge; GET /me has it. If reading the new generations back fails, generations holds only each id and status, and charged is null too: poll the ids as usual.
GET /generations/{id}
{
"id": "cmg7q2w8e0003abcd9012mnop",
"status": "done",
"kind": "image",
"model": "nano-banana-2",
"mode": "generate",
"effect": null,
"prompt": "a red fox in the snow, film photo",
"characters": [],
"voices": [],
"seed": null,
"source": "api",
"cost": 1.9,
"refunded": 0,
"createdAt": "2026-10-01T09:30:00.000Z",
"startedAt": "2026-10-01T09:30:01.000Z",
"finishedAt": "2026-10-01T09:30:24.000Z",
"outputs": [
{
"url": "https://cdn.salven.ai/generations/cmg7q2w8e0003abcd9012mnop/0.png",
"mimeType": "image/png",
"width": 2048,
"height": 1152,
"durationSeconds": null,
"thumbnailUrl": null
}
],
"error": null
}With ?wait=<seconds>, from 0 to 60, the answer is held until the generation is done or failed, or until the seconds pass; then it answers as things stand. An id that is not one of the account’s generations answers 404 not_found.
| Field | Meaning |
|---|---|
status | queued, processing, done or failed. |
kind | image, video, audio or 3d. |
model, mode, prompt | What ran. For a run of an effect these are null, and effect holds its slug, name and quality. |
characters | Who the prompt’s handles brought in, each with handle, name and kind. |
voices | The saved voices the request named, each with id and name, in @Voice order. |
seed | The seed it ran with, when one was sent. |
source | Where it was started: web, bot or api. |
cost, refunded | The credits taken, and the credits returned if it failed. |
createdAt, startedAt, finishedAt | Times. The last two are null until they happen. |
outputs | The files, once done: url, mimeType, width, height, durationSeconds, thumbnailUrl. Empty until then. |
error | null, or on a failed run an object with code (generation_failed), reason and message. |
A 3D result is a .glb file. Its output may also carry faces, and a rigged model fbxUrl and clips; SKILL.md describes them.
GET /generations
The account’s generations, newest first: all of them, not only the ones started through the API. Query: limit (20 by default, up to 50), kind (image, video, audio or 3d) and cursor. The answer is { "generations": [ … ], "nextCursor": … }: pass nextCursor as cursor for the next page; it is null on the last one. A bad query answers 400 invalid_kind or invalid_cursor.
Characters
A character is a person saved on the account: a name, an @handle and one to four photos of that one person, the first being the main photo. Write @anna in a prompt, and a model that takes characters gets anna’s photos with the request, so the result keeps her look. A mention costs nothing extra. AI influencers made on salven.ai have handles too and work the same way.
GET /charactersanswers{ "characters": [ … ] }: characters, newest first, then influencers. Each hashandle,name,kind(characterorinfluencer),photos(how many a mention sends),voice(its saved voice, ornull) andcreatedAt.GET /characters/{handle}answers{ "character": { … } }(the shape shown underPOST /charactersbelow), or 404character_not_found. Herephotosis a list, the main photo first, each withurl,widthandheight; an influencer’swidthandheightarenull.POST /charactersmakes a character. It is free and answers201with the same{ "character": { … } }.
{
"name": "Anna",
"handle": "anna",
"photos": [
"https://cdn.salven.ai/uploads/cmg1x2y3z0000abcd1234efgh/1790847000000-9f2c4e6a8b0d1f35.jpg",
"https://cdn.salven.ai/uploads/cmg1x2y3z0000abcd1234efgh/1790847060000-3b7d9e1c5a2f4860.jpg"
],
"voice": "vce_8c1e5a3f9b7d2046e1c3a5b7"
}{
"character": {
"handle": "anna",
"name": "Anna",
"kind": "character",
"photos": [
{
"url": "https://cdn.salven.ai/characters/chr_5d1f0a9c3e7b2468ace01357/4a6c8e0b2d4f6a81.jpg",
"width": 1536,
"height": 2048
},
{
"url": "https://cdn.salven.ai/characters/chr_5d1f0a9c3e7b2468ace01357/b3d5f7a9c1e30264.jpg",
"width": 1536,
"height": 2048
}
],
"voice": { "id": "vce_8c1e5a3f9b7d2046e1c3a5b7", "name": "Anna" },
"createdAt": "2026-10-01T09:30:00.000Z"
}
}| Field | Rules |
|---|---|
name | 1–60 characters on one line. |
handle | 2–30 characters of a-z, 0-9 and _, starting with a letter. Unique among the account’s characters and influencers. Names such as image1, video2, audio3, element1 and voice1 are reserved. |
photos | 1–4 URLs of your own pictures (uploads, or image outputs of your generations): PNG, JPEG or WebP, at least 300 px on each side and no more elongated than 2.5:1, all of the same person. Salven keeps its own copies. |
voice | Optional. One of your saved voices (GET /voices): the character then speaks in it on the models that take voices. An id that is not yours is refused with voice_unknown. |
There is no Idempotency-Key here: the handle is the key. A create retried after a lost answer is refused with 409 handle_taken if the first one went through, and GET /characters/{handle} then shows it. An account may have up to 50 characters (409 limit_reached). Editing and deleting are done on the characters page.
Using one: only a model whose spec says characters.supported: true takes characters, at most characters.maxCharacters in one run, and with characters.needsStartFrame: true a start frame must come too; characters.onlyWith names the only values of another param they go with (Kling 4.0 takes them on its regular version, without a start frame). On any other model a mention of your own handle is refused with character_not_supported, and nothing is charged. A handle the account does not have is not an error: it stays plain text and brings no photos, so take handles from GET /characters and check the finished generation’s characters. A character with a saved voice (voice when it was made, or one set on salven.ai) speaks in it on Kling 4.0’s regular version and on Seedance 2.5, with nothing to send; one run carries at most three voices, each character’s counting (voice_too_many).
Saved voices
A saved voice is a recording of one person speaking, 10 to 60 seconds long, saved on the account under a name. Speech models made for it read any text in that voice, and some video models make a person in the clip speak in it. Saving one is free, and a voice changes no price.
GET /voicesanswers{ "voices": [ … ] }, newest first. Each hasid,name,language,seconds,transcript(ornull),recordingUrl(Salven’s MP3 copy of the recording) andcreatedAt.POST /voicessaves a voice. It is free and answers201with{ "voice": { … } }.
{
"name": "Anna",
"language": "en",
"recording": "https://cdn.salven.ai/uploads/cmg1x2y3z0000abcd1234efgh/1790847120000-5e8a1c3d7f9b2460.mp3",
"transcript": "Hi, it's Anna. I'm recording this so my voice can read things for me."
}{
"voice": {
"id": "vce_8c1e5a3f9b7d2046e1c3a5b7",
"name": "Anna",
"language": "en",
"seconds": 18.4,
"transcript": "Hi, it's Anna. I'm recording this so my voice can read things for me.",
"recordingUrl": "https://cdn.salven.ai/voices/vce_8c1e5a3f9b7d2046e1c3a5b7/sample.mp3",
"createdAt": "2026-10-30T09:30:00.000Z"
}
}| Field | Rules |
|---|---|
name | 1–40 characters on one line. |
language | The language spoken in the recording: ru, en, es, fr, de, it, pt, zh, ja or ko. |
recording | The URL of your own audio file (an upload, or an audio output of your generations): MP3, WAV, M4A or OGG, 10 to 60 seconds of one person speaking clearly, without music or other voices. Salven keeps its own copy. |
transcript | Optional, up to 1000 characters: what the recording says, word for word. Some models read it. |
There is no Idempotency-Key here: a save retried after a lost answer may save the voice twice, so look at GET /voices first. An account may have up to 20 voices (409 limit_reached). Renaming and deleting are done on salven.ai, in the voice menu of a speech model.
Speech in a saved voice: a model whose spec says voices.min: 1 (Qwen3-TTS, Fish Audio Drama 3, HeyGen Voice) speaks the prompt in exactly one of your voices, sent in voices; without one it answers voice_required. The prompt is the line to say, word for word, as on every speech model.
{
"model": "my-voice-qwen3",
"prompt": "Good morning! Here is what's new on the site this week.",
"voices": ["vce_8c1e5a3f9b7d2046e1c3a5b7"],
"dryRun": true
}Your recording in a saved voice: the voice changer (voice-changer, mode voice-change, once GET /models lists it) takes your recording in inputs.sourceAudio, from 1 second to 5 minutes, and exactly one voice in voices, with no prompt. It returns the same words, pauses and intonation in that voice. It is billed per second of the recording; params.hd: true uses ChatterboxHD and 48 kHz audio at the same price.
A clip that speaks in it: a model whose spec says voices.promptTags: true (Seedance 2.5, and Kling 4.0’s regular version) takes up to voices.max voices. Send their ids in voices and name each in the prompt as @Voice1, @Voice2… in the order sent; a tag with no voice behind it is refused with reference_tag. A character’s own voice counts toward the same voices.max. Whether the clip really sounds like the recording is up to the model, so listen to it before you promise it. The finished generation’s voices lists the voices the request named.
A voice from a description
Without a recording, a voice can be made up from a description: POST /voices/design makes a trial recording of a text read in a voice that Qwen3-TTS Voice Design makes up from the description. It is a paid run like a generation: dryRun: true quotes it, an Idempotency-Key makes a retry safe, and the answer is 202 with { "generations": [ … ], "charged", "balance" }, as from POST /generations. Poll GET /generations/{id} until it is done, usually 20–30 seconds; its outputs hold the recording.
{
"description": "A low, slightly husky male voice, calm and unhurried, like a late-night radio host",
"text": "Good evening, and welcome back. The city is quiet now, the rain has stopped, and the last train has gone. Stay with me for an hour: there is music, there are a few stories, and there is no hurry at all.",
"language": "en"
}| Field | Rules |
|---|---|
description | 1–500 characters in any language: what the voice sounds like — gender, age, timbre, manner, mood. |
text | 150–500 characters in language: what the voice reads, about 10–30 seconds of speech. It becomes the saved voice’s transcript. |
language | The text’s language: ru, en, es, fr, de, it, pt, zh, ja or ko. |
dryRun | Optional. true answers the price and the balance and starts nothing. |
Listen to the recording before you keep it. To keep it, POST /voices with its output URL as recording, the same language and the text as transcript; from then on it is a saved voice like any other. If it does not sound right, make another one with another description: each recording is paid, saving a voice is not.
Effects
Effects are the ready-made looks from the effects page: Salven fixed the model, the prompt and the settings, and you give the picture or pictures and, for some, a line of text. A run of an effect is an ordinary generation.
GET /effects answers { "effects": [ … ] } in the page’s order. GET /effects/{slug} answers one effect, or 404 effect_not_found:
{
"slug": "wide-angle",
"name": "Wide Angle",
"description": "…",
"kind": "image",
"category": { "slug": "objects", "name": "Objects" },
"preview": {
"videoUrl": null,
"posterUrl": "https://cdn.salven.ai/effects/cmg2a3b4c0002abcd3456qrst/preview.jpg",
"width": 1024,
"height": 1365
},
"inputs": [
{
"key": "object",
"label": "Object",
"hint": "…",
"required": true,
"exampleUrl": "https://cdn.salven.ai/effects/cmg2a3b4c0002abcd3456qrst/object.jpg"
}
],
"text": null,
"pricing": { "unit": "image", "credits": 1.4 },
"quality": {
"default": "1K",
"options": [
{ "value": "1K", "label": "1K", "pricing": { "unit": "image", "credits": 1.4 } },
{ "value": "2K", "label": "2K", "pricing": { "unit": "image", "credits": 1.9 } },
{ "value": "4K", "label": "4K", "pricing": { "unit": "image", "credits": 2.8 } }
]
},
"maxCount": 4
}| Field | Meaning |
|---|---|
inputs | The pictures the effect takes, each sent by its key. required says which must be there; exampleUrl is a sample. |
text | null, or the effect’s text field: label, placeholder, maxLength, required. |
pricing | An image effect: credits per picture. A video effect: unit is run, and durations lists each length offered with its seconds and credits. |
quality | null, or the choice the effect offers: default, and options, each with its value, label and own pricing. The top-level pricing is the default’s. |
maxCount | How many pictures one request may make. |
{
"inputs": {
"object": "https://cdn.salven.ai/uploads/cmg1x2y3z0000abcd1234efgh/1790847000000-9f2c4e6a8b0d1f35.jpg"
},
"quality": "4K",
"count": 2,
"language": "en",
"expectedCost": 2.8,
"dryRun": false
}| Field | Meaning |
|---|---|
inputs | Input key → URL of your own picture (PNG, JPEG or WebP). |
text | Only when the effect has text; required when text.required is true. |
duration | Video effects: one of pricing.durations[].seconds. Default: the first. |
quality | Only when the effect has quality: one of quality.options[].value, exactly as written. Default: quality.default. |
count | Image effects: 1 to maxCount pictures, each charged. A video effect always makes one run. |
language | en or ru: the language of the effect’s name the run is recorded under in the gallery. Default en. |
expectedCost | Optional. One run’s price in credits as quoted (the dry run’s cost, e.g. 1.4); if the price is different now, the answer is 409 price_changed. |
dryRun | true: answer the price, start nothing. |
A dry run takes the same body, pictures included, and answers cost (one run), count, total, quality and balance. A run answers 202 like POST /generations, with the same Idempotency-Key rules; poll each generation with GET /generations/{id}. The usual order is to quote with dryRun, then send the same body with expectedCost set to the quoted cost.
Models
The API offers exactly the models the studio offers now, in the studio’s order. This table is read from the live catalogue. The price is the base price, before a mode’s multiplier and any priced option.
Image
| Model id | Name | Modes | Base price |
|---|---|---|---|
nano-banana-2-1 | Nano Banana 2.1 | generate | 1 credits / generation |
nano-banana-2 | Nano Banana 2 | generate | 1.4 credits / generation |
nano-banana-pro | Nano Banana Pro | generate | 2.4 credits / generation |
seedream-v5-pro | Seedream v5 Pro | generate | 1.6 credits / generation |
gpt-image-2-5 | GPT Image 2.5 | generate | 0.9 credits / generation |
gpt-image-2 | GPT Image 2 | generate | 4.8 credits / generation |
flux-kontext | FLUX.1 Kontext | generate | 1 credits / generation |
recraft-v4 | Recraft V4 | generate | 1 credits / generation |
seedream-4 | Seedream 4 | generate | 0.7 credits / generation |
qwen-image-3 | Qwen Image 3 | generate | 1 credits / generation |
ideogram-v4 | Ideogram v4 | generate | 0.9 credits / generation |
grok-imagine-2 | Grok Imagine 2 | generate | 1.4 credits / generation |
virtual-try-on | Virtual Try-On | try-on | 1.8 credits / generation |
image-upscaler | Image Upscaler | upscale | 1 credits / generation |
Video
| Model id | Name | Modes | Base price |
|---|---|---|---|
wan-3 | Wan 3.0 | generate | 2.3 credits / second |
kling-v4 | Kling 4.0 | generate | 3.6 credits / second |
seedance-2-5 | Seedance 2.5 | generate | 8.8 credits / second |
kling-v3-pro | Kling v3 Pro | generate | 2.6 credits / second |
veo-3 | Google Veo 3 | generate | 22 credits / generation |
kling-v3-pro-motion-control | Kling v3 Pro · Motion Control | motion-control | 3.9 credits / second |
video-upscaler | Video Upscaler · RealESRGAN | upscale | 1.7 credits / second |
veo-3-1-fast | Google Veo 3.1 Fast | generate | 17 credits / generation |
hailuo-02 | MiniMax Hailuo 02 | generate | 6.2 credits / generation |
seedance-v1-pro | Seedance v1 Pro | generate | 15 credits / generation |
minimax-h3-max-turbo | MiniMax H3-Max Turbo | generate | 1 credits / second |
minimax-h3-max | MiniMax H3-Max | generate | 1.9 credits / second |
lipsync-photo | Lipsync — from a photo | lipsync | 3.2 credits / second |
lipsync-redub | Lipsync — redub a clip | lipsync | 1.6 credits / second |
video-to-audio | Sound for video | sound | 0.8 credits / generation |
seedance-2-0 | Seedance 2.0 | generate | 5 credits / second |
seedance-2-0-4k | Seedance 2.0 4K | generate | 17.7 credits / second |
Audio
| Model id | Name | Modes | Base price |
|---|---|---|---|
elevenlabs-tts-v4 | ElevenLabs v4 | tts | 1.9 credits / 1000 characters |
minimax-speech-hd | MiniMax Speech 2.8 HD | tts | 2.3 credits / 1000 characters |
minimax-speech-turbo | MiniMax Speech 2.8 Turbo | tts | 1.4 credits / 1000 characters |
my-voice-qwen3 | Qwen3-TTS | tts | 2.1 credits / generation |
my-voice-fish | Fish Audio Drama 3 | tts | 0.7 credits / 1000 characters |
my-voice-heygen | HeyGen Voice | tts | 0.7 credits / 1000 characters |
voice-changer | Chatterbox | voice-change | 0.01 credits / second |
gemini-3-8-flash-tts | Gemini 3.8 Flash TTS | tts | 1.1 credits / 1000 characters |
gemini-3-8-flash-lite-tts | Gemini 3.8 Flash Lite TTS | tts | 0.7 credits / 1000 characters |
elevenlabs-tts-v3 | ElevenLabs v3 | tts | 2.3 credits / 1000 characters |
elevenlabs-tts-multilingual-v2 | ElevenLabs Multilingual v2 | tts | 2.3 credits / 1000 characters |
elevenlabs-tts-turbo-v2-5 | ElevenLabs Turbo v2.5 | tts | 1.2 credits / 1000 characters |
xai-tts | xAI TTS | tts | 0.4 credits / 1000 characters |
inworld-tts-1-5-max | Inworld TTS 1.5 Max | tts | 0.3 credits / 1000 characters |
lyria-3-pro | Lyria 3 Pro | music | 1.9 credits / generation |
stable-audio-3 | Stable Audio 3 | music | 0.9 credits / generation |
minimax-music | MiniMax Music 2.6 | music | 3.5 credits / generation |
lyria-3 | Lyria 3 | music | 1 credits / generation |
elevenlabs-music-v2-5 | ElevenLabs Music v2.5 | music | 14 credits / generation |
elevenlabs-sfx | ElevenLabs Sound Effects | sfx | 0.3 credits / generation |
sonilo-sfx | Sonilo Sound Effects | sfx | 0.5 credits / generation |
3D
| Model id | Name | Modes | Base price |
|---|---|---|---|
tripo-h3-1 | Tripo H3.1 | image | 6.9 credits / generation |
tripo-p2 | Tripo P2 | image | 25 credits / generation |
hunyuan-3d-pro | Hunyuan 3D Pro | image | 8.6 credits / generation |
rodin-2-5 | Rodin 2.5 | image | 9.1 credits / generation |
trellis-2 | Trellis 2 | image | 2.3 credits / generation |
tripo-h3-1-text | Tripo H3.1 | text | 4.6 credits / generation |
rodin-2-5-text | Rodin 2.5 | text | 9.1 credits / generation |
meshy-7-views | Meshy V7 | views | 28 credits / generation |
meshy-rigging | Meshy Rigging | animate | 4.6 credits / generation |
meshy-rigging-multi | Meshy Rigging Multi-Animation | animate | 4.6 credits / generation |
The list changes: models are added and hidden. A program should read GET /models instead of keeping its own copy, and take the price of a request from its dry run.
Prompts and seeds
Write prompts in any language. By default (enhancePrompt: true) Salven translates the prompt to English and rewrites it for the model. An image or video prompt comes back as at most about 80 English words; a long, detailed English prompt is kept nearly as it is.
enhancePrompt: false sends the prompt exactly as written, and the negative prompt too. Use it for a carefully written English prompt, for long lyrics, or for a precise edit order such as “make the jacket red, change nothing else”.
- Text in the picture. Put the words to be written in the picture in quotes, for example
a cake with the inscription «Happy birthday». - Speech. A speech model (mode
tts) reads the prompt verbatim, in its own language; it is never translated. The whole prompt is spoken and billed, so send only the words to say. - Music and sound. The prompt comes back as a short English description. Words to be sung or spoken stay as written, in quotes.
- 3D models and pure editors. The prompt is only translated, never embellished. An image model that takes reference pictures is not a pure editor: its prompt is rewritten like any other, so send a precise edit order in English with
enhancePrompt: false. - Negative prompts. Only on models whose spec has
negativePrompt. By default it is translated to English and never embellished. - Length. Stay within the model’s
prompt.maxLength, or the request is refused withprompt_too_long(withmaxandlength). - Characters. An
@handleof one of your characters survives translation and rewriting. Any other@wordis ordinary text. See Characters.
Translation is best-effort: when it cannot run, the prompt goes as typed. For image, video and 3D models English is the safe choice.
Seeds
A seed is the number the model starts its randomness from. The same seed with the same prompt, inputs and params gives the same result again, or very nearly, so a result you liked can be repeated with one thing changed.
- Only a model whose spec says
"seed": truetakes one, from 0 toseedMax. On any other model it is refused withseed_not_supported, and outside the range withinvalid_seed. - A seed never changes the price.
countwith a seed runs seed, seed + 1, seed + 2 and so on.- For an exact repeat, keep
enhancePromptthe same as in the first run;falseis the surest. - A finished generation’s
seedsays what it ran with.
Idempotency
Starting a generation charges, so it must not happen twice by accident. When an answer is lost (a timeout, a dropped connection, a 5xx), you cannot tell whether the run started. The Idempotency-Key header makes the retry safe: the same request with the same key returns the first answer instead of starting again.
- Send it with
POST /generationsandPOST /effects/{slug}/run. - The key is 1–128 characters of
A-Z,a-z,0-9,.,_,:and-; anything else is refused with 400invalid_idempotency_key. A UUID works. - Use a new key for every new generation, and the same key for every retry of it.
- A replayed answer carries the header
Idempotent-Replayed: true. - Only an answer after which generations started (a
202) is remembered. A refusal such as a400,402 insufficient_creditsor429 too_many_runningis not: once its cause is gone, the same request with the same key runs. - A
202is final for its key, a partial one included: repeating it replays that answer and starts nothing. - Keys are remembered for 24 hours.
- A dry run is never remembered or replayed: it only quotes.
| HTTP | Code | Meaning |
|---|---|---|
| 422 | idempotency_key_reused | The key was already used with a different body. Use a new key for a different request. |
| 409 | idempotency_in_progress | The first request with this key is still being handled. Wait a few seconds and repeat it with the same key. |
If the 409 keeps coming for more than a few minutes, the first attempt may have died after starting. Look at GET /generations for runs made since, and send the request under a new key only if nothing started.
Errors
Every error the API itself answers has one shape. Branch on code: it is stable. message is English and written for people. The other fields are details of that code. An answer that did not come from the API’s own code has no such body: the web server’s 413 for a request body over 25 MB, a bare 502 or 504 while the server restarts, or a 5xx with no code. Check the Content-Type before parsing an error body, and handle a 5xx without a code as the last row of the table says.
{
"error": {
"code": "insufficient_credits",
"message": "Not enough credits for this generation",
"required": 1.9,
"available": 0.6
}
}A refused request charges nothing. A generation that fails later, with status failed, returns its credits.
| HTTP | Code | What to do |
|---|---|---|
| 400 | invalid_json, invalid_request, invalid_prompt, invalid_params, invalid_inputs | Fix the body; the message names the field. |
| 400 | invalid_idempotency_key | Fix the header. See Idempotency. |
| 400 | invalid_kind, invalid_cursor | Fix the query of GET /models or GET /generations. |
| 400 | unknown_param, invalid_param_value, unknown_input, input_kind, input_missing, input_requires, input_takes_one_file, invalid_input_url, too_many_files, aspect_not_supported, invalid_aspect, prompt_required, negative_prompt_not_supported, count_too_large | Read the model’s spec again. The details say what is allowed: allowed, expected, max, input, param. |
| 400 | input_not_allowed | The URL is not your own file. Upload it with POST /uploads first. |
| 400 | prompt_too_long, negative_prompt_too_long | Shorten the text to max characters. |
| 400 | seed_not_supported, invalid_seed | Leave seed out for this model, or keep it within 0..seedMax. |
| 400 | media_missing, media_unreadable, media_foreign, media_length, media_resolution, reference_* | An input file cannot be read, or breaks the model’s limits: its length, its size (media_resolution: max is the largest short side in pixels), its format. |
| 400 | model_too_dense, model_untextured, model_unreadable, model_foreign, model_missing | The 3D model to animate cannot be rigged: too many faces, no texture, not a readable GLB, or not your file. |
| 400 | character_not_supported, character_too_many, character_needs_start_frame, character_with_frame, character_with_extend, character_no_room, character_no_photos | The prompt names your characters and the request cannot run with them. Choose a model whose spec takes characters, name fewer handles, or follow the message. See Characters. |
| 400 | voice_required, voice_too_many, voice_unknown, voice_not_supported, invalid_voices | Saved voices: send one id on a model whose voices.min is 1, name fewer voices or characters (each character’s voice counts), take ids from GET /voices (a character’s voice may have been deleted meanwhile), or choose a model, or a version of one (requires), that takes voices. Nothing is charged. See Saved voices. |
| 400 | name_invalid, handle_invalid, handle_reserved, photos_required, too_many_photos, photo_foreign, photo_unreadable, photo_too_small, photo_aspect, voice_unknown | POST /characters: fix the field. A photo refusal names the photo (index, photo); voice_unknown: voice is not one of your saved voices. |
| 400 | language_invalid, transcript_too_long, sample_foreign, sample_unreadable, sample_too_short, sample_too_long | POST /voices: fix the field. The recording must be your own audio file on Salven, 10 to 60 seconds long; name_invalid here is a name that is empty or over 40 characters. |
| 400 | design_description, design_text, language_invalid | POST /voices/design: fix the field: description 1–500 characters, text 150–500, language one of the voice languages. Nothing is charged. |
| 400 | text_required, text_too_long, duration_value, quality_value | Effects: read the effect’s spec again. |
| 400, 413, 415 | missing_file, invalid_form_data, file_too_large, unsupported_format, heic_not_supported | Uploads: see the upload rules. |
| 401 | unauthorized | The key is missing, wrong or revoked. |
| 402 | insufficient_credits | Compare required with available. Top up on salven.ai; do not retry. |
| 403 | api_not_available | The API is not open to this account. |
| 404 | model_not_found, mode_not_available, not_found, effect_not_found, character_not_found | Check the id against the matching list: GET /models, GET /generations, GET /effects, GET /characters. The details of mode_not_available list the model’s modes. |
| 404 | design_unavailable | POST /voices/design is switched off for now. |
| 409 | handle_taken, limit_reached | POST /characters: the handle is in use (or this was a retry and the first create went through), or the account has as many characters as it may (50). POST /voices: the account has as many saved voices as it may (20). |
| 409 | price_changed | Effects: the price changed since the quote. cost is one run’s price now. Quote again. |
| 409 | idempotency_in_progress | Wait a few seconds and repeat with the same key. See Idempotency. |
| 422 | idempotency_key_reused | Use a new Idempotency-Key for a different request. |
| 429 | rate_limited | Wait retryAfter seconds; the Retry-After header says the same. |
| 429 | too_many_running | running of your API runs are queued or being made, and this request would pass the ceiling. Wait for some to finish, then repeat it; the same Idempotency-Key is fine. |
| 5xx | internal_error, storage_error, uploads_disabled, or no code at all | Retry once after a short pause, with the same Idempotency-Key. Before retrying an expensive request, look at GET /generations for a run made in the last minute. A 502 or 504 whose body is not JSON comes from the web server during a restart and may have cut the request off part-way: the retry with the same key then answers the first result, or a 409. |
The complete list, with every detail field, is in SKILL.md.
Limits
| What | Limit |
|---|---|
| Requests | 120 per minute per key, reads included |
Generation requests: POST /generations, POST /effects/{slug}/run and POST /voices/design, dry runs included | 30 per minute per account |
Recordings of a voice from a description: POST /voices/design and the MCP tool design_voice, the site’s voice window included | 10 per minute per account |
| Uploads | 30 per minute per account, POST /uploads and the MCP tool upload_from_url together |
| Character creations | 20 per minute per account |
| Voice saves | 20 per minute per account |
| API runs queued or being made at once | 10 per account, generations, effects and voice recordings together |
| JSON body | 64 KB |
wait | Up to 60 seconds |
limit of GET /generations | 20 by default, up to 50 |
count | Up to the model’s maxCount |
| Prompt | The model’s prompt.maxLength, never more than 10 000 characters; a negative prompt never more than 4000 |
Size of a file you send: POST /uploads, an upload link | Images 10 MB, audio 20 MB, video and GLB models 25 MB |
Size of a file copied from a web address: the MCP tool upload_from_url | Images 10 MB, audio 20 MB, video and GLB models 60 MB |
| Idempotency keys | Remembered for 24 hours |
| API keys | 10 live keys per account |
| Characters | 50 per account, 1–4 photos each |
| Saved voices | 20 per account, 10–60 seconds each |
Over REST, passing a rate limit answers 429 rate_limited with retryAfter in seconds and a Retry-After header. A request whose count would take the open runs past 10 answers 429 too_many_running with running, the number open now.
For agents: SKILL.md
SKILL.md is the instruction file for coding agents such as Claude Code and Cursor. It is the complete reference of this API, written for an agent to read: every endpoint and refusal code, the rules for reference media, speech and 3D rigging, and how to behave with someone else’s credits (quote first, never loop on failures, never print the key).
- Download it: salven.ai/api/skill
- Read it in the browser: salven.ai/api/skill?view=1
- Make a key in Settings and set it as
SALVEN_API_KEYin the environment the agent runs in. - Give the agent the file. In Claude Code, save it as
.claude/skills/salven-api/SKILL.mdin a project, or as~/.claude/skills/salven-api/SKILL.mdfor every project. For another agent, add the file to its instructions or point it at the address above. - Ask for what you want. The agent reads the catalogue, quotes the price and runs the request.
An agent that speaks MCP does not need the file or a key: add the server from Connect Claude instead.
A Python helper
SKILL.md ends with a small Python client: it quotes, asks for a yes, runs with an Idempotency-Key, waits, and retries through rate limits, busy slots and restarts. It needs the requests package.
import os, time, uuid, requests
API = "https://salven.ai/api/v1"
H = {"Authorization": f"Bearer {os.environ['SALVEN_API_KEY']}"}
def upload(path):
with open(path, "rb") as f:
r = requests.post(f"{API}/uploads", headers=H, files={"file": f})
r.raise_for_status()
return r.json()["url"]
class StillInProgress(RuntimeError):
"""The request may have started: check GET /generations before sending it under a new key."""
def call(method, url, **kw):
"""One request, retried through rate limits, busy slots, restarts and one server error.
Every retry sends the same arguments, so a POST keeps its Idempotency-Key."""
busy_since, server_errors = None, 0
for attempt in range(80):
try:
r = requests.request(method, url, timeout=(10, 150), **kw)
except (requests.ConnectionError, requests.Timeout, requests.exceptions.ChunkedEncodingError):
time.sleep(10) # a restart, a dropped link: same request again
continue
if r.status_code == 409: # idempotency_in_progress
busy_since = busy_since or time.time()
if time.time() - busy_since > 300:
raise StillInProgress(url)
time.sleep(10)
continue
if r.status_code == 429:
code = r.json().get("error", {}).get("code") if "json" in r.headers.get("content-type", "") else None
time.sleep(60 if code == "too_many_running" else int(r.headers.get("Retry-After", 10)))
continue
if r.status_code >= 500 and server_errors == 0:
server_errors += 1
time.sleep(10)
continue
r.raise_for_status()
return r.json()
raise RuntimeError(f"{method} {url}: gave up")
MAX_WAIT_MIN = {"image": 5, "audio": 10, "video": 20, "3d": 60} # past this a run counts as stuck
def generate(body, confirm):
"""Quote, ask, run, wait. Returns the output URLs and what did not finish."""
quote = call("POST", f"{API}/generations", headers=H, json={**body, "dryRun": True})
if quote["total"] > quote["balance"]:
raise RuntimeError(f"needs {quote['total']} credits, the balance is {quote['balance']}")
if not confirm(quote["total"]): # total = cost × count
return None
kind = call("GET", f"{API}/models/{body['model']}", headers=H)["kind"]
key = str(uuid.uuid4()) # one key for this request and all its retries
started = call("POST", f"{API}/generations", headers={**H, "Idempotency-Key": key}, json=body)
result = {"urls": [], "failed": [], "stuck": [], "stopped": (started.get("stopped") or {}).get("code")}
deadline = time.time() + MAX_WAIT_MIN[kind] * 60
for gen in started["generations"]:
while gen["status"] in ("queued", "processing") and time.time() < deadline:
gen = call("GET", f"{API}/generations/{gen['id']}", headers=H, params={"wait": 60})
if gen["status"] == "done":
result["urls"] += [o["url"] for o in gen["outputs"]]
elif gen["status"] == "failed":
result["failed"].append(gen["id"]) # its credits were returned
else:
result["stuck"].append(gen["id"]) # tell the user; do not start it again
return result # "stopped": why fewer than `count` started, if so
start = upload("portrait.jpg")
print(generate({"model": "kling-v3-pro", "prompt": "she laughs and looks at the camera",
"params": {"duration": 5}, "inputs": {"startImage": start}},
confirm=lambda credits: input(f"Spend {credits} credits? [y/N] ").strip().lower() == "y"))Good to know
The API does not do the following. They are done on salven.ai:
- publishing a work to the community;
- deleting works;
- editing or deleting characters (the characters page);
- renaming or deleting saved voices;
- making AI influencers (an existing one is used by its
@handle, like a character); - topping up the balance;
- Kling’s multi-shot clips.
Output URLs are unguessable but public. Anyone who has the link can open the file, so share it only with the people who should see it.
Characters and voices of real people. Make a character only from photos of yourself or of people who agreed to it, and a voice only from such recordings, and never use either to pass a real person off as saying or doing something they did not.
Questions and problems: Support, or write to support@salven.ai.