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