Договор API
Договор API
Три пространства, физически разные точки входа: публичная отдача без всякого API, приватный API арендатора и админ-API для панели. Ответ всегда JSON — и всегда по-английски: на код отказа приходится ровно одна формулировка, чтобы один и тот же сбой читался одинаково в любом журнале.
Публичная отдача — без API вовсе
Файл ищется на диске веб-сервером. Не нашли — 404, а не «сгенерируй на лету».
GET https://img.example.com/i/<путь варианта>
GET https://v.keepix.cloud/v/<путь>/master.m3u8
Заголовки отдачи одинаковы для всех арендаторов и не меняются:
cache-control: public, max-age=31536000, immutable
x-content-type-options: nosniff
content-security-policy: default-src 'none'; sandbox
cross-origin-resource-policy: cross-origin
access-control-allow-origin: *
Ни одной Set-Cookie: на хосте отдачи нет ни сессий, ни
JavaScript.
Подпись
Каждый запрос приватного API подписывается. Четыре заголовка:
X-Media-Tenant: <код арендатора>
X-Media-Key-Id: <идентификатор ключа>
X-Media-Ts: <время unix, секунды>
X-Media-Sign: hex(HMAC-SHA256(secret, METHOD "\n" PATH "\n" TS "\n" sha256(тело)))
PATH — путь вместе со строкой запроса
(/api/v1/put?kind=prodimg&key=42). Иначе параметры остались бы
неподписанными и подмену kind или key никто бы не
заметил. Тело пустого запроса хешируется как пустая строка
(e3b0c442…), а не как литерал "".
Окно — 300 секунд. Секрет — ASCII-строка из 64 шестнадцатеричных знаков; ею и подписывают, декодировать ничего не надо.
Повтор подписывается заново. Время в подписи — секунды,
поэтому два одинаковых запроса в одну и ту же секунду дают байт в байт одну
подпись, и второй получит 409 sign_replay. Это защита от повтора,
и она работает как задумано. Отсюда два правила: не слать одинаковые
подписанные запросы параллельно, а при повторе после обрыва — пересчитать
подпись со свежим X-Media-Ts.
Минимальный подписывающий клиент
#!/bin/bash
METHOD=$1; PATHQ=$2; BODY=$3
TS=$(date +%s)
BH=$( [ -f "$BODY" ] && sha256sum "$BODY" | cut -d' ' -f1 \
|| printf '' | sha256sum | cut -d' ' -f1 )
SIG=$(printf '%s\n%s\n%s\n%s' "$METHOD" "$PATHQ" "$TS" "$BH" \
| openssl dgst -sha256 -hmac "$KP_SECRET" -hex | sed 's/.*= //')
curl -H "X-Media-Tenant: $KP_TENANT" -H "X-Media-Key-Id: $KP_KEYID" \
-H "X-Media-Ts: $TS" -H "X-Media-Sign: $SIG" \
--data-binary @"$BODY" -X "$METHOD" "https://keepix.cloud$PATHQ"
Точки
| Точка | Что делает |
|---|---|
POST /api/v1/put |
Принять оригинал. Параметры kind, key,
variants, owner_ref — в строке запроса; тело — сам файл.
Картинки готовы в ответе, видео уходит в очередь.
Идемпотентен: тот же sha256 при том же ключе не
делает работы заново и возвращает те же адреса. |
POST /api/v1/put-variant | Принять уже готовый вариант без обработки — для переноса накопленного. Перекодировать готовую копию второй раз значит потерять качество на ровном месте. Проверки приёма при этом те же самые. |
GET /api/v1/resolve | Какие адреса у этого ключа. Это ремонт: если запись у вас потерялась, вы спрашиваете, а не угадываете. |
POST /api/v1/resolve-batch |
То же до 1000 ключей за раз. Тело: {"kind":"prodimg","keys":["1","2"]}.
В ответе found и missing. |
POST /api/v1/reconcile |
Холостая сверка: долетело ли всё. Ничего не меняет. До 1000
записей за раз — проверять перенос по одному файлу значило бы сделать
столько запросов, сколько файлов. Тело:
{"kind":"prodimg","items":[{"key":"1","variant":"card","bytes":23438,"sha256":"…"}],"only":"problems"}.
Поля bytes и sha256 необязательны: без них сервис
просто расскажет, что у него есть; с ними — сверит. Ответ даёт по каждой
записи status: ok, missing,
mismatch, pending, unverified — и
digest, отпечаток всей пачки. См. Сверка. |
GET /api/v1/inventory |
Выгрузить нашу картину пачками: ?kind=prodimg&limit=1000&after=<ключ>.
Отдаёт ключи с вариантами (название, размер, хеш, адрес) по возрастанию ключа;
next — курсор для следующей страницы. Нужен, когда сверять надо не
свой список против нашего, а наоборот. Курсор — сам ключ, поэтому страница не
съезжает, если во время обхода что-то принято. |
DELETE /api/v1/object | Снять объект целиком: строки и байты с диска. Объекты стока отвечают 409 и сохраняют байты. |
POST /api/v1/erase |
Стереть обычные загрузки по owner_ref, если человек удалил аккаунт. Ответ
называет число стёртых объектов: ваша приёмка по персональным
данным сверяет исполнение, а не намерение.Исключение для стока: Если с этим owner_ref связан хотя бы один материал стока, включая снятый с витрины, erase отвечает 409 asset_in_stock и ничего не удаляет. Оригинал нужен для уже выданных лицензий. Используйте разные owner_ref для стока и обычных загрузок; закрытие аккаунта требует отдельной проверки. |
GET /api/v1/quota | Сколько занято, сколько осталось, сколько в очереди. |
GET /api/v1/ping | Проверка, что ключ действует. |
GET /healthz | Без подписи. Говорит, что фронт жив: имя службы, узел, версия, свободное место и время. Ни слова о базе и ни слова об арендаторах — по этому же ответу опрашивается здоровье узлов. |
Сверка: убедиться, что долетело всё
После большого переноса главный вопрос один: всё ли доехало. Отвечать на него по одному файлу — значит сделать столько запросов, сколько файлов. Поэтому сверка идёт пачками и ничего не меняет: это холостой ход.
Как это делается
- Нарезаете свой список на пачки по 1000 и на каждую шлёте
POST /api/v1/reconcileсо своими размерами и хешами. - Смотрите на
summary: еслиokравно числу записей — пачка сошлась, читать остальное незачем. - Не сошлось — в
itemsлежит, что именно и почему. С"only":"problems"сошедшиеся не возвращаются вовсе: ответ на удачную пачку в тысячу записей — около 270 байт.
Что значит каждый ответ
| status | что произошло |
|---|---|
ok | Есть, и размер с хешем сходятся. Если вы не
прислали ни размера, ни хеша, ok значит только «есть». |
missing | Такого ключа (или такого варианта у него) у сервиса нет. Это и есть то, что надо дослать. |
mismatch | Есть, но не то: в why
написано, размер разошёлся или хеш. |
pending | Объект принят, вариант ещё делается — видео в очереди. Дослать нечего, надо подождать. |
unverified | Есть, но сверить с вашим хешем нечем. Такое бывает у файлов, принятых до 4 сентября 2026 года. |
Отпечаток пачки
В ответе есть digest — sha256 по строкам
kind|key|variant|bytes|sha256, отсортированным как строки и
склеенным через \n. Посчитайте его у себя тем же способом: если
совпал, пачка сошлась целиком, и разбирать тысячу ответов не нужно. Порядок
записей в запросе на отпечаток не влияет.
Сверка не считается операцией и не тратит пакет. Проверять, что ваши файлы на месте, можно не оглядываясь на счёт.
С чем именно сравнивается
Сервис снимает EXIF и GPS со всего, что принимает. Значит файл, лежащий у
нас, байт в байт не равен присланному, если в нём были метаданные. Сравнение
поэтому идёт с тем, что вы прислали: размер и хеш
запоминаются до чистки. В ответе bytes и sha256 —
присланное, stored_bytes — то, что лежит на диске.
Оговорка для старых файлов: для принятых до 4 сентября 2026 года, 02:54,
хеш присланного не записывался и вычислен из хранимой копии задним числом.
Если такой файл нёс метаданные, наш хеш — хеш очищенной копии, и сверка
покажет mismatch законно. Проверяется просто: пришлите файл
заново, приём идемпотентен.
Отказы
Отказ приходит с причиной словами и машинным кодом. Причина не пересказывает исключение библиотеки: машинную строку читает оператор в журнале, а не вы в ответе.
{ "ok": false, "error": { "code": "sign_stale",
"message": "The signature is older than 300 s (clocks differ by 4000 s). Check the clock on your machine." } }
| Код | HTTP | Когда |
|---|---|---|
| Подпись | ||
sign_missing | 401 | заголовков подписи нет |
sign_bad_ts | 401 | X-Media-Ts не число секунд unix |
sign_stale | 401 | подпись старше 300 секунд — проверьте часы |
key_unknown | 401 | ключ не найден или отозван |
sign_bad | 403 | подпись не сходится; чаще всего забыли подписать строку запроса вместе с путём |
sign_replay | 409 | эта подпись уже использована — пересчитайте со свежим X-Media-Ts |
| Что прислали | ||
body_empty | 422 | тело пустое: сервис ждёт сам файл |
body_truncated | 400 | тело дошло не целиком — обрыв передачи, повторите |
too_large | 413 | файл больше вашего потолка |
svg_forbidden | 415 | прислан SVG — не принимается и не отдаётся никогда |
type_rejected | 415 | тип не принимается; в ответе сказано, что именно видно в байтах |
file_truncated | 422 | файл обрезан: длина не сходится с заголовком. Это обрыв, а не негодная картинка |
file_broken | 422 | заголовок похож на картинку, а разобраться в ней нельзя |
unreadable | 422 | файл не разбирается как картинка |
infected | 422 | файл отвергнут антивирусом |
| Что просили | ||
no_route | 404 | Такой точки нет. Метод и путь названы в тексте отказа — опечатка видна сразу. |
key_too_long | 422 | Ключ длиннее 190 байт. Ключ — ваш идентификатор, а не имя файла. |
variant_empty | 422 | Не назван ни один вариант, и у вида нет варианта по умолчанию. |
too_many_keys | 422 |
В resolve-batch больше 1000 ключей. |
args_missing | 422 | не хватает kind, key или variant |
kind_unknown | 422 | такой вид вам не разрешён; в ответе перечислены разрешённые |
variant_unknown | 422 | такого варианта у сервиса нет — набор именованный и конечный |
variant_not_allowed | 422 | вариант есть, но не заведён для этого вида |
not_found | 404 | такого объекта у сервиса нет |
asset_in_stock | 409 | объект по этому owner_ref связан со стоком; erase ничего не удаляет, даже если материал снят с витрины |
asset_busy | 503 | другая операция меняет этот файл; байты не удалены. Повторите запрос чуть позже с новой подписью |
| Потолки и деньги | ||
plan_required | 402 | на вашем тарифе это не включено |
wallet_empty | 402 | кошелёк в минусе: приём закрыт, отдача работает |
rate_limited | 429 | слишком много загрузок в минуту |
quota_exceeded | 507 | ваш потолок места исчерпан |
| Сверка | ||
body_not_json | 422 | Тело запроса сверки — не объект JSON. |
items_missing | 422 |
Нет списка items или он пуст. Сверять нечего. |
item_not_object | 422 | Запись в списке — не объект. Номер записи назван в тексте отказа. |
item_incomplete | 422 |
У записи нет kind или key. kind можно
задать один раз наверху запроса, тогда в записях он не нужен. |
batch_too_large | 413 | В пачке больше 1000 записей. Нарежьте мельче: на 691 314 ключей это 692 запроса, а не 691 314. |
| Наши беды, не ваши | ||
scanner_error | 503 | Антивирус не смог проверить файл. Приём отказан: пропускать непроверенное нельзя. Это наша беда — повторите позже. |
tmp_read | 500 | Принятый файл не читается на нашей стороне. Тоже наша беда. |
tenant_suspended | 403 | Арендатор приостановлен. Ключи при этом действуют — дело не в подписи. |
scanner_down | 503 | антивирус недоступен — приём закрыт, файл не потерян |
disk_full | 507 | на сервисе кончается место; повторите позже |
internal | 500 | сбой у нас; он записан в журнал |
Отказ всегда объясняет причину словами и никогда не пересказывает исключение библиотеки: машинную строку читает наш оператор в журнале, а не вы в ответе.
Админ-API
Отдельная точка входа /api/admin/v1/* со своим конфигом, своим
журналом и своей подписью. Ею пользуется только общая панель управления. Ключи
подписи арендаторов в панель не отдаются: это право писать в
чужое хранилище.