Quickstart
Get a key, create a task, collect the result. The whole path, end to end.
SpicyAPI puts image and video generation behind one authentication, task and result protocol, and serves text models in the OpenAI, Anthropic and Gemini formats you already use. Keep the same call flow when switching models, and build input from that model's inputSchema (the model's list of accepted fields, their types and allowed values).
This guide uses the native task API at https://api.spicyapi.ai/api/v1. Text and video compatibility endpoints use https://api.spicyapi.ai/v1. Both accept the same API key, but their request and response formats differ.
Follow this page to choose a model and retrieve a result. To see an exact price before you pay, see Quotes and compatibility. For chat, see Text and streaming. Before deploying your integration, work through Production integration; use Troubleshooting when a step fails. Unfamiliar words are explained in the glossary.
Want to see a result before writing code?
Open any model on spicyapi.ai/explore and use its Playground tab; its API tab shows the same request as a command you can copy. To work from a terminal without writing code, use the CLI, or let an AI assistant do it through the MCP server.
Official tools
Calling the HTTP API directly is fully supported and is what the rest of this page shows. If you would rather not write the HTTP calls yourself, there are four MIT-licensed npm packages, each installed on its own and each with its own guide:
@spicyapi/sdk
TypeScript SDK: OpenAPI types, adaptive waiting, uploads and webhook verification
@spicyapi/cli
The spicyapi command: browse the catalog, quote, create and follow tasks from a terminal
@spicyapi/mcp
MCP server that hands a coding agent the catalog, quotes and tasks, with confirmation on every billable step
@spicyapi/skill
Portable Agent Skill: the workflow and the spending boundaries, installable into your agent
Four more official SDKs cover the languages outside Node.js. Each implements the same workflow and is MIT-licensed; only the Go module installs from its repository with the usual command today, and the other three pages show the install to use until the packages are published:
Python SDK
spicyapi for Python 3.11+, with no runtime dependencies
Go SDK
github.com/spicyapi-ai/spicy-go for Go 1.23+, standard library only
PHP SDK
spicyapi/spicyapi for PHP 8.1+, with no library dependencies
Java SDK
ai.spicyapi:spicyapi-java for Java 17+, with a sealed error taxonomy
Working in ComfyUI instead? The ComfyUI nodes put every image, video and audio model on your canvas as a node, with its price shown before it runs.
Before you start
You need four things. Skipping one fails later with an error that does not say which one is missing.
- An account with a verified email. Sign up at spicyapi.ai and click the link in the verification email. Until you do, every call made with your key returns
403. - A balance. Generations are paid from a US dollar balance (there are no credits), and a new account starts at zero. Add funds under Console → Billing. Without enough balance, creating a task fails with business code
40201. Account setup walks through signing up, topping up and creating a key. - A terminal. macOS: the Terminal app. Linux: any shell. Windows: PowerShell. Where commands differ, each step has a Windows PowerShell tab. If you prefer, the macOS / Linux commands also run unchanged in WSL or Git Bash on Windows.
- A few cents. Step 3 creates one real image task, which is charged when it succeeds. Step 2 shows each model's price, and an optional command there tells you the exact amount first, for free.
Evaluating first: models and prices without an account
You do not need a key, or an account, to see what exists and what it costs. The public catalogue behind the marketing site is anonymously readable, and this command needs no substitution at all:
curl "https://api.spicyapi.ai/console/v1/catalog/models?locale=en"The envelope is the same one described at the bottom of this page. Under data.items, each entry carries:
model— the exact value you will later send asmodel(slugholds the same value);callable— whether the model can run right now. Pick items where it istrue:statusis our operating intent, and alivemodel can still be temporarily not callable;modalityandtasks— what kind of output it makes;price— the cheapest tier:amountin USD perunit(per_image,per_second,per_requestorper_1k_tokens), plustiers, the number of tiers behind it. For text models, readprice.perMillionTokens, which gives separate input, cached-input and output prices per million tokens;amounton its own is only the output rate.
Append a model ID for one model's full record, including its inputSchema, paramNotes and worked examples:
curl "https://api.spicyapi.ai/console/v1/catalog/models/MODEL_ID_FROM_CATALOG?locale=en"MODEL_ID_FROM_CATALOG is a placeholder used throughout these docs, not a real model: always copy a real ID from the live catalog.
locale defaults to en; pass ja, ko, de, fr, es, pt-BR, ru, it or pl for translated names, summaries and parameter notes. Any other value returns 400. strict=1 drops items whose text is not published in that locale instead of falling back to English; you normally do not need it. This surface is rate-limited by source address rather than by account, and it has its own bucket — see Rate limits. It is also cached, so a price change can take a while to appear here.
When tiers is greater than 1, the listed price is the cheapest tier, not the price of your settings. price.variant shows which settings that cheapest tier uses (for example 720p, or generate_audio=false;resolution=480p when several fields combine), and the single-model record's inputSchema["x-pricing"].variantFields, when present, lists every field that selects a tier. The record's pricingNote is a general reminder that the live estimate is authoritative; it does not list fields. For the exact amount of one request, use POST /api/v1/jobs/quote once you have a key.
Public catalogue for evaluating, authenticated catalogue for calling
The public catalogue answers "what is on sale and what does it list at". It cannot answer "what may my key call" or "what will my account be charged", because both are per-account. Once you have a key, switch to GET /api/v1/models in step 2 and treat that as the only source for callable IDs — a remembered ID, a launch snapshot or a marketing page is not a fallback.
Five steps
Get a key
An API key is the password your code sends with every request. It identifies your account and is how you are billed.
- Open Console → API keys and click Create key.
- Give it a name, for example
my-first-test. Leave everything under Advanced as it is. - Copy the key that starts with
sk-spicy-, or click Save as a file. It is shown exactly once; if you lose it, you create a new one. - In the terminal you will use for the next steps, store it in an environment variable (a named value the terminal hands to every command it runs), so the key never has to be typed into a command:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"
# Check: this should print sk-spicy-
echo "${SPICY_API_KEY:0:9}"What you should see: sk-spicy-. The value lasts until you close that terminal window, so run every later step in the same window.
If there is no Create key button, your role in the current team cannot create keys: ask the team owner, or use Start your own team on the same page. New keys have no spend cap unless you set one; a daily cap in the console gives a misconfiguration or a leak a ceiling. See Authentication.
Choose a callable model
The authenticated catalog is the only source for callable model IDs, current schemas and examples. Take it in two calls: the list is light and tells you what you may call, then one record gives you the schema for the model you picked. Reading the catalog is free.
curl "https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image" \
-H "Authorization: Bearer $SPICY_API_KEY"The response is long. The part you need looks like this (trimmed):
{
"code": 200,
"msg": "success",
"data": {
"total": 1,
"items": [
{
"model": "MODEL_ID_FROM_CATALOG",
"displayName": "…",
"modality": "image",
"tasks": ["text-to-image"],
"enabled": true,
"available": true,
"startingPrice": { "variant": "", "unit": "per_image", "price": "0.008", "currency": "USD" }
}
]
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}Pick an item whose enabled and available are both true, note its price, copy its model value, and keep it for the rest of this page. With jq installed, curl … | jq -r '.data.items[] | select(.enabled and .available) | .model' prints only the IDs you can call.
# Replace MODEL_ID_FROM_CATALOG with the model value you copied
export SPICY_MODEL="MODEL_ID_FROM_CATALOG"curl "https://api.spicyapi.ai/api/v1/models/${SPICY_MODEL}" \
-H "Authorization: Bearer $SPICY_API_KEY"Slashes in the ID pass through the path unchanged; percent-encode them only if your HTTP client insists a path parameter is a single segment. The record's inputSchema is the authority for input: required lists the fields you must send, and properties describes each one. examples holds inputs the server has already validated. How to read the rest of the record, including pricing, is in Model catalog and schemas.
Ask for schemas only where you need them. Adding includeSchema=1&includeExamples=1 to the list call returns every enabled model's full JSON Schema in one response, which is a much larger body. Catalog reads use the account’s read allowance, independently of task creation. Do not re-read the catalog before every task either; see Caching the catalog.
If no item matches, stop before creating a paid task. A launch snapshot, marketing page or remembered model ID is not a fallback for an empty live catalog. To select the model from code instead of by hand, see Selecting the model from code.
Optional: see the exact price first. A quote is free, creates nothing and reserves nothing. data.estimatedCost is what this exact request will cost at most, in USD:
curl -X POST https://api.spicyapi.ai/api/v1/jobs/quote \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
--data "{\"model\":\"${SPICY_MODEL}\",\"input\":{\"prompt\":\"A cinematic night portrait\"}}"Create a task
A task is a generation job that runs in the background: you get its ID immediately and the result later. This step costs money.
Every new task gets a new idempotency key: a random ID you attach so that sending the same request twice, for example after a network error, never creates two tasks. Generate it in its own command, then send the task:
SPICY_IDEMPOTENCY_KEY="$(uuidgen)"
# Linux without uuidgen: SPICY_IDEMPOTENCY_KEY="$(cat /proc/sys/kernel/random/uuid)"curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data "{\"model\":\"${SPICY_MODEL}\",\"input\":{\"prompt\":\"A cinematic night portrait\"}}"If 3b times out or your connection drops, run 3b again without running 3a: the same key returns the task you already started instead of paying twice. To make a different image, run 3a first. A 409 response means you reused a key with a different prompt or model. More in Idempotency.
This sends one field. If the schema you read in step 2 marks anything else as required, add it — what may go in input is defined by each model's own schema, and one unknown field is a 400.
The connection is never held open. You get a task ID back immediately:
HTTP/1.1 202 Accepted{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued",
"estimatedCost": "0.008",
"deadlineAt": "2026-08-28T09:22:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}On macOS / Linux, copy taskId from the response and keep it (PowerShell already did this for you):
export SPICY_TASK_ID="paste-the-taskId-here"202 means accepted, not completed
A newly created task has state: "queued"; a safe replay with the same Idempotency-Key returns the original task in its current state. HTTP 202 means the request was accepted, not that generation completed. Funds are held, not charged, when the task is first accepted: estimatedCost is set aside from your balance. That hold is also the task's charge ceiling: unused funds are released and no later amount is collected above it. Failed tasks normally release the hold; certain content refusals disclosed on the model page are charged at the accepted quote. Check settled and cost. deadlineAt is the latest time the task can finish before it ends as expired. The envelope's successful business code remains 200. An accepted task cannot be cancelled.
Collect the result
If your production request includes a callBackUrl, we call you back when the task reaches a terminal state: a callback (webhook) is an HTTPS address on your server that we call when the task finishes, so you do not have to keep asking. The minimal request above omits it so it can run without a public endpoint. Callbacks must be signature-verified — see Webhooks.
Without a callback endpoint, ask for the task:
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY"Use the same API key that created the task: another key gets 404. Look at data.state:
data.state | What it means | What to do |
|---|---|---|
queued or running | Still generating. Your funds are on hold, not spent | Wait a few seconds and run the same command again. Images usually take seconds; videos can take minutes. deadlineAt is the upper bound |
succeeded | Done | Go to the next step |
failed or expired | It did not work. The hold is normally released; some content refusals explicitly disclosed on the model page are charged. Check settled and cost | Read data.errorCode (stable) and data.errorMessage (human-readable), fix the input, and start again from 3a |
The envelope's code is 200 even when the task failed: code says whether reading the task worked. The task's own outcome is always in data.state. What the response looks like at each stage (trimmed):
{
"code": 200,
"msg": "success",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "MODEL_ID_FROM_CATALOG",
"state": "running",
"cost": "0.008",
"settled": false,
"createdAt": "2026-08-28T09:12:04Z",
"deadlineAt": "2026-08-28T09:22:04Z"
},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w4"
}While settled is false, cost is the amount on hold, not the final charge.
Download the artefact
Take data.output.assets[0].url from the successful result and download it directly. Do not attach your API key. The link works for about 20 minutes (expiresAt); if it has expired, run step 4 again, because each read returns a fresh link. Generated files are deleted after at most 14 days, so keep your own copy of anything you need.
# Paste data.output.assets[0].url. Keep the double quotes: the URL contains & characters.
RESULT_URL="paste-the-url-here"
curl -L "$RESULT_URL" -o result.png # use result.mp4 when mime is video/mp4With jq installed you can skip the copy and paste:
RESULT_URL="$(curl -s "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY" | jq -r '.data.output.assets[0].url')"What you should see: a result.png file in the folder your terminal is in. Check the charge under Console → Logs, or in cost once settled is true.
Text models: one request, the answer in the response
Text models reply in the same HTTP response: no polling, no download. The X-Spicy-Task-Id response header names the task that holds the charge, if you want to check it later. In step 2, list models with modality=text instead, export one as SPICY_MODEL, then:
curl https://api.spicyapi.ai/v1/chat/completions \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
--data "{\"model\":\"${SPICY_MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in one sentence.\"}]}"The answer is in choices[0].message.content. This call is charged for the tokens it uses. The endpoint uses the OpenAI response format, not the {code, msg, data} envelope. See Text and streaming.
Selecting the model from code
The five steps above keep a person in the loop. A deployed integration picks the model itself, immediately before it submits: a model that was callable last week may be paused today, and prices are per-account. Ask for the schema and a server-validated example in the same call, then require enabled and available:
CATALOG_JSON="$(curl --fail-with-body \
"https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1" \
-H "Authorization: Bearer $SPICY_API_KEY")"
MODEL_ID="$(printf '%s' "$CATALOG_JSON" | jq -er \
'[.data.items[] | select(.enabled == true and .available == true and (.examples | length > 0))][0].model')"
MODEL_INPUT="$(printf '%s' "$CATALOG_JSON" | jq -cer \
'[.data.items[] | select(.enabled == true and .available == true and (.examples | length > 0))][0].examples[0].input')"
printf 'Selected %s\n' "$MODEL_ID"SPICY_IDEMPOTENCY_KEY="$(uuidgen)" # new task, new key; reuse it only to resend this same request
TASK_PAYLOAD="$(jq -cn \
--arg model "$MODEL_ID" \
--argjson input "$MODEL_INPUT" \
'{model: $model, input: $input}')"
curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data "$TASK_PAYLOAD"If the selection yields nothing, stop there rather than falling back to a remembered ID. The three programs below do the same selection without jq.
Putting it together
All three programs below read the catalog, create a task, poll with backoff and print a download URL. In production, use webhooks instead — polling is the fallback, not the plan.
Running these programs creates a real, paid task
Each program picks the first callable text-to-image model in your catalog (not the cheapest) and submits one of its validated examples. Check that model's price first, or change the selection to the exact model you want.
| Language | Save as | Install first | Run |
|---|---|---|---|
| Node.js 20 or later | generate.mjs | nothing | node generate.mjs |
| Python 3.9 or later | generate.py | pip install requests | python3 generate.py (Windows: py generate.py) |
| Go 1.21 or later | main.go in an empty folder | go mod init example | go run . |
All three read your key from SPICY_API_KEY (step 1) and print the result URL when the task finishes. They wait up to ten minutes, which suits image models; for video, wait until the task's deadlineAt instead.
const BASE = 'https://api.spicyapi.ai/api/v1';
const KEY = process.env.SPICY_API_KEY;
const headers = {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
};
async function call(path, init) {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: { ...headers, ...init?.headers },
signal: AbortSignal.timeout(30_000),
});
const body = await res.json();
// Branch on `code`, not on res.ok — the business code says far more
if (body.code !== 200) {
throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
}
return body.data;
}
async function generate() {
const catalog = await call(
'/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1',
);
const selected = catalog.items.find(
(item) => item.enabled && item.available && item.examples?.length,
);
if (!selected) throw new Error('No callable text-to-image model with a validated example');
const idempotencyKey = crypto.randomUUID();
const { taskId } = await call('/jobs/createTask', {
method: 'POST',
headers: { 'Idempotency-Key': idempotencyKey },
body: JSON.stringify({
model: selected.model,
input: selected.examples[0].input,
}),
});
// Start at 2s, multiply by 1.5, cap at 15s. The first check is almost
// certainly still `queued`, so a shorter start just wastes a round trip.
const deadline = Date.now() + 10 * 60_000;
let wait = 2000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, wait));
wait = Math.min(wait * 1.5, 15000);
const task = await call(`/jobs/recordInfo?taskId=${taskId}`);
if (task.state === 'succeeded') {
const asset = task.output?.assets?.find((item) => item.url);
if (asset) return asset.url;
if (task.output?.assets?.some((item) => item.pending)) continue;
throw new Error('Result is unavailable');
}
// Anything other than queued / running is terminal. This also handles
// historical canceled records and future terminal states safely.
if (task.state !== 'queued' && task.state !== 'running') {
throw new Error(`${task.state}: ${task.errorMessage ?? 'no detail'}`);
}
}
throw new Error(`task ${taskId} timed out`);
}
generate().then(console.log);import os, time, uuid, requests
BASE = "https://api.spicyapi.ai/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
"Content-Type": "application/json",
}
def call(method: str, path: str, headers=None, **kwargs) -> dict:
res = requests.request(
method, f"{BASE}{path}", headers={**HEADERS, **(headers or {})}, timeout=30, **kwargs
)
body = res.json()
# Branch on `code`, not on status_code
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')
return body["data"]
def generate() -> str:
catalog = call(
"GET",
"/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1",
)
selected = next(
(
item
for item in catalog["items"]
if item["enabled"] and item["available"] and item.get("examples")
),
None,
)
if selected is None:
raise RuntimeError("No callable text-to-image model with a validated example")
idempotency_key = str(uuid.uuid4())
task_id = call("POST", "/jobs/createTask", headers={"Idempotency-Key": idempotency_key}, json={
"model": selected["model"],
"input": selected["examples"][0]["input"],
})["taskId"]
# Start at 2s, multiply by 1.5, cap at 15s
deadline = time.monotonic() + 600
wait = 2.0
while time.monotonic() < deadline:
time.sleep(wait)
wait = min(wait * 1.5, 15.0)
task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
if task["state"] == "succeeded":
assets = task.get("output", {}).get("assets", [])
ready = next((asset for asset in assets if asset.get("url")), None)
if ready:
return ready["url"]
if any(asset.get("pending") for asset in assets):
continue
raise RuntimeError("Result is unavailable")
# Anything other than queued / running is terminal.
if task["state"] not in {"queued", "running"}:
raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
raise TimeoutError(f"task {task_id} timed out")
print(generate())package main
import (
"bytes"
"crypto/rand"
"encoding/json"
"encoding/hex"
"fmt"
"math"
"net/http"
"os"
"time"
)
const base = "https://api.spicyapi.ai/api/v1"
// envelope is the shape every endpoint returns. Data stays raw so each
// call site can decode it into whatever it actually expects.
type envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
RequestID string `json:"request_id"`
}
var client = &http.Client{Timeout: 30 * time.Second}
func newIdempotencyKey() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return "job-" + hex.EncodeToString(b)
}
func call(method, path string, body any, idempotencyKey string, out any) error {
var rdr *bytes.Reader
if body != nil {
raw, err := json.Marshal(body)
if err != nil {
return err
}
rdr = bytes.NewReader(raw)
} else {
rdr = bytes.NewReader(nil)
}
req, err := http.NewRequest(method, base+path, rdr)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("SPICY_API_KEY"))
req.Header.Set("Content-Type", "application/json")
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return err
}
// Branch on Code, not on res.StatusCode
if env.Code != 200 {
return fmt.Errorf("%d %s (request_id=%s)", env.Code, env.Msg, env.RequestID)
}
return json.Unmarshal(env.Data, out)
}
func generate() (string, error) {
var catalog struct {
Items []struct {
Model string `json:"model"`
Enabled bool `json:"enabled"`
Available bool `json:"available"`
Examples []struct {
Input map[string]any `json:"input"`
} `json:"examples"`
} `json:"items"`
}
if err := call(http.MethodGet,
"/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1",
nil, "", &catalog); err != nil {
return "", err
}
var modelID string
var modelInput map[string]any
for _, item := range catalog.Items {
if item.Enabled && item.Available && len(item.Examples) > 0 {
modelID, modelInput = item.Model, item.Examples[0].Input
break
}
}
if modelID == "" {
return "", fmt.Errorf("no callable text-to-image model with a validated example")
}
var created struct {
TaskID string `json:"taskId"`
}
err := call(http.MethodPost, "/jobs/createTask", map[string]any{
"model": modelID,
"input": modelInput,
}, newIdempotencyKey(), &created)
if err != nil {
return "", err
}
// Start at 2s, multiply by 1.5, cap at 15s
wait := 2 * time.Second
deadline := time.Now().Add(10 * time.Minute)
for time.Now().Before(deadline) {
time.Sleep(wait)
wait = time.Duration(math.Min(float64(wait)*1.5, float64(15*time.Second)))
var task struct {
State string `json:"state"`
ErrorMsg string `json:"errorMessage"`
Output struct { Assets []struct { URL string `json:"url"`; Pending bool `json:"pending"` } `json:"assets"` } `json:"output"`
}
if err := call(http.MethodGet, "/jobs/recordInfo?taskId="+created.TaskID, nil, "", &task); err != nil {
return "", err
}
if task.State == "succeeded" {
pending := false
for _, asset := range task.Output.Assets {
if asset.URL != "" { return asset.URL, nil }
pending = pending || asset.Pending
}
if pending { continue }
return "", fmt.Errorf("result is unavailable")
}
// Terminal means "not one of the two in-flight states" — do not
// enumerate the terminal states, that breaks when a new one appears
if task.State != "queued" && task.State != "running" {
return "", fmt.Errorf("%s: %s", task.State, task.ErrorMsg)
}
}
return "", fmt.Errorf("task %s timed out", created.TaskID)
}
func main() {
url, err := generate()
if err != nil {
panic(err)
}
fmt.Println(url)
}The response envelope
Native task API JSON responses use this envelope; /v1 compatibility and SSE use their own protocol shapes:
{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }codeis a business code, not an HTTP status.200means success. The HTTP status is set correctly too, so you may branch on either — but pick one. The two diverge on the specific codes (an insufficient balance iscode: 40201with HTTP402), and mixing them means eventually missing a case.- On failure,
datais absent — notnull. request_idis present on success and failure alike, and is echoed in theX-Request-Idresponse header. Quote it when reporting a problem.
Next
Authentication
Key format, per-key limits, what to do after a leak
Model catalog and schemas
The live catalog, prices, and how to render a parameter table from inputSchema
Asynchronous tasks
State machine, collecting results, polling backoff
Webhooks
Payload shape, signature verification, retries
Media
Direct reference uploads and artefact downloads
Idempotency
Retry safely without double-generating or double-billing
Errors
Every business code, and which retries are pointless

