Договор API — keepix.cloud Перейти к содержимому

Договор 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 Без подписи. Говорит, что фронт жив: имя службы, узел, версия, свободное место и время. Ни слова о базе и ни слова об арендаторах — по этому же ответу опрашивается здоровье узлов.

Сверка: убедиться, что долетело всё

После большого переноса главный вопрос один: всё ли доехало. Отвечать на него по одному файлу — значит сделать столько запросов, сколько файлов. Поэтому сверка идёт пачками и ничего не меняет: это холостой ход.

Как это делается

  1. Нарезаете свой список на пачки по 1000 и на каждую шлёте POST /api/v1/reconcile со своими размерами и хешами.
  2. Смотрите на summary: если ok равно числу записей — пачка сошлась, читать остальное незачем.
  3. Не сошлось — в 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_missing401заголовков подписи нет
sign_bad_ts401X-Media-Ts не число секунд unix
sign_stale401подпись старше 300 секунд — проверьте часы
key_unknown401ключ не найден или отозван
sign_bad403подпись не сходится; чаще всего забыли подписать строку запроса вместе с путём
sign_replay409эта подпись уже использована — пересчитайте со свежим X-Media-Ts
Что прислали
body_empty422тело пустое: сервис ждёт сам файл
body_truncated400тело дошло не целиком — обрыв передачи, повторите
too_large413файл больше вашего потолка
svg_forbidden415прислан SVG — не принимается и не отдаётся никогда
type_rejected415тип не принимается; в ответе сказано, что именно видно в байтах
file_truncated422файл обрезан: длина не сходится с заголовком. Это обрыв, а не негодная картинка
file_broken422заголовок похож на картинку, а разобраться в ней нельзя
unreadable422файл не разбирается как картинка
infected422файл отвергнут антивирусом
Что просили
no_route404 Такой точки нет. Метод и путь названы в тексте отказа — опечатка видна сразу.
key_too_long422 Ключ длиннее 190 байт. Ключ — ваш идентификатор, а не имя файла.
variant_empty422 Не назван ни один вариант, и у вида нет варианта по умолчанию.
too_many_keys422 В resolve-batch больше 1000 ключей.
args_missing422не хватает kind, key или variant
kind_unknown422такой вид вам не разрешён; в ответе перечислены разрешённые
variant_unknown422такого варианта у сервиса нет — набор именованный и конечный
variant_not_allowed422вариант есть, но не заведён для этого вида
not_found404такого объекта у сервиса нет
asset_in_stock409объект по этому owner_ref связан со стоком; erase ничего не удаляет, даже если материал снят с витрины
asset_busy503другая операция меняет этот файл; байты не удалены. Повторите запрос чуть позже с новой подписью
Потолки и деньги
plan_required402на вашем тарифе это не включено
wallet_empty402кошелёк в минусе: приём закрыт, отдача работает
rate_limited429слишком много загрузок в минуту
quota_exceeded507ваш потолок места исчерпан
Сверка
body_not_json422 Тело запроса сверки — не объект JSON.
items_missing422 Нет списка items или он пуст. Сверять нечего.
item_not_object422 Запись в списке — не объект. Номер записи назван в тексте отказа.
item_incomplete422 У записи нет kind или key. kind можно задать один раз наверху запроса, тогда в записях он не нужен.
batch_too_large413 В пачке больше 1000 записей. Нарежьте мельче: на 691 314 ключей это 692 запроса, а не 691 314.
Наши беды, не ваши
scanner_error503 Антивирус не смог проверить файл. Приём отказан: пропускать непроверенное нельзя. Это наша беда — повторите позже.
tmp_read500 Принятый файл не читается на нашей стороне. Тоже наша беда.
tenant_suspended403 Арендатор приостановлен. Ключи при этом действуют — дело не в подписи.
scanner_down503антивирус недоступен — приём закрыт, файл не потерян
disk_full507на сервисе кончается место; повторите позже
internal500сбой у нас; он записан в журнал

Отказ всегда объясняет причину словами и никогда не пересказывает исключение библиотеки: машинную строку читает наш оператор в журнале, а не вы в ответе.

Админ-API

Отдельная точка входа /api/admin/v1/* со своим конфигом, своим журналом и своей подписью. Ею пользуется только общая панель управления. Ключи подписи арендаторов в панель не отдаются: это право писать в чужое хранилище.