SDK Java
Le SDK Java officiel de SpicyAPI (ai.spicyapi spicyapi-java) — l’installer, obtenir un devis, lancer votre première génération, téléverser un fichier local, vérifier les callbacks et parcourir de façon exhaustive une taxonomie d’erreurs scellée.
spicyapi-java est le SDK Java officiel de SpicyAPI. Il place la génération d’images, de vidéos et de sons derrière un seul SpicyClient : il joint votre clé API, projette l’enveloppe de réponse sur des records, attend une tâche avec une temporisation bornée, téléverse les fichiers locaux en un seul appel, vérifie les signatures de callback et transforme chaque échec en un type d’exception scellé, si bien qu’un switch sur la taxonomie est exhaustif dès la compilation.
Il demande Java 17 ou plus récent et a une seule dépendance : Jackson (com.fasterxml.jackson.core:jackson-databind), en 2.12 ou plus récent — c’est la version à partir de laquelle les records sont projetables, et les types utilisés ici sont des records. Jackson est une simple dépendance de compilation, ni optional ni relocalisée : c’est donc la version que votre projet possède déjà qui l’emporte. Sur le chemin de modules, le client est ai.spicyapi, fixé dans le manifeste plutôt que déduit du nom du fichier JAR.
Est-ce le bon outil ?
| Vous voulez… | Utilisez |
|---|---|
| Appeler SpicyAPI depuis votre propre code Java ou JVM | Ce SDK |
| L’appeler depuis Node.js ou TypeScript | Le SDK TypeScript |
| L’appeler depuis Python, Go ou PHP | Le SDK Python, Go ou PHP |
| Générer des contenus depuis un terminal sans écrire de code | La CLI |
| Laisser un assistant IA appeler SpicyAPI pour vous | Le serveur MCP et l’Agent Skill |
| Utiliser un modèle de chat ou de texte | Un client Java établi pour les formats OpenAI, Anthropic ou Gemini ; voir Texte et streaming |
L’installer
Ajoutez la dépendance depuis Maven Central :
<dependency>
<groupId>ai.spicyapi</groupId>
<artifactId>spicyapi-java</artifactId>
<version>0.1.0</version>
</dependency>Ou, avec Gradle (Kotlin DSL) :
implementation("ai.spicyapi:spicyapi-java:0.1.0")Placez votre clé dans l’environnement, pour qu’elle n’apparaisse jamais dans votre code ni dans votre gestionnaire de versions :
export SPICY_API_KEY="sk-spicy-..."Votre premier résultat
Demandez le prix, décidez, créez la tâche, attendez, lisez la sortie. Le devis est gratuit et ne réserve rien. run enchaîne createTask puis waitForTerminal.
SpicyClient client = SpicyClient.builder().apiKeyFromEnvironment().build(); // SPICY_API_KEY
CreateTaskRequest request = CreateTaskRequest.of(
"MODEL_ID_FROM_CATALOG",
Map.of("prompt", "a lantern in fog"));
TaskQuote quote = client.quote(request); // gratuit ; ne réserve rien
System.out.println("this will cost at most " + quote.maxCharge());
TaskRecord task = client.run( // createTask + waitForTerminal
request.withQuote(quote), UUID.randomUUID().toString());
task.outputText().ifPresent(System.out::println); // certains points répondent en texte, pas en fichiers
for (TaskAsset asset : task.assets()) {
asset.url().ifPresent(System.out::println);
}MODEL_ID_FROM_CATALOG est un emplacement réservé. Prenez un vrai identifiant dans le catalogue authentifié — client.listModels(), c’est-à-dire GET /api/v1/models — et jamais de mémoire ni d’une page ancienne : des modèles sont ajoutés, suspendus et retirés. Construisez l’entrée à partir de l’inputSchema propre à ce modèle, que renvoie listModels(ModelFilter.all().withIncludeSchema(true)) ; chaque modèle accepte des champs différents.
Vérifiez outputText() avant de parcourir assets(). La transcription et les points de terminaison de ce genre placent tout le résultat dans le champ texte et ne renvoient aucun fichier : un client qui ne cherche que des fichiers déclare qu’une tâche parfaitement réussie n’a rien produit.
Les montants sont des chaînes décimales en dollars américains, jamais des flottants, parce qu’un flottant binaire ne peut pas contenir exactement la plupart des montants décimaux. Il n’y a ni crédits ni points nulle part dans cette API.
Partir d’un fichier local
L’image vers vidéo, le remplacement de personne et la retouche d’image demandent tous que votre matière première soit d’abord chez nous. uploadFile réalise les trois étapes en un seul appel et renvoie une URI spicy://. C’est cette URI qui part dans l’entrée. Une URL HTTPS publiquement joignable peut y aller directement, sans aucun téléversement.
UploadedFile uploaded = client.uploadFile(Path.of("/path/to/reference.png"));
// uploaded.uri() -> "spicy://f/fil_..."
TaskRecord task = client.run(
CreateTaskRequest.of("MODEL_ID_FROM_CATALOG",
Map.of("image", uploaded.uri(), "prompt", "slow dolly in")),
UUID.randomUUID().toString());Les trois étapes sont aussi publiques, pour des octets diffusés depuis ailleurs :
UploadTicket ticket = client.createUploadUrl(UploadContentType.IMAGE_PNG, data.length);
client.putUploadBytes(ticket, data); // part vers le stockage objet, pas vers l’API
UploadedFile file = client.commitUploadedFile(ticket.fileId());Deux détails de l’étape du milieu échouent de façon opaque si vous la réimplémentez. Les en-têtes du ticket font partie de ce que l’URL présignée a signé : ils sont donc transmis tels quels — en supprimer un, en renommer un ou en ajouter un pour bien faire, et le stockage répond 403. Et aucune clé SpicyAPI n’y est jointe : l’URL est elle-même l’autorisation, et elle pointe vers le stockage, pas vers nous.
Les images sont acceptées jusqu’à 10 Mio, l’audio et la vidéo jusqu’à 90 Mio. Le client refuse localement un fichier trop volumineux, avant même de demander le ticket. Voir Médias.
Conserver le résultat moins longtemps
Les sorties et le texte de requête enregistré sont conservés aussi longtemps que le compte le prévoit. Une tâche peut demander moins :
TaskRecord task = client.run(request, idempotencyKey, Duration.ofHours(1));
task.retention().outputsExpireAt(); // ce qui a réellement été appliquéCela ne fait jamais qu’écourter : le service retient la plus petite des trois valeurs — la vôtre, le réglage du compte et le maximum de la plateforme — si bien qu’une requête ne peut jamais racheter une fenêtre plus longue que ce que le compte autorise. Duration.ZERO est une valeur valable : les sorties disparaissent dès que la tâche atteint un état final. Une valeur trop grande est ramenée à la borne plutôt que rejetée, et c’est pourquoi ce qui a été appliqué se lit dans retention() sur le record, pas dans le nombre que vous avez envoyé. retryTask n’a pas de durée de conservation propre : une relance hérite de celle de sa tâche d’origine. Voir Conservation et destruction.
Solde, consommation et historique
Balance balance = client.getBalance(); // pour tout le compte
UsageReport usage = client.getUsage( // cette clé uniquement
LocalDate.of(2026, 9, 13), LocalDate.of(2026, 9, 20)); // [from, to)
TaskPage page = client.listTasks(TaskFilter.all().withState(TaskState.FAILED));Un solde est un indicateur de budget, pas une décision d’admission. Une partie peut être une dotation réservée à certains modèles — balance.fundingSources() en donne le détail, et un modelSlugs vide sur une dotation signifie tous les modèles, pas aucun. La réponse exacte pour une requête exacte, c’est quote(), qui applique les mêmes règles d’admission et ne réserve rien.
La consommation sert au rapprochement comptable, pas au suivi d’avancement. Elle ne compte que les montants définitivement réglés : une période encore en cours affiche donc toujours un chiffre trop bas ; un règlement tardif peut modifier un jour passé ; et elle porte sa propre limite de débit valable pour tout le compte, dont le limiteur se ferme en cas de doute, si bien que l’interroger en boucle peut priver toutes les clés du compte de leurs propres rapports. Suivez plutôt une seule tâche avec waitForTerminal.
listTasks est limité à la clé appelante et ne renvoie que des métadonnées. Parcourez les pages en faisant bouger le curseur et rien d’autre, et fixez from et to avant de commencer : laissés vides, le service recalcule sa fenêtre par défaut (to vaut demain en UTC) à chaque requête, si bien qu’un parcours qui franchit minuit pose deux questions différentes et recolle les réponses. Rien ne le signale.
Callbacks
Réglez callBackUrl sur la requête et le record final vous est livré. Vérifiez-le avant d’y croire :
CallbackSignatureVerifier verifier =
new CallbackSignatureVerifier(System.getenv("SPICY_WEBHOOK_SECRET"));
byte[] body = request.getInputStream().readAllBytes(); // les octets bruts, jamais sérialisés à nouveau
WebhookDelivery delivery = verifier.verify(body, request::getHeader);
delivery.task().ifPresent(record -> { /* le même record que renvoie getTask */ });Trois façons de se tromper ici, que cette classe évite toutes : sérialiser à nouveau le corps avant de le vérifier (un readValue suivi d’un writeValueAsString modifie les octets, et l’empreinte ne correspondra plus jamais), comparer les signatures avec equals (il rend la main au premier octet différent, ce qui constitue un écart de temps mesurable) et sauter le contrôle de fraîcheur (une signature valide le reste pour toujours, donc une livraison capturée peut être rejouée).
Les livraisons sont réessayées. Dédoublonnez sur delivery.deliveryId(). Voir Webhooks.
Erreurs
Chaque échec est une SpicyException, qui est scellée : un switch sur la taxonomie est donc exhaustif.
| Type | Signifie |
|---|---|
SpicyApiException | Le service a répondu, et la réponse était non |
SpicyProtocolException | Quelque chose a répondu, mais pas avec notre enveloppe : un proxy, un portail captif, une mauvaise URL de base |
SpicyTransportException | Aucune réponse exploitable : DNS, TLS, une connexion coupée |
SpicyTimeoutException | Un budget de requête local est écoulé. Résultat inconnu, et non « cela n’a pas eu lieu » |
SpicyWaitTimeoutException | L’interrogation a abandonné. La tâche continue et reste facturable ; l’exception porte son identifiant |
SpicyUploadException | Le stockage objet a refusé le PUT présigné |
SpicyWebhookException | Une livraison entrante n’était pas authentique, fraîche et bien formée |
Branchez sur businessCode(), jamais sur le message : les messages sont de la prose, ils sont traduits et ils ne font pas partie du contrat. Le statut HTTP 503 est partagé par trois codes métier différents : le statut seul ne peut donc pas vous dire quoi faire.
| Code | Signification | Que faire |
|---|---|---|
40003 | Les octets téléversés ne correspondent pas à leur ticket | Recommencez avec un ticket neuf ; valider de nouveau n’y changera rien |
40004 | Aucun déploiement ne sert cette combinaison de paramètres | Changez le paramètre nommé dans le message |
40201 | Solde insuffisant | Rechargez |
40901 | Le prix a bougé avant la création de la tâche | Redemandez un devis, en gardant la même clé d’idempotence |
503 | Une dépendance est brièvement indisponible | Patientez selon Retry-After |
50301 | Aucun déploiement ni prix utilisable pour l’instant | Patientez, ou choisissez un autre modèle |
50302 | Une génération synchrone a échoué en amont et a été remboursée | Réessayez sous une nouvelle clé d’idempotence ; l’originale rejoue l’échec |
remediation() renvoie les mêmes conseils à l’exécution, et describe() place statut, code, identifiant de requête, message et conseil sur une seule ligne, prête pour un journal. Citez requestId() quand vous contactez le support. Tous les codes sont listés dans Erreurs.
Deux choses qui vous feront économiser
Gardez une clé d’idempotence par soumission. Réutilisez-la pour chaque renvoi, y compris après un dépassement de délai. Une réponse perdue ne prouve pas que la tâche n’a pas été créée : une clé neuve transforme un résultat inconnu en une seconde tâche payante. C’est aussi pourquoi createTask ne réessaie automatiquement que si vous avez passé une clé : sans elle, une relance après un dépassement de délai serait une deuxième génération et une deuxième facturation. Voir Idempotence.
Une tâche qui réussit est facturée, même si le résultat déçoit. quote() ne réserve rien et ne crée rien : demandez donc un devis d’abord quand le prix compte. createTask met de côté estimatedCost, et ce montant est le plafond : les fonds inutilisés reviennent, et rien au-delà n’est prélevé plus tard.
Ce que ce paquet ne fait pas
- Les modèles de texte. Ils parlent les formats réseau d’OpenAI, d’Anthropic et de Gemini : les clients Java établis pour ces formats fonctionnent déjà avec
https://api.spicyapi.ai/v1et la même clé. Voir Texte et streaming. - Les applications côté client. N’embarquez jamais cette clé dans une application Android, un binaire de bureau ou quoi que ce soit qu’un utilisateur peut ouvrir. Appelez depuis votre serveur.
- Choisir un modèle ou remplir ses champs. Il vous fournit le catalogue et le schéma de chaque modèle ; c’est vous qui décidez quoi envoyer.
- Annuler une tâche. Une fois acceptée, une tâche va jusqu’au bout.
Le code source
L’artefact est sous licence MIT et développé au grand jour sur github.com/spicyapi-ai/spicy-java. mvn -B verify exécute la suite de tests unitaires contre un bouchon local, plus un contrôle de dérive du contrat qui compare le contrat embarqué à celui publié sur https://docs.spicyapi.ai/openapi.yaml, puis revérifie chaque énumération que le paquet code en dur — parce qu’un contrat qui gagne une valeur sans que le client la gagne aussi n’échoue nulle part : il se contente de rejeter la nouvelle valeur localement, en silence.
É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
Devis et compatibilité
Voir le prix exact d’une requête avant de la payer
Webhooks
Format des livraisons, calendrier des nouvelles tentatives et vérification des signatures
Erreurs
Chaque code d’erreur, et ceux qui valent la peine d’être retentés
SDK PHP
Le même déroulé depuis PHP, sans aucune dépendance de bibliothèque
SDK PHP
Le SDK PHP officiel de SpicyAPI (spicyapi/spicyapi) — l’installer avec Composer, obtenir un devis, lancer votre première génération, téléverser un fichier local, vérifier les webhooks sans risque et traiter chaque code d’erreur.
Agents et automatisation
Quel outil officiel choisir, et un guide d’exploitation en production pour les agents IA, les workers backend et les chaînes de production de médias reproductibles.

