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 :
- 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. - Vous revenez plus tard demander si ce
taskIdest 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. - Lorsque
statevautsucceeded, le lien du fichier se trouve dansdata.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. Consultezsettledetcostpour 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/createTaskCet 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ête | Obligatoire | Remarques |
|---|---|---|
Authorization | Oui | Bearer sk-spicy-… |
Content-Type | Oui | application/json |
Idempotency-Key | Non | Fortement recommandé : permet de réessayer sans risque après une erreur réseau. Voir Idempotence |
X-Spicy-Retention | Non | Ré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: 2Le 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"
}| Champ | Remarques |
|---|---|
taskId | Identifiant de tâche, préfixé par job_. Toutes les opérations suivantes en ont besoin : conservez-le |
state | queued 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é |
estimatedCost | Montant 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 |
deadlineAt | Derniè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.
| HTTP | code | Signification habituelle | Action |
|---|---|---|---|
| 400 | 400 | Un champ de input n’appartient pas au schéma du modèle, une valeur est hors plage ou callBackUrl est invalide | Comparez avec inputSchema du modèle ; msg précise le problème |
| 400 | 40004 | Chaque champ est valide, mais cette combinaison ne peut pas être exécutée actuellement | Modifiez la combinaison, par exemple la résolution ou la durée |
| 401 | 401 | Clé API absente, incorrecte ou désactivée | Vérifiez l’en-tête Authorization |
| 402 | 40201 | Solde insuffisant pour réserver estimatedCost | Ajoutez des fonds dans la console, puis renvoyez avec la même Idempotency-Key |
| 402 | 40202 | Plafond de dépenses atteint sur cette clé, équipe ou compte | Relevez le plafond dans la console ou attendez la réinitialisation du plafond quotidien ou mensuel |
| 403 | 40301 | Cette clé ne peut pas appeler ce modèle | Utilisez une autre clé ou modifiez ses restrictions de modèles |
| 409 | 409 / 40901 | Conflit d’idempotence ou devis qui ne correspond plus | Voir Idempotence et Devis |
| 413 | 413 | Corps de requête trop volumineux, généralement à cause de médias intégrés | Téléversez le fichier et transmettez son URI spicy:// ; voir Médias |
| 429 | 429 | Trop de requêtes sur une courte période | Attendez selon Retry-After, puis renvoyez avec la même clé |
| 503 | 50301 | Modèle temporairement indisponible | Ré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.
# 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.
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);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
queuedrunningsucceededfailedexpiredUne 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.
| État | Signification | Final | Facturation |
|---|---|---|---|
queued | Créée, fonds réservés, en attente de prise en charge | Non | Réservation, sans règlement |
running | Le modèle génère ; résultat attendu | Non | Réservation, sans règlement |
succeeded | Génération réussie | Oui | Règlement selon l’usage réel, limité à la réservation |
failed | Génération échouée | Oui | Libé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 |
canceled | Compatibilité avec l’historique uniquement ; aucune action d’annulation disponible | Oui | Libération intégrale |
expired | Durée maximale d’exécution du modèle dépassée | Oui | Libé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 -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ésultat | Réponse |
|---|---|
| État final atteint dans le délai | 200 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éconnectez | Aucun 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 "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
| Champ | Type | Remarques |
|---|---|---|
taskId | string | Identifiant de tâche |
sourceTaskId | string | Uniquement pour une tâche créée par relance : tâche d’origine |
model | string | Identifiant du modèle |
state | string | Voir le tableau ci-dessus |
input | object | Paramètres validés ; les images intégrées deviennent des URI de fichiers de la plateforme. Omis après expiration des entrées |
output | object | Description du résultat. Pertinent uniquement à l’état succeeded |
errorCode | string | Pré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 |
errorMessage | string | Motif lisible de l’échec, en anglais sauf si une autre langue est sélectionnée. Présent uniquement en cas d’échec |
cost | string | Montant 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 |
settled | boolean | Indique si la facturation est définitive. Tant que la valeur est false, cost peut encore changer |
createdAt | string | RFC 3339 |
deadlineAt | string | Échéance d’exécution enregistrée à l’acceptation ; peut être omise dans les anciens événements webhook |
completedAt | string | RFC 3339. États finaux uniquement |
contentState | string | present, expired pour une suppression après conservation ou purged pour une suppression demandée par vous. Voir Conservation et suppression |
contentRemovedBy | string | user ou system ; omis tant que le contenu est présent |
purgedAt | string | RFC 3339. Uniquement après une suppression demandée par vous |
retention | object | outputsExpireAt 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
taskIdet consultez plus tard cette même tâche pour reprendre. deadlineAtest 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 etsettledplutôt que de déduire un remboursement d’un minuteur local.output.assets[].expiresAtfait 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.
{
"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"
}{
"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[].
| Champ | Type | Remarques |
|---|---|---|
key | string | Identifiant de compatibilité permettant l’actualisation facultative du lien. Vide tant que pending est actif |
url | string | URL GET signée directe. Omise si le fichier est en attente ou indisponible ; n’ajoutez pas de clé API |
expiresAt | string | Expiration RFC 3339 de cette URL ; consultez de nouveau pour l’actualiser pendant la conservation |
mime | string | Type de contenu, comme image/png ou video/mp4 |
width · height | integer | Dimensions en pixels. Absentes pour l’audio et le texte |
durationSeconds | number | Durée en secondes. Vidéo et audio uniquement |
bytes | integer | Taille en octets |
pending | boolean | Voir ci-dessous |
unavailable | boolean | Voir ci-dessous |
layer | object | Uniquement 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 ;keyeturlsont provisoirement vides. Consultez de nouveau dans quelques secondes ; appelerdownload-urlsanskeyà ce stade renvoie409.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.
| Callback | Polling | |
|---|---|---|
| Latence | Dès la fin de la tâche | Selon votre intervalle de consultation |
| Requêtes | 1 par tâche, envoyée par nous | N par tâche, envoyées par vous |
| Consomme votre quota de requêtes | Non | Oui ; voir Limites de débit |
| Nécessite un point de terminaison HTTPS public | Oui | Non |
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
deadlineAtdans la réponsecreateTask, 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.
// 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);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 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é.

