API Pro : solde, limites et personnages entraînés
Deux routes de lecture, toujours autorisées quelle que soit les permissions de la clé, plus le journal d'activité disponible dans l'app.
Solde et limites
GET /credits
{ "balance": 1830, "licenseId": "lic_...", "key": "k_1a2b3c4d",
"limits": { "concurrent": 2, "perMinute": 60, "perDay": 0 }, "running": 1,
"buy_url": "https://tendre.ai/credits?license=lic_..." }| Champ | Signification |
|---|---|
balance | Crédits restants. null quand le solde est inconnu (environnement de test). |
licenseId | La licence à laquelle appartient cette clé. |
key | L'identifiant public de la clé, sûr à logger. Jamais le secret. |
limits.concurrent | Rendus tournant en parallèle, 2 par défaut. |
limits.perMinute | Soumissions par minute, 60 par défaut. |
limits.perDay | Plafond de crédits journalier que tu as défini sur la clé. 0 signifie aucun plafond. |
running | Rendus de cette clé encore en attente de récupération. |
buy_url | La page d'achat de crédits de ta licence, par carte ou en crypto. |
C'est l'appel le moins coûteux de l'API et le bon moyen de vérifier qu'une clé fonctionne.
Acheter des crédits sur ta carte enregistrée
POST /credits/purchase
{ "pack": "m" }pack est un id de pack (s, m, l, xl, xxl ou xxxl), ou "c25000" pour un montant libre au-delà du plus gros pack. Le paiement part sur la carte enregistrée dans la recharge automatique de ta page compte, seulement si tu y as autorisé ton agent à acheter, dans le plafond mensuel que tu y as fixé (recharges automatiques comprises). Un e-mail confirme chaque achat. Un achat à la fois.
La réponse est 200 { credits, eur, balance }. Chaque refus porte buy_url, le lien d'achat ordinaire, à utiliser à la place : 402 confirmation_required (ta banque demande ta confirmation) ou payment_declined ; 409 agent_not_allowed, no_saved_card, monthly_cap_reached, topup_in_progress ou purchase_in_progress ; 400 unknown_pack. Sur 503 payment_unavailable, on ne sait pas si le débit a eu lieu : ne relance pas tout de suite, un débit passé est crédité.
Personnages entraînés
Ce sont les personnages entraînés en LoRA, pour les images et les vidéos ordinaires. Les personnages du Multi-Ref Vidéo, sept vues fabriquées à partir d'une photo, sont autre chose : voir Personnages.
GET /loras
{ "image": [ { "name": "megan_krea2", "trigger": "megan", "base": "krea2", "size": 0 } ],
"video": [ { "name": "megan_ltx", "trigger": "megan", "size": 0 } ] }Passe name comme valeur de lora, en le prenant dans la liste image pour les images et dans la liste video pour les vidéos, et mets son trigger dans le prompt, sinon le personnage n'apparaîtra pas. Un personnage qui n'appartient pas au compte répond 403 unknown_lora.
Importer ton propre LoRA
POST /loras/upload
Un fichier .safetensors que tu possèdes déjà, entraîné ailleurs ou hébergé sur ton propre serveur, est ajouté à l'espace du compte et devient utilisable via lora, exactement comme un personnage entraîné dans l'app. Deux méthodes sont disponibles : en une seule fois jusqu'à 90 Mo, ou en plusieurs parties au-delà. Un LoRA pèse généralement entre 200 et 500 Mo, donc la seconde option est le cas habituel. Le plafond est de 800 Mo dans les deux cas.
En une seule fois : un corps brut (application/octet-stream, Content-Length obligatoire), les paramètres dans l'URL.
Au-delà de 100 Mo, la requête n'atteint jamais l'API. L'infrastructure en amont la rejette, ce qui renvoie un 413 brut sans corps JSON : un client qui analyse chaque erreur en JSON plantera plutôt que de la signaler correctement. Cette taille doit être envoyée en plusieurs parties.
| Paramètre | Rôle |
|---|---|
name | Nom du fichier, .safetensors, lettres, chiffres, . _ -, 80 caractères maximum. Un nom déjà utilisé est remplacé. |
kind | image (par défaut) ou video. Nécessite la permission images ou videos en conséquence. |
trigger | Le mot déclencheur à remettre dans le prompt, 60 caractères maximum. |
curl -s -X POST "$B/loras/upload?name=my_character.safetensors&kind=image&trigger=ohwx" -H "Authorization: Bearer $KEY" -H "Content-Type: application/octet-stream" --data-binary @my_character.safetensorsLa réponse est { "ok": true, "name": "my_character.safetensors", "kind": "image", "size": 314572800, "trigger": "ohwx", "architecture": "krea2", "storage": { ... } }.
La compatibilité est vérifiée à l'import. Les images sont rendues par Krea 2, qui possède sa propre architecture : seul un LoRA Krea 2 s'applique, qu'il ait été entraîné dans tendre.AI ou pour Krea 2 ailleurs. Note qu'un LoRA FLUX.1 est refusé, ce qui n'est pas forcément évident au premier abord. Les vidéos sont rendues par LTX 2.5, donc un LoRA LTX 2.5 ou 2.3.
L'API lit l'en-tête du fichier avant de stocker quoi que ce soit. Un LoRA reconnu comme SDXL, SD 1.5, SD3, FLUX.1, Wan ou HunyuanVideo est rejeté avec 415 incompatible_lora, qui indique l'architecture détectée, et un fichier qui n'est pas un safetensors avec 415 invalid_safetensors. Rien n'est stocké et aucun quota n'est consommé dans ces deux cas.
Un cas passe quand même : une architecture que l'en-tête n'identifie pas est acceptée, avec architecture: "inconnue", et n'est jugée qu'au moment du rendu. C'est la seule erreur qui te coûte un rendu à découvrir.
Envoyer un grand LoRA en plusieurs parties
Trois appels, plus un pour abandonner.
1. Ouvrir la session. POST /loras/upload/start avec { "name", "kind", "size", "trigger" }, où size est la taille totale du fichier en octets. Cela vérifie le nom, la taille et le quota, puis répond :
{ "uploadId": "...", "part_size": 20971520, "max_part_size": 104857600, "parts_expected": 15 }2. Envoyer chaque partie. POST /loras/upload/part?uploadId=...&part=N, corps brut, parties numérotées à partir de 1, part_size octets chacune sauf la dernière, jamais plus que max_part_size. Chaque réponse contient un etag, et tu as besoin de tous pour l'étape suivante, garde-les au fur et à mesure.
La partie 1 est spéciale : elle contient l'en-tête du fichier, c'est donc là que l'architecture est évaluée. Un LoRA incompatible est détecté à ce stade avec un 415 et la session est annulée. Tu le sais après 20 Mo plutôt qu'après 400.
3. Fermer la session. POST /loras/upload/finish avec { "uploadId", "parts": [ { "part": 1, "etag": "..." }, ... ] }. Cela assemble le fichier et l'enregistre, en répondant exactement comme l'upload en une seule fois.
Pour abandonner, POST /loras/upload/abort avec { "uploadId" }. Une session laissée sans finish est supprimée après six heures. Rien n'apparaît dans GET /loras et rien ne compte dans le quota tant que finish ne réussit pas, donc un upload interrompu ne te coûte aucun espace de stockage.
import os, requests
B, H = "https://api.tendre.ai/pro/v1", {"Authorization": "Bearer " + KEY}
path = name = "my_character.safetensors"
s = requests.post(f"{B}/loras/upload/start", headers=H,
json={"name": name, "kind": "image", "size": os.path.getsize(path), "trigger": "ohwx"}).json()
parts = []
with open(path, "rb") as f:
n = 1
while chunk := f.read(s["part_size"]):
r = requests.post(f"{B}/loras/upload/part",
headers={**H, "Content-Type": "application/octet-stream"},
params={"uploadId": s["uploadId"], "part": n}, data=chunk)
r.raise_for_status()
parts.append({"part": n, "etag": r.json()["etag"]})
n += 1
done = requests.post(f"{B}/loras/upload/finish", headers=H,
json={"uploadId": s["uploadId"], "parts": parts}).json()
print(done["name"], done["architecture"], done["storage"])Un script PowerShell prêt à l'emploi est livré avec l'app, dans scripts/api-pro-lora.ps1.
Supprimer un LoRA
DELETE /loras/{name}?kind=image|video supprime le fichier et libère le quota. La réponse est { "ok": true, "name": ..., "storage": { ... } }, ou 404 unknown_lora si le nom n'existe pas sur le compte.
Quota de stockage
Chaque licence dispose de 20 fichiers et 10 Go au maximum, images et vidéos confondus, et les personnages entraînés dans l'app comptent dedans. GET /loras rapporte l'état dans storage :
{ "count": 3, "max_count": 20, "bytes": 943718400, "max_bytes": 10737418240 }Au-delà de l'une ou l'autre limite, un upload répond 413 lora_quota accompagné de ce qu'il reste. Supprime un fichier pour libérer de la place.
L'entraînement d'un personnage n'est pas encore exposé par l'API. Cela se fait dans l'app, et le résultat apparaît dans cette liste.
Activité
L'app conserve le journal de chaque rendu effectué par tes clés (date, clé, type, coût, statut) ainsi que la consommation par clé, dans Paramètres > API > Activité.
C'est aussi là que tu révoqués une clé qui a fuité, sans toucher aux autres.