spicyapiDocs
Main content

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:

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:

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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:

Public catalogue — no key, no account
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 as model (slug holds the same value);
  • callable — whether the model can run right now. Pick items where it is true: status is our operating intent, and a live model can still be temporarily not callable;
  • modality and tasks — what kind of output it makes;
  • price — the cheapest tier: amount in USD per unit (per_image, per_second, per_request or per_1k_tokens), plus tiers, the number of tiers behind it. For text models, read price.perMillionTokens, which gives separate input, cached-input and output prices per million tokens; amount on its own is only the output rate.

Append a model ID for one model's full record, including its inputSchema, paramNotes and worked examples:

One model, in full — replace MODEL_ID_FROM_CATALOG with a model value from the list above
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.

  1. Open Console → API keys and click Create key.
  2. Give it a name, for example my-first-test. Leave everything under Advanced as it is.
  3. 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.
  4. 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.

1. What can this key call?
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"
2. What fields does that one model take?
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:

Optional quote (macOS / Linux)
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:

3a. A new key for each new image (run once per image)
SPICY_IDEMPOTENCY_KEY="$(uuidgen)"
# Linux without uuidgen: SPICY_IDEMPOTENCY_KEY="$(cat /proc/sys/kernel/random/uuid)"
3b. Send the task (safe to run again after a network error)
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.stateWhat it meansWhat to do
queued or runningStill generating. Your funds are on hold, not spentWait a few seconds and run the same command again. Images usually take seconds; videos can take minutes. deadlineAt is the upper bound
succeededDoneGo to the next step
failed or expiredIt did not work. The hold is normally released; some content refusals explicitly disclosed on the model page are charged. Check settled and costRead 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/mp4

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

Select a live text-to-image model (requires jq)
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"
Create the task from those two values (this costs money)
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.

LanguageSave asInstall firstRun
Node.js 20 or latergenerate.mjsnothingnode generate.mjs
Python 3.9 or latergenerate.pypip install requestspython3 generate.py (Windows: py generate.py)
Go 1.21 or latermain.go in an empty foldergo mod init examplego 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.

Node.js 20+
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);
Python 3.9+
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())
Go 1.21+
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_…" }
  • code is a business code, not an HTTP status. 200 means 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 is code: 40201 with HTTP 402), and mixing them means eventually missing a case.
  • On failure, data is absent — not null.
  • request_id is present on success and failure alike, and is echoed in the X-Request-Id response header. Quote it when reporting a problem.

Next

On this page