Pro API: баланс, лимиты и обученные персонажи
Два маршрута для чтения, доступных при любых правах ключа, плюс журнал активности, который хранится в приложении.
Баланс и лимиты
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_..." }| Поле | Значение |
|---|---|
balance | Оставшиеся кредиты. null, если баланс неизвестен (тестовая среда). |
licenseId | Лицензия, к которой принадлежит этот ключ. |
key | Публичный идентификатор ключа, безопасный для логирования. Никогда не секрет. |
limits.concurrent | Количество одновременных рендеров, по умолчанию 2. |
limits.perMinute | Запросов в минуту, по умолчанию 60. |
limits.perDay | Дневной лимит кредитов, установленный для ключа. 0 означает отсутствие лимита. |
running | Рендеры этого ключа, ожидающие получения. |
buy_url | Страница покупки кредитов для твоей лицензии, картой или криптовалютой. |
Это самый дешёвый вызов в API и правильный способ проверить работоспособность ключа.
Покупка кредитов с сохранённой карты
POST /credits/purchase
{ "pack": "m" }В pack укажи идентификатор пакета (s, m, l, xl, xxl или xxxl) или "c25000" для произвольного количества сверх самого большого пакета. Оплата идёт с карты, сохранённой в автопополнении на странице твоего аккаунта, только если ты там разрешил агенту покупать, в пределах месячного лимита, который ты там задал (автопополнения учитываются). Каждую покупку подтверждает письмо. Только одна покупка за раз.
Ответ: 200 { credits, eur, balance }. Каждый отказ содержит buy_url, обычную ссылку на покупку, которой можно воспользоваться вместо этого: 402 confirmation_required (банк просит твоего подтверждения) или payment_declined; 409 agent_not_allowed, no_saved_card, monthly_cap_reached, topup_in_progress или purchase_in_progress; 400 unknown_pack. При 503 payment_unavailable мы не знаем, прошло ли списание: не повторяй сразу, прошедшее списание будет зачислено.
Обученные персонажи
Это персонажи, обученные как LoRA, для изображений и обычных видео. Персонажи Multi-Ref Видео, семь ракурсов из одной фотографии, это другое: смотри Characters.
GET /loras
{ "image": [ { "name": "megan_krea2", "trigger": "megan", "base": "krea2", "size": 0 } ],
"video": [ { "name": "megan_ltx", "trigger": "megan", "size": 0 } ] }Передавай name как lora, беря его из списка image для изображений и из списка video для видео, и вставляй trigger в промпт, иначе персонаж не появится. Персонаж, не принадлежащий аккаунту, возвращает 403 unknown_lora.
Загрузи собственную LoRA
POST /loras/upload
Файл .safetensors, который у тебя уже есть, обученный в другом месте или лежащий на твоём сервере, попадает в пространство аккаунта и становится доступным через lora точно так же, как персонаж, обученный в приложении. Есть два способа загрузки: одним запросом до 90 МБ или по частям при большем размере. LoRA обычно весит от 200 до 500 МБ, так что обычно подходит второй вариант. Максимальный размер в обоих случаях: 800 МБ.
Одним запросом: тело запроса в сыром виде (application/octet-stream, Content-Length обязателен), параметры передаются в URL.
Запросы свыше 100 МБ до API не доходят. Инфраструктура перед ним отклоняет их, поэтому в ответ приходит голый 413 без тела JSON: клиент, который парсит все ошибки как JSON, сломается вместо того, чтобы сообщить об ошибке. Такой размер надо загружать по частям.
| Параметр | Роль |
|---|---|
name | Имя файла, .safetensors, буквы, цифры, . _ -, не более 80 символов. Имя, которое уже занято, заменяется. |
kind | image (по умолчанию) или video. Требует соответственно разрешения images или videos. |
trigger | Триггерное слово для подстановки в промпт, не более 60 символов. |
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Ответ: { "ok": true, "name": "my_character.safetensors", "kind": "image", "size": 314572800, "trigger": "ohwx", "architecture": "krea2", "storage": { ... } }.
Совместимость проверяется при загрузке. Изображения рендерятся через Krea 2, у которого собственная архитектура: подходит только Krea 2 LoRA, обученная в tendre.AI или для Krea 2 в другом месте. Обрати внимание, что LoRA FLUX.1 отклоняется, что не для всех очевидно. Видео рендерятся через LTX 2.5, поэтому нужна LTX 2.5 или 2.3 LoRA.
API читает заголовок файла до его сохранения. LoRA, распознанная как SDXL, SD 1.5, SD3, FLUX.1, Wan или HunyuanVideo, отклоняется с 415 incompatible_lora, в котором указывается найденная architecture, а файл, не являющийся safetensors, получает 415 invalid_safetensors. В обоих случаях ничего не сохраняется и квота не тратится.
Один случай всё же проходит: архитектура, которую заголовок не определяет, принимается с architecture: "inconnue" и оценивается только во время рендера. Это единственная ошибка, которая обойдётся тебе в один рендер.
Отправка большой LoRA по частям
Три вызова плюс один для отмены.
1. Открой сессию. POST /loras/upload/start с { "name", "kind", "size", "trigger" }, где size означает полный размер файла в байтах. Проверяются имя, размер и квота, затем возвращается:
{ "uploadId": "...", "part_size": 20971520, "max_part_size": 104857600, "parts_expected": 15 }2. Отправь каждую часть. POST /loras/upload/part?uploadId=...&part=N, тело в сыром виде, части нумеруются с 1, каждая по part_size байт кроме последней, и не более max_part_size. Каждый ответ содержит etag, и они все нужны тебе для следующего шага, так что сохраняй их по ходу.
Часть 1 особенная: она содержит заголовок файла, поэтому именно здесь проверяется архитектура. Несовместимая LoRA отклоняется с 415, и сессия отменяется. Ты узнаешь об этом после 20 МБ, а не после 400.
3. Закрой сессию. POST /loras/upload/finish с { "uploadId", "parts": [ { "part": 1, "etag": "..." }, ... ] }. Файл собирается и регистрируется, ответ полностью совпадает с ответом одного запроса.
Для отмены: POST /loras/upload/abort с { "uploadId" }. Сессия без finish удаляется через шесть часов. В GET /loras ничего не отображается и в квоту ничего не засчитывается до успешного выполнения finish, так что прерванная загрузка не занимает место.
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"])Готовый скрипт PowerShell поставляется с приложением: scripts/api-pro-lora.ps1.
Удаление LoRA
DELETE /loras/{name}?kind=image|video удаляет файл и освобождает квоту. Ответ: { "ok": true, "name": ..., "storage": { ... } }, или 404 unknown_lora, если имя не существует на аккаунте.
Квота хранилища
Каждая лицензия получает максимум 20 файлов и 10 ГБ, изображения и видео вместе, причём персонажи, обученные в приложении, тоже учитываются. GET /loras возвращает состояние в поле storage:
{ "count": 3, "max_count": 20, "bytes": 943718400, "max_bytes": 10737418240 }При превышении любого из лимитов загрузка возвращает 413 lora_quota вместе с остатком. Удали файл, чтобы освободить место.
Обучение персонажей через API пока недоступно. Оно происходит в приложении, а результат появляется в этом списке.
Активность
Приложение хранит журнал всех рендеров, выполненных твоими ключами (дата, ключ, тип, стоимость, статус), а также расход по каждому ключу в разделе Настройки > API > Activity.
Там же ты можешь отозвать скомпрометированный ключ, не затрагивая остальные.