spicyapiDocumentation
Développer avec l’API
Contenu principal

Tâches asynchrones

Contrats de createTask et recordInfo, signification des six états et choix entre callbacks et interrogation périodique.

La génération est asynchrone : envoyer une tâche et récupérer son résultat sont deux appels distincts.

Concrètement :

  1. Vous envoyez le traitement et recevez immédiatement un taskId. Rien n’est encore terminé ; le prix du traitement est mis de côté sur votre solde, sous forme de réservation.
  2. Vous revenez plus tard demander si ce taskId est terminé : c’est le polling, ou interrogation périodique. Une requête toutes les quelques secondes suffit. Vous pouvez aussi nous fournir une adresse web à laquelle nous envoyons le résultat une fois prêt : c’est un callback, ou webhook.
  3. Lorsque state vaut succeeded, le lien du fichier se trouve dans data.output.assets[0].url. Ouvrez-le ou téléchargez-le dans les 20 minutes environ ; s’il a expiré, demandez un nouveau lien. Une tâche réussie ne coûte jamais plus que sa réservation ; une tâche échouée ou expirée libère normalement celle-ci, avec les exceptions indiquées sur la page du modèle pour certains refus de contenu facturés. Consultez settled et cost pour les frais définitifs.

Pourquoi séparer ces appels ? Une image prend quelques secondes et une vidéo, couramment plusieurs minutes. Un point de terminaison synchrone immobiliserait votre processus sur une connexion ; au moindre incident, vous n’auriez ni résultat ni méthode sûre pour réessayer, faute de savoir si la première tentative est déjà en cours. Avec deux appels, l’identifiant de tâche sert de repère : reconnectez-vous et continuez à consulter le même identifiant.

1. Créer une tâche

POST /api/v1/jobs/createTask

Cet appel engage des fonds

Une tâche acceptée réserve immédiatement estimatedCost sur votre solde et ne peut pas être annulée. Pour consulter le prix sans rien créer, demandez un devis. Les plafonds de votre clé limitent ses dépenses ; voir Facturation.

En-têtes

En-têteObligatoireRemarques
AuthorizationOuiBearer sk-spicy-…
Content-TypeOuiapplication/json
Idempotency-KeyNonFortement recommandé : permet de réessayer sans risque après une erreur réseau. Voir Idempotence
X-Spicy-RetentionNonRéduit la durée de conservation des résultats et du prompt de cette tâche, en secondes. Voir Conservation et suppression

Corps

Propriété

Type

callBackUrl doit être une adresse publique HTTPS accessible à nos serveurs sur Internet. Les URL en http://, les ports explicites autres que 443 ou 80, les URL contenant un nom d’utilisateur ou un mot de passe, les adresses IP privées ou de boucle locale et les noms d’hôte impossibles à résoudre sont refusés, car le corps du callback contient votre prompt et les liens signés des résultats. http://localhost ne fonctionne donc pas en développement local : utilisez le polling ou exposez votre machine via un tunnel fournissant une adresse HTTPS publique. Une valeur invalide renvoie HTTP 400, avec le code: 400 métier, msg: "Invalid callback URL" et un request_id. L’erreur ne reprend ni votre URL, ni votre nom d’hôte, ni votre IP, ni les détails DNS. Nous contrôlons de nouveau la destination au moment de la connexion ; voir Webhooks.

Écrivez callBackUrl avec un B majuscule

Seul input est validé champ par champ. Un champ de premier niveau inconnu, comme callback_url ou webhookUrl, est actuellement ignoré : la tâche est donc acceptée, mais aucun callback n’est envoyé. Si vous attendez un callback qui n’arrive pas, vérifiez d’abord cette orthographe.

Réponse

HTTP/1.1 202 Accepted
Location: /api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3
Retry-After: 2

Le statut de transport est 202 Accepted. Le corps conserve l’enveloppe standard ; son code métier de réussite reste donc 200. Location indique l’adresse à interroger et Retry-After: 2 suggère d’attendre deux secondes avant la première consultation.

{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "state": "queued",
    "estimatedCost": "0.008",
    "deadlineAt": "2026-08-28T09:42:04Z"
  },
  "request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}
ChampRemarques
taskIdIdentifiant de tâche, préfixé par job_. Toutes les opérations suivantes en ont besoin : conservez-le
statequeued pour une nouvelle tâche ; état actuel de la tâche d’origine lors d’une répétition idempotente avec la même clé
estimatedCostMontant réservé à l’acceptation, par exemple "0.008", en dollars américains sous forme de chaîne, et frais maximaux de cette tâche. Les fonds inutilisés sont libérés ; aucun supplément dépassant la réservation n’est encaissé ensuite
deadlineAtDernière échéance jusqu’à laquelle le serveur traite cette tâche. Sans résultat à cette date, elle passe à expired et ne coûte rien. Utilisez-la pour décider combien de temps attendre

Les champs monétaires sont des chaînes sans symbole de devise

estimatedCost, cost et les trois champs du solde sont des chaînes décimales simples, comme "0.008" : aucun $, aucun séparateur de milliers. parseFloat / float() fonctionnent directement dessus.

Ces champs utilisent des chaînes plutôt que des nombres JSON parce que, dans la plupart des langages, un nombre JSON devient un float64, et 0.008 n’a pas de représentation binaire exacte. Une fois un montant converti en flottant, sa valeur exacte n’est plus garantie : utilisez un type décimal, comme decimal.Decimal ou BigDecimal, pour votre comptabilité.

La devise est toujours l’USD. Voir Facturation.

Échec de création

Voici les erreurs couramment rencontrées lors d’une première intégration. Aucun montant n’est réservé ni facturé lorsque createTask renvoie une erreur.

HTTPcodeSignification habituelleAction
400400Un champ de input n’appartient pas au schéma du modèle, une valeur est hors plage ou callBackUrl est invalideComparez avec inputSchema du modèle ; msg précise le problème
40040004Chaque champ est valide, mais cette combinaison ne peut pas être exécutée actuellementModifiez la combinaison, par exemple la résolution ou la durée
401401Clé API absente, incorrecte ou désactivéeVérifiez l’en-tête Authorization
40240201Solde insuffisant pour réserver estimatedCostAjoutez des fonds dans la console, puis renvoyez avec la même Idempotency-Key
40240202Plafond de dépenses atteint sur cette clé, équipe ou compteRelevez le plafond dans la console ou attendez la réinitialisation du plafond quotidien ou mensuel
40340301Cette clé ne peut pas appeler ce modèleUtilisez une autre clé ou modifiez ses restrictions de modèles
409409 / 40901Conflit d’idempotence ou devis qui ne correspond plusVoir Idempotence et Devis
413413Corps de requête trop volumineux, généralement à cause de médias intégrésTéléversez le fichier et transmettez son URI spicy:// ; voir Médias
429429Trop de requêtes sur une courte périodeAttendez selon Retry-After, puis renvoyez avec la même clé
50350301Modèle temporairement indisponibleRéessayez plus tard avec la même clé

Liste complète : Erreurs.

Avant de commencer : choisir un modèle et des entrées valides

Les exemples ci-dessous nécessitent un véritable identifiant de modèle et un input qu’il accepte. Le catalogue fournit les deux : includeExamples=1 ajoute des entrées déjà validées par le schéma de chaque modèle. L’exemple utilise jq, un petit outil JSON en ligne de commande ; remplacez image par video ou audio pour d’autres types de modèles.

curl -s "https://api.spicyapi.ai/api/v1/models?modality=image&includeExamples=1" \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  | jq -c '.data.items[] | select(.enabled and .available) | {model, input: .examples[0].input}'

Chaque ligne produite est un corps de requête prêt à l’emploi avec deux champs, model et input. Choisissez-en une. Si elle contient "input": null, le modèle n’a aucun exemple enregistré : consultez son inputSchema via GET /api/v1/models/{model} et écrivez vous-même les entrées. Voir Catalogue des modèles et schémas.

Exemples

MODEL_ID_FROM_CATALOG ci-dessous est seulement un emplacement réservé : remplacez tout le corps de requête par la ligne choisie dans le catalogue actuel. N’envoyez pas cet emplacement réservé tel quel.

curl
# Paste one line printed by the command above between the single quotes.
TASK='{"model":"MODEL_ID_FROM_CATALOG","input":{"prompt":"a folded paper lantern, hard side light"}}'
# One new key per new task. After a network error, rerun only the curl command
# so the same key is sent again and no second task is created.
IDEMPOTENCY_KEY="$(uuidgen)"

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: $IDEMPOTENCY_KEY" \
  --data "$TASK"

Vous devez recevoir le corps 202 présenté plus haut, avec code: 200 et un taskId. Conservez ce taskId pour récupérer le résultat. Pour recevoir un callback, ajoutez "callBackUrl":"https://…" avec votre propre adresse HTTPS dans le JSON.

JavaScript
const res = await fetch('https://api.spicyapi.ai/api/v1/jobs/createTask', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SPICY_API_KEY}`,
    'Content-Type': 'application/json',
    // One key per logical request; reuse it across retries
    'Idempotency-Key': 'order-8814-render-1',
  },
  body: JSON.stringify({
    model: 'MODEL_ID_FROM_CATALOG',
    input: { prompt: 'a folded paper lantern, hard side light' },
    // Your own public HTTPS endpoint; delete this line to poll instead
    callBackUrl: 'https://your-app.example.com/hooks/spicy',
  }),
});

const body = await res.json();
if (res.status !== 202 || body.code !== 200) throw new Error(`${res.status} ${body.code} ${body.msg}`);
console.log(body.data.taskId, body.data.deadlineAt);
Python
import os, requests

res = requests.post(
    "https://api.spicyapi.ai/api/v1/jobs/createTask",
    headers={
        "Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
        "Content-Type": "application/json",
        # One key per logical request; reuse it across retries
        "Idempotency-Key": "order-8814-render-1",
    },
    json={
        "model": "MODEL_ID_FROM_CATALOG",
        "input": {"prompt": "a folded paper lantern, hard side light"},
        # Your own public HTTPS endpoint; delete this line to poll instead
        "callBackUrl": "https://your-app.example.com/hooks/spicy",
    },
)

body = res.json()
if res.status_code != 202 or body["code"] != 200:
    raise RuntimeError(f'{res.status_code} {body["code"]} {body["msg"]}')
print(body["data"]["taskId"], body["data"]["deadlineAt"])

Une réponse 202 ne signifie pas toujours queued

La première requête acceptée crée une tâche queued. Si la même clé API répète la même requête avec la même Idempotency-Key, cette réponse 202 renvoie l’état actuel de la tâche d’origine. Elle peut déjà être running ou terminée, mais aucune seconde tâche ni réservation n’est créée.

2. États

queued
running
succeededfailedexpired
Une tâche n'occupe qu'un seul état à la fois. Seul succeeded est facturé ; tout autre état final restitue intégralement le montant réservé.

Une tâche acceptée ne peut pas être annulée : ni queued ni running ne propose d’action d’annulation. L’API peut toujours lire les anciens enregistrements canceled, mais aucune nouvelle action d’utilisateur ou d’opérateur ne crée cet état.

ÉtatSignificationFinalFacturation
queuedCréée, fonds réservés, en attente de prise en chargeNonRéservation, sans règlement
runningLe modèle génère ; résultat attenduNonRéservation, sans règlement
succeededGénération réussieOuiRèglement selon l’usage réel, limité à la réservation
failedGénération échouéeOuiLibération normalement intégrale ; certains résultats content_rejected, explicitement mentionnés sur la page du modèle, sont facturés au devis accepté. Consultez settled et cost
canceledCompatibilité avec l’historique uniquement ; aucune action d’annulation disponibleOuiLibération intégrale
expiredDurée maximale d’exécution du modèle dépasséeOuiLibération intégrale

Consultez le règlement, pas seulement l’état de la tâche

Une tâche échouée ou expirée libère normalement toute la réservation. Certains refus de contenu sont facturés au devis accepté uniquement lorsque la page du modèle mentionne explicitement cette exception. Consultez settled et cost pour les frais définitifs. Un refus facturé conserve state: "failed" et errorCode: "content_rejected". Un processus de récupération termine automatiquement tout règlement ou toute libération encore en attente.

Testez l’état final avec state absent de {queued, running}, plutôt qu’en énumérant les valeurs finales. Cela prend en charge les anciens enregistrements canceled et évite qu’un futur état final provoque une interrogation sans fin.

2.5 Éviter la boucle de polling avec wait

Ajoutez ?wait=<seconds> à createTask : nous maintenons la connexion ouverte jusqu’à l’état final, sans boucle de polling pour ce premier appel.

curl
curl -X POST "https://api.spicyapi.ai/api/v1/jobs/createTask?wait=30" \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"MODEL_ID_FROM_CATALOG","input":{"prompt":"a lighthouse at dusk"}}'
RésultatRéponse
État final atteint dans le délai200 avec l’enregistrement de la tâche, dans le même format que recordInfo
Délai dépasséRéponse 202 habituelle, identique à un appel sans wait. Continuez ensuite par polling
Vous vous déconnectezAucun changement pour la tâche. Elle continue et suit la facturation habituelle

Un état final n’est pas nécessairement une réussite

Ici, 200 signifie que la tâche est terminée, pas qu’elle a réussi. Vérifiez data.state : failed et expired utilisent le même statut HTTP, exactement comme dans recordInfo.

Le plafond est de 60 secondes. Les valeurs supérieures sont ramenées au plafond plutôt que refusées ; toute valeur autre qu’un entier positif est ignorée plutôt que rejetée. Une faute dans un paramètre d’optimisation ne doit pas faire échouer une génération valide.

Ce plafond tient au délai d’expiration de 100 secondes de la couche edge devant l’API. Au-delà, le CDN coupe la connexion, et non notre serveur : vous pourriez recevoir un 524 sans identifiant de tâche ni état permettant de reprendre, alors que la tâche existe déjà et les fonds sont réservés. Les 60 secondes laissent une marge pour la requête elle-même.

Réservez cette option aux images plutôt qu’aux vidéos

Les modèles vidéo prennent couramment plusieurs minutes. wait arrivera simplement à expiration, la connexion ouverte ajoutera de la latence et vous devrez tout de même utiliser le polling. L’option est utile pour les modèles qui terminent généralement en quelques secondes, comme la plupart des points de terminaison d’image et de retouche, mais peu pertinente dans les autres cas. Elle ne peut volontairement pas devenir le comportement par défaut.

Pour les intégrations en production, les webhooks restent préférables : aucune connexion n’est maintenue ouverte et vous recevez aussi les résultats des tâches dépassant n’importe quel délai d’attente choisi.

3. Consulter une tâche

GET /api/v1/jobs/recordInfo?taskId=job_…

Un paramètre de requête est obligatoire : taskId. Une tâche est lisible uniquement avec la même clé API que celle qui l’a créée. Une autre clé, même du même compte ou remplaçant l’ancienne après rotation, reçoit 404, comme pour une tâche inexistante. Les différences d’erreurs ne permettent donc pas d’énumérer les identifiants de tâches d’autres utilisateurs. La même règle s’applique à download-url et retry. Avant de renouveler une clé, terminez la récupération des résultats des tâches créées avec l’ancienne.

curl
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3" \
  -H "Authorization: Bearer $SPICY_API_KEY"

La consultation est gratuite. Remplacez l’identifiant d’exemple par votre taskId.

Champs de la réponse

ChampTypeRemarques
taskIdstringIdentifiant de tâche
sourceTaskIdstringUniquement pour une tâche créée par relance : tâche d’origine
modelstringIdentifiant du modèle
statestringVoir le tableau ci-dessus
inputobjectParamètres validés ; les images intégrées deviennent des URI de fichiers de la plateforme. Omis après expiration des entrées
outputobjectDescription du résultat. Pertinent uniquement à l’état succeeded
errorCodestringPrésent lorsque state vaut failed ou expired ; une tâche expirée utilise timeout. Neuf valeurs possibles : voir Codes d’échec des tâches. Branchez votre logique sur ce champ, jamais sur errorMessage
errorMessagestringMotif lisible de l’échec, en anglais sauf si une autre langue est sélectionnée. Présent uniquement en cas d’échec
coststringMontant réglé ou réservation si le règlement n’est pas terminé ; les frais définitifs ne peuvent pas dépasser cette réservation
settledbooleanIndique si la facturation est définitive. Tant que la valeur est false, cost peut encore changer
createdAtstringRFC 3339
deadlineAtstringÉchéance d’exécution enregistrée à l’acceptation ; peut être omise dans les anciens événements webhook
completedAtstringRFC 3339. États finaux uniquement
contentStatestringpresent, expired pour une suppression après conservation ou purged pour une suppression demandée par vous. Voir Conservation et suppression
contentRemovedBystringuser ou system ; omis tant que le contenu est présent
purgedAtstringRFC 3339. Uniquement après une suppression demandée par vous
retentionobjectoutputsExpireAt pour la suppression des résultats, promptsExpireAt pour l’effacement du prompt et source pour le réglage déterminant

Quatre échéances différentes

  • Votre propre attente relève de votre choix. Y mettre fin n’annule pas la tâche distante : conservez le taskId et consultez plus tard cette même tâche pour reprendre.
  • deadlineAt est l’échéance d’exécution côté serveur. Une répétition idempotente ne la prolonge pas ; une relance explicite crée une nouvelle tâche avec sa propre échéance. Consultez l’état final et settled plutôt que de déduire un remboursement d’un minuteur local.
  • output.assets[].expiresAt fait expirer un lien temporaire de résultat, normalement après 20 minutes. Une nouvelle consultation actualise le lien sans créer de tâche.
  • Les médias générés sont conservés environ 14 jours au maximum après la fin ; les entrées téléversées sur la plateforme, un jour au maximum. Ce sont des plafonds : vous pouvez choisir des délais plus courts dans Conservation et suppression ou supprimer le contenu d’une tâche à la demande. Actualiser un lien ne prolonge pas la conservation du média.

Avec le SDK JavaScript, client.run({ model, input }, { idempotencyKey, onAccepted, timeoutMs }) envoie une seule fois puis consulte avec un intervalle croissant de 2 à 10 secondes au maximum. Son timeoutMs concerne uniquement l’attente locale : conservez donc l’identifiant reçu par onAccepted. Voir SDK. Préférez les webhooks pour les traitements en arrière-plan. Après vérification d’un callback de résultat complet, utilisez directement son résultat sans nouvelle consultation.

Utilisez directement l’URL du résultat

Les fichiers prêts incluent url et expiresAt, en plus de key et des métadonnées. Téléchargez directement output.assets[].url sans identifiants API, par exemple avec curl -L -o result.png "<url>". Consultez de nouveau la tâche après expiration de l’URL ; /common/download-url reste facultatif pour les intégrations existantes. Les URL durent normalement 20 minutes et ne prolongent pas la conservation de 14 jours du résultat.

succeeded
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "MODEL_ID_FROM_CATALOG",
    "state": "succeeded",
    "input": { "prompt": "a folded paper lantern, hard side light" },
    "output": {
      "assets": [
        {
          "key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/9f3c1d0a7b4e2f68c5a1d3e9b0472fa1.png",
          "url": "https://example.r2.cloudflarestorage.com/results/result.png?X-Amz-Signature=SIGNATURE_FROM_RESPONSE",
          "expiresAt": "2026-08-28T09:32:11Z",
          "mime": "image/png",
          "width": 1024,
          "height": 1024,
          "bytes": 1483920
        }
      ]
    },
    "cost": "0.008",
    "settled": true,
    "createdAt": "2026-08-28T09:12:04Z",
    "deadlineAt": "2026-08-28T09:42:04Z",
    "completedAt": "2026-08-28T09:12:11Z",
    "contentState": "present"
  },
  "request_id": "req_01k3m8xa1b2c3d4e5f6g7h8j9k0m"
}
failed
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "MODEL_ID_FROM_CATALOG",
    "state": "failed",
    "errorCode": "upstream_failed",
    "errorMessage": "Generation failed; the charge has been refunded",
    "cost": "0",
    "settled": true,
    "createdAt": "2026-08-28T09:12:04Z",
    "deadlineAt": "2026-08-28T09:42:04Z",
    "completedAt": "2026-08-28T09:12:39Z",
    "contentState": "present"
  },
  "request_id": "req_01k3m8xb2c3d4e5f6g7h8j9k0m1n"
}

Une consultation réussie ne signifie pas que la tâche a réussi

Les deux réponses contiennent code: 200 : la consultation a réussi. Le résultat de la tâche se lit dans data.state. Le code métier décrit la réponse ; state décrit la tâche. Ne les confondez pas.

Le corps d’un webhook utilise la même structure : enveloppe code: 200 et champs data identiques. Le même parseur traite cette réponse et un callback sans modification : deux modes de réception, un seul parseur.

Structure de output

output comporte deux parties indépendantes. Les fichiers générés, images, vidéos ou audio, arrivent dans output.assets[]. Les résultats textuels arrivent dans output.text, sous forme de chaîne simple.

La plupart des modèles remplissent une seule partie, mais un modèle peut remplir les deux. La partie reçue dépend du point de terminaison, pas de sa modality : un point de terminaison speech-to-text est classé dans audio parce qu’il lit de l’audio, tout en répondant par du texte.

`output.assets` n’est pas toujours présent

Un point de terminaison produisant du texte n’a aucune clé assets, pas même un tableau vide. Un code qui ouvre le résultat avec task.output.assets[0].url déclenche une exception sur ces tâches, au lieu de fournir une valeur à vérifier. Lisez d’abord output.text s’il est présent et traitez assets comme facultatif :

const { text, assets = [] } = task.output ?? {};
if (text) console.log(text);
for (const asset of assets) console.log(asset.url);

Un résultat textuel n’a rien à télécharger ni aucun lien à faire expirer, mais reste un résultat de tâche : il est supprimé selon le même calendrier que les autres résultats et contentState passe alors à expired. Copiez ce que vous souhaitez garder. Un résultat reçu en plusieurs parties, par exemple les segments d’une transcription, est assemblé en une chaîne avec des retours à la ligne entre les parties.

La suite de cette section décrit assets[].

ChampTypeRemarques
keystringIdentifiant de compatibilité permettant l’actualisation facultative du lien. Vide tant que pending est actif
urlstringURL GET signée directe. Omise si le fichier est en attente ou indisponible ; n’ajoutez pas de clé API
expiresAtstringExpiration RFC 3339 de cette URL ; consultez de nouveau pour l’actualiser pendant la conservation
mimestringType de contenu, comme image/png ou video/mp4
width · heightintegerDimensions en pixels. Absentes pour l’audio et le texte
durationSecondsnumberDurée en secondes. Vidéo et audio uniquement
bytesintegerTaille en octets
pendingbooleanVoir ci-dessous
unavailablebooleanVoir ci-dessous
layerobjectUniquement pour une décomposition en calques : zIndex, entier, boundingBox, valeur [x1, y1, x2, y2] facultative, et name, facultatif. Voir ci-dessous

Les champs inconnus sont omis, sans null ni 0. Traitez-les comme facultatifs.

Deux indicateurs demandent une gestion particulière :

  • pending: true : le fichier est encore en cours de copie vers notre stockage ; key et url sont provisoirement vides. Consultez de nouveau dans quelques secondes ; appeler download-url sans key à ce stade renvoie 409.
  • unavailable: true : le fichier a expiré ou reste définitivement indisponible après un échec de transfert. Il ne reviendra pas en attendant ; régénérez-le. Si aucun fichier d’une tâche réussie n’a pu être stocké, cette tâche n’est pas facturée.

Une décomposition en calques ajoute le champ layer. Le premier fichier est l’image de base, soit l’image source dont les éléments séparés ont été retirés, avec zIndex 0 ; tous les autres sont des calques PNG transparents. Empilez-les par zIndex croissant pour reconstruire l’image. boundingBox utilise [x1, y1, x2, y2] en pixels de l’image de base : redimensionnez le calque pour remplir ce rectangle afin de le replacer correctement. Les modèles sans information de position omettent ce champ, mais les calques restent utilisables comme éléments détourés. name est le libellé du modèle et n’apparaît que s’il en fournit un. Chaque image renvoyée est facturée, y compris l’image de base.

4. Privilégier les callbacks, garder le polling en secours

Le polling consiste à appeler recordInfo régulièrement jusqu’à la fin de la tâche. Avec un callback, nous envoyons une fois la tâche terminée à votre serveur. Les deux fournissent les mêmes données.

CallbackPolling
LatenceDès la fin de la tâcheSelon votre intervalle de consultation
Requêtes1 par tâche, envoyée par nousN par tâche, envoyées par vous
Consomme votre quota de requêtesNonOui ; voir Limites de débit
Nécessite un point de terminaison HTTPS publicOuiNon

Utilisez les callbacks en production. Une vidéo de trois minutes consultée chaque seconde représente 180 requêtes apportant presque aucune information nouvelle. Le mécanisme actuel de protection de l’API publique, à 1 000 requêtes par 10 secondes, ne gêne pas la génération payante normale ; un polling trop fréquent crée pourtant du trafic inutile, et une boucle incontrôlée épuise son propre quota de lecture jusqu’à ce que recordInfo renvoie 429.

Le polling est pertinent dans deux situations : développement local et services sans point d’entrée public.

Espacer progressivement les consultations

recordInfo utilise le quota de lecture, et createTask celui d’écriture : le polling ne peut donc pas priver les envois de leur quota. En revanche, un polling excessif fait renvoyer par recordInfo lui-même le code 429 : vous perdez la visibilité sur les tâches jusqu’à la reconstitution du quota.

Une approche adaptée :

  • Commencez à 2–3 secondes. Un intervalle plus court est inutile ; même les modèles d’image les plus rapides prennent quelques secondes.
  • Multipliez par 1,5, avec un plafond de 10–15 secondes.
  • Arrêtez selon l’échéance propre à la tâche, pas après une durée fixe en minutes. Certains modèles vidéo peuvent fonctionner jusqu’à une heure : un délai fixe de 10 minutes abandonnerait des tâches encore normales. Utilisez deadlineAt dans la réponse createTask, avec quelques minutes supplémentaires pour l’enregistrement du résultat côté serveur.

Arrêter d’attendre n’est pas un échec de tâche

À la fin de votre attente, vous avez seulement cessé d’attendre. La tâche continue, peut réussir et être facturée. Ne la renvoyez pas avec une nouvelle Idempotency-Key : cela crée une deuxième tâche payante. Consultez plus tard le même taskId ou retrouvez-le dans l’historique.

JavaScript
// created is the data object returned by createTask.
async function waitForTask(created) {
  // Stop a few minutes after the server's own deadline.
  const giveUpAt = Date.parse(created.deadlineAt) + 5 * 60 * 1000;
  let wait = 2000;

  while (Date.now() < giveUpAt) {
    await new Promise((r) => setTimeout(r, wait));
    wait = Math.min(wait * 1.5, 15000);

    const res = await fetch(
      `https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${encodeURIComponent(created.taskId)}`,
      { headers: { Authorization: `Bearer ${process.env.SPICY_API_KEY}` } },
    );
    const body = await res.json();

    // Being rate-limited is not a task failure — honour Retry-After
    if (body.code === 429) {
      wait = Number(res.headers.get('retry-after') ?? 1) * 1000;
      continue;
    }
    if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);

    // Terminal means "not one of the two in-flight states"
    const { state } = body.data;
    if (state !== 'queued' && state !== 'running') return body.data;
  }
  // Not a failure: query this taskId again later instead of resubmitting.
  throw new Error(`stopped waiting for ${created.taskId}`);
}

// created = body.data from the createTask example above
const task = await waitForTask(created);
if (task.state === 'succeeded') console.log(task.output.assets[0].url);
else console.log(task.state, task.errorCode);
Python
import os, time, requests
from datetime import datetime

BASE = "https://api.spicyapi.ai/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}"}


def wait_for_task(created: dict) -> dict:
    """created is the data object returned by createTask."""
    # Stop a few minutes after the server's own deadline.
    deadline = datetime.fromisoformat(created["deadlineAt"].replace("Z", "+00:00"))
    give_up_at = deadline.timestamp() + 5 * 60
    wait = 2.0

    while time.time() < give_up_at:
        time.sleep(wait)
        wait = min(wait * 1.5, 15.0)

        res = requests.get(f"{BASE}/jobs/recordInfo", headers=AUTH, params={"taskId": created["taskId"]})
        body = res.json()

        # Being rate-limited is not a task failure — honour Retry-After
        if body["code"] == 429:
            wait = float(res.headers.get("retry-after", 1))
            continue
        if body["code"] != 200:
            raise RuntimeError(f'{body["code"]} {body["msg"]}')

        # Terminal means "not one of the two in-flight states"
        if body["data"]["state"] not in ("queued", "running"):
            return body["data"]

    # Not a failure: query this taskId again later instead of resubmitting.
    raise TimeoutError(f'stopped waiting for {created["taskId"]}')


# created = body["data"] from the createTask example above
task = wait_for_task(created)
if task["state"] == "succeeded":
    print(task["output"]["assets"][0]["url"])
else:
    print(task["state"], task.get("errorCode"))

5. Vérifier votre solde

GET /api/v1/chat/credit renvoie available, les fonds pouvant financer de nouvelles tâches, held, les fonds bloqués par les tâches en cours, et total. available peut être négatif lorsqu’un crédit approuvé est utilisé et les fonds promotionnels de funding.grants peuvent être réservés à certains modèles. Détails : Consulter votre solde.

curl
curl https://api.spicyapi.ai/api/v1/chat/credit \
  -H "Authorization: Bearer $SPICY_API_KEY"

Historique et récupération des tâches

GET /api/v1/jobs liste les tâches visibles créées par la clé API actuelle. Il permet de retrouver un traitement après un redémarrage de l’application ou un callback manqué. Les tâches d’autres clés et les enregistrements masqués sont exclus. Les réponses contiennent les identifiants de tâches, le modèle, l’état, le coût, l’état du règlement, les horodatages et, lorsqu’il a été enregistré, le requestId de l’appel de création ; pour un appel textuel, c’est la même valeur que l’en-tête X-Request-Id. Elles n’incluent ni prompts, ni entrées, ni sorties, ni liens de médias. Consultez une tâche choisie avec recordInfo pour récupérer son résultat.

Filtrez selon state, un model exact du catalogue ou un alias, et des dates UTC from / to au format YYYY-MM-DD, borne de début incluse et borne de fin exclue. La période par défaut est de sept jours jusqu’à demain en UTC ; le maximum est de 92 jours. limit vaut 20 par défaut et ne peut pas dépasser 100.

Omettez les filtres inutilisés : une valeur vide telle que model=, un paramètre répété ou un paramètre inconnu renvoie 400. Une requête dépassant cinq secondes renvoie 503 ; réessayez avec une période plus courte.

curl --fail-with-body \
  'https://api.spicyapi.ai/api/v1/jobs?from=2026-09-01&to=2026-09-07&state=failed&limit=20' \
  -H "Authorization: Bearer $SPICY_API_KEY"

Lisez data.items. Si data.hasMore vaut true, utilisez data.nextCursor comme cursor de la requête suivante, en conservant tous les filtres et les dates explicites. L’ordre est décroissant selon createdAt, puis l’identifiant de tâche. Les pages reflètent l’état actuel, sans constituer un instantané figé. Ne construisez pas vos propres curseurs. Consulter l’historique ne crée ni ne relance aucune tâche.

La fonction existe aussi via client.listTasks(...), spicyapi tasks list et l’outil MCP en lecture seule spicyapi_tasks_list. Conservez cost en chaîne décimale USD ; lorsque settled vaut false, il représente encore l’estimation réservée, pas les frais définitifs.

Si la réponse de création a été perdue, réessayez d’abord la requête inchangée avec son Idempotency-Key d’origine. Une page d’historique vide n’autorise pas à recréer ce même traitement payant avec une nouvelle clé.

Sur cette page