Краткое руководство B — серверный вызов¶
Отправляйте примерки со своего сервера секретным ключом, а результат читайте опросом или из вебхука. Это подходит, когда фото покупателя попадает на ваш бэкенд (приложение, форма, сообщение), а не на страницу товара. Для страницы товара см. Краткое руководство A — чистый HTML.
Базовый адрес: https://api.ziex-tryon.com. Все пути ниже начинаются с /api/v1/tryon.
Секретный ключ остаётся на сервере
Аутентифицируйтесь заголовком X-API-Key: tryon_YOUR_SECRET_KEY. Никогда не отдавайте этот ключ в коде страницы, в
мобильном приложении или в публичном репозитории: он же открывает вход в личный кабинет клиента, где видны фото ваших
покупателей. Страница использует публикуемый ключ pk_… (см. Обзор и понятия).
Всегда отправляйте собственный User-Agent
Browser Integrity Check Cloudflare отвечает 403 с ошибкой 1010 (HTML-страницей, а не нашим JSON) на User-Agent по
умолчанию стандартной библиотеки Python, Python-urllib/3.x. curl, requests, curl в PHP и браузеры проходят как
есть. Что бы вы ни использовали, задайте явный User-Agent с названием вашего магазина и версией, например
shop-example-tryon/1.0.
Схема работы¶
POST /api/v1/tryonс заголовкомIdempotency-Key. Ответ:201сjob_idи заголовкамиX-Job-IdиLocation.- Опрашивайте
GET /api/v1/tryon/status/{job_id}, покаstatusне станетcompleted,failedилиinterrupted, — либо примите вебхук. - Если ответ на
POSTпотерян, найдите задачу черезGET /api/v1/tryon/attempts/{idempotency_key}.
1. Отправьте примерку¶
POST https://api.ziex-tryon.com/api/v1/tryon, Content-Type: application/json.
| Поле | Обязательно | Значение |
|---|---|---|
model_image |
да | Фото покупателя: http(s) URL или data URI (data:image/jpeg;base64,…). |
garment_image |
да | Одежда: URL или data URI. |
category |
нет | tops (по умолчанию), outerwear, bottoms, one-pieces или auto. Актуальный список — GET /api/v1/tryon/capabilities. Категория, которую сервис сейчас не может примерить, — 422 CATEGORY_UNSUPPORTED. |
mode |
нет | quality (по умолчанию, выше точность) или balanced (быстрее). |
product_title |
нет | Название товара, хранится с задачей. Принимается до 1000 символов; сохраняемый текст очищается и обрезается до 200. |
product_url |
нет | Страница товара. Принимается до 4096 символов; сохраняется, только если это обычный http(s) URL не длиннее 2048 символов, иначе отбрасывается — примерка при этом выполняется. |
webhook_url |
нет | Публичный HTTPS-адрес, который вызовут, когда задача завершится. Только секретный ключ. |
integration |
нет | Метка вашей интеграции, name/version, например custom/acme-shop. Никогда не повод отказать в запросе. |
Каждое декодированное изображение должно быть не больше 10 МБ, не больше 16 мегапикселей и не меньше 256 x 256
пикселей, иначе ответ — 422 VALIDATION_ERROR. Всё тело запроса — не больше 12 МБ; большее получит от nginx
413 PAYLOAD_TOO_LARGE. URL загружает сам сервис, поэтому он должен быть доступен из интернета.
Тестовым ключам нужна загрузка
Живой секретный ключ может отправлять оба изображения ссылками. Тестовый ключ (tryon_test_…) обязан передать
model_image как data URI, иначе ответ — 422 PHOTO_UPLOAD_REQUIRED. См. Локальная разработка.
Заголовок Idempotency-Key¶
Отправляйте его с каждой отправкой. Это строка, подходящая под ^[A-Za-z0-9_-]{22,64}$ (UUID с дефисами или без
подходит), — новая для каждой примерки и одна и та же при каждом повторе этой же примерки. Сохраните её у себя вместе
с заказом или запросом до отправки.
- Первый запрос с ключом создаёт задачу:
201. - Повтор с тем же ключом никогда не создаёт вторую задачу и никогда не списывает повторно. Он отвечает
200с тем жеjob_id, текущим статусом задачи и заголовкомIdempotent-Replayed: true. Тело повтора с первым не сравнивается, поэтому другое фото под тем же ключом вернёт первую задачу. - Пока первый запрос ещё выполняется, повтор — это
409 IDEMPOTENT_IN_PROGRESSсRetry-After: подождите и повторите с тем же ключом. - Ключ действует в пределах API-ключа и его вида: та же строка под другим ключом — другая попытка.
Некорректный ключ — 422 VALIDATION_ERROR.
curl -sS -i https://api.ziex-tryon.com/api/v1/tryon \
-H 'X-API-Key: tryon_YOUR_SECRET_KEY' \
-H 'User-Agent: shop-example-tryon/1.0' \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{
"model_image": "https://shop.example/uploads/shopper-1234.jpg",
"garment_image": "https://shop.example/images/linen-shirt.jpg",
"category": "tops",
"mode": "quality",
"product_title": "Льняная рубашка",
"product_url": "https://shop.example/p/linen-shirt",
"integration": "custom/acme-shop"
}'
Оболочка не может передать data URI в несколько мегабайт аргументом; для загрузки фото используйте пример на Python или PHP.
import base64
import time
import uuid
import requests
API = "https://api.ziex-tryon.com/api/v1/tryon"
HEADERS = {
"X-API-Key": "tryon_YOUR_SECRET_KEY",
"User-Agent": "shop-example-tryon/1.0",
}
def data_uri(path, mime="image/jpeg"):
with open(path, "rb") as f:
return f"data:{mime};base64," + base64.b64encode(f.read()).decode("ascii")
def submit(idempotency_key):
body = {
"model_image": data_uri("shopper.jpg"),
"garment_image": "https://shop.example/images/linen-shirt.jpg",
"category": "tops",
"mode": "quality",
"product_title": "Льняная рубашка",
"product_url": "https://shop.example/p/linen-shirt",
"integration": "custom/acme-shop",
}
return requests.post(
API,
json=body,
headers={**HEADERS, "Idempotency-Key": idempotency_key},
timeout=60,
)
def wait_for_result(job_id):
deadline = time.time() + 50 * 60 # любая задача завершается за 45 минут
while time.time() < deadline:
r = requests.get(f"{API}/status/{job_id}", headers=HEADERS, timeout=30)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "1")))
continue
r.raise_for_status()
job = r.json()
if job["status"] in ("completed", "failed", "interrupted"):
return job
time.sleep(2)
raise TimeoutError(job_id)
key = uuid.uuid4().hex # 32 символа; сохраните для повторов
response = submit(key)
if response.status_code in (200, 201):
job = wait_for_result(response.json()["job_id"])
print(job["status"], job.get("result_url"), job.get("error_code"))
else:
print(response.status_code, response.json()["error"]["code"])
import json
import urllib.error
import urllib.request
import uuid
API = "https://api.ziex-tryon.com/api/v1/tryon"
body = json.dumps({
"model_image": "https://shop.example/uploads/shopper-1234.jpg",
"garment_image": "https://shop.example/images/linen-shirt.jpg",
"category": "tops",
}).encode("utf-8")
request = urllib.request.Request(
API,
data=body,
method="POST",
headers={
"X-API-Key": "tryon_YOUR_SECRET_KEY",
"Content-Type": "application/json",
"Idempotency-Key": uuid.uuid4().hex,
# Без этой строки urllib отправит "Python-urllib/3.x", и Cloudflare ответит 403, ошибка 1010.
"User-Agent": "shop-example-tryon/1.0",
},
)
try:
with urllib.request.urlopen(request, timeout=60) as response:
print(response.status, response.headers["X-Job-Id"], json.load(response))
except urllib.error.HTTPError as err:
print(err.code, err.headers.get("Retry-After"), json.load(err))
<?php
const ZIEX_API = 'https://api.ziex-tryon.com/api/v1/tryon';
const ZIEX_KEY = 'tryon_YOUR_SECRET_KEY';
// Возвращает [HTTP-статус, заголовки ответа (имена в нижнем регистре), разобранное тело JSON].
function ziex_request(string $method, string $path, ?array $body = null, ?string $idempotencyKey = null): array
{
$headers = [];
$request = [
'X-API-Key: ' . ZIEX_KEY,
'Content-Type: application/json',
];
if ($idempotencyKey !== null) {
$request[] = 'Idempotency-Key: ' . $idempotencyKey;
}
$ch = curl_init(ZIEX_API . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $request,
CURLOPT_USERAGENT => 'shop-example-tryon/1.0',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return strlen($line);
},
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_SLASHES));
}
$raw = curl_exec($ch);
if ($raw === false) {
throw new RuntimeException('transport error: ' . curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [$status, $headers, json_decode($raw, true)];
}
$photo = 'data:image/jpeg;base64,' . base64_encode(file_get_contents('shopper.jpg'));
$key = bin2hex(random_bytes(16)); // 32 символа; сохраните для повторов
[$status, $headers, $job] = ziex_request('POST', '', [
'model_image' => $photo,
'garment_image' => 'https://shop.example/images/linen-shirt.jpg',
'category' => 'tops',
'product_title' => 'Льняная рубашка',
'product_url' => 'https://shop.example/p/linen-shirt',
'integration' => 'custom/acme-shop',
], $key);
if ($status === 200 || $status === 201) {
do {
sleep(2);
[$status, $headers, $job] = ziex_request('GET', '/status/' . $job['job_id']);
} while (in_array($job['status'] ?? '', ['pending', 'processing'], true));
echo $job['status'], ' ', $job['result_url'] ?? $job['error_code'], "\n";
} else {
echo $status, ' ', $job['error']['code'], "\n";
}
Цикл дан в самой короткой форме. В продакшене также остановитесь через 50 минут и учитывайте Retry-After при 429.
Ответ¶
201 Created с заголовками X-Job-Id: <job_id> и Location: /api/v1/tryon/status/<job_id>:
{
"job_id": "3f1b8c0e-5d52-4f0a-9d0e-0c6b3f5f2a11",
"status": "pending",
"poll_url": "/api/v1/tryon/status/3f1b8c0e-5d52-4f0a-9d0e-0c6b3f5f2a11",
"estimated_seconds": 20,
"request_id": "8a6c1b2e"
}
estimated_seconds — оценка для индикатора прогресса, а не обещание. Называйте X-Request-ID (он есть в каждом ответе
приложения), когда обращаетесь к нам по конкретному запросу.
2. Прочитайте результат¶
Опрос¶
GET /api/v1/tryon/status/{job_id} с тем же X-API-Key. Спрашивайте раз в 2 секунды. Секретный ключ читает живые
задачи своего аккаунта; идентификатор вне этой области — 404 NOT_FOUND.
{
"job_id": "3f1b8c0e-5d52-4f0a-9d0e-0c6b3f5f2a11",
"status": "completed",
"progress": 100,
"result_url": "https://media.ziex-tryon.com/RESULT_FILE",
"processing_time_ms": 21400,
"error_code": null,
"error_message": null,
"cost_credits": null
}
status |
Значение |
|---|---|
pending |
Принята, ждёт воркер. progress равен 0. |
processing |
Генерируется. progress — оценка, останавливается на 95. |
completed |
Готово. result_url — изображение; скачайте его оттуда. |
failed |
Не удалось сгенерировать. error_code — один из GENERATION_FAILED, INVALID_IMAGE, CONTENT_BLOCKED, TIMEOUT; error_message — безопасный текст, который можно показать. |
interrupted |
Остановлена перезапуском сервиса; error_code — INTERRUPTED. Отправьте заново под новым ключом. |
Любая задача завершается не позже чем через 45 минут. Задача failed или interrupted автоматически возвращается в ваш
тариф; completed — нет. cost_credits всегда null.
Если эндпоинт статуса отвечает 429, подождите Retry-After секунд. Опрос ограничен 20 запросами в секунду на IP
(всплеск 30) — одна задача раз в 2 секунды намного ниже.
Вебхук¶
Вместо опроса передайте webhook_url (публичный HTTPS-адрес; только секретный ключ), и Ziex Try-On вызовет его, когда
задача завершится событием job.completed или job.failed. Вебхук подписан; содержимое, проверка подписи и расписание
повторов описаны на странице о вебхуках. Вебхуки доставляются по мере возможности, поэтому оставляйте опрос как
страховку для задачи, чей вебхук так и не пришёл. Машина за межсетевым экраном или на localhost вебхук не получит —
см. Локальная разработка.
3. Если ответ потерян¶
Таймаут или обрыв соединения после отправки POST не означает, что задачи нет: запрос мог дойти до сервиса и
создать её. Никогда не отправляйте ту же примерку заново под новым ключом. Спросите попытку:
curl -sS https://api.ziex-tryon.com/api/v1/tryon/attempts/YOUR_IDEMPOTENCY_KEY \
-H 'X-API-Key: tryon_YOUR_SECRET_KEY' \
-H 'User-Agent: shop-example-tryon/1.0'
| Ответ | Значение |
|---|---|
200 |
Задача: то же тело, что у эндпоинта статуса, плюс X-Job-Id и Location. |
409 IDEMPOTENT_IN_PROGRESS |
Отправка ещё обрабатывается. Спросите снова после Retry-After. |
404 NOT_FOUND |
С этим ключом задача не создавалась. Окончательно это только примерно через 2 минуты после отправки POST; раньше спрашивайте снова. |
503 ATTEMPT_LOOKUP_UNAVAILABLE |
Сейчас определить нельзя. Спросите снова после Retry-After. |
Повтор самого POST с тем же Idempotency-Key равнозначен и тоже безопасен.
Что делать с ошибкой¶
Тело ошибки всегда {"error": {"code": "...", "message": "..."}}. Ориентируйтесь на error.code и HTTP-статус, а не
на message, который может измениться. Полная таблица — на странице об ошибках; случаи, определяющие, повторять ли
запрос:
| Ответ | Что произошло | Что делать |
|---|---|---|
400, 401, 403, 422 |
Неверен запрос или ключ. | Исправьте. Повтор того же запроса не поможет. |
429 RATE_LIMITED, LIMIT_EXCEEDED, DAILY_CAP_REACHED |
Лимит. Ничего не создано. | Подождите Retry-After секунд и повторите — тот же ключ можно использовать снова. |
409 IDEMPOTENT_IN_PROGRESS |
Первый запрос с этим ключом ещё выполняется. | Подождите Retry-After, повторите с тем же ключом. |
503 OVERLOADED (Retry-After: 5, без X-Job-Id) |
Сервис был занят; ничего не создано и не списано. | Повторите тот же запрос с тем же ключом после Retry-After. |
503 SUBMIT_UNCONFIRMED (с X-Job-Id) |
Задача существует, но, возможно, не поставлена в очередь. | Опрашивайте X-Job-Id или найдите попытку. Не отправляйте заново под новым ключом. |
500 |
Окончательный ответ. Если задача уже была создана, она теперь failed, а её квота вернётся. |
Повтор с тем же ключом ответит 200 с этой проваленной задачей. Для новой попытки используйте новый ключ. |
502, 503, 504 SERVICE_UNAVAILABLE, таймаут, обрыв соединения |
Сервис перезапускался или не ответил вовремя; исход неизвестен. | Найдите попытку (раздел 3), затем повторите с паузами. |
Лимиты, которые стоит знать¶
- 30 отправок в минуту на IP-адрес клиента, плюс ограничение nginx в 2 запроса в секунду (всплеск 5) на
POST. Сервер, пересылающий запросы всех своих покупателей с одного адреса, делит этот один бюджет. - Часовой лимит ключа, генерации вашего тарифа и суточные потолки; каждый отказ несёт
Retry-After. - Загруженные фото и результаты хранятся 180 дней.
Цифры по каждому лимиту — на странице о лимитах; весь API — в Справочнике API.