Pro API: balance, caps and trained characters

Two read routes, always allowed whatever the key's permissions, plus the activity log kept in the app.

Balance and caps

GET /credits

json
{ "balance": 1830, "licenseId": "lic_...", "key": "k_1a2b3c4d",
  "limits": { "concurrent": 2, "perMinute": 60, "perDay": 0 }, "running": 1,
  "buy_url": "https://tendre.ai/credits?license=lic_..." }
FieldMeaning
balanceCredits left. null when the balance is unknown (test environment).
licenseIdThe licence this key belongs to.
keyThe key's public identifier, safe to log. Never the secret.
limits.concurrentRenders running at once, 2 by default.
limits.perMinuteSubmissions per minute, 60 by default.
limits.perDayDaily credit cap you set on the key. 0 means no cap.
runningRenders from this key still waiting to be collected.
buy_urlThe credit purchase page of your licence, by card or crypto.

This is the cheapest call in the API and the right one to check a key works.

Buy credits on your saved card

POST /credits/purchase

json
{ "pack": "m" }

pack is a pack id (s, m, l, xl, xxl or xxxl), or "c25000" for a custom amount beyond the largest pack. The payment goes on the card saved in the automatic top-up of your account page, only if you allowed your agent to buy there, within the monthly limit you set there (automatic top-ups included). An email confirms each purchase. One purchase at a time.

The answer is 200 { credits, eur, balance }. Every refusal carries buy_url, the ordinary purchase link, to use instead: 402 confirmation_required (your bank asks for your confirmation) or payment_declined; 409 agent_not_allowed, no_saved_card, monthly_cap_reached, topup_in_progress or purchase_in_progress; 400 unknown_pack. On 503 payment_unavailable we do not know whether the charge went through: do not retry right away, a charge that went through is credited.

Trained characters

These are the characters trained as a LoRA, for images and ordinary videos. The characters of Multi-Ref Video, seven views made from one photo, are another thing: see Characters.

GET /loras

json
{ "image": [ { "name": "megan_krea2", "trigger": "megan", "base": "krea2", "size": 0 } ],
  "video": [ { "name": "megan_ltx", "trigger": "megan", "size": 0 } ] }

Pass name as lora, taking it from the image list for images and the video list for videos, and put its trigger in the prompt, otherwise the character will not appear. A character that does not belong to the account answers 403 unknown_lora.

Upload your own LoRA

POST /loras/upload

A .safetensors file you already have, trained elsewhere or sitting on your own server, goes into the account's space and becomes usable through lora, exactly like a character trained in the app. There are two ways in: one shot up to 90 MB, or in parts beyond that. A LoRA usually weighs 200 to 500 MB, so the second one is the ordinary case. The ceiling is 800 MB either way.

One shot: a raw body (application/octet-stream, Content-Length required), parameters in the URL.

Past 100 MB the request never reaches the API. The infrastructure in front turns it away, so what comes back is a bare 413 with no JSON body: a client that parses every error as JSON will choke on it rather than report it. That size belongs in parts.

ParameterRole
nameFile name, .safetensors, letters, digits, . _ -, 80 characters at most. A name already taken is replaced.
kindimage (the default) or video. Needs the images or the videos permission accordingly.
triggerThe trigger word to put back in the prompt, 60 characters at most.
bash
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.safetensors

The answer is { "ok": true, "name": "my_character.safetensors", "kind": "image", "size": 314572800, "trigger": "ohwx", "architecture": "krea2", "storage": { ... } }.

Compatibility is checked on upload. Images are rendered by Krea 2, which has an architecture of its own: only a Krea 2 LoRA applies, whether trained in tendre.AI or for Krea 2 elsewhere. Note that a FLUX.1 LoRA is refused, which is not everyone's first guess. Videos are rendered by LTX 2.5, so a LTX 2.5 or 2.3 LoRA.

The API reads the file header before storing anything. A LoRA recognised as SDXL, SD 1.5, SD3, FLUX.1, Wan or HunyuanVideo is turned away with 415 incompatible_lora, which names the architecture it found, and a file that is not a safetensors with 415 invalid_safetensors. Nothing is stored and no quota is spent in either case.

One case still gets through: an architecture the header does not identify is accepted, with architecture: "inconnue", and is only judged at render time. That is the one mistake that costs you a render to discover.

Send a large LoRA in parts

Three calls, plus one to give up.

1. Open the session. POST /loras/upload/start with { "name", "kind", "size", "trigger" }, where size is the whole file in bytes. It checks the name, the size and the quota, then answers:

json
{ "uploadId": "...", "part_size": 20971520, "max_part_size": 104857600, "parts_expected": 15 }

2. Send each part. POST /loras/upload/part?uploadId=...&part=N, raw body, parts numbered from 1, part_size bytes each except the last, never more than max_part_size. Each answer carries an etag, and you need all of them for the next step, so keep them as you go.

Part 1 is special: it carries the file header, so that is where the architecture is judged. An incompatible LoRA is caught there with 415 and the session is cancelled. You find out after 20 MB rather than after 400.

3. Close it. POST /loras/upload/finish with { "uploadId", "parts": [ { "part": 1, "etag": "..." }, ... ] }. It assembles the file and registers it, answering exactly like the one-shot upload.

To give up, POST /loras/upload/abort with { "uploadId" }. A session left without finish is dropped after six hours. Nothing shows in GET /loras and nothing counts against the quota until finish succeeds, so an interrupted upload costs you no storage.

python
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"])

A ready-made PowerShell script ships with the app, at scripts/api-pro-lora.ps1.

Delete a LoRA

DELETE /loras/{name}?kind=image|video removes the file and frees the quota. It answers { "ok": true, "name": ..., "storage": { ... } }, or 404 unknown_lora when the name does not exist on the account.

Storage quota

Each licence gets 20 files and 10 GB at most, images and videos together, and the characters trained in the app count towards it. GET /loras reports the state in storage:

json
{ "count": 3, "max_count": 20, "bytes": 943718400, "max_bytes": 10737418240 }

Past either limit, an upload answers 413 lora_quota along with what is left. Delete a file to make room.

Training a character is still not exposed by the API. It happens in the app, and the result shows up in this list.

Activity

The app keeps the log of every render your keys have made (date, key, type, cost, status) and the consumption per key, under Settings > API > Activity.

That is also where you revoke a key that has been leaking, without touching the others.

Windows · Mac · Cloud or local

Generate privately. Tonight.

Download tendre.AI for free and generate without limits, privately, on your own GPU.

OS Windows 10/11GPU NVIDIA 8GB+VRAM 12GB recommendedDisk 20GB

Mac: Apple silicon (M1 or later), macOS 13 or later. Intel Macs are not supported.

Local image generation on Mac needs an M4 with 24 GB of memory, or an M2 or M3 Pro, Max or Ultra with 24 GB. Below that, everything runs online with credits. Retouching and video run online on Mac for now.

Everything about tendre.AI for Mac

No NVIDIA GPU? Start 100% in the cloud: 5 free images and 1 free video every day. An NVIDIA card (8GB+) unlocks unlimited local generation.