SDK TypeScript
Un guide pas à pas du SDK TypeScript @spicyapi/sdk pour l’installer, obtenir votre premier résultat et retrouver chaque méthode, option et erreur expliquée avec des mots simples. Les SDK Python, Go, PHP et Java sont documentés sur leurs propres pages.
@spicyapi/sdk est la bibliothèque officielle TypeScript et JavaScript de SpicyAPI. Cette page part du principe que vous ne l’avez jamais utilisée. Elle vous mène d’un dossier vide à une image enregistrée, puis décrit chaque fonction du paquet : à quoi elle sert, ce que vous lui passez, ce qu’elle renvoie, si elle coûte de l’argent et ce qui peut mal tourner.
Qu’est-ce que le SDK ?
Un SDK est une bibliothèque de code toute prête que vous ajoutez à votre propre programme. Sans lui, votre programme doit construire chaque requête HTTP à la main : joindre la clé API, transformer la réponse JSON en données, demander encore et encore si une tâche est terminée, réessayer quand le réseau coupe, téléverser les fichiers en trois étapes distinctes et vérifier la signature de chaque webhook. Le SDK fait tout cela pour vous, si bien qu’une génération complète tient en un seul appel :
const task = await client.run({ model: modelId, input: { prompt: "A paper lantern at night" } });Il vous convient si vous écrivez, ou allez écrire, un programme Node.js (JavaScript ou TypeScript) qui s’exécute sur un serveur, dans un script sur votre propre ordinateur ou dans une tâche d’arrière-plan. Si vous utilisez TypeScript, vous bénéficiez en plus de l’autocomplétion et de la vérification des types pour chaque champ.
Est-ce le bon outil ?
| Vous voulez… | Utilisez |
|---|---|
| Appeler SpicyAPI depuis votre propre code Node.js ou TypeScript | Ce SDK |
| Générer des contenus sans écrire de code, depuis un terminal | La CLI |
| Laisser un assistant IA comme Claude Code ou Cursor appeler SpicyAPI pour vous | Le serveur MCP et l’Agent Skill |
| Utiliser un modèle de chat ou de texte | Le client officiel d’OpenAI, pointé vers SpicyAPI ; voir Texte et streaming |
| Utiliser Python, Go, PHP ou Java | Le SDK Python, le SDK Go, le SDK PHP ou le SDK Java officiels |
| Utiliser n’importe quel autre langage | De simples appels HTTP, ou un client généré à partir de la référence de l’API |
Ce qu’il ne fait pas
- Il ne fonctionne ni dans un navigateur ni dans une application mobile. Il a besoin de Node.js, et votre clé API ne doit jamais quitter votre serveur : n’importe qui peut lire une clé dans une page web ou une application. Faites appeler votre propre backend par votre site, et laissez ce backend appeler SpicyAPI.
- Il ne gère pas les modèles de chat. La génération de texte passe par des points de terminaison compatibles OpenAI, et le paquet officiel
openaile fait déjà très bien. Voir Texte et streaming. - Il ne choisit pas le modèle et ne remplit pas les champs à votre place. Chaque modèle accepte ses propres champs. Le SDK vous fournit la liste des modèles et le schéma de chacun (la liste des champs qu’il accepte) ; c’est vous qui décidez quoi envoyer.
- Il ne peut pas annuler une tâche. Une fois qu’une tâche est acceptée par SpicyAPI, elle va jusqu’au bout. Arrêter votre programme n’arrête que votre attente.
- Il ne gère pas votre compte. Les recharges, la création de clés, les plafonds de dépense et les clés de signature des webhooks se règlent dans la console.
Votre clé API reste sur votre serveur
Ne mettez jamais SPICY_API_KEY dans du JavaScript exécuté par un navigateur, dans une application mobile, un dépôt public, une capture d’écran ou un message de discussion. Une clé divulguée peut dépenser votre solde jusqu’à ce que vous la désactiviez. Le SDK lit la clé dans une variable d’environnement, pour qu’elle n’ait jamais à apparaître dans votre code.
Vocabulaire de cette page
| Terme | Ce qu’il signifie ici |
|---|---|
| Identifiant de modèle | Le nom exact d’un modèle, de la forme publisher/model/task. Copiez-le toujours depuis le catalogue en direct (listModels()), jamais de mémoire |
| Entrée | Les réglages que vous envoyez avec une tâche, comme prompt ou image_url. Chaque modèle a sa propre liste de champs autorisés, appelée son schéma |
| Tâche | Un travail de génération. Vous la créez, SpicyAPI la traite en arrière-plan, et vous récupérez le résultat. Chaque tâche a un identifiant qui commence par job_ |
| Réservation | Quand une tâche est acceptée, son prix maximal est mis de côté sur votre solde. Les tâches échouées ou expirées libèrent normalement toute la réservation. Certains refus de contenu sont facturés au devis accepté, uniquement si la page du modèle indique explicitement cette exception. Consultez settled et cost pour connaître le montant final. |
| Clé d’idempotence | Une étiquette que vous inventez pour une opération donnée, comme order-8814-image-1. Si vous envoyez deux fois la même requête avec la même étiquette, SpicyAPI renvoie la première tâche au lieu d’en créer et d’en facturer une seconde |
| Webhook | Une adresse web à vous que SpicyAPI appelle quand une tâche se termine, pour que vous n’ayez pas à demander sans arrêt |
Ce que vous pouvez faire
Chaque fonction ci-dessous est une méthode du client (client.listModels()), sauf mention contraire. « Payant » signifie que l’appel peut entraîner un prélèvement sur votre solde.
| Ce que vous voulez faire | Méthode | Payant ? |
|---|---|---|
| Vérifier que SpicyAPI est joignable (sans clé) | getStatus | Non |
| Voir la liste des modèles en direct, vos prix et les champs de chaque modèle | listModels, getModel | Non |
| Connaître le prix exact d’une requête avant de dépenser quoi que ce soit | quoteTask | Non |
| Lancer une génération | createTask | Oui |
| Lancer une génération et attendre le résultat en un seul appel | run | Oui |
| Consulter une tâche une fois | getTask | Non |
| Attendre qu’une tâche se termine | waitForTask | Non |
| Relancer une tâche échouée | retryTask | Oui (une nouvelle tâche) |
| Lister les tâches créées avec votre clé | listTasks | Non |
| Consulter votre solde | getBalance | Non |
| Voir ce que vous avez dépensé, par jour et par modèle | getUsage | Non |
| Téléverser une image, une vidéo ou un fichier audio local | uploadFile, uploadBytes, uploadBase64 | Non |
| Obtenir un nouveau lien de téléchargement pour un résultat | createDownloadUrl | Non |
| Supprimer les fichiers stockés et le prompt d’une tâche terminée | purgeTask | Non (et sans remboursement) |
| Vérifier qu’un webhook vient bien de SpicyAPI | verifyWebhook (une fonction simple) | Non |
| Chercher dans la documentation ou lire le contrat de l’API hors ligne | searchDocumentation, readOpenApiContract (des fonctions simples) | Non |
Avant de commencer
Comptez une dizaine de minutes. Chaque étape explique ce que fait la commande, pour que rien ne s’exécute à l’aveugle.
Installer Node.js 22.13 ou plus récent
Node.js est le programme qui exécute du JavaScript en dehors d’un navigateur. Installez la version LTS depuis nodejs.org ; toute version à partir de 22.13 convient. Ouvrez ensuite un terminal (Terminal sur macOS, PowerShell sur Windows) et vérifiez :
node -vLa commande affiche la version installée, par exemple v22.13.0 ou v24.8.0. Si le numéro est inférieur à 22.13, ou si la commande est introuvable, réinstallez Node.js et ouvrez une nouvelle fenêtre de terminal.
Créer un dossier de projet et installer le SDK
Lancez ces commandes une par une. Elles fonctionnent de la même façon sur macOS, Linux et Windows PowerShell.
mkdir spicy-demo
cd spicy-demo
npm init -y
npm pkg set type=module
npm install @spicyapi/sdk| Commande | Ce qu’elle fait |
|---|---|
mkdir spicy-demo | Crée un dossier vide pour votre projet |
cd spicy-demo | Place le terminal dans ce dossier ; les commandes suivantes s’y appliquent |
npm init -y | Crée package.json, le fichier qui liste les réglages et les bibliothèques de votre projet |
npm pkg set type=module | Indique à Node.js que vos fichiers utilisent la syntaxe moderne import, exigée par ce SDK |
npm install @spicyapi/sdk | Télécharge le SDK dans un dossier node_modules et l’inscrit dans package.json |
Le SDK n’est publié que sous forme de module ES moderne : import { SpicyClient } from "@spicyapi/sdk" fonctionne, tandis que l’ancienne forme require("@spicyapi/sdk") échoue avec ERR_PACKAGE_PATH_NOT_EXPORTED. Si vous sautez npm pkg set type=module, donnez plutôt l’extension .mjs à vos fichiers.
Créer une clé API
- Connectez-vous sur spicyapi.ai. Pas encore de compte ? Inscrivez-vous sur spicyapi.ai/register ; si les nouveaux comptes sont suspendus, cette page propose une liste d’attente.
- Ouvrez la page Clés API de la console et cliquez sur Créer une clé. Seul un nom est obligatoire.
- Copiez la clé tout de suite. Elle commence par
sk-spicy-et n’est affichée qu’une seule fois. Si vous la perdez, créez-en une nouvelle.
Vous pouvez régler un plafond de dépense quotidien sur la même page ; les nouvelles clés n’en ont aucun tant que vous n’en ajoutez pas un. Pour lancer des tâches payantes, il vous faut aussi un solde, que vous approvisionnez dans Facturation.
Placer la clé dans une variable d’environnement
Une variable d’environnement est une valeur nommée que votre terminal transmet à chaque programme qu’il lance. Le SDK en cherche une appelée SPICY_API_KEY : la clé n’a donc jamais besoin d’être écrite dans votre code.
# À définir pour cette fenêtre de terminal (collez votre propre clé)
export SPICY_API_KEY="sk-spicy-paste-your-key-here"
# Vérification : ceci doit afficher sk-spicy-
echo "${SPICY_API_KEY:0:9}"La valeur disparaît quand vous fermez la fenêtre. Pour la garder sur votre ordinateur, ajoutez la ligne export à ~/.zshrc (macOS) ou à ~/.bashrc (Linux), puis ouvrez une nouvelle fenêtre.
Vous préférez un fichier .env ? Créez dans le dossier du projet un fichier nommé .env contenant une seule ligne, SPICY_API_KEY=sk-spicy-paste-your-key-here, et lancez votre programme avec node --env-file=.env yourfile.mjs. Le SDK ne lit pas les fichiers .env de lui-même : c’est cette option qui s’en charge. Ajoutez .env à votre fichier .gitignore avant de lancer le moindre git add, pour que la clé ne soit jamais versionnée.
Sur un vrai serveur, définissez SPICY_API_KEY dans les réglages de secrets ou de variables d’environnement de votre hébergeur, ou transmettez la clé depuis votre gestionnaire de secrets avec new SpicyClient({ apiKey }).
Lancer votre premier fichier
Dans le dossier du projet, créez avec n’importe quel éditeur de texte un fichier nommé check.mjs, collez-y ce code et enregistrez :
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const status = await client.getStatus();
console.log(status.operational ? "SpicyAPI is up" : "SpicyAPI is not reachable right now");
console.log("Checked at", status.checkedAt);Lancez-le :
node check.mjsSpicyAPI is up
Checked at 2026-09-17T08:00:00.000ZCette vérification ne demande aucune clé et ne coûte rien : elle prouve simplement que Node.js et le SDK sont correctement installés.
Vous utilisez TypeScript ? Enregistrez le même code sous le nom check.ts. Les versions récentes de Node.js 22 et 24 l’exécutent directement avec node check.ts. Si la vôtre proteste à cause de l’extension .ts, lancez node --experimental-strip-types check.ts, ou npx tsx check.ts.
Votre premier résultat
Ce programme fait tout le parcours : il vérifie le service, choisit un modèle d’image que votre clé peut utiliser, demande le prix exact et s’arrête. Relancez-le avec --yes : il crée alors l’image, attend qu’elle soit prête et l’enregistre à côté du script.
L’exécution avec --yes coûte de l’argent
Sans --yes, le programme se contente de lire : rien n’est créé et rien n’est facturé. Avec --yes, il crée une tâche d’image, qui réserve le montant du devis et le facture si l’image est réussie.
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";
// Lit votre clé dans la variable d’environnement SPICY_API_KEY.
const client = new SpicyClient();
async function main() {
// Étape 1 — SpicyAPI est-il joignable ? Gratuit, et sans clé.
const status = await client.getStatus();
if (!status.operational) {
throw new Error("SpicyAPI is not reachable right now. Try again in a few minutes.");
}
// Étape 2 — Quels modèles d’image cette clé peut-elle appeler ? Gratuit.
const catalog = await client.listModels({ modality: "image", includeExamples: true });
const model = catalog.items.find(
(item) => item.enabled && item.available && item.examples?.length,
);
if (!model) throw new Error("No image model is available to this key right now.");
console.log(`Model: ${model.displayName} (${model.model})`);
// Étape 3 — Construire l’entrée. On part d’un exemple déjà validé par le serveur,
// et on y met votre propre prompt si le modèle en accepte un.
const input = { ...model.examples?.[0]?.input };
if ("prompt" in input) input.prompt = "A paper lantern glowing on a rainy street at night";
// Étape 4 — Demander le prix exact. Gratuit : rien n’est créé ni réservé.
const request = { model: model.model, input };
const quote = await client.quoteTask(request);
console.log(`Price: $${quote.estimatedCost} USD`);
if (!process.argv.includes("--yes")) {
console.log("Nothing was charged. Run again with --yes to generate the image.");
return;
}
// Étape 5 — Créer la tâche et l’attendre. C’est l’étape qui coûte de l’argent.
const task = await client.run(
{ ...request, quoteId: quote.quoteId, expectedCost: quote.estimatedCost },
{
idempotencyKey: randomUUID(),
onAccepted: (accepted) => console.log(`Task accepted: ${accepted.taskId}`),
onUpdate: (update) => console.log(`State: ${update.state}`),
},
);
// Étape 6 — Une tâche échouée reste une réponse normale. Vérifiez le règlement.
if (task.state !== "succeeded") {
console.log(`The task ended as ${task.state}: ${task.errorCode} ${task.errorMessage ?? ""}`);
console.log("Check settled and cost for the final charge.");
return;
}
// Étape 7 — Télécharger le résultat maintenant. Le lien expire au bout d’environ 20 minutes.
const asset = task.output?.assets?.find((item) => item.url);
if (!asset?.url) throw new Error("The task succeeded but its file is unavailable.");
const download = await fetch(asset.url); // n’envoyez jamais votre clé API ici
if (!download.ok) throw new Error(`Download failed with HTTP ${download.status}`);
const extension = asset.mime?.split("/")[1] ?? "png";
await writeFile(`result.${extension}`, Buffer.from(await download.arrayBuffer()));
console.log(`Saved result.${extension}. Charged: $${task.cost}${task.settled ? "" : " (not final yet)"}`);
}
main().catch((error) => {
if (error instanceof SpicyApiError) {
console.error(`SpicyAPI error ${error.code}: ${error.message} (request_id ${error.requestId})`);
} else {
console.error(error);
}
process.exitCode = 1;
});Lancez-le deux fois :
node first-result.mjs
node first-result.mjs --yesModel: <display name> (<model ID>)
Price: $<amount> USD
Nothing was charged. Run again with --yes to generate the image.
Model: <display name> (<model ID>)
Price: $<amount> USD
Task accepted: job_…
State: running
State: succeeded
Saved result.png. Charged: $<amount>Ce que fait chaque étape
getStatus()demande au point de terminaison de santé de SpicyAPI si le service fonctionne. Elle ne lève jamais d’erreur pour un problème réseau ; elle renvoieoperational: falseà la place.listModels()renvoie le catalogue des modèles en direct (live catalog), avec les prix de votre compte. N’utilisez que les éléments dontenabledetavailablevalent tous deuxtrue.includeExamples: trueajoute des exemples d’entrées déjà validés par le serveur, le point de départ le plus sûr. La valeur que vous enverrez ensuite est le champmodelde l’élément : l’identifiant de modèle.- L’entrée. Chaque modèle accepte des champs différents ; envoyer un champ que le modèle ne connaît pas est refusé avec l’erreur
400. Copier un exemple validé et ne changer que le prompt évite ce piège. Pour voir tous les champs qu’accepte un modèle, appelezgetModel()et lisez soninputSchema. quoteTask()calcule le prix de cette requête précise. Elle ne crée rien et ne réserve rien. Les montants sont des dollars américains écrits sous forme de texte, comme"0.04", pour éviter toute erreur d’arrondi.run()crée la tâche, puis la surveille jusqu’à ce qu’elle se termine. TransmettrequoteIdetexpectedCostrevient à dire « n’accepte cette tâche qu’au prix qu’on m’a montré » : si le prix a changé, l’appel échoue avec40901au lieu de facturer un autre montant. L’idempotencyKeyrend sûre une nouvelle tentative après un problème réseau ; dans une vraie application, enregistrez-la avant l’appel, pour qu’un programme redémarré puisse la réutiliser.onAcceptedvous donne l’identifiant de la tâche dès qu’elle existe : enregistrez-le, pour pouvoir consulter la tâche plus tard même si votre programme s’arrête.- L’état de la tâche.
run()rend la main normalement, y compris pour les tâches échouées. Les tâches échouées ou expirées libèrent normalement toute la réservation. Certains refus de contenu sont facturés au devis accepté, uniquement si la page du modèle indique explicitement cette exception. Consultezsettledetcostpour connaître le montant final. - Le téléchargement. Chaque résultat est fourni avec une
urltemporaire. Téléchargez-le sans votre clé API et gardez-en votre propre copie : le lien dure environ 20 minutes (rappelezgetTask()pour en obtenir un nouveau), et SpicyAPI supprime les fichiers générés au bout d’environ 14 jours au plus. Voir Conservation et destruction.
La vidéo fonctionne de la même façon, mais peut prendre plusieurs minutes ; passez un timeoutMs plus long à run() (l’attente par défaut est de 10 minutes). Dans les extraits ci-dessous, l’identifiant de modèle apparaît sous la forme MODEL_ID_FROM_CATALOG : c’est un emplacement réservé, pas un vrai modèle. Remplacez-le par la valeur model d’un élément du catalogue en direct, obtenu avec listModels() dans le SDK ou avec le point de terminaison authentifié GET /api/v1/models, dont enabled et available valent tous deux true.
Référence de l’API
Chaque entrée ci-dessous indique à quoi sert la méthode, comment l’appeler, ce qu’elle renvoie, si elle peut coûter de l’argent et quelles erreurs attendre. Les classes d’erreur comme SpicyApiError sont expliquées dans Gérer les erreurs. Tous les montants sont des dollars américains écrits sous forme de texte ("0.04"), et toutes les dates sont des chaînes ISO 8601 en UTC.
Créer un client
new SpicyClient(options?: SpicyClientOptions)La plupart des programmes ne passent rien : new SpicyClient() lit la clé dans SPICY_API_KEY et dialogue avec le service de production. Une option invalide lève immédiatement une TypeError, avant qu’aucune requête ne soit envoyée.
Propriété
Type
Nouvelles tentatives automatiques
Le SDK ne renvoie une requête de lui-même que lorsque cela ne peut pas vous coûter deux fois :
| Appel | Nouvelle tentative automatique ? |
|---|---|
Lectures : getStatus, listModels, getModel, getTask, listTasks, getBalance, getUsage ; ainsi que quoteTask, purgeTask, commitUploadedFile | Oui |
createTask, run (son étape de création), retryTask | Seulement si vous passez une idempotencyKey. Sans elle, une erreur réseau vous est remontée directement, car renvoyer la requête pourrait créer une seconde tâche |
createUploadUrl, l’envoi des octets lui-même, createDownloadUrl | Non |
Une nouvelle tentative a lieu quand aucune réponse n’arrive (erreur réseau ou délai local dépassé) ou que la réponse est un HTTP 408, 429, 500, 502, 503 ou 504. La pause correspond à ce que demande l’en-tête Retry-After du serveur ; à défaut, 250 ms, puis 500 ms, puis 1 s, et ainsi de suite, chacune variant de 25 % au plus et plafonnée à maxRetryDelayMs. Si toutes les tentatives échouent, vous recevez la dernière erreur.
Arrêter un appel avec signal
Toutes les méthodes acceptent une option signal, un AbortSignal standard. Utilisez AbortSignal.timeout(5_000) pour fixer une échéance, ou un AbortController pour arrêter à la demande. L’appel lève alors la raison de l’interruption. Arrêter ne met fin qu’à votre requête ou à votre attente : une tâche déjà acceptée par SpicyAPI continue de s’exécuter et peut quand même être facturée.
getStatus()
Vérifie que SpicyAPI est joignable. Pratique en première ligne d’un script, ou comme vérification de santé dans votre propre supervision.
getStatus(options?: { signal?: AbortSignal }): Promise<ServiceStatus>| Champ renvoyé | Signification |
|---|---|
operational | true quand le service répond. C’est le champ à utiliser |
health | Le résultat brut de la vérification de santé : path, ok, le status HTTP, les 512 premiers caractères de body, et error quand aucune réponse n’est arrivée |
readiness | La même forme pour une seconde vérification. Sur le service public, elle répond 404, ce qui est normal et ne signifie pas que le service est en panne |
checkedAt | Le moment où la vérification a eu lieu |
Payant : non. Clé requise : non. Erreurs : elle ne lève pas d’erreur pour les problèmes réseau ; vous obtenez operational: false et la cause dans health.error. Elle ne lève une erreur que si vous l’interrompez avec signal. Voir Lancer votre premier fichier pour un exemple.
listModels(options?)
Liste le catalogue des modèles en direct, avec les prix de votre compte. C’est de là que vient chaque identifiant de modèle.
listModels(options?: ListModelsOptions): Promise<ModelList>Propriété
Type
Elle renvoie { total, items }. Les champs les plus utiles de chaque élément :
| Champ | Signification |
|---|---|
model | L’identifiant de modèle. Envoyez exactement cette valeur quand vous créez une tâche |
displayName | Un nom lisible |
modality, tasks | Ce que le modèle produit et le travail qu’il effectue |
provider, family | Qui a créé le modèle et à quelle gamme de produits il appartient |
enabled, available | Les deux doivent valoir true avant d’utiliser le modèle |
pricing | Une entrée par palier de prix : price, unit (per_image, per_second, per_request ou per_1k_tokens) et variant (le palier concerné, souvent une résolution). Pendant une remise, regularPrice indique le prix sans remise |
startingPrice | Le palier le moins cher. Pour un montant exact, utilisez quoteTask |
quantityField | Le champ d’entrée qui fixe la quantité facturée, comme une durée. S’il est vide, chaque appel compte pour une unité |
taskTimeoutSeconds | La durée pendant laquelle une tâche peut s’exécuter avant de se terminer en expired |
maxOutputDurationSeconds | La durée de sortie la plus longue autorisée par le modèle, lorsqu’il a une durée |
inputSchema, examples | Présents uniquement si vous les avez demandés |
updatedAt, version | Changent chaque fois que les détails du modèle changent ; servez-vous-en pour savoir quand rafraîchir un schéma enregistré |
Payant : non. Clé requise : oui. Erreurs : SpicyApiError (par exemple 401 pour un problème de clé, 429 pour trop de requêtes), SpicyTransportError.
const { items } = await client.listModels({ modality: "video", task: "image-to-video" });
for (const item of items) {
if (!item.enabled || !item.available) continue;
const from = item.startingPrice ? `from $${item.startingPrice.price} ${item.startingPrice.unit}` : "";
console.log(item.model, "|", item.displayName, "|", from);
}Les lectures du catalogue utilisent le quota de lecture du compte, indépendamment de la création de tâches. Enregistrez la liste et rafraîchissez-la de temps en temps, ou après un 400 ou un 50301, plutôt que de la relire avant chaque tâche.
getModel(modelId)
Renvoie un modèle avec son schéma complet : la liste des champs d’entrée qu’il accepte, ceux qui sont obligatoires et les valeurs autorisées.
getModel(modelId: string, options?: { signal?: AbortSignal }): Promise<ApiModel>Le résultat contient tous les champs décrits dans listModels, avec toujours inputSchema et examples. Le schéma est du JSON Schema standard : properties liste chaque champ avec son type et souvent enum (les valeurs autorisées), minimum / maximum et default ; required liste les champs que vous devez envoyer.
Payant : non. Clé requise : oui. Erreurs : SpicyApiError avec 404 quand aucun modèle ne porte cet identifiant ; TypeError si l’identifiant est vide.
const model = await client.getModel("MODEL_ID_FROM_CATALOG");
console.log("Required fields:", model.inputSchema?.required);
console.log(JSON.stringify(model.inputSchema?.properties, null, 2));
console.log("A request that is known to work:", model.examples?.[0]?.input);quoteTask(request)
Donne le prix exact d’une requête sans rien créer. Servez-vous-en pour montrer un prix à quelqu’un, ou pour refuser un travail qui dépasse un budget.
quoteTask(request: { model: string; input: object; callBackUrl?: string }, options?: { signal?: AbortSignal }): Promise<TaskQuote>Passez les mêmes model, input et callBackUrl facultative que ceux avec lesquels vous comptez créer la tâche.
| Champ renvoyé | Signification |
|---|---|
quoteId | À passer à createTask ou à run pour bloquer ce prix |
estimatedCost | Le montant qui sera réservé si vous créez la tâche maintenant |
maxCharge | Le montant maximal que peut coûter cette requête |
currency | Toujours USD |
quantity, unit | La quantité facturée, comme "5" et per_second |
expiresAt | Le devis est valable cinq minutes |
Payant : non. Clé requise : oui. Erreurs : SpicyApiError, par exemple 400 quand l’entrée ne correspond pas au schéma du modèle, 50301 quand le modèle est indisponible, 429.
const request = { model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } };
const quote = await client.quoteTask(request);
console.log(`This will cost $${quote.estimatedCost}. Quote valid until ${quote.expiresAt}.`);createTask(request, options?)
Lance une génération et rend la main immédiatement avec un identifiant de tâche. Elle n’attend pas le résultat : enchaînez avec waitForTask ou un webhook, ou utilisez run pour faire les deux en un seul appel.
createTask(request: CreateTaskInput, options?: CreateTaskOptions): Promise<CreateTaskResult>La requête :
Propriété
Type
Les options :
Propriété
Type
| Champ renvoyé | Signification |
|---|---|
taskId | L’identifiant de la nouvelle tâche. Enregistrez-le tout de suite |
state | queued pour une nouvelle tâche. Si vous avez réutilisé une clé d’idempotence, l’état actuel de la tâche d’origine |
estimatedCost | Le montant désormais réservé, qui est aussi le maximum que cette tâche peut facturer |
deadlineAt | Le moment où SpicyAPI cesse de travailler sur la tâche si elle n’est pas terminée |
Payant : oui. L’estimatedCost est réservé sur votre solde. Si la tâche réussit, vous êtes facturé selon l’usage réel, jamais plus que le montant réservé, et le reste est libéré. Les tâches échouées ou expirées libèrent normalement toute la réservation. Certains refus de contenu sont facturés au devis accepté, uniquement si la page du modèle indique explicitement cette exception. Consultez settled et cost pour connaître le montant final.
Erreurs :
error.code | Signification | Que faire |
|---|---|---|
400 | L’entrée ne correspond pas au schéma du modèle, ou la callBackUrl n’est pas autorisée | Comparez avec getModel().inputSchema et corrigez la requête |
401 | La clé est absente, mal formée ou révoquée | Vérifiez SPICY_API_KEY |
40201 | Votre solde ne couvre pas la réservation | Rechargez votre solde sur la page Facturation de la console |
40202 | Un plafond de dépense quotidien, mensuel ou cumulé est atteint | Relevez le plafond sur la page Clés API, ou attendez sa remise à zéro |
40301 | Cette clé n’est pas autorisée à utiliser ce modèle | Modifiez la liste des modèles de la clé dans la console |
409 | Cette clé d’idempotence a déjà servi pour une autre requête | Utilisez une nouvelle clé pour une nouvelle opération |
40901 | Le devis a expiré, ou le prix ou la requête a changé | Redemandez un devis et confirmez le nouveau prix |
413 | Le corps de la requête est trop volumineux | Téléversez les fichiers médias au lieu de les intégrer à la requête |
429 | Trop de requêtes | Attendez error.retryAfterSeconds, puis réessayez |
50301 | Le modèle ne peut pas prendre de tâches pour le moment | Réessayez plus tard, ou choisissez un autre modèle |
Une SpicyTransportError ou une SpicyTimeoutError signifie qu’aucune réponse n’est arrivée : impossible de savoir si la tâche a été créée. Renvoyez la même requête avec la même clé d’idempotence : soit vous la créez, soit vous récupérez la tâche existante. Une TypeError signifie qu’une option était invalide (par exemple un retentionSeconds négatif) et que rien n’a été envoyé.
import { randomUUID } from "node:crypto";
const idempotencyKey = randomUUID(); // dans une vraie application, enregistrez-la d’abord avec votre commande
const accepted = await client.createTask(
{ model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } },
{ idempotencyKey },
);
console.log(accepted.taskId, accepted.state, "on hold: $" + accepted.estimatedCost);run(request, options?)
La façon la plus simple de générer quelque chose : elle appelle createTask, puis waitForTask, et renvoie la tâche terminée.
run(request: CreateTaskInput, options?: RunTaskOptions): Promise<TaskRecord>request est identique à celle de createTask. Les options sont toutes celles qu’accepte createTask, plus :
Propriété
Type
Elle renvoie la tâche terminée, avec les champs décrits dans getTask. Pour une tâche réussie, elle attend aussi que la copie des fichiers de résultat soit terminée, pour que output.assets[].url soit prête à être téléchargée.
Payant : oui, exactement comme createTask. Une tâche échouée est renvoyée, pas levée comme erreur : vérifiez task.state.
Erreurs : tout ce que peut lever createTask, plus :
SpicyWaitTimeoutErrorquandtimeoutMsest écoulé après l’acceptation de la tâche. La tâche continue de s’exécuter chez SpicyAPI. Appelez plus tardwaitForTask(error.taskId)pour la récupérer.SpicyTimeoutErrorquand le temps s’écoule avant que SpicyAPI ait confirmé la tâche. Renvoyez la même requête avec la même clé d’idempotence pour en avoir le cœur net.- Toute erreur levée dans votre propre
onAcceptedouonUpdateinterrompt l’attente ; la tâche elle-même continue de s’exécuter. TypeErrorpour untimeoutMsou unintervalMsinvalide. Ces valeurs sont contrôlées avant toute création.
Voir Votre premier résultat pour un exemple complet.
getTask(taskId)
Consulte une tâche une fois : son état, ses liens de résultat et ce qu’elle a coûté.
getTask(taskId: string, options?: { signal?: AbortSignal }): Promise<TaskRecord>| Champ | Signification |
|---|---|
taskId, model | Quelle tâche, créée avec quel modèle |
state | queued ou running pendant le travail ; succeeded, failed ou expired une fois terminée (canceled n’apparaît que sur d’anciens enregistrements) |
output.assets | Les fichiers de résultat, décrits dans le tableau suivant. Uniquement pour succeeded |
output.text | La sortie texte, pour les modèles qui renvoient du texte |
errorCode, errorMessage | La raison de l’échec d’une tâche. Fondez vos décisions sur errorCode ; la formulation du message peut changer |
cost | Tant que settled vaut false, le montant réservé ; une fois true, le montant définitivement facturé |
settled | Indique si cost est définitif |
createdAt, completedAt, deadlineAt | Quand la tâche a été créée, quand elle s’est terminée, et quand elle expirerait |
input | L’entrée telle qu’elle a été stockée. Supprimée à la fin de la durée de conservation des prompts |
sourceTaskId | Présent sur une tâche créée par retryTask, pointe vers la tâche d’origine |
contentState | present ; expired (supprimé par les règles de conservation) ; ou purged (vous l’avez supprimé avec purgeTask) |
retention | Quand les fichiers (outputsExpireAt) et le prompt (promptsExpireAt) seront supprimés |
Chaque élément de output.assets :
| Champ | Signification |
|---|---|
url | Un lien de téléchargement temporaire. Téléchargez-le sans votre clé API |
expiresAt | Le moment où ce lien cesse de fonctionner, normalement au bout d’environ 20 minutes. Rappelez getTask pour obtenir un nouveau lien |
mime | Le type de fichier, comme image/png ou video/mp4 |
width, height, durationSeconds, bytes | Les informations de taille, lorsqu’elles s’appliquent |
pending | true tant que le fichier est encore en cours de copie vers le stockage. Revérifiez dans quelques secondes |
unavailable | true quand le fichier a définitivement disparu. Attendre n’y changera rien ; relancez une génération |
key | Un nom de fichier interne, utile uniquement pour createDownloadUrl |
Un champ pour lequel SpicyAPI n’a pas de valeur est omis plutôt que mis à null : considérez donc chaque champ ci-dessus comme facultatif.
Payant : non. Erreurs : SpicyApiError avec 404 quand aucune tâche portant cet identifiant n’est visible pour votre clé.
const task = await client.getTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3"); // l’identifiant que vous avez enregistré
if (task.state === "succeeded") {
for (const asset of task.output?.assets ?? []) console.log(asset.mime, asset.url);
} else if (task.state === "queued" || task.state === "running") {
console.log("Still working. On hold: $" + task.cost);
} else {
console.log(`Finished as ${task.state}: ${task.errorCode}`);
}waitForTask(taskId, options?)
Consulte une tâche à répétition jusqu’à ce qu’elle se termine, puis la renvoie. Utilisez-la pour une tâche créée précédemment, par exemple après le redémarrage de votre programme.
waitForTask(taskId: string, options?: WaitForTaskOptions): Promise<TaskRecord>Options : timeoutMs (600000 par défaut, jusqu’à 86400000), intervalMs, onUpdate et signal, tous tels que décrits dans run.
Elle rend la main dès que state vaut succeeded, failed, expired ou canceled, et, pour une tâche réussie, dès qu’aucun fichier de résultat n’est plus pending. Chaque consultation est une requête getTask et compte dans la limite de débit de votre compte : c’est pourquoi l’intervalle par défaut s’allonge avec le temps.
Payant : non. Erreurs : SpicyWaitTimeoutError quand timeoutMs est écoulé. Elle contient taskId, timeoutMs et lastTask (le dernier état observé). La tâche n’est pas annulée ; vous pouvez simplement attendre de nouveau. Également SpicyApiError (par exemple 404) et SpicyTransportError.
const task = await client.waitForTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3", {
timeoutMs: 30 * 60_000,
onUpdate: (update) => console.log(new Date().toISOString(), update.state),
});
console.log(task.state, task.output?.assets?.[0]?.url);retryTask(taskId, options?)
Relance une tâche échouée, sous la forme d’une toute nouvelle tâche avec le même modèle et la même entrée. La tâche d’origine reste exactement telle qu’elle était, et la nouvelle pointe vers elle via sourceTaskId.
retryTask(taskId: string, options?: { idempotencyKey?: string; signal?: AbortSignal }): Promise<RetryTaskResult>Elle fonctionne sur les tâches failed et expired (et les anciennes tâches canceled). Une tâche réussie ou encore en cours est refusée avec 409. Le prix, votre solde et les autorisations de la clé sont vérifiés de nouveau, et la nouvelle tâche reprend le réglage de conservation de la tâche d’origine.
Comme l’entrée est réutilisée telle quelle, une relance n’aide que si l’échec était temporaire. Lisez d’abord l’errorCode de la tâche d’origine : rate_limited, upstream_unavailable, timeout et upstream_failed justifient une relance après une pause ; generation_failed et invalid_asset demandent une requête modifiée, envoyée avec createTask ; content_rejected, invalid_request et unsupported_combination ne doivent pas être relancés sans modification. Voir Codes d’erreur courants et Erreurs.
Elle renvoie taskId (la nouvelle tâche), sourceTaskId (la tâche d’origine), state, estimatedCost et deadlineAt.
Payant : oui. La nouvelle tâche a sa propre réservation et est facturée séparément si elle réussit. Passez une idempotencyKey pour qu’exécuter deux fois votre code de relance ne crée qu’une seule relance.
Erreurs : 409 (la tâche ne peut pas être relancée), ainsi que les erreurs de solde, de plafond et de disponibilité listées dans createTask. Si l’entrée d’origine utilisait un fichier spicy:// téléversé, ce fichier n’est utilisable qu’un jour ; téléversez-le de nouveau et utilisez plutôt createTask.
retryTask ne sert pas pour les erreurs réseau
Si c’est createTask elle-même qui a échoué sur une erreur réseau, vous n’avez aucune tâche à relancer. Renvoyez createTask avec la même clé d’idempotence. N’utilisez retryTask que pour une tâche dont vous avez lu que le state vaut failed ou expired.
Un exemple complet se trouve dans Relancer une tâche échouée.
listTasks(options?)
Liste les tâches créées avec cette clé API, des plus récentes aux plus anciennes, une page à la fois. Utile pour retrouver du travail après un redémarrage ou un webhook manqué.
listTasks(options?: ListTasksOptions): Promise<TaskList>Propriété
Type
Elle renvoie items, hasMore et, quand il reste des pages, nextCursor. Chaque élément comporte taskId, model, state, cost, settled, createdAt, deadlineAt et completedAt. Les entrées, les résultats et les liens n’y figurent pas ; appelez getTask pour une tâche dont vous avez besoin.
Payant : non. Erreurs : SpicyApiError (par exemple 400 pour une plage de dates invalide), SpicyTransportError.
let cursor;
do {
const page = await client.listTasks({
from: "2026-09-01", // gardez les mêmes dates sur chaque page
to: "2026-09-08",
state: "failed",
limit: 100,
cursor,
});
for (const item of page.items) console.log(item.taskId, item.model, item.createdAt);
cursor = page.hasMore ? page.nextCursor : undefined;
} while (cursor);getBalance()
Indique combien d’argent contient votre compte.
getBalance(options?: { signal?: AbortSignal }): Promise<Balance>| Champ | Signification |
|---|---|
available | L’argent disponible pour financer de nouvelles tâches. Il peut être négatif si votre compte utilise une ligne de crédit approuvée |
held | L’argent mis de côté pour des tâches encore en cours |
total | available plus held |
funding | Détail facultatif : fonds prépayés, dotations promotionnelles (avec les modèles concernés et leur date d’expiration) et éventuelle ligne de crédit. Les réponses plus anciennes peuvent l’omettre |
Payant : non. Inutile de vérifier le solde avant chaque tâche : SpicyAPI le vérifie quand vous en créez une et répond 40201 s’il est insuffisant. Voir Facturation pour tous les champs de funding.
const balance = await client.getBalance();
console.log(`Available: $${balance.available} (on hold: $${balance.held})`);getUsage(options?)
Indique ce que cette clé API a dépensé, regroupé par jour et par modèle.
getUsage(options?: { from?: string; to?: string; signal?: AbortSignal }): Promise<UsageReport>from et to sont des dates UTC (YYYY-MM-DD) ; from est inclus, to ne l’est pas. Omettez les deux pour les sept derniers jours, aujourd’hui compris. La plage peut couvrir 92 jours au plus.
| Champ | Signification |
|---|---|
totalCalls | Le nombre de tâches sur la période |
totalSpend | Uniquement les montants définitivement facturés ; l’argent encore réservé n’est pas compté |
days | Une entrée par jour comportant des appels : day, calls, succeeded, failed, spend |
models | Une entrée par modèle : model, calls, succeeded, failed, spend |
Payant : non. Ce rapport est plus lourd que les autres lectures : il est donc soumis à une limite supplémentaire de 30 requêtes par minute ; réservez-le aux rapports, pas à une boucle. Erreurs : SpicyApiError (429 au-delà de cette limite), SpicyTransportError.
const usage = await client.getUsage({ from: "2026-09-01", to: "2026-09-08" });
console.log(`${usage.totalCalls} tasks, $${usage.totalSpend} spent`);
for (const row of usage.models) console.log(row.model, row.calls, "$" + row.spend);Téléverser des fichiers
Quand un modèle a besoin d’une image, d’une vidéo ou d’un son en entrée, vous pouvez placer directement dans le champ d’entrée un lien HTTPS public vers le fichier. Pour un fichier qui se trouve sur votre ordinateur, téléversez-le d’abord. Vous obtenez une adresse spicy://, que vous placez dans le champ d’entrée à la place. Le téléversement est gratuit.
- Images (JPEG, PNG, WebP, GIF) : jusqu’à 10 MiB.
- Vidéo (MP4, WebM) et audio (MP3, WAV) : jusqu’à 90 MiB et 600 secondes.
- Un modèle peut fixer une limite plus basse dans son schéma.
- Un fichier téléversé est utilisable pendant un jour, et uniquement par votre compte.
uploadFile(path, options?)
Téléverse un fichier depuis le disque.
uploadFile(path: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>Le type de fichier est déduit de l’extension : .jpg, .jpeg, .png, .webp, .gif, .mp4, .webm, .mp3 ou .wav. Pour tout autre nom, passez contentType : image/jpeg, image/png, image/webp, image/gif, video/mp4, video/webm, audio/mpeg ou audio/wav.
| Champ renvoyé | Signification |
|---|---|
uri | L’adresse spicy:// à placer dans l’entrée d’une tâche |
expiresAt | Le moment où l’adresse cesse de fonctionner (au bout d’un jour) |
fileId, status, bytes, contentType, sha256 | Les détails de ce qui a été stocké ; status vaut ready |
width, height, durationSeconds | Les dimensions ou la durée mesurées, lorsqu’elles s’appliquent |
Payant : non. Erreurs : TypeError quand le type ne peut pas être déduit (son message liste toutes les extensions reconnues : contentType is required unless the file extension is one of: gif, jpeg, jpg, png, webp, mp4, webm, mp3, wav) ; une erreur Node.js ENOENT quand le chemin est faux ; SpicyUploadError (avec status) quand le stockage a refusé les octets ou que le fichier dépasse la taille autorisée (413) ; SpicyApiError quand SpicyAPI a refusé le téléversement ; SpicyTimeoutError quand l’envoi a pris plus de temps que uploadTimeoutMs.
const file = await client.uploadFile("./photo.jpg");
console.log(file.uri, "can be used until", file.expiresAt);uploadBytes(data, options)
Téléverse des octets que vous avez déjà en mémoire, comme un fichier qu’un utilisateur a envoyé à votre serveur. Un Buffer Node.js fonctionne.
uploadBytes(data: Uint8Array, options: { contentType: string; signal?: AbortSignal }): Promise<UploadedFile>contentType est obligatoire. Le résultat et les erreurs sont les mêmes que pour uploadFile.
const response = await fetch("https://example.com/reference.png");
const bytes = new Uint8Array(await response.arrayBuffer());
const file = await client.uploadBytes(bytes, { contentType: "image/png" });uploadBase64(value, options?)
Téléverse un fichier dont vous disposez sous forme de texte Base64 : soit une data URI comme data:image/png;base64,iVBOR…, soit du Base64 brut accompagné de contentType.
uploadBase64(value: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>L’encodage et la taille sont d’abord vérifiés sur votre machine ; des données invalides ou trop volumineuses lèvent une TypeError sans rien téléverser. Le résultat est le même que pour uploadFile.
createUploadUrl et commitUploadedFile
Les trois fonctions ci-dessus reposent sur ces deux appels de plus bas niveau. Vous n’en avez besoin que pour envoyer les octets depuis un autre endroit que votre processus Node.js.
createUploadUrl(contentType: string, bytes: number, options?: { signal?: AbortSignal }): Promise<UploadTicket>
commitUploadedFile(fileId: string, options?: { signal?: AbortSignal }): Promise<UploadedFile>createUploadUrlrenvoiefileId,uploadUrl,method(PUT),headers,maxBytesetexpiresAt.- Envoyez le fichier par un
PUTversuploadUrl, avec exactement lesheadersindiqués et sans en-têteAuthorization. La taille déclarée doit correspondre au fichier. commitUploadedFile(fileId)vérifie le fichier stocké et renvoie l’urispicy://. Un fichier téléversé mais jamais validé ne peut pas être utilisé.
Le processus complet, téléversements depuis un navigateur compris, est décrit dans Médias.
createDownloadUrl(taskId, key?)
Crée un nouveau lien de téléchargement temporaire pour un fichier de résultat. Vous en aurez rarement besoin : getTask, waitForTask, run et les webhooks incluent déjà une url prête pour chaque résultat.
createDownloadUrl(taskId: string, key?: string, options?: { signal?: AbortSignal }): Promise<DownloadTicket>Passez la key du fichier produit pour choisir un fichier, ou omettez-la pour obtenir le premier. Elle renvoie key, url et expiresAt (environ 20 minutes).
Payant : non. Erreurs : SpicyApiError avec 409 tant que le fichier est encore en cours de copie (attendez quelques secondes), ou 404 quand la tâche n’a pas réussi ou que la clé ne lui appartient pas.
purgeTask(taskId)
Supprime définitivement les médias générés, le résultat (les données de résultat stockées), le prompt et les autres textes d’entrée d’une tâche terminée, avant la fin de la durée de conservation. Utilisez-la quand un résultat ne doit pas être conservé.
purgeTask(taskId: string, options?: { signal?: AbortSignal }): Promise<PurgeTaskResult>| Champ renvoyé | Signification |
|---|---|
contentState | purged quand le contenu est supprimé. present signifie que rien n’a été supprimé cette fois-ci ; réessayez plus tard |
purgedAt | Le moment où le contenu a été supprimé pour la première fois |
contentRemovedBy | user quand c’est vous qui l’avez supprimé |
billingRetained | Toujours true : l’enregistrement de ce qu’a coûté la tâche est conservé |
mediaDeletionPending | true tant que les fichiers stockés sont encore en cours de suppression en arrière-plan, normalement pendant moins d’une minute |
Payant : non, et ce n’est pas un remboursement. L’écriture comptable, le montant facturé, le modèle, l’état, les horodatages et l’identifiant de requête restent enregistrés. La suppression est irréversible. Rappeler la méthode sur la même tâche est sans risque. Erreurs : SpicyApiError avec 400 quand la tâche est encore queued ou running, 404 quand elle n’existe pas. Une tâche acceptée ne peut pas être annulée et aucune API d’annulation n’existe : attendez-la avec waitForTask, puis détruisez son contenu. Voir Conservation et destruction.
const result = await client.purgeTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3");
console.log(result.contentState, "billing kept:", result.billingRetained);Fonctions pour les webhooks
verifyWebhook et computeWebhookSignature sont des fonctions simples, pas des méthodes du client. Elles sont décrites, avec un exemple fonctionnel, dans Webhooks.
Utilitaires hors ligne et types
Ils fonctionnent sans connexion réseau et sans clé.
| Export | Ce qu’il vous fournit |
|---|---|
searchDocumentation(query, limit) | Cherche dans un petit index intégré de ce site de documentation. Renvoie jusqu’à limit entrées (de 1 à 25, 10 par défaut), chacune avec slug, title, summary, keywords et url, où url a la forme https://docs.spicyapi.ai/docs/<slug>. Sans correspondance, renvoie un tableau vide ; une query vide renvoie les premières entrées |
DOCUMENTATION | Cet index intégré complet sous forme de tableau, une entrée par page de documentation |
readOpenApiContract() | Le contrat OpenAPI 3.1 fourni avec cette version du SDK, sous forme de texte YAML |
DEFAULT_API_BASE_URL, DEFAULT_SERVICE_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_UPLOAD_TIMEOUT_MS, DEFAULT_WAIT_TIMEOUT_MS, DEFAULT_WAIT_INTERVAL_MS, MAX_API_RESPONSE_BYTES | Les valeurs par défaut décrites sur cette page. Les réponses de l’API plus volumineuses que MAX_API_RESPONSE_BYTES (4 MiB) sont refusées |
Les types TypeScript de chaque requête et de chaque réponse sont également exportés, par exemple import type { TaskRecord, ApiModel, Balance } from "@spicyapi/sdk".
| Chemin d’import | Contenu |
|---|---|
@spicyapi/sdk | Tout ce qui figure sur cette page |
@spicyapi/sdk/webhooks | Uniquement verifyWebhook, computeWebhookSignature et SpicyWebhookVerificationError : un import léger pour un récepteur de webhooks |
@spicyapi/sdk/openapi | Uniquement les types OpenAPI bruts paths, components et operations |
Webhooks
Un webhook permet à SpicyAPI de vous prévenir quand une tâche est terminée, au lieu que votre programme pose la question toutes les quelques secondes. Vous donnez à chaque tâche une callBackUrl, une adresse sur votre propre serveur, et SpicyAPI y envoie l’enregistrement de la tâche terminée.
Pourquoi vérifier chaque webhook
L’adresse de votre webhook est une URL publique : quiconque la devine peut y envoyer un faux message du type « tâche réussie, voici le fichier ». Si votre code croit ce message, il risque de livrer le mauvais fichier à un client ou de marquer une commande comme terminée.
Chaque livraison authentique est signée avec une clé de signature secrète que seuls vous et SpicyAPI connaissez. verifyWebhook recalcule cette signature à partir des octets exacts que vous avez reçus et compare les deux. Elle vérifie aussi l’heure de livraison, pour qu’un attaquant ne puisse pas intercepter un message authentique et vous le renvoyer des heures plus tard. N’agissez sur un message qu’une fois cette vérification passée.
Mise en place
Obtenir votre clé de signature
Une clé de signature est créée automatiquement à l’inscription, mais sa valeur n’est jamais affichée : vous devez donc en créer une que vous pourrez copier. Dans la console, ouvrez Webhooks et cliquez sur Générer (ou sur Renouveler si la carte indique qu’une clé est configurée). La nouvelle clé n’est affichée qu’une seule fois : copiez-la et rangez-la sur votre serveur sous le nom SPICY_WEBHOOK_SECRET. Si vous la perdez, renouvelez-la de nouveau.
Le renouvellement ne prévoit aucune période de chevauchement : l’ancienne clé cesse immédiatement de signer, et même les livraisons encore en cours de nouvelle tentative sont signées avec la nouvelle. Mettez à jour tous vos récepteurs en service juste après le renouvellement. Les détails figurent dans Webhooks.
Faire tourner un récepteur sur une adresse HTTPS publique
SpicyAPI ne peut appeler qu’une adresse joignable depuis Internet et commençant par https://. Le http:// simple, localhost et les adresses de réseau privé sont refusés. Pendant le développement, exposez votre serveur local au moyen d’un service de tunnel qui lui donne une adresse HTTPS publique, ou faites du polling jusqu’au déploiement. Les règles complètes figurent dans Webhooks.
Envoyer callBackUrl avec vos tâches
Ajoutez callBackUrl: "https://your-app.example.com/hooks/spicy" à la requête que vous passez à createTask. Voir Recevoir les résultats par webhook.
Un récepteur minimal
Ce récepteur n’utilise que le module http intégré à Node.js : il n’y a donc rien d’autre à installer. Enregistrez-le sous le nom receiver.mjs et lancez-le avec node receiver.mjs.
import { createServer } from "node:http";
import { SpicyWebhookVerificationError, verifyWebhook } from "@spicyapi/sdk/webhooks";
const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");
// Identifiants des livraisons déjà traitées. En production, utilisez une table de base
// de données, pour que la liste survive à un redémarrage.
const handled = new Set();
const server = createServer(async (request, response) => {
if (request.method !== "POST" || request.url !== "/hooks/spicy") {
response.writeHead(404).end();
return;
}
if (request.headers["x-webhook-payload-version"] !== "2") {
response.writeHead(400).end();
return;
}
// 1. Lire le corps sous forme d’octets bruts. Ne pas l’analyser avant la vérification.
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const rawBody = Buffer.concat(chunks);
// 2. Vérifier la signature et l’horodatage.
let event;
try {
event = verifyWebhook({
rawBody,
secret,
timestamp: String(request.headers["x-webhook-timestamp"] ?? ""),
signature: String(request.headers["x-webhook-signature"] ?? ""),
payloadVersion: 2,
});
} catch (error) {
const reason = error instanceof SpicyWebhookVerificationError ? error.reason : String(error);
console.warn("Rejected a webhook:", reason);
response.writeHead(401).end();
return;
}
// 3. Répondre vite. SpicyAPI attend 15 secondes au plus, puis réessaie.
response.writeHead(204).end();
// 4. Une même livraison peut arriver plusieurs fois. Ne traiter chacune qu’une fois.
if (!event.deliveryId || handled.has(event.deliveryId)) return;
handled.add(event.deliveryId);
if (!("data" in event.payload)) return; // en version 2, le corps range la tâche sous data
const task = event.payload.data; // les mêmes champs que ceux renvoyés par getTask()
console.log("Task", task.taskId, "is", task.state);
if (task.state === "succeeded") {
console.log("Result:", task.output?.assets?.[0]?.url);
}
});
server.listen(8080, () => console.log("Listening on http://localhost:8080/hooks/spicy"));Les points importants :
- Des octets bruts. La signature porte sur les octets exacts envoyés par SpicyAPI. Si un framework analyse d’abord le JSON et que vous le reconvertissez en texte, les octets changent et chaque vérification échoue. Avec Express, utilisez
express.raw({ type: "application/json" })sur cette route. - Répondre vite. Toute réponse
2xxsignifie « reçu ». Sans réponse dans les 15 secondes, ou avec tout autre statut, SpicyAPI retente la livraison plusieurs fois au cours des heures suivantes. Faites le travail long après avoir répondu. - Les doublons arrivent. Les nouvelles tentatives et les aléas du réseau peuvent livrer deux fois le même message.
event.deliveryIdreste identique d’une tentative à l’autre : mémorisez donc les identifiants déjà traités. - La réussite se lit dans
state. Une livraison pour une tâche échouée reste une livraison valide. Liseztask.state, jamais lecodede l’enveloppe. - En production, enregistrez d’abord l’événement vérifié dans votre base de données et ne répondez
2xxqu’une fois l’enregistrement réussi, et ignorez les identifiants de tâche que votre application n’a pas créés. Le guide Webhooks propose une version adossée à une base de données.
Tester votre récepteur avant la mise en production
computeWebhookSignature produit une vraie signature : vous pouvez donc vous envoyer une livraison de test signée. Avec receiver.mjs qui tourne dans un terminal et la même SPICY_WEBHOOK_SECRET définie dans un autre, lancez :
import { computeWebhookSignature } from "@spicyapi/sdk";
const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");
const body = JSON.stringify({
code: 200,
msg: "success",
request_id: "whd_local_test_1",
data: {
taskId: "job_local_test",
state: "succeeded",
cost: "0.01",
settled: true,
output: { assets: [{ url: "https://example.com/result.png" }] },
},
});
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = computeWebhookSignature("job_local_test", timestamp, body, secret);
const response = await fetch("http://localhost:8080/hooks/spicy", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Timestamp": timestamp,
"X-Webhook-Signature": signature,
"X-Webhook-Payload-Version": "2",
},
body,
});
console.log("Your receiver answered", response.status); // attendu : 204Lancez-le deux fois : le récepteur n’affiche la tâche qu’une fois, car la seconde livraison a le même request_id. Modifiez un caractère de body après la signature, et le récepteur répond 401.
verifyWebhook(options)
verifyWebhook(options: VerifyWebhookOptions): VerifiedWebhookPropriété
Type
Elle renvoie payload (le corps analysé ; en version 2, la tâche se trouve dans payload.data), payloadVersion, taskId, deliveryId (le request_id de la version 2, identique d’une tentative à l’autre) et timestamp. La fonction est synchrone : pas besoin d’await.
Quand la vérification échoue, elle lève une SpicyWebhookVerificationError, dont la reason vaut invalid_signature, stale_timestamp, invalid_timestamp, invalid_json, invalid_payload, body_too_large ou invalid_secret. Répondez 401 et n’agissez pas sur le message.
computeWebhookSignature(taskId, timestamp, rawBody, secret) renvoie la signature Base64 d’un corps. Vous n’en avez besoin que pour des tests comme celui ci-dessus. L’algorithme de signature complet, pour les autres langages, figure dans Webhooks.
Astuce TypeScript : payload est typé pour accepter l’un ou l’autre format. Affinez le type avec "data" in event.payload, comme le fait le récepteur, ou passez le type attendu : verifyWebhook<components["schemas"]["TaskRecordEnvelope"]>(…) avec import type { components } from "@spicyapi/sdk".
Gérer les erreurs
Les choses peuvent mal tourner de deux façons différentes, qui demandent chacune un traitement différent :
- Un appel échoue. Le SDK lève une erreur : votre clé était mauvaise, votre solde était insuffisant, le réseau a coupé. Interceptez-la avec
try/catch. - Une tâche échoue. L’appel a fonctionné, mais pas la génération. Aucune erreur n’est levée : la tâche renvoyée a
state: "failed"et unerrorCode. Les tâches échouées ou expirées libèrent normalement toute la réservation. Certains refus de contenu sont facturés au devis accepté, uniquement si la page du modèle indique explicitement cette exception. Consultezsettledetcostpour connaître le montant final.
Les classes d’erreur
| Classe | Quand vous la rencontrez | Champs utiles |
|---|---|---|
SpicyApiError | SpicyAPI a répondu par une erreur, par exemple une clé refusée ou un solde insuffisant | code (le code métier sur lequel agir, comme 40201), status (statut HTTP), message, requestId, retryAfterSeconds, rateLimitLimit, rateLimitRemaining |
SpicyTransportError | Aucune réponse exploitable n’est arrivée : panne réseau, problème DNS ou réponse de plus de 4 MiB | message, cause (l’erreur sous-jacente) |
SpicyTimeoutError | Une seule requête a duré plus longtemps que timeoutMs (ou uploadTimeoutMs). C’est une sorte de SpicyTransportError | timeoutMs |
SpicyWaitTimeoutError | run ou waitForTask n’a plus de temps. La tâche est toujours en cours | taskId, timeoutMs, lastTask |
SpicyUploadError | Le stockage a refusé un fichier téléversé, ou le fichier était trop volumineux | status |
SpicyWebhookVerificationError | Un webhook n’a pas passé la vérification | reason |
TypeError | L’appel lui-même était invalide : aucune clé définie, une option hors limites, un identifiant vide. Rien n’a été envoyé | message |
Décidez quoi faire d’après code, jamais d’après message : les messages s’adressent à des personnes, leur formulation peut changer et ils suivent le réglage de langue des erreurs de votre compte. Joignez toujours le requestId quand vous contactez l’assistance : il nous permet de retrouver la requête exacte.
import { randomUUID } from "node:crypto";
import {
SpicyApiError,
SpicyClient,
SpicyTransportError,
SpicyWaitTimeoutError,
} from "@spicyapi/sdk";
const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // un vrai identifiant obtenu avec listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
const idempotencyKey = randomUUID(); // gardez-la : il vous faut la même pour réessayer
try {
const task = await client.run(
{ model: MODEL_ID, input: { prompt: "A paper lantern on a rainy street" } },
{ idempotencyKey, onAccepted: ({ taskId }) => console.log("Task ID:", taskId) },
);
if (task.state === "succeeded") console.log("Done:", task.output?.assets?.[0]?.url);
else console.log(`Finished as ${task.state} (${task.errorCode}). Cost: ${task.cost}; settled: ${task.settled}.`);
} catch (error) {
if (error instanceof SpicyWaitTimeoutError) {
// Votre attente est terminée, pas la tâche. Consultez-la plus tard avec le même identifiant.
console.log(`Still running: ${error.taskId}. Call waitForTask() again later.`);
} else if (error instanceof SpicyApiError) {
console.log(`SpicyAPI said no: ${error.code} (HTTP ${error.status}) ${error.message}`);
console.log(`Quote this if you contact support: ${error.requestId}`);
if (error.code === 40201) console.log("Your balance is too low. Top up in the console.");
if (error.code === 40202) console.log("A spend cap was reached. Raise it on the API keys page.");
if (error.code === 429) console.log(`Too many requests. Wait ${error.retryAfterSeconds ?? 1} s.`);
} else if (error instanceof SpicyTransportError) {
// Panne réseau ou délai local dépassé : la tâche existe peut-être, peut-être pas.
// Relancez la même requête avec la MÊME idempotencyKey pour le savoir sans risque.
console.log(`Network problem (${error.message}). Retry with key ${idempotencyKey}.`);
} else {
throw error; // ici, une TypeError signale un bug dans l’appel lui-même, par exemple une clé manquante
}
}Codes d’erreur courants
error.code | Signification | Faut-il réessayer ? |
|---|---|---|
400 | Requête invalide, généralement un champ d’entrée que le modèle n’accepte pas | Non. Corrigez la requête |
401 | Clé absente, mal formée, révoquée ou expirée | Non. Vérifiez la clé |
40201 | Solde insuffisant pour la réservation | Non. Rechargez d’abord votre solde |
40202 | Un plafond de dépense est atteint | Non. Relevez le plafond, ou attendez sa remise à zéro quotidienne ou mensuelle |
40301 | Cette clé ne peut pas utiliser ce modèle | Non. Modifiez la liste des modèles de la clé |
404 | Aucun modèle ou aucune tâche de ce nom pour cette clé | Non |
409 | Conflit avec l’état actuel, par exemple une clé d’idempotence réutilisée pour une autre opération | Cela dépend ; voir Erreurs |
40901 | Devis expiré ou prix modifié | Redemandez un devis, puis confirmez |
429 | Trop de requêtes | Oui, après retryAfterSeconds. Le SDK réessaie déjà les lectures pour vous |
500 | Un problème est survenu du côté de SpicyAPI | Oui, après une pause |
50301 | Le modèle est indisponible pour le moment | Oui, plus tard, ou choisissez un autre modèle |
Quand une tâche échoue, task.errorCode vaut invalid_request, unsupported_combination, content_rejected, rate_limited, upstream_unavailable, generation_failed, timeout, invalid_asset ou upstream_failed. retryTask renvoie l’entrée d’origine sans la modifier : choisissez donc l’étape suivante d’après le code :
task.errorCode | Que faire |
|---|---|
rate_limited, upstream_unavailable, timeout, upstream_failed | Attendez, puis retryTask une fois. Si un timeout se répète, essayez une durée plus courte ou une résolution plus basse avec createTask |
generation_failed | Modifiez le prompt ou l’image et envoyez une nouvelle tâche avec createTask ; la même entrée échoue généralement de la même façon |
invalid_asset | Téléversez de nouveau le fichier et envoyez une nouvelle tâche avec createTask en utilisant la nouvelle uri ; une relance réutiliserait l’ancien fichier |
invalid_request, unsupported_combination | Ne relancez pas sans modification. Changez les réglages, puis utilisez createTask |
content_rejected | Ne relancez pas sans modification. Le modèle choisi a décliné la requête en vertu de sa propre politique ; modifiez le prompt ou choisissez un autre modèle |
Le tableau complet figure dans Erreurs.
Conserver vos résultats
- Une
urlde résultat fonctionne pendant environ 20 minutes. RappelezgetTaskpour en obtenir une nouvelle. - Les fichiers générés sont supprimés au bout d’environ 14 jours au plus, plus tôt si votre compte ou
retentionSecondsle prévoit. Les fichiers d’entrée téléversés durent un jour, et les prompts stockés 30 jours. - Téléchargez ce que vous voulez garder dans votre propre stockage dès qu’une tâche réussit. N’enregistrez pas l’
urltemporaire elle-même dans votre base de données. Détails : Conservation et destruction.
Recettes courantes
Les recettes utilisent MODEL_ID_FROM_CATALOG comme emplacement réservé. Avant d’en lancer une, remplacez-le par la valeur model d’un élément adapté du catalogue en direct (listModels() ou GET /api/v1/models) dont enabled et available valent tous deux true. Chaque recette s’arrête avec un message clair si vous oubliez.
Image vers vidéo : téléverser d’abord une image locale
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
// Remplacez par un identifiant de modèle image-to-video du catalogue en direct :
// await client.listModels({ modality: "video", task: "image-to-video" })
const MODEL_ID = "MODEL_ID_FROM_CATALOG";
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
// 1. Téléverser l’image locale. Gratuit. L’adresse spicy:// fonctionne pendant un jour.
const file = await client.uploadFile("./photo.jpg");
console.log("Uploaded:", file.uri);
// 2. Afficher le schéma du modèle : quel champ reçoit l’image, quels champs sont obligatoires.
const { inputSchema } = await client.getModel(MODEL_ID);
console.log(JSON.stringify(inputSchema, null, 2));
// 3. Créer la vidéo et l’attendre. Une vidéo prend souvent plusieurs minutes.
const task = await client.run(
{
model: MODEL_ID,
input: {
prompt: "Slow push-in, light rain, neon reflections on wet stone",
image_url: file.uri, // utilisez le nom de champ indiqué par le schéma ci-dessus
},
},
{
idempotencyKey: randomUUID(),
timeoutMs: 30 * 60_000,
onAccepted: ({ taskId }) => console.log("Task ID (save it):", taskId),
},
);
console.log(task.state, task.output?.assets?.[0]?.url ?? task.errorCode);Le schéma affiché fait foi pour les noms de champs et les champs obligatoires ; un modèle vidéo peut aussi exiger une durée ou une résolution, et le prix en dépend souvent. Si votre image est déjà en ligne à une adresse HTTPS publique, sautez le téléversement et placez directement cette adresse dans le champ d’image. Lancez d’abord quoteTask avec la même requête si vous voulez voir le prix.
Lancer un lot en limitant la concurrence
Lancer des centaines de tâches d’un coup peut vous faire atteindre vos plafonds de dépense et votre limite de débit. Ce script traite une liste de prompts avec au plus deux tâches en cours à la fois.
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // un vrai identifiant obtenu avec listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
// Changez le nom du lot chaque fois que vous changez les prompts.
const BATCH_NAME = "lanterns-2026-09-17";
const prompts = [
"A paper lantern on a rainy street",
"A paper lantern floating on a lake",
"A paper lantern in a snowy forest",
"A paper lantern above a night market",
];
const CONCURRENCY = 2; // nombre de tâches qui peuvent s’exécuter en même temps
const results = prompts.map((prompt) => ({ prompt, outcome: "not started", detail: "" }));
let nextIndex = 0;
async function worker() {
while (nextIndex < prompts.length) {
const index = nextIndex++;
try {
const task = await client.run(
{ model: MODEL_ID, input: { prompt: prompts[index] } },
{
// Même nom de lot + même position = même clé. Si le script plante et que vous
// le relancez dans les 24 heures, vous récupérez les tâches existantes au lieu
// de les payer deux fois.
idempotencyKey: `${BATCH_NAME}-${index}`,
timeoutMs: 20 * 60_000,
},
);
results[index].outcome = task.state;
results[index].detail = task.output?.assets?.[0]?.url ?? task.errorCode ?? "";
} catch (error) {
results[index].outcome = "error";
results[index].detail = error instanceof Error ? error.message : String(error);
// Solde épuisé (40201) ou plafond de dépense dépassé (40202) : ne plus lancer de nouvelles tâches.
if (error instanceof SpicyApiError && (error.code === 40201 || error.code === 40202)) {
nextIndex = prompts.length;
}
}
}
}
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
console.table(results);- Pourquoi les clés sont construites à partir du nom du lot. Une clé d’idempotence liée à « ce lot, élément 3 » fait qu’une nouvelle exécution après un plantage réutilise les tâches déjà créées. Une clé aléatoire à chaque exécution paierait de nouveau la même image.
- Pourquoi le nom du lot doit changer avec les prompts. Réutiliser une clé pour une requête différente est refusé avec
409. - Choisir
CONCURRENCY. Chaque tâche en cours réserve une partie de votre solde, et chaque consultation d’état compte dans la limite de débit de votre compte. Commencez petit et augmentez la valeur une fois que le lot tourne sans accroc.
Recevoir les résultats par webhook plutôt que par polling
Dans une application web ou un processus d’arrière-plan, attendre à l’intérieur d’une requête est du gaspillage. Créez la tâche avec une callBackUrl, enregistrez l’identifiant de la tâche, et laissez le récepteur traiter le résultat quand il arrive.
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // un vrai identifiant obtenu avec listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
// Dans une vraie application, enregistrez cette clé avec la commande du client avant l’appel.
const idempotencyKey = randomUUID();
const accepted = await client.createTask(
{
model: MODEL_ID,
input: { prompt: "A paper lantern on a rainy street" },
callBackUrl: "https://your-app.example.com/hooks/spicy",
},
{ idempotencyKey },
);
console.log(`Accepted ${accepted.taskId}; $${accepted.estimatedCost} is on hold.`);
// Enregistrez accepted.taskId maintenant. Quand la tâche se termine, SpicyAPI appelle votre récepteur.Gardez un filet de sécurité pour les livraisons qui n’arrivent jamais, par exemple parce que votre serveur était en panne pendant toutes les tentatives : de temps en temps, appelez getTask pour les tâches enregistrées qui ont largement dépassé leur deadlineAt sans webhook, ou parcourez listTasks. L’historique des livraisons de webhooks, avec un bouton pour renvoyer manuellement une livraison, se trouve sur la page Webhooks de la console.
Relancer une tâche échouée
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const taskId = process.argv[2]; // à lancer ainsi : node retry.mjs job_...
if (!taskId) throw new Error("Usage: node retry.mjs <taskId>");
// Une relance renvoie l’entrée d’origine sans la modifier : seuls les échecs temporaires en valent la peine.
// Pour tous les autres codes, modifiez la requête et utilisez plutôt createTask.
const WORTH_ONE_RETRY = new Set(["rate_limited", "upstream_unavailable", "timeout", "upstream_failed"]);
const source = await client.getTask(taskId);
if (
(source.state !== "failed" && source.state !== "expired") ||
!WORTH_ONE_RETRY.has(source.errorCode ?? "")
) {
console.log(`Not retrying: the task is ${source.state} (${source.errorCode ?? "no error code"}).`);
} else {
// Une clé fixe par tentative de relance : lancer ce script deux fois crée une relance, pas deux.
const retry = await client.retryTask(source.taskId, {
idempotencyKey: `retry-${source.taskId}-attempt-1`,
});
console.log(`New task ${retry.taskId}; $${retry.estimatedCost} is on hold.`);
const finished = await client.waitForTask(retry.taskId, { timeoutMs: 20 * 60_000 });
console.log("The retry finished as", finished.state);
}Le script ne relance que les échecs qui peuvent se passer autrement la fois suivante (un service surchargé ou temporairement indisponible, un dépassement de délai ou une erreur inconnue), et une seule fois, avec une clé d’idempotence fixe. Pour invalid_asset, téléversez de nouveau le fichier et créez une nouvelle tâche, car la relance réutiliserait le téléversement expiré. Pour generation_failed, modifiez le prompt ou l’image et créez une nouvelle tâche. Pour content_rejected, invalid_request et unsupported_combination, modifiez la requête plutôt que de la relancer. Si l’unique relance échoue elle aussi, arrêtez-vous et examinez l’erreur ; pour un timeout répété, essayez une durée plus courte ou une résolution plus basse avec createTask. Voir Erreurs.
Dépannage et FAQ
Le code source
Le paquet est sous licence MIT et développé au grand jour sur github.com/spicyapi-ai/spicy-sdk. C’est là qu’on lit le code derrière chaque méthode de cette page, qu’on signale un comportement qui s’écarte de ce qui est décrit ici et qu’on propose une modification.
Étapes suivantes
Catalogue des modèles et schémas
Lire l’inputSchema d’un modèle et choisir des entrées valides
Tâches asynchrones
États des tâches, polling et signification de chaque champ
Webhooks
Format des livraisons, calendrier des nouvelles tentatives et vérification dans d’autres langages
Médias
Téléversements, limites du Base64 et téléchargement des résultats
Erreurs
Chaque code d’erreur, et ceux qui valent la peine d’être retentés
Facturation
Réservations, règlement, plafonds de dépense et solde
Idempotence
Réessayer sans risque, sans payer deux fois
Outil en ligne de commande
Les mêmes fonctionnalités depuis un terminal, sans écrire de code
Serveur MCP
Laisser un assistant de code IA utiliser SpicyAPI pour vous
SDK Python
Le même déroulé depuis Python, sans aucune dépendance d’exécution
SDK Go
Le même déroulé depuis Go, avec la seule bibliothèque standard
SDK PHP
Le même déroulé depuis PHP, sans aucune dépendance de bibliothèque
SDK Java
Le même déroulé depuis Java, avec une taxonomie d’erreurs scellée
Intégration en production
Une liste de contrôle avant lancement pour une intégration capable de se rétablir — le bon modèle, des clés et un solde protégés, des coûts confirmés, un suivi durable des tâches, des callbacks fiables et les tests à mener avant la mise en ligne.
SDK Python
Le SDK Python officiel de SpicyAPI (spicyapi) — l’installer, obtenir un devis, lancer votre première génération, téléverser un fichier local, vérifier les webhooks et lire chaque code d’erreur.

