Договір 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/* зі своїм конфігом, своїм
журналом і своїм підписом. Нею користується лише спільна панель керування. Ключі
підпису орендарів до панелі не віддаються: це право писати в
чуже сховище.