Inicio rápido
Obtén una clave, crea una tarea y recupera su resultado. Recorre el proceso completo de principio a fin.
SpicyAPI reúne la generación de imágenes y video bajo un solo protocolo de autenticación, tareas y resultados, y ofrece modelos de texto en los formatos de OpenAI, Anthropic y Gemini que ya utilizas. Conserva el flujo de llamadas al cambiar de modelo y construye input a partir del inputSchema de ese modelo: su lista de campos admitidos, tipos y valores permitidos.
Esta guía usa la API nativa de tareas en https://api.spicyapi.ai/api/v1. Los endpoints compatibles de texto y video utilizan https://api.spicyapi.ai/v1. Ambos aceptan la misma clave de API, pero sus formatos de solicitud y respuesta son distintos.
Sigue esta página para elegir un modelo y recuperar un resultado. Para conocer el precio exacto antes de pagar, consulta Cotizaciones y compatibilidad. Para chat, consulta Texto y streaming. Antes de desplegar tu integración, revisa Integración en producción; usa Solución de problemas si falla un paso. El glosario explica los términos que no conozcas.
¿Quieres ver un resultado antes de escribir código?
Abre cualquier modelo en spicyapi.ai/explore y usa su pestaña Playground; la pestaña API muestra la misma solicitud como un comando que puedes copiar. Para trabajar desde la terminal sin escribir código, usa la CLI o deja que un asistente de IA lo haga mediante el servidor MCP.
Herramientas oficiales
Puedes llamar directamente a la API HTTP; es lo que muestra el resto de esta página. Si prefieres no escribir las llamadas HTTP, hay cuatro paquetes npm con licencia MIT, cada uno con instalación y guía propias:
@spicyapi/sdk
SDK de TypeScript: tipos de OpenAPI, espera adaptativa, subidas y verificación de webhooks
@spicyapi/cli
El comando spicyapi: explora el catálogo, cotiza, crea y sigue tareas desde la terminal
@spicyapi/mcp
Servidor MCP que da a un agente de programación acceso al catálogo, cotizaciones y tareas, con confirmación en cada paso de pago
@spicyapi/skill
Agent Skill portátil: el flujo y los límites de gasto, instalable en tu agente
Otros cuatro SDK oficiales cubren los lenguajes fuera de Node.js. Todos implementan el mismo flujo y tienen licencia MIT; actualmente solo el módulo de Go se instala desde su repositorio con el comando habitual. Las otras tres páginas muestran la instalación que debes usar hasta que se publiquen los paquetes:
SDK de Python
spicyapi para Python 3.11+, sin dependencias en tiempo de ejecución
SDK de Go
github.com/spicyapi-ai/spicy-go para Go 1.23+, solo con la biblioteca estándar
SDK de PHP
spicyapi/spicyapi para PHP 8.1+, sin dependencias de bibliotecas
SDK de Java
ai.spicyapi:spicyapi-java para Java 17+, con una taxonomía cerrada de errores
¿Trabajas en ComfyUI? Los nodos de ComfyUI incorporan a tu lienzo cada modelo de imagen, video y audio como un nodo, con el precio visible antes de ejecutarlo.
Antes de empezar
Necesitas cuatro cosas. Si omites una, aparecerá después un error que no indica cuál falta.
- Una cuenta con correo verificado. Regístrate en spicyapi.ai y pulsa el enlace del correo de verificación. Hasta hacerlo, todas las llamadas con tu clave devuelven
403. - Saldo. Las generaciones se pagan con saldo en dólares estadounidenses, sin créditos, y una cuenta nueva empieza en cero. Añade fondos en Consola → Facturación. Sin saldo suficiente, crear una tarea falla con el código de negocio
40201. Configuración de la cuenta explica el registro, la recarga y la creación de una clave. - Una terminal. En macOS, la app Terminal; en Linux, cualquier shell; en Windows, PowerShell. Si los comandos cambian, cada paso tiene una pestaña Windows PowerShell. Si lo prefieres, los comandos de macOS / Linux también funcionan sin cambios en WSL o Git Bash en Windows.
- Unos centavos. El paso 3 crea una tarea real de imagen, que se cobra si termina correctamente. El paso 2 muestra el precio de cada modelo y un comando opcional te indica gratis el importe exacto antes de empezar.
Evalúa modelos y precios sin una cuenta
No necesitas clave ni cuenta para ver qué existe y cuánto cuesta. El catálogo público del sitio comercial permite consultas anónimas y este comando no necesita sustituir ningún valor:
curl "https://api.spicyapi.ai/console/v1/catalog/models?locale=en"El formato de respuesta es el descrito al final de esta página. Cada entrada de data.items incluye:
model: el valor exacto que después enviarás comomodel;slugcontiene el mismo valor.callable: si el modelo puede ejecutarse en este momento. Elige entradas contrue:statusindica nuestra intención operativa, y un modelolivepuede no estar disponible temporalmente para llamadas.modalityytasks: el tipo de salida que produce.price: el nivel más barato, conamounten USD porunit(per_image,per_second,per_requestoper_1k_tokens), mástiers, el número de niveles. Para modelos de texto, consultaprice.perMillionTokens, que separa los precios por millón de tokens de entrada, entrada en caché y salida;amountpor sí solo solo es la tarifa de salida.
Añade un ID de modelo para consultar su registro completo, incluidos inputSchema, paramNotes y examples de uso:
curl "https://api.spicyapi.ai/console/v1/catalog/models/MODEL_ID_FROM_CATALOG?locale=en"MODEL_ID_FROM_CATALOG es un marcador utilizado en esta documentación, no un modelo real: copia siempre un ID del catálogo actualizado.
locale utiliza en de forma predeterminada; envía ja, ko, de, fr, es, pt-BR, ru, it o pl para obtener nombres, resúmenes y notas de parámetros traducidos. Otro valor devuelve 400. strict=1 excluye entradas cuyo texto no se haya publicado en ese idioma en lugar de usar inglés; normalmente no lo necesitas. Este acceso se limita por dirección de origen, no por cuenta, y tiene su propio contador; consulta Límites de solicitudes. También tiene caché, por lo que un cambio de precio puede tardar en aparecer aquí.
Si tiers es mayor que 1, el price publicado es el del nivel más barato, no el de tus ajustes. price.variant muestra los ajustes de ese nivel, como 720p o generate_audio=false;resolution=480p si se combinan varios campos. inputSchema["x-pricing"].variantFields del registro individual, si aparece, enumera todos los campos que seleccionan un nivel. Su pricingNote es un recordatorio general de que prevalece la estimación actual; no enumera campos. Para conocer el importe exacto de una solicitud, usa POST /api/v1/jobs/quote cuando tengas una clave.
Catálogo público para evaluar; catálogo autenticado para llamar
El catálogo público responde «qué se vende y a qué precio se anuncia». No puede responder «qué puede llamar mi clave» ni «qué se cobrará a mi cuenta», porque ambos dependen de la cuenta. Cuando tengas una clave, cambia a GET /api/v1/models en el paso 2 y úsalo como única fuente de ID disponibles para llamadas; un ID recordado, una captura de lanzamiento o una página comercial no son alternativas.
Cinco pasos
Obtén una clave
Una clave de API es la contraseña que tu código envía con cada solicitud. Identifica tu cuenta y permite facturar el uso.
- Abre Consola → Claves de API y pulsa Crear clave.
- Dale un nombre, por ejemplo
my-first-test. Deja los valores de Avanzado como están. - Copia la clave que empieza por
sk-spicy-o pulsa Guardar como archivo. Se muestra una sola vez; si la pierdes, debes crear otra. - En la terminal que usarás para los siguientes pasos, guárdala en una variable de entorno, un valor con nombre que la terminal proporciona a cada comando, para no tener que escribir la clave dentro de un comando:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"
# Check: this should print sk-spicy-
echo "${SPICY_API_KEY:0:9}"Qué deberías ver: sk-spicy-. El valor dura hasta cerrar esa ventana de terminal, así que ejecuta todos los pasos siguientes en la misma.
Si no aparece Crear clave, tu rol en el equipo actual no permite crear claves: pide ayuda al propietario o usa Crear tu propio equipo en la misma página. Las claves nuevas no tienen límite de gasto salvo que lo establezcas; un límite diario en la consola limita el daño de una configuración incorrecta o una filtración. Consulta Autenticación.
Elige un modelo disponible para llamadas
El catálogo autenticado es la única fuente de ID disponibles, esquemas actuales y ejemplos. Consúltalo en dos llamadas: la lista es ligera e indica qué puedes llamar; después, un registro proporciona el esquema del modelo elegido. Consultar el catálogo es gratis.
curl "https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image" \
-H "Authorization: Bearer $SPICY_API_KEY"La respuesta es larga. Esta es la parte que necesitas, abreviada:
{
"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"
}Elige una entrada donde enabled y available sean ambos true, revisa su precio, copia el valor de model y consérvalo para el resto de la página. Con jq instalado, curl … | jq -r '.data.items[] | select(.enabled and .available) | .model' muestra únicamente los ID que puedes llamar.
# 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"Las barras del ID pasan sin cambios por la ruta; codifícalas con porcentaje solo si tu cliente HTTP exige que un parámetro de ruta sea un único segmento. El inputSchema del registro define lo permitido en input: required enumera los campos obligatorios y properties describe cada uno. examples contiene entradas ya validadas por el servidor. Para interpretar el resto del registro, incluido pricing, consulta Catálogo de modelos y esquemas.
Solicita los esquemas solo donde los necesites. Añadir includeSchema=1&includeExamples=1 a la llamada de lista devuelve el JSON Schema completo de todos los modelos habilitados en una sola respuesta mucho más grande. Las consultas del catálogo utilizan la capacidad de lectura de la cuenta, independientemente de la creación de tareas. Tampoco vuelvas a leer el catálogo antes de cada tarea; consulta Caché del catálogo.
Si no coincide ninguna entrada, detente antes de crear una tarea de pago. Una captura de lanzamiento, una página comercial o un ID recordado no sustituyen a un catálogo actualizado vacío. Para seleccionar el modelo desde código, consulta Seleccionar el modelo desde código.
Opcional: consulta antes el precio exacto. Cotizar es gratis, no crea nada ni reserva fondos. data.estimatedCost es lo que costará como máximo esta solicitud exacta, en 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\"}}"Crea una tarea
Una tarea es un trabajo de generación en segundo plano: recibes su ID de inmediato y el resultado después. Este paso cuesta dinero.
Cada tarea nueva obtiene una clave de idempotencia nueva: un ID aleatorio que adjuntas para que enviar dos veces la misma solicitud, por ejemplo tras un error de red, no cree dos tareas. Genérala en un comando independiente y después envía la tarea:
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\"}}"Si 3b agota el tiempo o pierdes la conexión, ejecuta 3b otra vez sin ejecutar 3a: la misma clave devuelve la tarea que ya empezaste y evita pagar dos veces. Para crear otra imagen, ejecuta primero 3a. Una respuesta 409 significa que reutilizaste una clave con otro prompt o modelo. Más información en Idempotencia.
Esto envía un solo campo. Si el esquema del paso 2 marca otros como obligatorios, añádelos: cada esquema de modelo define lo que puede ir en input, y un campo desconocido produce un 400.
La conexión nunca se mantiene abierta. Recibes de inmediato un ID de tarea:
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"
}En macOS / Linux, copia taskId de la respuesta y guárdalo; PowerShell ya lo hizo por ti:
export SPICY_TASK_ID="paste-the-taskId-here"202 significa aceptada, no terminada
Una tarea recién creada tiene state: "queued"; repetir la solicitud de forma segura con el mismo Idempotency-Key devuelve la tarea original en su estado actual. HTTP 202 indica que se aceptó la solicitud, no que terminó la generación. Al aceptarla por primera vez se retienen fondos, no se cobran: estimatedCost se reserva de tu saldo. Esa retención también es el cargo máximo: los fondos no utilizados se liberan y no se cobra después nada por encima. Las tareas fallidas normalmente liberan la retención; ciertos rechazos de contenido indicados en la página del modelo se cobran al precio aceptado. Comprueba settled y cost. deadlineAt es el último momento en que puede terminar antes de pasar a expired. El code de negocio de una respuesta correcta permanece en 200. Una tarea aceptada no puede cancelarse.
Recupera el resultado
Si tu solicitud de producción incluye callBackUrl, te enviamos un callback cuando la tarea alcanza un estado final: un callback o webhook es una dirección HTTPS de tu servidor a la que llamamos al terminar, para que no tengas que consultar continuamente. La solicitud mínima anterior lo omite para funcionar sin un endpoint público. Debes verificar la firma de los callbacks; consulta Webhooks.
Sin un endpoint de callback, consulta la tarea:
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY"Usa la misma clave de API que creó la tarea: otra clave obtiene 404. Revisa data.state:
data.state | Significado | Qué hacer |
|---|---|---|
queued o running | Sigue generando. Los fondos están retenidos, no gastados | Espera unos segundos y ejecuta el mismo comando. Las imágenes suelen tardar segundos y los videos, minutos. deadlineAt es el límite superior |
succeeded | Terminó | Continúa con el siguiente paso |
failed o expired | No funcionó. Normalmente se libera la retención; ciertos rechazos de contenido indicados expresamente en la página del modelo se cobran. Consulta settled y cost | Lee data.errorCode, estable, y data.errorMessage, para personas; corrige la entrada y empieza de nuevo desde 3a |
El code de la respuesta es 200 incluso si falló la tarea: code indica si funcionó la lectura de la tarea. Su resultado siempre está en data.state. Estas son las respuestas de cada etapa, abreviadas:
{
"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"
}Mientras settled sea false, cost es el importe retenido, no el cargo final.
Descarga el archivo generado
Toma data.output.assets[0].url del resultado correcto y descárgalo directamente. No adjuntes tu clave de API. El enlace funciona unos 20 minutos (expiresAt); si venció, ejecuta de nuevo el paso 4, porque cada lectura devuelve uno nuevo. Los archivos generados se eliminan después de un máximo de 14 días, así que guarda tu propia copia de lo que necesites.
# 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/mp4Con jq instalado puedes evitar copiar y pegar:
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')"Qué deberías ver: un archivo result.png en la carpeta actual de tu terminal. Comprueba el cargo en Consola → Logs o en cost cuando settled sea true.
Modelos de texto: una solicitud y la respuesta en la misma conexión
Los modelos de texto responden en la misma respuesta HTTP: sin sondeo ni descarga. El encabezado de respuesta X-Spicy-Task-Id indica la tarea asociada al cargo, si quieres comprobarla después. En el paso 2, lista modelos con modality=text, guarda uno como SPICY_MODEL y ejecuta:
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.\"}]}"La respuesta está en choices[0].message.content. Esta llamada se cobra por los tokens utilizados. El endpoint utiliza el formato de respuesta de OpenAI, no {code, msg, data}. Consulta Texto y streaming.
Seleccionar el modelo desde código
Los cinco pasos anteriores incluyen intervención humana. Una integración desplegada elige el modelo por sí misma justo antes de enviar: un modelo disponible la semana pasada puede estar pausado hoy, y los precios dependen de la cuenta. Solicita el esquema y un ejemplo validado por el servidor en la misma llamada y exige enabled y 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"Si la selección no encuentra nada, detente en lugar de usar un ID recordado. Los tres programas siguientes realizan la misma selección sin jq.
Todo junto
Los tres programas siguientes leen el catálogo, crean una tarea, consultan con intervalos crecientes y muestran una URL de descarga. En producción utiliza webhooks: el sondeo es una alternativa de respaldo.
Estos programas crean una tarea real de pago
Cada programa elige el primer modelo de texto a imagen disponible de tu catálogo, no el más barato, y envía uno de sus ejemplos validados. Comprueba primero su precio o cambia la selección al modelo exacto que quieres.
| Lenguaje | Guardar como | Instalar antes | Ejecutar |
|---|---|---|---|
| Node.js 20 o posterior | generate.mjs | Nada | node generate.mjs |
| Python 3.9 o posterior | generate.py | pip install requests | python3 generate.py; Windows: py generate.py |
| Go 1.21 o posterior | main.go en una carpeta vacía | go mod init example | go run . |
Los tres leen la clave de SPICY_API_KEY, paso 1, y muestran la URL del resultado al terminar. Esperan hasta diez minutos, adecuado para modelos de imagen; para video, espera hasta el deadlineAt de la tarea.
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)
}Formato de la respuesta
Las respuestas JSON de la API nativa de tareas utilizan este formato; la compatibilidad de /v1 y SSE tienen los formatos de sus propios protocolos:
{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }codees un código de negocio, no un estado HTTP.200significa éxito. El estado HTTP también es correcto, por lo que puedes basar condiciones en cualquiera, pero elige uno. Difieren para códigos específicos, como saldo insuficiente, que escode: 40201con HTTP402; mezclarlos acabará dejando un caso sin cubrir.- En caso de fallo,
datano aparece, en lugar de sernull. request_idestá presente tanto en éxito como en fallo y se repite en el encabezadoX-Request-Id. Indícalo al informar de un problema.
Siguientes pasos
Autenticación
Formato de claves, límites por clave y qué hacer tras una filtración
Catálogo de modelos y esquemas
Catálogo actualizado, precios y cómo mostrar una tabla de parámetros desde inputSchema
Tareas asíncronas
Máquina de estados, recuperación de resultados y sondeo con espera creciente
Webhooks
Formato del contenido, verificación de firmas y reintentos
Medios
Subida directa de referencias y descarga de archivos generados
Idempotencia
Reintenta con seguridad sin duplicar generaciones ni cobros
Errores
Todos los códigos de negocio y los reintentos que no ayudan
Configura tu cuenta
Pasa de no tener cuenta a tener una clave de API que funciona en unos quince minutos. Regístrate, verifica tu correo, añade saldo, crea una clave y haz una llamada de prueba gratuita, con cada pantalla explicada.
Autenticación
Formato de claves, restricciones por clave, tareas visibles para cada una y qué hacer en cuanto una clave se filtra.

