Démarrage rapide
Obtenez une clé, créez une tâche et récupérez le résultat : le parcours complet, de bout en bout.
SpicyAPI regroupe la génération d’images et de vidéos derrière un seul protocole d’authentification, de tâches et de résultats et sert les modèles de texte aux formats OpenAI, Anthropic et Gemini que vous utilisez déjà. Gardez le même déroulement d’appels en changeant de modèle et construisez input depuis son inputSchema, la liste des champs acceptés, de leurs types et valeurs autorisées.
Ce guide utilise l’API native de tâches à https://api.spicyapi.ai/api/v1. Les points de terminaison compatibles de texte et de vidéo utilisent https://api.spicyapi.ai/v1. Les deux acceptent la même clé API, mais leurs formats de requête et de réponse diffèrent.
Suivez cette page pour choisir un modèle et récupérer un résultat. Pour connaître un montant exact avant paiement, voir Devis et compatibilité. Pour le chat, voir Texte et streaming. Avant de déployer l’intégration, suivez Intégration en production ; utilisez Dépannage lorsqu’une étape échoue. Les termes inconnus sont expliqués dans le glossaire.
Vous voulez voir un résultat avant d’écrire du code ?
Ouvrez un modèle sur spicyapi.ai/explore et utilisez son onglet Playground ; l’onglet API présente la même requête sous forme de commande copiable. Pour travailler en terminal sans écrire de code, utilisez la CLI ou laissez un assistant IA le faire avec le serveur MCP.
Outils officiels
L’appel HTTP direct est entièrement pris en charge et présenté dans le reste de cette page. Si vous préférez ne pas écrire les appels vous-même, quatre packages npm sous licence MIT s’installent séparément et disposent de leurs propres guides :
@spicyapi/sdk
SDK TypeScript : types OpenAPI, attente adaptative, envois de fichiers et vérification des webhooks
@spicyapi/cli
Commande spicyapi : parcourir le catalogue, obtenir un devis, créer et suivre des tâches en terminal
@spicyapi/mcp
Serveur MCP donnant à un agent de code le catalogue, les devis et les tâches, avec confirmation de chaque étape facturable
@spicyapi/skill
Agent Skill portable : workflow et limites de dépenses, installable dans votre agent
Quatre autres SDK officiels couvrent les langages hors de Node.js. Chacun implémente le même workflow sous licence MIT. Actuellement, seul le module Go s’installe depuis son dépôt par la commande habituelle ; les trois autres pages indiquent l’installation à utiliser en attendant la publication des packages :
SDK Python
spicyapi pour Python 3.11+, sans dépendance à l’exécution
SDK Go
github.com/spicyapi-ai/spicy-go pour Go 1.23+, bibliothèque standard uniquement
SDK PHP
spicyapi/spicyapi pour PHP 8.1+, sans dépendance de bibliothèque
SDK Java
ai.spicyapi:spicyapi-java pour Java 17+, avec une hiérarchie fermée de types d’erreurs
Vous travaillez dans ComfyUI ? Les nœuds ComfyUI placent chaque modèle d’image, de vidéo et d’audio sur votre canevas, avec son tarif affiché avant l’exécution.
Avant de commencer
Il vous faut quatre éléments. En omettre un provoque plus tard une erreur qui ne dit pas lequel manque.
- Un compte avec adresse e-mail vérifiée. Inscrivez-vous sur spicyapi.ai et ouvrez le lien du mail de vérification. Avant cela, chaque appel de votre clé renvoie
403. - Un solde. Les générations sont payées depuis un solde en dollars américains, sans crédits, et un nouveau compte commence à zéro. Ajoutez des fonds dans Console → Facturation. Sans solde suffisant, la création échoue avec le code métier
40201. Configuration du compte explique l’inscription, l’ajout de fonds et la création d’une clé. - Un terminal. macOS : l’application Terminal. Linux : n’importe quel shell. Windows : PowerShell. Lorsque les commandes diffèrent, chaque étape comporte un onglet Windows PowerShell. Les commandes macOS / Linux fonctionnent aussi sans modification dans WSL ou Git Bash sous Windows.
- Quelques centimes. L’étape 3 crée une véritable tâche d’image facturée si elle réussit. L’étape 2 montre les tarifs et une commande facultative permet de connaître gratuitement le montant exact avant de commencer.
Évaluer d’abord : modèles et tarifs sans compte
Aucune clé ni aucun compte n’est nécessaire pour voir l’offre et ses tarifs. Le catalogue public du site marketing est lisible anonymement, et cette commande ne nécessite aucune substitution :
curl "https://api.spicyapi.ai/console/v1/catalog/models?locale=en"L’enveloppe est celle décrite en bas de page. Dans data.items, chaque entrée contient :
model: la valeur exacte à envoyer ensuite commemodel;slugporte la même valeur ;callable: indique si le modèle peut s’exécuter maintenant. Choisissez les entrées àtrue:statusest notre intention d’exploitation, et un modèlelivepeut être temporairement non appelable ;modalityettasks: type de sortie ;price: palier le moins cher, avecamounten USD parunit(per_image,per_second,per_requestouper_1k_tokens), ettiers, le nombre de paliers. Pour le texte, lisezprice.perMillionTokens, avec des tarifs distincts par million de tokens d’entrée, d’entrée en cache et de sortie ;amountseul ne donne que le tarif de sortie.
Ajoutez un ID de modèle pour sa fiche complète, avec inputSchema, paramNotes et les examples détaillés :
curl "https://api.spicyapi.ai/console/v1/catalog/models/MODEL_ID_FROM_CATALOG?locale=en"MODEL_ID_FROM_CATALOG est une valeur à remplacer dans cette documentation, pas un modèle réel : copiez toujours un véritable ID du catalogue actuel.
locale vaut en par défaut ; fournissez ja, ko, de, fr, es, pt-BR, ru, it ou pl pour les noms, résumés et notes de paramètres traduits. Toute autre valeur renvoie 400. strict=1 retire les entrées dont le texte n’est pas publié dans cette langue au lieu de revenir à l’anglais ; ce réglage n’est généralement pas nécessaire. Cette API est limitée par adresse source, pas par compte, avec son propre quota ; voir Limites de débit. Elle est aussi mise en cache : un changement de tarif peut donc mettre un certain temps à apparaître.
Si tiers est supérieur à 1, le price affiché est le palier le moins cher, pas le prix de vos réglages. price.variant indique les réglages de ce palier, par exemple 720p ou generate_audio=false;resolution=480p pour plusieurs champs combinés. Le inputSchema["x-pricing"].variantFields de la fiche d’un modèle, lorsqu’il existe, liste tous les champs sélectionnant un palier. pricingNote rappelle généralement que l’estimation actuelle fait autorité, sans lister les champs. Pour le montant exact, utilisez POST /api/v1/jobs/quote une fois votre clé obtenue.
Catalogue public pour évaluer, catalogue authentifié pour appeler
Le catalogue public répond à « que proposez-vous et à quel tarif affiché ? ». Il ne répond pas à « qu’est-ce que ma clé peut appeler ? » ni « combien mon compte sera-t-il facturé ? », car cela dépend du compte. Avec une clé, passez à GET /api/v1/models à l’étape 2 et utilisez-le comme seule source d’IDs appelables. Un ID mémorisé, un instantané de lancement ou une page marketing n’est pas un repli.
Cinq étapes
Obtenir une clé
Une clé API est le mot de passe envoyé par votre code à chaque requête. Elle identifie votre compte et permet sa facturation.
- Ouvrez Console → Clés API et cliquez sur Créer une clé.
- Donnez-lui un nom, par exemple
my-first-test. Laissez les réglages Avancé inchangés. - Copiez la clé commençant par
sk-spicy-ou cliquez sur Enregistrer dans un fichier. Elle est affichée une seule fois ; si vous la perdez, créez-en une nouvelle. - Dans le terminal utilisé pour la suite, stockez-la dans une variable d’environnement, une valeur nommée transmise par le terminal aux commandes, pour ne jamais avoir à la saisir dans une commande :
export SPICY_API_KEY="sk-spicy-paste-your-key-here"
# Check: this should print sk-spicy-
echo "${SPICY_API_KEY:0:9}"Résultat attendu : sk-spicy-. La valeur reste définie jusqu’à la fermeture de cette fenêtre ; exécutez donc la suite dans la même fenêtre.
Si Créer une clé est absent, votre rôle dans l’équipe actuelle ne permet pas de créer de clés : demandez au propriétaire ou utilisez Créer votre propre équipe sur la même page. Les nouvelles clés n’ont aucun plafond de dépenses sauf si vous en définissez un. Un plafond quotidien dans la console limite les conséquences d’une mauvaise configuration ou d’une fuite. Voir Authentification.
Choisir un modèle appelable
Le catalogue authentifié est la seule source d’IDs appelables, de schémas actuels et d’exemples. Procédez en deux appels : une liste légère indique ce que vous pouvez appeler, puis une fiche donne le schéma du modèle choisi. La lecture est gratuite.
curl "https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image" \
-H "Authorization: Bearer $SPICY_API_KEY"La réponse est longue. Voici sa partie utile, abrégée :
{
"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"
}Choisissez une entrée dont enabled et available sont tous deux true, notez son tarif, copiez sa valeur model et conservez-la pour la suite. Avec jq, curl … | jq -r '.data.items[] | select(.enabled and .available) | .model' n’affiche que les IDs appelables.
# 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"Les barres obliques de l’ID restent telles quelles dans le chemin ; encodez-les seulement si votre client HTTP exige qu’un paramètre de chemin soit un segment unique. L’inputSchema de la fiche fait autorité pour input : required liste les champs à envoyer et properties décrit chacun. examples contient les entrées déjà validées par le serveur. Pour lire le reste de la fiche, dont pricing, voir Catalogue des modèles et schémas.
Demandez les schémas uniquement où ils sont nécessaires. Ajouter includeSchema=1&includeExamples=1 à la liste renvoie en une réponse le JSON Schema complet de tous les modèles activés : le corps est beaucoup plus volumineux et consomme le quota de lecture du compte, indépendamment de la création de tâches. Ne relisez pas non plus le catalogue avant chaque tâche ; voir Mise en cache du catalogue.
Si aucune entrée ne correspond, arrêtez-vous avant une tâche payante. Un instantané, une page marketing ou un ID mémorisé ne remplace pas un catalogue actuel vide. Pour une sélection par code plutôt que manuelle, voir Sélectionner le modèle par code.
Facultatif : connaître d’abord le montant exact. Un devis est gratuit, ne crée rien et ne réserve rien. data.estimatedCost est le coût maximal de cette requête exacte, 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\"}}"Créer une tâche
Une tâche est un travail de génération en arrière-plan : vous recevez son ID immédiatement, puis le résultat plus tard. Cette étape coûte de l’argent.
Chaque nouvelle tâche reçoit une nouvelle clé d’idempotence, un ID aléatoire empêchant que deux envois identiques, par exemple après une erreur réseau, créent deux tâches. Générez-la dans une commande séparée, puis envoyez la tâche :
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 expire ou que la connexion se coupe, répétez 3b sans exécuter 3a : la même clé renvoie la tâche déjà commencée au lieu de payer deux fois. Pour une autre image, exécutez d’abord 3a. Un 409 signifie que vous avez réutilisé une clé avec un prompt ou modèle différent. Voir Idempotence.
Un seul champ est envoyé ici. Si le schéma lu à l’étape 2 en impose d’autres, ajoutez-les : l’input autorisé dépend du schéma du modèle, et un champ inconnu suffit à provoquer un 400.
La connexion ne reste jamais ouverte. Vous recevez immédiatement un ID :
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"
}Sous macOS / Linux, copiez le taskId de la réponse et conservez-le ; PowerShell l’a déjà fait :
export SPICY_TASK_ID="paste-the-taskId-here"202 signifie accepté, pas terminé
Une nouvelle tâche a state: "queued" ; une répétition sûre avec le même Idempotency-Key renvoie la tâche originale dans son état actuel. HTTP 202 signifie que la requête est acceptée, pas que la génération est terminée. À la première acceptation, les fonds sont réservés, pas facturés : estimatedCost est mis de côté sur le solde. Cette réservation constitue aussi le plafond de facturation de la tâche : les fonds inutilisés sont libérés et aucun montant supérieur n’est prélevé ensuite. Les tâches échouées libèrent normalement la réservation ; certains refus de contenu annoncés sur la page du modèle sont facturés au devis accepté. Vérifiez settled et cost. deadlineAt est l’échéance maximale avant que la tâche ne se termine en expired. Le code métier de réussite de l’enveloppe reste 200. Une tâche acceptée ne peut pas être annulée.
Récupérer le résultat
Si la requête de production comprend une callBackUrl, nous vous rappelons quand la tâche atteint un état terminal : un callback, ou webhook, est une adresse HTTPS de votre serveur appelée à la fin de la tâche pour éviter les interrogations répétées. La requête minimale précédente l’omet pour fonctionner sans point d’accès public. Vérifiez obligatoirement les signatures des callbacks ; voir Webhooks.
Sans adresse de callback, interrogez la tâche :
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
-H "Authorization: Bearer $SPICY_API_KEY"Utilisez la même clé API que pour la création ; une autre reçoit 404. Examinez data.state :
data.state | Signification | Action |
|---|---|---|
queued ou running | Génération en cours. Les fonds sont réservés, pas dépensés | Attendez quelques secondes puis répétez la commande. Une image prend généralement des secondes, une vidéo des minutes. deadlineAt est la borne supérieure |
succeeded | Terminé | Passez à l’étape suivante |
failed ou expired | Échec. La réservation est normalement libérée ; certains refus de contenu explicitement annoncés sur la page sont facturés. Vérifiez settled et cost | Lisez data.errorCode stable et data.errorMessage destiné à la lecture, corrigez l’entrée et reprenez à 3a |
Le code de l’enveloppe vaut 200 même si la tâche a échoué : code indique si la lecture de la tâche a réussi. Son résultat propre est toujours dans data.state. Voici les réponses abrégées aux différents stades :
{
"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"
}Tant que settled est false, cost est le montant réservé, pas le coût final.
Télécharger le fichier
Prenez data.output.assets[0].url du résultat réussi et téléchargez directement, sans clé API. Le lien fonctionne environ 20 minutes (expiresAt) ; s’il a expiré, répétez l’étape 4, car chaque lecture renvoie un nouveau lien. Les fichiers générés sont supprimés après au maximum 14 jours : gardez vos copies.
# 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/mp4Avec jq, vous pouvez éviter le copier-coller :
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')"Résultat attendu : un fichier result.png dans le dossier du terminal. Vérifiez le coût dans Console → Journaux, ou dans cost lorsque settled est true.
Modèles de texte : une requête, la réponse directement
Les modèles de texte répondent dans la même réponse HTTP, sans polling ni téléchargement. L’en-tête X-Spicy-Task-Id désigne la tâche portant la facture pour une vérification ultérieure. À l’étape 2, listez plutôt les modèles avec modality=text, exportez-en un dans SPICY_MODEL, puis :
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 réponse figure dans choices[0].message.content. L’appel est facturé selon les tokens utilisés. Le point de terminaison suit le format OpenAI, pas l’enveloppe {code, msg, data}. Voir Texte et streaming.
Sélectionner le modèle par code
Les cinq étapes précédentes gardent une personne dans la boucle. Une intégration déployée choisit elle-même le modèle juste avant l’envoi : un modèle appelable la semaine dernière peut être suspendu aujourd’hui, et les tarifs dépendent du compte. Demandez le schéma et un exemple validé côté serveur dans le même appel, puis exigez enabled et 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 sélection est vide, arrêtez-vous plutôt que de reprendre un ID mémorisé. Les trois programmes ci-dessous font la même sélection sans jq.
Assembler le tout
Les trois programmes lisent le catalogue, créent une tâche, l’interrogent avec temporisation progressive et affichent un lien de téléchargement. En production, utilisez plutôt les webhooks : le polling est un repli, pas le plan principal.
Ces programmes créent une véritable tâche payante
Chaque programme choisit le premier modèle texte-vers-image appelable, pas le moins cher, puis envoie l’un de ses exemples validés. Vérifiez d’abord son tarif ou adaptez la sélection au modèle exact voulu.
| Langage | Fichier | Installation préalable | Exécution |
|---|---|---|---|
| Node.js 20 ou ultérieur | generate.mjs | Aucune | node generate.mjs |
| Python 3.9 ou ultérieur | generate.py | pip install requests | python3 generate.py (Windows : py generate.py) |
| Go 1.21 ou ultérieur | main.go dans un dossier vide | go mod init example | go run . |
Les trois lisent la clé depuis SPICY_API_KEY, étape 1, et affichent l’URL à la fin. Ils attendent au maximum dix minutes, adapté aux images ; pour la vidéo, attendez plutôt jusqu’à la deadlineAt de la tâche.
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)
}L’enveloppe de réponse
Les réponses JSON natives de tâches utilisent cette enveloppe ; la compatibilité /v1 et les SSE utilisent leurs propres formats :
{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }codeest un code métier, pas un statut HTTP.200signifie succès. Le statut HTTP est correct aussi : vous pouvez décider selon l’un ou l’autre, mais choisissez-en un seul. Ils divergent pour les codes spécifiques, par exemple un solde insuffisant donnecode: 40201avec HTTP402, et les mélanger finit par faire manquer un cas.- En cas d’échec,
dataest absent, pasnull. request_idfigure en cas de succès comme d’échec et dans l’en-têteX-Request-Id. Indiquez-le pour signaler un problème.
Suite
Authentification
Format des clés, restrictions et actions après une fuite
Catalogue des modèles et schémas
Catalogue actuel, tarifs et table de paramètres à partir d’inputSchema
Tâches asynchrones
États, récupération des résultats et temporisation du polling
Webhooks
Corps des messages, vérification des signatures et nouvelles tentatives
Médias
Envois directs de références et téléchargement des résultats
Idempotence
Réessayer sans génération ni facturation en double
Erreurs
Tous les codes métier et les tentatives inutiles
Préparer votre compte
Passez de l’absence de compte à une clé API qui fonctionne en une quinzaine de minutes. Inscription, vérification de l’e-mail, ajout de fonds, création d’une clé et appel de test gratuit, avec chaque écran expliqué.
Authentification
Format des clés, restrictions, accès aux tâches et mesures à prendre dès qu’une clé est divulguée.

