Créer une clé et l’envoyer comme jeton Bearer
Les clés se créent dans la Console, sur la page Clés API, et cette réponse-là est le seul endroit où la clé complète s’affiche : toute lecture ultérieure de votre liste renvoie un identifiant, un nom et un préfixe, plus jamais le secret. Enregistrez-la sur-le-champ. Le préfixe existe pour qu’une ligne de journal puisse désigner une clé sans la contenir.
Envoyez-la dans l’en-tête Authorization de chaque requête, sous la forme du mot Bearer suivi de la clé. Aucune variante dans l’URL n’est prévue : la RFC 6750 range parmi ses recommandations que « Bearer tokens SHOULD NOT be passed in page URLs (for example, as query string parameters) », parce que les URL porteuses de jetons finissent dans l’historique du navigateur, les en-têtes Referer et les journaux des serveurs.
Sans clé, ou avec une clé révoquée, la réponse est un 401 avec authentication_required pour code. Toutes les erreurs partagent le même corps, un objet error qui contient message, type, param et code. Aiguillez votre logique sur code : message est rédigé pour un humain, et sa formulation changera.
Lire le catalogue au lieu de le coder en dur
GET /v1/models renvoie deux listes, image et video. Une entrée d’image énumère les tailles acceptées, le coût de chacune en crédits et une taille par défaut ; une entrée vidéo porte des résolutions avec un coût par seconde, une durée plancher et une durée plafond, ainsi que les éventuels formats d’image acceptés. Une liste de tailles appartient à un modèle et non au catalogue, tout comme une valeur par défaut : aucune valeur unique ne s’applique partout sans risque. Les pages du catalogue des modèles et des tarifs interrogent ce même endpoint. Chaque page de modèle indique aussi l’identifiant à envoyer, par exemple Seedance 2.5 pour la vidéo et Qwen 3 pour les images, et les pages API d’image NSFW et API vidéo NSFW résument la forme des requêtes.
Le champ kind indique si un modèle produit des images ou de la vidéo ; chaque entrée liste ensuite un mode texte et un mode référence. Vous envoyez le même identifiant dans les deux cas : joignez des images de référence et ce sont les tailles et les prix du mode référence qui s’appliquent. Un modèle inconnu, une taille invalide ou une durée hors plage reviennent en 400, avec param qui nomme le champ, et une durée hors plage est rejetée plutôt que rognée pour tenir.
Lancer le rendu, puis interroger l’enregistrement
Envoyez un POST à /v1/generate/image ou /v1/generate/video en indiquant l’identifiant du modèle, votre prompt et les réglages admis par ce modèle. La réponse est un 202 qui porte un id, le statut processing et le nombre de crédits tout juste débités : les crédits partent au début d’un rendu, pas à la fin. Interrogez ensuite GET /v1/generations/{id} à intervalles ; dès que status ne vaut plus queued ni processing, le rendu est fini. Une fois l’enregistrement à completed, son output_url pointe vers notre endpoint de fichiers ; un enregistrement en échec porte error_code, une valeur tirée d’un ensemble fermé, à côté de error, un bref résumé destiné à vos journaux.
Espacez vos interrogations et glissez du hasard dans ce délai. L’expérience de Marc Brooker sur le backoff, publiée sur l’AWS Architecture Blog, simule des clients qui se disputent une même ligne et rapporte qu’avec du jitter, « in the case with 100 contending clients, we've reduced our call count by more than half ». Interroger vous apprend l’issue, cela ne la provoque pas : les rendus sont réconciliés côté serveur, que quelqu’un observe ou non, si bien qu’une connexion coupée vous fait perdre la main sur le résultat, pas le résultat lui-même.
Toute la boucle dans un seul fichier
Enregistrez ceci dans un fichier .mjs, définissez VELLRIA_API_KEY et exécutez-le avec une version récente de Node. Aucun identifiant de modèle, aucune taille ni aucun coût n’y est codé en dur.
```js import { writeFile } from "node:fs/promises"; const BASE = "https://vellria.com"; const AUTH = { Authorization: `Bearer ${process.env.VELLRIA_API_KEY}` }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function call(path, init) { const res = await fetch(BASE + path, { ...init, headers: { ...AUTH, ...init?.headers } }); if (res.status === 429) { await sleep(Number(res.headers.get("retry-after") ?? 5) * 1000); return call(path, init); } if (!res.ok) throw new Error((await res.json().catch(() => null))?.error?.code ?? res.status); return res.json(); } const catalog = await call("/v1/models"); const model = catalog.image[0]; let rec = await call("/v1/generate/image", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: model.id, prompt: "a lighthouse in fog", size: model.text.default_size }), }); while (rec.status === "queued" || rec.status === "processing") { await sleep(2000 + Math.random() * 2000); rec = await call(`/v1/generations/${rec.id}`); } if (rec.status !== "completed") throw new Error(rec.error_code ?? "generation_failed"); const file = await fetch(rec.output_url, { headers: AUTH }); const ext = file.headers.get("content-type") === "image/png" ? "png" : "jpg"; await writeFile(`${rec.id}.${ext}`, Buffer.from(await file.arrayBuffer())); ```
Pour un modèle qui accepte des images, envoyez d’abord le fichier en POST à /v1/uploads avec un en-tête Content-Type conforme aux octets, puis passez l’adresse obtenue dans image_urls ; transformer une image en vidéo donne les formats acceptés et la liste des rejets. Qu’un modèle veuille des images est une propriété du modèle, et un corps de la mauvaise forme est refusé plutôt qu’ajusté en silence, ce que garder un personnage cohérent développe. Les corps vidéo portent duration et resolution au lieu de size, sujet de choisir résolution et durée. Chaque identifiant de modèle vidéo, avec son type et son prix à la seconde, figure dans un tableau unique sur la page de l’API vidéo NSFW.
Échecs, contre-pression et expiration
Quand un rendu échoue après son lancement, ses crédits retournent d’eux-mêmes sur le solde dès que l’enregistrement passe à failed. Tenez failed pour un état final et soldé : lisez error_code pour connaître la raison, et ne recalculez jamais les soldes de votre côté. Le remboursement est écrit sous une condition qu’un seul rédacteur peut satisfaire : une notification du prestataire livrée deux fois ne peut ni vous créditer en double, ni vous oublier.
Franchir une limite de débit renvoie 429 avec le code rate_limit_exceeded et un en-tête Retry-After. La RFC 6585 définit ce statut : « the 429 status code indicates that the user has sent too many requests in a given amount of time ». La RFC 9110, section 10.2.3, définit l’en-tête comme celui qui « indicates how long the user agent ought to wait before making a follow-up request ». Lisez-le au lieu de deviner, et voyez storyboard et prévisualisation pour ce que cela implique quand vous lancez un lot. Un solde insuffisant donne 402, et aucun enregistrement n’est créé. Le détail complet figure dans la référence de l’API.




