Перейти к содержанию

Краткое руководство 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.

Схема работы

  1. POST /api/v1/tryon с заголовком Idempotency-Key. Ответ: 201 с job_id и заголовками X-Job-Id и Location.
  2. Опрашивайте GET /api/v1/tryon/status/{job_id}, пока status не станет completed, failed или interrupted, — либо примите вебхук.
  3. Если ответ на 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.