CDN API — справочник
Общие требования
Base URL: https://cdnapi.denchikp.com
CDN URL файлов: https://cdn.denchikp.com/<domain>/<folder>/<filename>
Версия документации: соответствует CDN API 6.18.0 (актуальная версия сервиса — GET /version)
Аутентификация
Большинство CDN‑эндпоинтов требуют:
- заголовок
X-Api-Key: <API_KEY>
или
- активную cookie‑сессию, где ключ определяется автоматически: вход по ключу (
/auth/cdn/login) либо OAuth‑вход (Google/Discord/GitHub/Telegram) с привязанным ключом.
Если переданы оба варианта, используется заголовок X-Api-Key. Для запросов с cookie‑сессией из браузера нужно отправлять cookies (credentials: "include").
Ключ может быть ограничен списком разрешённых IP‑адресов и доменов. Ключи с is_admin=true имеют доступ ко всем доменам и к административным эндпоинтам. Эндпоинты /bot/* — внутренние и требуют заголовок X-Bot-Token (см. раздел «Bot»).
Во многих операциях, где передаётся url, принимаются только ссылки вида:
https://cdn.denchikp.com/<domain>/<folder>/<filename>
Формат ошибок
Ошибки возвращаются в виде JSON:
{"detail":"Текст ошибки"}
| Код | Когда возникает |
|---|---|
| 400 | некорректные параметры, URL не в CDN‑формате, недопустимое имя файла или папки |
| 401 | нет ключа или авторизации («Отсутствует ключ API»), ключ недействителен, аккаунт бота не привязан к ключу |
| 403 | ключ отключён, IP не разрешён, нет доступа к домену, превышена квота, нужен admin‑ключ, неверный X-Bot-Token |
| 404 | файл, сессия или объект не найден |
| 409 | имя файла уже занято (загрузка без overwrite, перенос и копирование через бота) |
| 413 | файл больше 500 MB |
| 422 | тело запроса не прошло валидацию (отсутствует обязательное поле) |
| 500 | внутренняя ошибка или неверная конфигурация сервера |
Папки и домены
Папка выбирается автоматически по расширению и MIME‑типу файла:
| Папка | Расширения |
|---|---|
| img | jpg, jpeg, jfif, pjpeg, png, apng, webp, avif, svg, gif, ico, cur, bmp, tif, tiff, heic, heif, jxl |
| video | mp4, m4v, webm, mov, mkv, avi, ogv, wmv, flv, 3gp, mpg, mpeg, ts, m2ts |
| audio | mp3, wav, ogg, oga, opus, flac, m4a, aac, weba, wma, aif, aiff, mid, midi |
| docs | pdf, doc, docx, xls, xlsx, ppt, pptx, odt, ods, odp, rtf, txt, csv, md, epub |
| css | css |
| js | js, mjs, cjs |
| fonts | woff, woff2, ttf, otf, eot |
| files | всё остальное (архивы, программы и т. д.) |
Папка выбирается по расширению файла, регистр не важен (с версии 6.16.2; раньше дополнительно требовалось совпадение MIME-типа). MIME учитывается, только если у файла нет расширения. Уже загруженные файлы остаются на своих местах. Папки video, audio и docs появились в 6.16.2: до этого такие файлы попадали в files.
Пустой domain означает public. Домен не должен содержать .., / и \. Файлы в домене public автоматически удаляются 1‑го числа каждого месяца в 03:00 (UTC+0).
Мониторинг
Эндпоинты без авторизации.
GET/healthz
Проверка «жив ли» сервис.
Response 200:
{"status":"ok"}
GET/readyz
Проверка готовности: база данных и каталог хранилища.
Response 200:
{
"status":"ready",
"mysql":"ok",
"storage":{"free_bytes":812345678901,"total_bytes":2000000000000}
}
Ошибки:
- 500 — каталог хранилища недоступен или нет соединения с базой
GET/version
Версия сервиса и текущее время сервера.
Response 200:
{"service":"DP-CDN-API","version":"6.18.0","time":"2026-10-02T15:10:28.123456"}
GET/status
Общий статус CDN и агрегированная статистика.
Response 200 (пример):
{
"status":"online",
"service":"DenchikP CDN",
"max_file_size":"500.0 MB",
"supported_folders":["img","css","js","fonts","video","audio","docs"],
"api_version":"6.18.0",
"stats":{"total_files":900,"total_size_mb":117.74,"domains_count":10}
}
Ключ / домены / квоты
GET/api-key/me
Информация о текущем API‑ключе (по X-Api-Key или по сессии).
Headers:
X-Api-Key: <API_KEY>(рекомендуется)
Response 200:
{
"status":"ok",
"key":{
"name":"ClientName",
"is_admin":false,
"domains":["public"],
"owned_domains":["example.com"],
"ips":["1.2.3.4"],
"kind":"personal",
"kind_label":"Личный",
"kind_description":"Для одного человека: кабинет, боты, ShareX и личные проекты.",
"max_domains":10,
"bindable":true
}
}
Поле kind — тип ключа (personal, work или service, см. «Типы ключей и свои домены»), owned_domains — подтверждённые за ключом домены, max_domains — сколько своих доменов можно подтвердить, bindable — можно ли привязывать ключ к аккаунтам.
Ошибки:
- 401 — нет ключа и нет валидной авторизации
GET/domains
Список доменов, доступных для ключа.
Response 200 (примеры):
- admin‑ключ:
{"status":"ok","mode":"any","domains":[]}
- allowlist‑ключ:
{"status":"ok","mode":"allowlist","domains":["public","example.com"]}
- ключ без доменов (разрешён как минимум public):
{"status":"ok","mode":"any","domains":["public"]}
В ответе также есть поле owned — домены, подтверждённые за ключом. Они всегда входят в domains (у админа domains остаётся пустым: ограничений нет).
GET/usage
Текущее использование (без лимитов квоты).
Response 200 (пример):
{
"status":"ok",
"usage":{
"total_files":120,
"total_bytes":123456789,
"month":"2026-06",
"monthly_uploaded_files":10,
"monthly_uploaded_bytes":12300000
}
}
GET/quota
Квоты + использование + остатки (remaining). Поле billing показывает источник лимитов: legacy (ручная квота ключа) или billing (активные подписки, см. «Тарифы»), а billing.store_url — ссылку на магазин приложения в Discord (с версии 6.16.4). Значение null в лимите означает «без лимита».
Response 200 (пример):
{
"status":"ok",
"api_key_name":"ClientName",
"is_admin":false,
"billing":{"source":"legacy","plan":null,"entitlements":[],"store_url":"https://discord.com/application-directory/1488322420703100949/store"},
"quota":{
"max_file_bytes":524288000,
"max_total_bytes":10737418240,
"max_total_files":50000,
"max_monthly_upload_bytes":5368709120,
"max_monthly_upload_files":null
},
"usage":{
"total_files":120,
"total_bytes":123456789,
"month":"2026-06",
"monthly_uploaded_files":10,
"monthly_uploaded_bytes":12300000
},
"remaining":{
"total_bytes":10000000000,
"total_files":49880,
"monthly_upload_bytes":5300000000,
"monthly_upload_files":null
}
}
Ошибки:
- 403 — если ключ невалиден/запрещён по IP и т.п.
Типы ключей и свои домены
Типы ключей
У каждого API ключа есть тип. Он виден в кабинете, админ-панели, в GET /api-key/me, /auth/me и в ботах (/me, /cdn, /bind).
| kind | Название | Для чего | Своих доменов | Привязка к аккаунтам |
|---|---|---|---|---|
personal | Личный | один человек: кабинет, боты, ShareX, личные проекты | до 10 | да |
work | Рабочий | команда или проект: общие файлы и больше собственных доменов | до 25 | да |
service | Сервисный | автоматизация и интеграции: CI, серверы, скрипты | до 10 | нет |
- Тип задаёт администратор:
kindвPOST /admin/api-keys(по умолчаниюpersonal) иPATCH /admin/api-keys/{id}. Существующие ключи после обновления получили типpersonal. - Сервисный ключ нельзя привязать к Google, Discord, GitHub и Telegram, и войти по нему в кабинет нельзя (403): он только для API. Если ключ стал сервисным после привязки, привязки остаются, а PATCH возвращает
warning. - Лимит считается по подтверждённым доменам. У админ-ключей лимита нет.
Владение доменом
Домен — это папка на CDN (https://cdn.denchikp.com/<домен>/…). После подтверждения владения домен закрепляется за ключом:
- загружать, менять и удалять файлы в нём может только ключ-владелец и администраторы;
- остальным ключам доступ закрыт, даже если у них нет ограничений по доменам; исключение: домен явно добавлен ключу администратором (
POST /admin/api-keys/{id}/domains); - владелец получает доступ к домену, даже если у него задан список разрешённых доменов;
- токены доступа наследуют правило; токен с ограничением по доменам расширить доступ не может;
- чтение файлов остаётся публичным: подтверждение управляет только записью.
Подтверждается основной домен без поддоменов: https://www.Example.com/page превращается в example.com, Unicode-домены переводятся в punycode. Нельзя занять публичные суффиксы (co.uk), public и домены denchikp.*. Домен, который уже используют другие ключи (в их списках разрешённых или как владельцы файлов), закрепляет только администратор: POST /admin/domains/assign.
Заявка действует 7 дней, затем удаляется; неподтверждённых заявок не больше 10 на ключ. Управлять доменами можно основным ключом или сессией; токены доступа получают 403. Автоматической перепроверки владения нет: при смене владельца домена освободите его через DELETE /domains/claims/{id} или обратитесь к администратору. Ключи и домены, созданные до 6.16.0, работают как раньше; ограничение действует только для доменов, подтверждённых после обновления.
Способы подтверждения:
| method | Что сделать | Как проверяется |
|---|---|---|
dns_txt (рекомендуется) | TXT-запись _dpcdn-challenge.<домен> со значением dpcdn-verification=<token> | публичные DNS-резолверы Cloudflare и Google (DNS over HTTPS) |
dns_cname | CNAME-запись _dpcdn-<token>.<домен> на verify.cdn.denchikp.com | те же резолверы; для провайдеров без TXT |
file | файл https://<домен>/.well-known/dpcdn/<token>.txt, содержащий только <token> | запрос к вашему сайту (также www. и http://); содержимое должно совпасть целиком; редиректы только на тот же домен или www.; адреса во внутренних сетях запрещены |
meta | тег <meta name="dpcdn-verify" content="<token>"> в HTML главной страницы | запрос к https://<домен>/ (читаются первые 256 КБ), тег должен быть в HTML, который отдаёт сервер |
cloudflare | автоматическое добавление TXT-записи через API Cloudflare (см. ниже) | после создания записи выполняется проверка dns_txt |
Если сайт закрыт защитой от ботов или фильтром по IP, способы file и meta могут не сработать: проверки идут с серверов CDN. Тогда используйте DNS.
GET/domains/claims
Домены текущего ключа: подтверждённые и ожидающие подтверждения.
Response 200 (пример):
{
"status":"ok",
"limit":3,
"used":1,
"kind":{"kind":"personal","label":"Личный","max_domains":10,"bindable":true},
"claims":[
{
"id":21,
"domain":"example.net",
"status":"pending",
"method":"dns_txt",
"token":"3f9c1a7be25d4086a1c3e9f07b52d8e4",
"created_at":"2026-10-03T12:00:00",
"expires_at":"2026-10-10T12:00:00",
"last_check_at":"2026-10-03T12:10:00",
"check_count":2,
"last_error":"TXT-запись _dpcdn-challenge.example.net не найдена. DNS может обновляться до часа, повторите позже",
"instructions":{
"dns_txt":{"record":{"type":"TXT","host":"_dpcdn-challenge","name":"_dpcdn-challenge.example.net","value":"dpcdn-verification=3f9c1a7b…"},"steps":["…"]},
"dns_cname":{"record":{"type":"CNAME","host":"_dpcdn-3f9c1a7b…","value":"verify.cdn.denchikp.com"},"steps":["…"]},
"file":{"url":"https://example.net/.well-known/dpcdn/3f9c1a7b….txt","content":"3f9c1a7b…","steps":["…"]},
"meta":{"tag":"<meta name=\"dpcdn-verify\" content=\"3f9c1a7b…\">","url":"https://example.net/","steps":["…"]},
"cloudflare":{"steps":["…"]}
}
},
{"id":20,"domain":"example.com","status":"verified","method":"dns_txt","verified_via":"dns_txt","verified_at":"2026-09-21T12:00:00"}
]
}
Поля token и instructions есть только у заявок со статусом pending. Для админ-ключа limit равен null.
POST/domains/claims
Добавить домен и получить инструкции. Повторный вызов с тем же доменом возвращает ту же заявку.
Body (JSON):
{"domain":"example.net","method":"dns_txt"}
method необязателен (по умолчанию dns_txt): это способ, который будет проверяться по умолчанию; инструкции приходят сразу для всех способов.
Response 200: {"status":"ok","claim":{…}} (объект заявки, как в списке).
Ошибки:
- 400 — неверный домен или способ, зарезервированный домен, публичный суффикс
- 403 — достигнут лимит доменов для типа ключа
- 409 — домен уже подтверждён вами или другим владельцем, либо используется другими ключами (обратитесь к администратору)
- 429 — слишком много неподтверждённых заявок
POST/domains/claims/{claim_id}/check
Проверить подтверждение. Между проверками одной заявки должно пройти не менее 5 секунд, не более 20 проверок в минуту на ключ.
Body (JSON, необязательно): {"method":"file"} — проверить конкретным способом (dns_txt, dns_cname, file, meta); выбранный способ запоминается в заявке.
Response 200:
{"status":"ok","verified":false,"error":"TXT-запись _dpcdn-challenge.example.net не найдена. DNS может обновляться до часа, повторите позже","claim":{…}}
При успехе "verified":true, заявка получает статус verified, остальные заявки на этот домен удаляются, отправляется webhook DOMAIN_VERIFIED.
Ошибки: 400 — неизвестный способ, 404 — заявка не найдена или чужая, 429 — слишком частые проверки, 502 — публичные DNS-серверы не ответили.
POST/domains/claims/{claim_id}/cloudflare
Автоматическое подтверждение для доменов на Cloudflare: сервис находит зону, создаёт TXT-запись _dpcdn-challenge и сразу проверяет домен.
Body (JSON):
{"api_token":"<CLOUDFLARE_API_TOKEN>","zone_id":"необязательно, 32 hex-символа"}
Токену нужны права Zone → Zone → Read и Zone → DNS → Edit (подойдёт шаблон «Edit zone DNS», ограниченный одной зоной). Токен используется только для этого запроса: он не сохраняется в базе, не пишется в логи и не возвращается в ответе. После подтверждения его можно удалить в Cloudflare.
Response 200: как у /check. Если запись создана, но DNS ещё не обновился, verified равен false и есть message: повторите проверку через минуту.
Ошибки: 400 — неверный формат токена или Zone ID, либо Cloudflare отклонил токен или запрос; 404 — зона не найдена в аккаунте; 409 — домен уже подтверждён; 502 — Cloudflare недоступен.
DELETE/domains/claims/{claim_id}
Удалить заявку или освободить подтверждённый домен. Файлы в папке остаются, но домен перестаёт быть закреплён за ключом.
Response 200: {"status":"ok","domain":"example.net"}
Webhook-события: DOMAIN_VERIFIED (domain, method) и DOMAIN_REMOVED (domain). Все три события (DOMAIN_VERIFIED, DOMAIN_REMOVED, DOMAIN_ASSIGNED) пишутся в журнал CDN.
Боты: Discord — /mydomains, /domain_add, /domain_check, /domain_remove; Telegram — /mydomains, /adddomain, /checkdomain, /removedomain. Автоматическое подтверждение через Cloudflare в ботах недоступно: токен нельзя отправлять в чат.
Пример (curl):
curl -X POST https://cdnapi.denchikp.com/domains/claims \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{"domain":"example.net"}'
curl -X POST https://cdnapi.denchikp.com/domains/claims/21/check \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{}'
Если ничего не находится, проверьте запись вручную: nslookup -type=TXT _dpcdn-challenge.example.net 1.1.1.1.
Общие ошибки: 401 — нет ключа или авторизации, 403 — токен доступа или лимит, 404 — заявка не найдена.
Тарифы (Billing)
Лимиты ключа складываются из ручной квоты и тарифов:
- без активных подписок действует ручная квота ключа (по умолчанию: файл до 500 MB, 10 GB хранилища, 50 000 файлов, 5 GB загрузок в месяц);
- активная базовая подписка (
kind=base) задаёт абсолютные лимиты; используется самая свежая; - дополнения (
kind=addon) прибавляют дельты к лимитам (например, +50 GB хранилища).
Подписки выдаются через Discord Premium Apps (см. «Discord Premium Apps webhook») или вручную администратором (/admin/billing/entitlements). Текущие лимиты ключа и его подписки видны в поле billing ответа GET /quota.
GET/billing/plans
Список активных тарифов. Без авторизации.
Response 200 (пример):
{
"status":"ok",
"plans":[
{
"id":1,
"sku":"FREE",
"name":"Free",
"kind":"base",
"price_cents":0,
"currency":"EUR",
"duration_days":null,
"quota":{
"max_file_bytes":524288000,
"max_total_bytes":10737418240,
"max_total_files":50000,
"max_monthly_upload_bytes":5368709120,
"max_monthly_upload_files":null
},
"addons":{
"storage_bytes_delta":null,
"total_files_delta":null,
"monthly_upload_bytes_delta":null,
"monthly_upload_files_delta":null
}
}
]
}
Тарифы по умолчанию:
| SKU | Тип | Хранилище | Файлов | Загрузка в месяц | Цена | Срок |
|---|---|---|---|---|---|---|
| FREE | base | 10 GB | 50 000 | 5 GB | 0 € | — |
| PRO | base | 25 GB | 50 000 | 100 GB, 10 000 файлов | 2.99 € | 30 дней |
| PRO_PLUS | base | 100 GB | 100 000 | 500 GB, 50 000 файлов | 5.99 € | 30 дней |
| LIFETIME_PRO_PLUS | base | 100 GB | 100 000 | 500 GB, 50 000 файлов | 49.99 € | бессрочно |
| STORAGE_50 | addon | +50 GB | — | — | 1.99 € | бессрочно |
| STORAGE_100 | addon | +100 GB | — | — | 2.99 € | бессрочно |
Максимальный размер одного файла во всех тарифах — 500 MB. Значения могут быть изменены администратором; актуальный список всегда отдаёт этот эндпоинт.
Файлы (БД / meta / exists)
GET/files
Список «моих» файлов из БД.
Headers:
X-Api-Key: <API_KEY>
Query parameters (все опциональны):
| Name | Type | Default | Description |
|---|---|---|---|
| q | string | — | поиск по filename/original_filename (LIKE) |
| domain | string | — | фильтр домена (нормализуется) |
| folder | string | — | фильтр папки |
| ext | string | — | фильтр расширения |
| exists_on_disk | bool | true | фильтр по наличию на диске |
| limit | int | 50 | максимум 200 |
| offset | int | 0 | смещение |
Response 200 (пример):
{
"status":"ok",
"total":123,
"limit":50,
"offset":0,
"files":[
{
"id":1,
"domain":"public",
"folder":"img",
"filename":"a.png",
"original_filename":"cat.png",
"url":"https://cdn.denchikp.com/public/img/a.png",
"size":12345,
"created_at":"2026-06-16T10:00:00",
"exists_on_disk":true
}
]
}
POST/file/meta
Метаданные файла: сведения из БД + проверка на диске.
Body (JSON):
{"url":"https://cdn.denchikp.com/public/img/a.png"}
Response 200 (схема):
{
"status":"ok",
"url":"...",
"domain":"public",
"folder":"img",
"filename":"a.png",
"db":{
"id":1,
"owner":"ClientName",
"api_key_id":10,
"size":12345,
"exists_on_disk":true,
"created_at":"..."
},
"disk":{
"exists":true,
"size":12345,
"mime_type":"image/png"
}
}
Ошибки:
- 400 — URL не CDN формата/не парсится
- 403 — нет доступа к домену
- 401 — нет ключа
POST/file/details
Все сведения о файле одним запросом (с версии 6.16.1). Нужен доступ к домену файла на просмотр.
Body (JSON):
{"url":"https://cdn.denchikp.com/example.com/img/hero.png"}
Response 200 (сокращённо):
{
"status":"ok",
"file":{"name":"Баннер.png","stored_name":"hero.png","domain":"example.com","folder":"img","extension":"png",
"size":2457600,"mime_by_extension":"image/png","mime_by_content":"image/png","mime_on_upload":"image/png",
"exists_on_disk":true,"uploaded_at":"2026-10-02T22:40:00","modified_at":"2026-10-02T22:40:00","record_id":1842},
"image":{"width":1920,"height":1080,"format":"PNG","mode":"RGBA","frames":1},
"hash":{"sha256":"9f2c…4d6e"},
"uploader":{"key_name":"Demo project","kind":"personal","kind_label":"Личный",
"key_id":1,"is_admin":false,"enabled":true,"key_created_at":"…",
"accounts":[{"provider":"Google","name":"Иван Петров","login":"[email protected]"}]},
"you_uploaded":true,
"domain":{"name":"example.com","verified_owner":"Demo project","you_own":true},
"visibility":{"private":false},
"expiry":null,
"urls":{"cdn":"https://cdn.denchikp.com/example.com/img/hero.png","api_download":"/download/example.com/img/hero.png"},
"shares":[{"url":"https://cdnapi.denchikp.com/s/Qm7xKp2Lw9","enabled":true,"has_password":false,"expires_at":"…","downloads":37,"max_downloads":null}],
"stats":{"api_downloads":12,"share_downloads":37,"share_links":1},
"access":{"full":true,"is_admin":false}
}
mime_by_contentопределяется по первым байтам файла, а не по расширению.imageесть только у картинок (кроме SVG);hashсчитается для файлов до 1 ГБ и кэшируется.- Поля
key_id,is_admin,enabled,key_created_at,accountsи чужие публичные ссылки отдаются только загрузившему файл, владельцу подтверждённого домена и администратору (access.full = true). Остальным видны имя и тип загрузившего ключа.
Ошибки: 400 — адрес не на cdn.denchikp.com; 403 — нет доступа к домену; 404 — файла нет ни на диске, ни в базе.
POST/file/exists
Проверка существования файла (в БД и на диске).
Body (JSON):
{"url":"https://cdn.denchikp.com/public/img/a.png"}
Response 200:
{"status":"ok","exists_in_db":true,"exists_on_disk":true}
GET/file/redirect
Публичный редирект на CDN URL.
Query:
?url=https://cdn.denchikp.com/...
Ошибки:
- 400 — если url не начинается с
https://cdn.denchikp.com/
Загрузка
POST/upload
Загрузка файла (multipart/form-data).
Headers:
X-Api-Key: <API_KEY>
Content-Type: multipart/form-data
Max size: 500 MB
Form fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| file | file | да | — | загружаемый файл |
| domain | string | нет | "" | "" → public |
| name_mode | string | нет | uuid | uuid | original | custom |
| custom_filename | string | нет | "" | используется при name_mode=custom |
| conflict_action | string | нет | "" | overwrite → перезаписать (с проверками прав) |
| dedupe | bool | нет | false | true → не сохранять файл, если в домене уже есть такой же (см. раздел «Дубликаты»), и вернуть адрес существующего с deduplicated=true |
| ttl | int | нет | — | срок жизни файла в секундах (от 60 до 10 лет); по истечении файл удаляется автоматически, в ответе появляется expires_at. См. POST /files/expire |
Response 200 (пример):
{
"status":"ok",
"url":"https://cdn.denchikp.com/public/img/...",
"size":12345,
"owner":"ClientName",
"ip":"1.2.3.4",
"domain":"public",
"folder":"img",
"filename":"..."
}
Response 409 (конфликт имени, если не overwrite):
{
"status":"conflict",
"message":"FILE_EXISTS",
"requested_filename":"a.png",
"existing_url":"https://cdn.denchikp.com/public/img/a.png",
"domain":"public",
"folder":"img"
}
Ошибки (частые):
- 403 — квота превышена / домен не разрешён / IP не разрешён / overwrite запрещён
- 413 — файл слишком большой
- 500 — внутренняя ошибка
Webhook event: UPLOAD (payload.source="api")
Пример (curl):
curl -X POST "https://cdnapi.denchikp.com/upload" \
-H "X-Api-Key: <API_KEY>" \
-F "domain=public" \
-F "name_mode=uuid" \
-F "file=@./cat.png"
POST/upload/by-url
Сервер скачивает файл по ссылке и сохраняет в CDN.
Body (JSON):
{
"source_url":"https://example.com/file.png",
"domain":"public",
"folder":"img"
}
Ограничения:
- только http/https
- SSRF‑защита: запрещено скачивать с private/loopback/link-local/reserved/multicast адресов
- max 500 MB
Response 200 (пример):
{"status":"ok","url":"https://cdn.denchikp.com/public/img/...","size":12345,"domain":"public","folder":"img"}
Webhook event: UPLOAD (payload.source="by_url")
Multipart Upload
POST/upload/session/start
Создать multipart‑сессию.
Body (JSON):
{
"domain":"public",
"ext":"mp4",
"mime_type":"video/mp4",
"folder":"files"
}
Response 200:
{"status":"ok","upload_id":"","chunk_size":8388608}
PUT/upload/session/{upload_id}/part/{part_no}
Загрузить часть файла (chunk).
Content-Type: multipart/form-data
Form-data:
file— chunk
Response 200:
{"status":"ok","part_no":1,"bytes":8388608}
Ошибки:
- 404 — upload_id не найден
- 403 — upload_id принадлежит другому ключу (для не‑админов)
POST/upload/session/{upload_id}/complete
Склеить части и создать итоговый файл.
Ошибки:
- 400 — нет частей
- 413 — итоговый файл > 500MB
- 403 — квота/права
Webhook event: UPLOAD (payload.source="multipart")
POST/upload/session/{upload_id}/abort
Отменить multipart‑сессию и удалить временные части.
Response 200:
{"status":"ok"}
Операции с файлами
POST/files/bulk-delete
Удалить много файлов за один запрос (до 500 URL).
Body (JSON):
{"urls":["https://cdn.denchikp.com/public/files/a.txt","..."]}
Response 200 (пример):
{
"status":"ok",
"results":[
{"url":"...","status":"ok"},
{"url":"...","status":"error","error":"..."}
]
}
Webhook event: BULK_DELETE
POST/file/move
Перемещение/переименование файла (можно менять домен/папку/имя).
Body (JSON):
{
"url":"https://cdn.denchikp.com/public/img/a.png",
"new_domain":"public",
"new_folder":"files",
"new_filename":"b.png"
}
Правила:
new_folderдолжен быть одним из: img/video/audio/docs/css/js/fonts/filesnew_filenameне должен содержать..,/,\- если файл с целевым путём уже существует, он будет перезаписан (в отличие от
/bot/file/move, который возвращает 409)
Response 200:
{"status":"ok","from":"https://.../a.png","to":"https://.../b.png"}
Webhook event: FILE_MOVE
POST/file/rename
Переименовать файл (alias для /file/move, но меняется только имя).
Body (JSON):
{
"url":"https://cdn.denchikp.com/public/img/a.png",
"new_filename":"b.png"
}
Ошибки:
- 400 —
new_filenameобязателен
POST/file/copy
Копирование файла (квоты учитываются как новая загрузка). Целевой путь должен отличаться от исходного; существующий файл по целевому пути будет перезаписан.
Body (JSON):
{
"url":"https://cdn.denchikp.com/public/img/a.png",
"target_domain":"public",
"target_folder":"img",
"target_filename":"copy.png"
}
Response 200:
{"status":"ok","url":"https://cdn.denchikp.com/public/img/copy.png","size":12345}
Webhook event: FILE_COPY
POST/file/hash
Посчитать хэш файла.
Body (JSON):
{"url":"https://cdn.denchikp.com/public/files/a.bin","algo":"sha256"}
algo: sha256 | sha1 | md5
Response 200:
{"status":"ok","algo":"sha256","hash":"","url":"https://..."}
POST/files/zip
Скачать несколько файлов одним ZIP‑архивом.
Body (JSON):
{
"urls":["https://cdn.denchikp.com/public/img/a.png","https://cdn.denchikp.com/public/files/b.txt"],
"name":"my-files.zip"
}
Ограничения:
- до 200 файлов за запрос, суммарный размер до 1 GB
- требуется доступ к домену каждого файла (как для
/download) nameнеобязателен; по умолчаниюcdn-<domain>.zip
Response 200: application/zip; внутри архива файлы лежат по путям <domain>/<folder>/<filename>.
Ошибки:
- 400 — пустой список, больше 200 файлов или недопустимый URL
- 403 — нет доступа к домену
- 404 — один из файлов не найден
- 413 — суммарный размер больше 1 GB
Каждый файл записывается в статистику скачиваний.
Browse / Folders / Download
POST/browse
Просмотр файлов на диске в домене.
Body (JSON):
{"domain":"public","folder":"img","filter":"cat"}
Поля:
domain— обязательноfolder— опциональноfilter— подстрока в имени файла (опционально)
Response 200 (схема):
{"status":"ok","domain":"public","total_files":2,"files":[{"name":"a.png","path":"img/a.png","size":12345,"url":"..."}]}
GET/folders/{domain}
Список папок домена + агрегаты.
Response 200 (пример):
{
"status":"ok",
"domain":"public",
"total_files":10,
"total_size":1234567,
"folders":[{"name":"img","file_count":5,"size":1000000,"path":"public/img"}]
}
GET/download/{domain}/{folder}/{filename:path}
Скачивание файла.
Важно:
хотя параметр {filename:path} технически может содержать “/”, сервер отклонит имя, если в нём есть / или \ или ...
Response 200:
- бинарный поток (FileResponse),
application/octet-stream
Ошибки:
- 404 — файл не найден
- 403 — нет доступа к домену
Изображения: размер и формат
GET/file/transform/{domain}/{folder}/{filename}
Отдаёт изображение с изменением размера и/или формата «на лету» (методы GET и HEAD). Без авторизации, как и сами файлы на CDN. Готовые результаты кешируются на сервере.
Пример:
https://cdn.denchikp.com/file/transform/public/img/photo.png?w=400&format=webp
Path params:
domain— домен файлаfolder—imgилиfilesfilename— имя файла: jpg, jpeg, png, webp или gif
Query parameters (все опциональны):
| Name | Type | Default | Description |
|---|---|---|---|
| w | int | — | максимальная ширина, 1–4096 px |
| h | int | — | максимальная высота, 1–4096 px |
| fit | string | scale-down | scale-down (вписать, без увеличения) | contain (то же) | cover (обрезать точно по рамке w×h, нужны оба размера) |
| format | string | auto | auto | webp | avif | jpeg | png |
| q | int | 80 | качество 1–100 (для webp, avif, jpeg) |
'''Формат auto:''' если браузер сообщает поддержку WebP в заголовке Accept, отдаётся WebP, иначе формат исходника (gif отдаётся как png). В ответе при этом есть Vary: Accept.
Особенности:
- исходное изображение никогда не увеличивается
- поворот по EXIF применяется, метаданные в результат не копируются
- прозрачность при конвертации в JPEG заменяется белым фоном
- анимированные GIF возвращаются без изменений
- ответы кешируются:
Cache-Control: public, max-age=604800
Response 200: изображение (image/webp, image/avif, image/jpeg или image/png).
Ошибки:
- 400 — недопустимый путь, размер вне диапазона, неизвестные
fit/format - 404 — файл не найден
- 413 — изображение слишком большое для обработки (больше 100 мегапикселей)
- 415 — исходный файл не jpg/png/webp/gif
- 503 — сервер занят обработкой изображений, повторите запрос
Validate URL
POST/validate
Валидация ссылки (http/https). Делает HEAD, при необходимости GET range 0-0, возвращает HTTP статус и заголовки.
Если URL — CDN URL, дополнительно парсит domain/folder/filename и проверяет наличие на диске.
Body (JSON):
{"url":"https://example.com/file.png"}
Response 200 (схема, укорочено):
{
"status":"ok",
"checked_by":"ClientName",
"url":"https://example.com/file.png",
"is_valid_url":true,
"is_cdn_url":false,
"available":true,
"http_status":200,
"headers":{"content_type":"image/png","content_length":"12345","last_modified":null,"etag":null,"server":"nginx"},
"file_info":{"filename":"file.png","extension":"png","host":"example.com","path":"/file.png"},
"cdn_info":null,
"error":null
}
Delete file
DELETE/delete
Удаление файла по CDN URL.
Body (JSON):
{"url":"https://cdn.denchikp.com/public/files/a.txt"}
Response 200 (пример):
{
"status":"ok",
"message":"Файл успешно удален",
"domain":"public",
"folder":"files",
"filename":"a.txt",
"url":"https://cdn.denchikp.com/public/files/a.txt",
"owner":"ClientName",
"ip":"1.2.3.4"
}
Webhook event: DELETE (payload.source="api")
Токены доступа и лимиты
Токен — дополнительный ключ с ограниченными правами, привязанный к вашему API ключу. Используется в том же заголовке X-Api-Key, начинается с dpt_. Токены удобны для скриптов, CI и сторонних сервисов: основной ключ им не нужен, а токен можно отозвать в любой момент. Сервер хранит только хэш токена, поэтому показать его повторно нельзя.
Права (scopes):
| Право | Что разрешает |
|---|---|
| read | просмотр и скачивание (/browse, /download, /folders, /file/meta|exists|hash|content, /files/zip, /file/sign, POST /dedupe/scan, GET /dedupe/groups, GET /av/status, POST /av/scan (action=trash требует права delete)), /quota, /usage, а также (без ограничения по доменам) /domains, /files, /files/expiring, /trash, /stats/me, /validate, GET /shares |
| upload | /upload, /upload/by-url, /upload/session/* |
| delete | /delete, /files/bulk-delete, /trash (перенос, восстановление, очистка), /files/expire, /dedupe/resolve |
| write | /file/move, /file/rename, /file/copy, /file/save, /files/visibility, POST|PATCH|DELETE /shares |
Что токену недоступно всегда: управление ключами и токенами, webhook’и, админ‑методы, боты, вход через OAuth, /events, экспорт статистики. Токен никогда не имеет прав администратора.
Ограничение по доменам. Если у токена указан список доменов, он работает только с ними. Такой токен не может вызывать методы, которые затрагивают все домены сразу: /domains, /files, /files/expiring, GET /trash, DELETE /trash, /stats/me, /validate (ответ 403). Если у основного ключа есть ограничение по IP, оно действует и для токенов.
Лимит запросов. У каждого токена свой лимит запросов в минуту (по умолчанию 120, максимум 6000), скользящее окно. Ответы содержат заголовки X-RateLimit-Limit и X-RateLimit-Remaining; при превышении сервер возвращает 429 с заголовком Retry-After (секунды). Публичные методы (/version, /healthz, /file/transform/*, /file/signed/*, /s/*) в лимит не входят.
Защита от подбора ключей. После 30 неудачных попыток авторизации с одного IP за 5 минут все запросы с этого адреса получают 429 с Retry-After, пока окно не истечёт.
POST/tokens
Создать токен. Вызывается основным ключом (не токеном), максимум 50 токенов на ключ.
Body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | да | название, 1–100 символов |
| scopes | string[] | да | непустой список из read, upload, delete, write |
| domains | string[] | нет | ограничить токен доменами (должны быть доступны вашему ключу) |
| rpm | int | нет | лимит запросов в минуту, 1–6000 (по умолчанию 120) |
| expires_in_days | int | нет | срок действия, 1–3650 дней (по умолчанию бессрочно) |
Response 200 (пример):
{
"status":"ok",
"token":"dpt_Xk3...",
"warning":"Токен показывается только один раз — сохраните его",
"info":{"id":4,"name":"CI","prefix":"dpt_Xk3abcde","scopes":["upload"],"domains":["public"],"rpm":120,"enabled":true,"expires_at":null,"created_at":"2026-10-03T12:00:00Z","last_used_at":null}
}
Ошибки:
- 400 — неверные
scopes,rpm, срок или превышен лимит токенов - 403 — запрос выполнен токеном, либо нет доступа к указанному домену
GET/tokens
Список токенов ключа (без самих токенов).
Response 200:
{"status":"ok","default_rpm":120,"tokens":[{"id":4,"name":"CI","prefix":"dpt_Xk3abcde","scopes":["upload"],"domains":["public"],"rpm":120,"enabled":true,"expires_at":null,"created_at":"2026-10-03T12:00:00Z","last_used_at":"2026-10-03T12:05:00Z"}]}
PATCH/tokens/{token_id}
Изменить токен: name, enabled (временно отключить), rpm. Изменения применяются в течение 15 секунд.
Body (JSON): {"enabled":false}
Ошибки:
- 404 — токен не найден
DELETE/tokens/{token_id}
Отозвать токен. Он перестаёт работать сразу (ответ 401 «Недействительный токен»).
Response 200: {"status":"ok"}
Ошибки:
- 404 — токен не найден
Коды ошибок токена: 401 — токен недействителен или истёк; 403 — токен отключён, нет нужного права или метод недоступен токенам; 429 — превышен лимит запросов.
Приватные файлы и защита от хотлинка
Приватный файл лежит в папке private своего домена, например https://cdn.denchikp.com/example.com/private/report.pdf. Веб‑сервер должен отдавать на такие адреса 403 (см. настройку ниже), поэтому открыть файл можно только через API с ключом (/download) или по подписанной ссылке с ограниченным сроком действия.
Подписанная ссылка содержит срок действия и подпись HMAC‑SHA256; изменить срок, имя файла или режим скачивания нельзя — подпись перестанет совпадать. Ссылка открывается без ключа и работает и для обычных файлов. Отозвать отдельную ссылку нельзя: она перестаёт работать по истечении срока, при перемещении, переименовании или удалении файла. Секрет подписи берётся из переменной SIGNING_SECRET (если её нет — используется SECRET_KEY), адрес хоста ссылок — из PUBLIC_API_BASE (по умолчанию https://cdnapi.denchikp.com). Смена секрета обесценивает все выданные ссылки.
POST/file/sign
Создать подписанную ссылку. Нужен доступ к домену файла (как для /download); для токена достаточно права read.
Body (JSON):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| url | string | да | — | CDN URL файла (в том числе из папки private) |
| ttl_seconds | int | нет | 3600 | срок действия: от 10 секунд до 30 дней |
| download | bool | нет | false | true → отдавать как вложение (скачивание) |
Response 200 (пример):
{
"status":"ok",
"url":"https://cdnapi.denchikp.com/file/signed/example.com/private/report.pdf?exp=1790000000&sig=Zk3...&dl=1",
"expires_at":"2026-10-03T13:00:00Z",
"private":true
}
Ошибки:
- 400 — недопустимый срок или URL
- 403 — нет доступа к домену
- 404 — файл не найден
- 503 — на сервере не задан секрет подписи
GET/file/signed/{domain}/{folder}/{filename}
Отдаёт файл по подписанной ссылке (методы GET и HEAD). Авторизация не нужна, параметры exp, sig, dl берутся из ссылки как есть. Ответ не кешируется общими кешами (Cache-Control: private). HTML, SVG, XML и JavaScript всегда отдаются как вложение, чтобы исключить выполнение чужих скриптов на домене API.
Ошибки:
- 403 — подпись не совпадает
- 404 — файл не найден
- 410 — срок действия ссылки истёк
POST/files/visibility
Сделать файлы приватными (переносит в private) или снова публичными (возвращает в папку по типу файла). Адрес файла при этом меняется. До 500 файлов за запрос. Нужен доступ к домену на перемещение (для токена — право write).
Body (JSON):
{"urls":["https://cdn.denchikp.com/example.com/img/photo.png"],"visibility":"private"}
Response 200 (пример):
{
"status":"ok",
"results":[{"url":"https://cdn.denchikp.com/example.com/img/photo.png","status":"ok","new_url":"https://cdn.denchikp.com/example.com/private/photo.png","visibility":"private"}],
"private_access_blocked":true
}
private_access_blocked — результат пробного запроса к приватному адресу: true — веб‑сервер его закрывает, false — папка private всё ещё открыта (приватности нет, пока не настроен Apache), null — проверить не удалось. Поле присутствует всегда, но значимо только для visibility=private.
Ошибки по файлам возвращаются в results: файл уже в нужном состоянии, не найден, либо в целевой папке уже есть файл с таким именем. Срок жизни файла (/files/expire) переносится вместе с ним. Webhook event: FILE_MOVE.
Ошибки:
- 400 — неверный
visibility, пустой список или больше 500 URL
Настройка Apache: закрыть /private и включить защиту от хотлинка
Добавьте в виртуальный хост CDN (после RewriteEngine On) и перезапустите Apache. Без первого правила приватные файлы остаются доступными по прямой ссылке.
# 1. Папка private недоступна напрямую
RewriteRule ^/[^/]+/private/ - [F,L]
# 2. Хотлинк: медиа, шрифты и архивы доменов (кроме public)
# открываются только со страниц этого же домена и с *.denchikp.com
RewriteCond %{HTTP_HOST} ^cdn\.denchikp\.com$ [NC]
RewriteCond %{REQUEST_URI} \.(?:jpe?g|jfif|png|apng|gif|webp|avif|svg|ico|bmp|tiff?|heic|heif|jxl|mp4|m4v|webm|mov|mkv|ogv|mp3|ogg|oga|opus|wav|flac|m4a|aac|woff2?|ttf|otf|eot|zip|pdf)$ [NC]
RewriteCond %{REQUEST_URI} !^/public/ [NC]
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://([^/:]+\.)?denchikp\.(com|is)(:[0-9]+)?(/|$) [NC]
RewriteCond %{REQUEST_URI} ^/([^/]+)/
RewriteCond %{HTTP_REFERER}|%1 !^https?://(?:[^/:]+\.)?([^/:|]+)(?::[0-9]+)?(?:/[^|]*)?\|\1$ [NC]
RewriteRule ^ - [F]
Правило хотлинка пропускает запросы без заголовка Referer (прямое открытие, мессенджеры, большинство скриптов), домен public и страницы самого домена, которому принадлежит файл. Для защиты других доменов расширьте список расширений или условия под себя. Заголовок Referer не является настоящей защитой: его легко подделать вне браузера, поэтому для действительно закрытых файлов используйте приватный доступ и подписанные ссылки.
Публичные ссылки (шаринг)
Публичная ссылка открывает страницу со сведениями о файле и кнопкой «Скачать» — без ключа и без знания адреса файла. В отличие от подписанных ссылок, такие ссылки хранятся на сервере: их можно отключить, продлить, защитить паролем и ограничить числом скачиваний. Подходят и для приватных файлов (папка private). Ссылка следует за файлом при его перемещении, переименовании и смене доступа; если файл удалён или находится в корзине, страница сообщает, что файл недоступен.
Вид ссылки: https://cdnapi.denchikp.com/s/<код> (хост задаётся переменной SHARE_BASE_URL, по умолчанию это PUBLIC_API_BASE). Код — 10 случайных символов; на ключ можно создать до 500 ссылок. Страницы ссылок не индексируются (noindex), не кешируются и не содержат скриптов. Каждое скачивание попадает в статистику скачиваний владельца.
Правила:
- лимит скачиваний считается атомарно; повторные запросы с заголовком
Range(докачка) не со смещения 0 не учитываются; - предпросмотр изображения (png, jpg, gif, webp) показывается только у ссылок без пароля и без лимита скачиваний — иначе его можно было бы использовать для обхода ограничений;
- после 10 неверных паролей с одного IP на одну ссылку в течение 10 минут сервер отвечает 429;
- пароль хранится только в виде хэша (scrypt);
- ссылки, истёкшие более 30 дней назад, удаляются автоматически.
GET/s/{code}
Страница ссылки (HTML). Без авторизации. Для ссылки с паролем показывается форма ввода пароля.
Статусы:
- 404 — ссылки нет или она отключена, либо файл недоступен
- 410 — срок действия истёк или исчерпан лимит скачиваний
GET/s/{code}/download, POST /s/{code}/download
Скачать файл (всегда как вложение). Без авторизации.
- Без пароля — достаточно
GET. - С паролем — форма страницы отправляет
POSTс полемpassword; для скриптов можно передать пароль в заголовкеX-Share-Password. Без пароляGETперенаправляет (303) на страницу ссылки.
Статусы: 200, 303 (нужен пароль), 403 (неверный пароль), 404, 410, 429.
GET/s/{code}/raw
Предпросмотр изображения (inline) для ссылок без пароля и без лимита скачиваний; в остальных случаях 404. В счётчик скачиваний не входит.
POST/shares
Создать публичную ссылку. Нужен доступ к домену файла (как для /download). Для токена нужно право write.
Body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | да | CDN URL файла |
| ttl_seconds | int | нет | срок действия ссылки: от 60 секунд до 10 лет (по умолчанию бессрочно) |
| password | string | нет | пароль, 4–128 символов |
| max_downloads | int | нет | лимит скачиваний, 1–1000000 |
| name | string | нет | название на странице (до 200 символов), по умолчанию имя файла |
Response 200 (пример):
{
"status":"ok",
"share":{
"id":12,"code":"aB3dE5gH7k","url":"https://cdnapi.denchikp.com/s/aB3dE5gH7k","name":null,
"domain":"public","folder":"files","filename":"report.pdf","file_url":"https://cdn.denchikp.com/public/files/report.pdf",
"has_password":true,"expires_at":"2026-10-10T12:00:00Z","max_downloads":5,"downloads":0,"enabled":true,
"created_at":"2026-10-03T12:00:00Z","last_access_at":null
}
}
Ошибки:
- 400 — неверный пароль, лимит, срок или превышено 500 ссылок
- 403 — нет доступа к домену
- 404 — файл не найден
GET/shares
Список ссылок ключа (новые первыми).
Query parameters:
domain— только один доменlimit— по умолчанию 200, максимум 500
Response 200: {"status":"ok","shares":[ ... ]} (объекты как в POST /shares). Токен с ограничением по доменам этот метод вызвать не может.
PATCH/shares/{share_id}
Изменить ссылку.
Body (JSON), все поля необязательны:
| Field | Type | Description |
|---|---|---|
| enabled | bool | включить или временно отключить |
| ttl_seconds | int | новый срок действия (отсчёт от текущего момента) |
| clear_expiry | bool | сделать бессрочной |
| max_downloads | int | новый лимит (счётчик скачиваний не сбрасывается) |
| clear_max_downloads | bool | снять лимит |
| password | string | установить или сменить пароль |
| clear_password | bool | убрать пароль |
| name | string | название на странице |
Ошибки:
- 400 — недопустимые значения
- 404 — ссылка не найдена (или принадлежит другому ключу)
DELETE/shares/{share_id}
Отозвать ссылку. Она перестаёт работать сразу.
Response 200: {"status":"ok"}
Ошибки:
- 404 — ссылка не найдена
Необязательно: короткий адрес на CDN. Чтобы ссылки выглядели как https://cdn.denchikp.com/s/<код>, добавьте в виртуальный хост CDN проксирование и задайте SHARE_BASE_URL=https://cdn.denchikp.com в .env:
ProxyPass "/s/" "http://127.0.0.1:8001/s/"
ProxyPassReverse "/s/" "http://127.0.0.1:8001/s/"
Дубликаты
Поиск файлов с одинаковым содержимым (по SHA‑256) и безопасная очистка. Работает с файлами, загруженными вашим ключом. Сравниваются только файлы одинакового размера, поэтому хэшируется лишь то, что реально может оказаться копией. Рассчитанные хэши кешируются и пересчитываются автоматически, если файл изменился (по размеру и времени изменения). Дубликаты никогда не удаляются сами: /dedupe/resolve переносит лишние копии в корзину (/trash), откуда их можно восстановить.
Загрузка без дубликатов. Поле dedupe=true в POST /upload включает проверку при загрузке: если в том же домене у вашего ключа уже есть файл с таким же содержимым, новый файл не сохраняется, а в ответ возвращается адрес существующего с "deduplicated": true. Имя (name_mode, custom_filename), ttl и conflict_action в этом случае игнорируются. Без поля dedupe загрузка работает как раньше. Пример ответа:
{"status":"ok","url":"https://cdn.denchikp.com/public/img/1b9d...png","size":5000,"owner":"ClientName","ip":"1.2.3.4","domain":"public","folder":"img","filename":"1b9d...png","deduplicated":true}
POST/dedupe/scan
Посчитать хэши файлов‑кандидатов. Работа ограничена по времени (по умолчанию 15 секунд за вызов): пока в ответе pending больше нуля, вызывайте метод повторно.
Body (JSON):
{"domain":"public","budget_seconds":15}
Оба поля необязательны (budget_seconds — от 1 до 25; без domain проверяются все домены ключа).
Response 200:
{"status":"ok","candidates":12,"hashed_now":8,"pending":0}
Для токена достаточно права read; токен с ограничением по доменам этот метод вызвать не может.
GET/dedupe/groups
Группы файлов с одинаковым содержимым (по хэшам, рассчитанным /dedupe/scan).
Query parameters:
domain— только один доменmin_size— минимальный размер файла в байтах (по умолчанию 1)
Response 200 (пример):
{
"status":"ok",
"total_groups":1,
"duplicate_files":3,
"wasted_bytes":10000,
"pending":0,
"groups":[
{
"sha256":"9f86d0...","size":5000,"count":3,"wasted_bytes":10000,
"files":[
{"url":"https://cdn.denchikp.com/public/files/a.bin","domain":"public","folder":"files","filename":"a.bin","created_at":"2026-09-01T10:00:00Z"}
]
}
]
}
Файлы в группе отсортированы от старых к новым; в ответе не более 200 групп с наибольшей экономией. pending — сколько файлов‑кандидатов ещё не хэшировано или изменилось (вызовите /dedupe/scan).
POST/dedupe/resolve
Оставить один файл и перенести остальные копии в корзину. Сервер сам убеждается, что содержимое удаляемых файлов совпадает с оставляемым (иначе ответ по этому файлу — «Содержимое отличается», он не трогается). Публичные ссылки для шаринга удалённых копий перенаправляются на оставшийся файл. Нужен доступ на удаление к домену каждого файла; для токена — право delete (токен с ограничением по доменам вызвать метод не может).
Body (JSON):
{"keep":"https://cdn.denchikp.com/public/files/a.bin","remove":["https://cdn.denchikp.com/public/files/b.bin","https://cdn.denchikp.com/public/files/c.bin"]}
Response 200 (пример):
{"status":"ok","kept":"https://cdn.denchikp.com/public/files/a.bin","trashed":[{"url":"https://cdn.denchikp.com/public/files/b.bin","id":21,"size":5000}],"failed":[],"freed_bytes":5000,"retention_days":30}
Адреса удалённых копий перестают работать, поэтому перед очисткой убедитесь, что на них нет ссылок на сайтах. Webhook event: DELETE с source="dedupe" и trashed=true.
Ошибки:
- 400 — пустой
remove, больше 200 файлов или недопустимый URL - 404 — файл
keepне найден - 403 — нет доступа к домену
Антивирус (ClamAV)
Загружаемые файлы можно проверять антивирусом ClamAV (демон clamd, протокол INSTREAM по TCP). Проверка выключена, пока не настроена. Включается в .env сервера:
| Переменная | По умолчанию | Описание |
|---|---|---|
| CLAMAV_ENABLED | выкл | 1 / true — включить проверку |
| CLAMAV_HOST, CLAMAV_PORT | 127.0.0.1, 3310 | адрес clamd |
| CLAMAV_MAX_SCAN_BYTES | 26214400 (25 МБ) | файлы больше этого размера не проверяются; значение должно совпадать с StreamMaxLength в clamd.conf |
| CLAMAV_FAIL_MODE | open | что делать, если clamd недоступен: open — принимать файл (ошибка пишется в лог), closed — отвечать 503 |
Проверяются все способы загрузки: POST /upload, /upload/by-url, завершение multipart‑загрузки и загрузки через Discord/Telegram‑ботов. Если антивирус нашёл угрозу, файл не сохраняется, а загрузка завершается ответом 422 с названием сигнатуры; событие записывается в журнал (AV_INFECTED). Файлы больше лимита сканирования и ответы с ошибкой clamd в режиме open пропускаются без проверки.
GET/av/status
Состояние проверки.
Response 200 (пример):
{"status":"ok","enabled":true,"fail_mode":"open","max_scan_bytes":26214400,"reachable":true,"version":"ClamAV 1.3.1/27000/Fri Oct 2 08:25:11 2026"}
reachable и version заполняются, только если проверка включена (null в противном случае). Для токена достаточно права read.
POST/av/scan
Проверить уже загруженный файл. Нужен доступ к домену файла (как для /download).
Body (JSON):
{"url":"https://cdn.denchikp.com/public/files/a.zip","action":"report"}
action: report (по умолчанию) — только сообщить результат; trash — при обнаружении угрозы перенести файл в корзину (нужен доступ на удаление, для токена — право delete).
Response 200 (пример):
{"status":"ok","url":"https://cdn.denchikp.com/public/files/a.zip","result":"infected","signature":"Win.Test.EICAR_HDB-1","trashed_id":42}
result: clean, infected, skipped (файл больше лимита, причина в detail) или error (clamd не ответил или файл недоступен для чтения; причина в detail).
Ошибки:
- 400 — неверный
actionили URL - 403 — нет доступа к домену либо права на удаление
- 404 — файл не найден
- 503 — проверка не настроена на сервере
Метрики (Prometheus)
GET/metrics
Метрики в текстовом формате Prometheus. Доступ: ключ администратора в заголовке X-Api-Key либо, если в .env задан METRICS_TOKEN, заголовок Authorization: Bearer <METRICS_TOKEN> (удобно для настройки скрейпера). Токены доступа (dpt_…) метрики не читают.
Пример конфигурации Prometheus:
scrape_configs:
- job_name: dpcdn
scheme: https
metrics_path: /metrics
authorization:
credentials: ""
static_configs:
- targets: ["cdnapi.denchikp.com"]
Метрики:
| Метрика | Тип | Описание |
|---|---|---|
| dpcdn_info{version} | gauge | версия API |
| dpcdn_uptime_seconds | gauge | время работы процесса |
| dpcdn_http_requests_total{method,route,status} | counter | запросы с момента запуска; route — шаблон маршрута (например /file/transform/{domain}/{folder}/{filename}), неизвестные пути — unmatched |
| dpcdn_http_request_duration_seconds{route} | histogram | длительность запросов |
| dpcdn_auth_failures_total | counter | неудачные попытки авторизации |
| dpcdn_token_rate_limited_total | counter | запросы токенов, отклонённые лимитом (429) |
| dpcdn_av_scanned_total, _infected_total, _errors_total, _skipped_total | counter | антивирусные проверки |
| dpcdn_files_total, dpcdn_files_bytes | gauge | файлы на диске по данным БД |
| dpcdn_trash_items, dpcdn_trash_bytes | gauge | корзина |
| dpcdn_files_expiring | gauge | файлов со сроком жизни |
| dpcdn_share_links_active, dpcdn_tokens_total, dpcdn_api_keys_enabled, dpcdn_webhooks_enabled, dpcdn_file_hashes | gauge | активные публичные ссылки, токены, ключи, webhook’и, закешированные хэши |
| dpcdn_image_cache_bytes | gauge | размер кеша миниатюр |
| dpcdn_clamav_enabled | gauge | 1, если антивирусная проверка включена |
Счётчики запросов хранятся в памяти и обнуляются при перезапуске API (Prometheus корректно обрабатывает такой сброс). Показатели хранилища пересчитываются не чаще раза в 30 секунд.
Ошибки:
- 401 — нет ключа
- 403 — ключ не администратора
OpenAPI
Машиночитаемое описание API в формате OpenAPI 3 (с указанием авторизации X-Api-Key и тегами):
- схема:
https://cdnapi.denchikp.com/openapi.json - Swagger UI:
https://cdnapi.denchikp.com/docs - ReDoc:
https://cdnapi.denchikp.com/redoc
Схему можно импортировать в Postman, Insomnia или генераторы клиентов. В Swagger UI кнопка Authorize принимает основной ключ или токен доступа. Публичные методы (/version, /s/*, /file/signed/*, /file/transform/* и другие) помечены как не требующие ключа. Эта страница остаётся основным справочником: в ней больше примеров и пояснений.
Bot: расширенные операции
Единая точка для новых возможностей CDN из Discord и Telegram ботов: корзина, срок жизни, публичные и подписанные ссылки, приватность, дубликаты, антивирус, ссылки на изображения и статистика. Бот обращается с внутренним токеном (X-Bot-Token) и идентификатором пользователя; операция выполняется от имени API ключа, к которому привязан этот аккаунт, с теми же проверками прав и доменов, что и у обычных запросов.
POST/bot/ops/{op}
Discord‑бот. Заголовок X-Bot-Token — значение DISCORD_BOT_INTERNAL_TOKEN.
POST/bot/telegram/ops/{op}
Telegram‑бот. Заголовок X-Bot-Token — значение TELEGRAM_BOT_INTERNAL_TOKEN.
Body (JSON):
{"discord_id":"123456789012345678","params":{"urls":["https://cdn.denchikp.com/example.com/files/a.pdf"]}}
Для Telegram вместо discord_id передаётся telegram_id. Ответ — тот же JSON, что у соответствующего обычного эндпоинта.
'''Операции (op) и параметры (params):'''
| op | params | Аналог |
|---|---|---|
| trash | urls[] | POST /trash |
| trash_list | domain, limit, offset | GET /trash |
| trash_restore | ids[] | POST /trash/restore |
| trash_purge | ids[] или all=true | DELETE /trash |
| expire | url, ttl_seconds или clear=true | POST /files/expire |
| expiring | domain, limit | GET /files/expiring |
| share_create | url, ttl_seconds, password, max_downloads, name | POST /shares |
| share_list | domain, limit | GET /shares |
| share_revoke | share_id | DELETE /shares/{id} |
| sign | url, ttl_seconds, download | POST /file/sign |
| visibility | urls[], visibility (private или public) | POST /files/visibility |
| dedupe_scan | domain, budget_seconds | POST /dedupe/scan |
| dedupe_groups | domain, min_size | GET /dedupe/groups |
| dedupe_resolve | keep, remove[] | POST /dedupe/resolve |
| dedupe_lookup | domain, size, sha256 | поиск файла с таким же содержимым перед загрузкой: {"found":true,"duplicate":{"url":"…"}} |
| av_status | — | GET /av/status |
| av_scan | url, action (report или trash) | POST /av/scan |
| image_url | url, w, h, fit, format, q | собирает ссылку на изображение нужного размера и формата (проверяет доступ и существование файла) |
| my_stats | period, domain | GET /stats/me |
| domain_list | — | GET /domains/claims |
| domain_add | domain, method (dns_txt, dns_cname, file, meta) | POST /domains/claims (способ cloudflare в ботах запрещён: токен нельзя передавать в чат) |
| domain_check | claim_id, method | POST /domains/claims/{id}/check |
| domain_remove | claim_id | DELETE /domains/claims/{id} |
Ошибки:
- 400 — не указаны или неверны параметры операции
- 401 — аккаунт не привязан к API ключу
- 403 — неверный токен бота либо нет доступа к домену
- 404 — неизвестная операция или файл не найден
Команды ботов, использующие эти операции: /trash, /trashlist, /restore, /emptytrash, /ttl, /expiring, /share, /shares, /unshare, /sign, /private, /public, /dedupe, /avscan, /image, /mystats, а также команды доменов (/mydomains, /domain_add, /domain_check, /domain_remove в Discord; /mydomains, /adddomain, /checkdomain, /removedomain в Telegram); в /upload добавлены срок жизни (ttl) и пропуск дубликатов (dedupe).
Корзина и срок жизни
Мягкое удаление: файл переносится в корзину, ссылка перестаёт работать, но файл можно восстановить. Корзина хранится вне веб‑каталога; срок хранения по умолчанию 30 дней (настраивается переменной TRASH_RETENTION_DAYS), затем файлы удаляются навсегда. Файлы в корзине не учитываются в квоте. DELETE /delete по‑прежнему удаляет файл сразу и безвозвратно.
POST/trash
Переместить файлы в корзину (до 500 за запрос). Нужен доступ к домену каждого файла (как для /delete).
Body (JSON):
{"urls":["https://cdn.denchikp.com/public/img/a.png","https://cdn.denchikp.com/public/files/b.txt"]}
Response 200 (пример):
{
"status":"ok",
"retention_days":30,
"trashed":[{"url":"https://cdn.denchikp.com/public/img/a.png","id":17,"size":12345,"purge_at":"2026-11-01T12:00:00Z"}],
"failed":[{"url":"https://cdn.denchikp.com/public/files/b.txt","error":"Файл не найден"}]
}
Ошибки по отдельным файлам возвращаются в failed, запрос в целом остаётся 200. Webhook event: DELETE с trashed=true. Время во всех полях — UTC.
Ошибки:
- 400 — пустой список или больше 500 URL
GET/trash
Содержимое корзины текущего ключа (админ видит все).
Query parameters:
domain— только один доменlimit— по умолчанию 100, максимум 500offset— смещение
Response 200 (пример):
{
"status":"ok",
"retention_days":30,
"count":1,
"bytes":12345,
"items":[{"id":17,"url":"https://cdn.denchikp.com/public/img/a.png","domain":"public","folder":"img","filename":"a.png","size":12345,"deleted_at":"2026-10-02T12:00:00Z","purge_at":"2026-11-01T12:00:00Z"}]
}
POST/trash/restore
Восстановить файлы из корзины по id (до 500 за запрос). Файл возвращается по прежнему URL. Нужен доступ к домену на загрузку.
Body (JSON):
{"ids":[17,18]}
Response 200 (пример):
{"status":"ok","restored":[{"id":17,"url":"https://cdn.denchikp.com/public/img/a.png","size":12345}],"failed":[{"id":18,"error":"Файл с таким именем уже существует"}]}
Если на месте файла уже лежит другой с тем же именем, он не перезаписывается — элемент остаётся в корзине (failed). Webhook event: RESTORE.
DELETE/trash
Удалить из корзины навсегда.
Body (JSON): {"ids":[17,18]} или {"all":true} для полной очистки.
Response 200:
{"status":"ok","purged":2,"freed_bytes":24690}
Ошибки:
- 400 — не указаны
idsиall
POST/files/expire
Установить или снять срок жизни файла. По истечении срока файл удаляется автоматически (безвозвратно, минуя корзину; проверка раз в минуту). Нужен доступ к домену на удаление.
Body (JSON): один из вариантов
{"url":"https://cdn.denchikp.com/public/files/a.txt","ttl_seconds":86400}
{"url":"https://cdn.denchikp.com/public/files/a.txt","expires_at":"2026-12-31T00:00:00Z"}
{"url":"https://cdn.denchikp.com/public/files/a.txt","clear":true}
Допустимый срок: от 60 секунд до 10 лет. expires_at — ISO‑8601 (без часового пояса считается UTC).
Response 200:
{"status":"ok","url":"https://cdn.denchikp.com/public/files/a.txt","expires_at":"2026-10-03T12:00:00Z"}
Срок привязан к URL: при перезаписи файла по тому же имени он сохраняется. Перенос файла в корзину срок снимает. Webhook event: EXPIRE.
Ошибки:
- 400 — недопустимый срок или URL
- 404 — файл не найден
GET/files/expiring
Файлы со сроком жизни (ближайшие первыми).
Query parameters:
domain— только один доменlimit— по умолчанию 200, максимум 1000
Response 200 (пример):
{"status":"ok","items":[{"url":"https://cdn.denchikp.com/public/files/a.txt","domain":"public","folder":"files","filename":"a.txt","expires_at":"2026-10-03T12:00:00Z","seconds_left":86400}]}
Text Editor (Read/Save)
Только для “текстовых” файлов и ограничение по размеру: ≤ 2 MB.
POST/file/content
Получить содержимое текстового файла.
Body (JSON):
{"url":"https://cdn.denchikp.com/public/files/app.js"}
Response 200 (пример):
{
"status":"ok",
"url":"https://cdn.denchikp.com/public/files/app.js",
"domain":"public",
"folder":"files",
"filename":"app.js",
"mime_type":"application/javascript",
"size":1200,
"content":"console.log('hi');\n"
}
Ошибки:
- 400 — файл не текстовый / >2MB / неподдерживаемая кодировка
- 404 — файл не найден
POST/file/save
Сохранить содержимое текстового файла.
Body (JSON):
{"url":"https://cdn.denchikp.com/public/files/app.js","content":"console.log('hi');\n"}
Response 200:
{"status":"ok","message":"Файл успешно сохранен","url":"...","domain":"public","folder":"files","filename":"app.js","size":1200}
Webhook event: FILE_SAVE
Статистика
GET/stats/public-summary
Публичная краткая статистика CDN. Без авторизации.
Response 200 (пример):
{
"status":"ok",
"stats":{
"uploaded_files":1000,
"current_files":900,
"total_size_bytes":123456789,
"total_size_mb":117.74,
"domains":10,
"clients":3,
"registered_api_clients":5
}
}
GET/stats/me
Статистика использования текущего ключа: загрузки, удаления и скачивания по дням, а также текущее содержимое хранилища.
Query parameters:
| Name | Type | Default | Description |
|---|---|---|---|
| period | string | month | day | week | month | year; другое значение заменяется на month |
| domain | string | — | ограничить статистику одним доменом |
Response 200 (пример):
{
"status":"ok",
"period":"month",
"since":"2026-09-02",
"domain":null,
"totals":{"uploads":61,"uploaded_bytes":480000000,"deletions":4,"downloads":12},
"daily":[
{"date":"2026-09-02","uploads":3,"uploaded_bytes":4500000,"deletions":0,"downloads":1}
],
"storage":{
"files":412,
"bytes":4200000000,
"by_folder":[{"folder":"img","files":300,"bytes":3000000000}],
"by_domain":[{"domain":"public","files":200,"bytes":2000000000}],
"largest":[{"url":"https://cdn.denchikp.com/public/files/big.zip","domain":"public","folder":"files","filename":"big.zip","size":420000000}]
}
}
Важно:
- загрузки, удаления и скачивания считаются по имени ключа (
owner); downloadsучитывает только скачивания через API (/download,/files/zip, боты), а не обращения к файлам по CDN‑ссылкам;- блок
storageсчитается по файлам ключа, которые существуют на диске.
GET/stats/top-files
Топ файлов по скачиваниям или по суммарному размеру загрузок. Только admin‑ключ.
Query parameters:
| Name | Type | Default | Description |
|---|---|---|---|
| period | string | month | day | week | month (30 дней) | year (365 дней); любое другое значение — за всё время |
| by | string | downloads | downloads | size |
| limit | int | 50 | максимум 200 |
Response 200 (by=downloads):
{"status":"ok","by":"downloads","items":[{"domain":"public","filename":"a.png","count":42}]}
Response 200 (by=size):
{"status":"ok","by":"size","items":[{"domain":"public","filename":"a.png","total_size":123456}]}
Ошибки:
- 400 —
byнеdownloads/size - 403 — не admin
GET/stats/export
Экспорт статистики в CSV. Только admin‑ключ.
Query parameters:
| Name | Type | Default | Description |
|---|---|---|---|
| period | string | month | как у /stats/top-files |
| kind | string | uploads | uploads | downloads | deletions | browses |
| format | string | csv | поддерживается только csv |
Response 200: text/csv; charset=utf-8, не более 20 000 последних строк. Имя файла: <kind>_<period>.csv.
Колонки: timestamp, owner, domain, filename, size, folder, ip
Ошибки:
- 400 — неверный
kindилиformat - 403 — не admin
Webhooks (Reference)
Webhooks позволяют получать события от сервиса (загрузка, удаление, перенос и т.д.) на ваш HTTPS‑адрес. Webhook принадлежит ключу и получает события, вызванные этим ключом.
Создание / управление
POST/webhooks
Создать webhook.
Headers:
X-Api-Key: <API_KEY>
Body (JSON):
{
"url":"https://example.com/hook",
"events":["UPLOAD","DELETE"],
"secret":"your-hmac-secret",
"enabled":true
}
Требования:
url— толькоhttps://, безuser:password; адреса private/loopback/link‑local блокируются (защита от SSRF)secret— обязателен, используется для подписи запросов (HMAC)events— список имён событий (см. «События»)
Response 200:
{"status":"ok","id":123}
Ошибки:
- 400 — URL не https / некорректный URL / не указан
secret - 401/403 — нет доступа
- 403 — URL указывает на private/loopback адрес
GET/webhooks
Список webhooks текущего ключа (секрет не возвращается).
Response 200 (пример):
{
"status":"ok",
"webhooks":[
{
"id":123,
"url":"https://example.com/hook",
"events":["UPLOAD","DELETE"],
"enabled":true,
"created_at":"2026-06-16T12:00:00"
}
]
}
DELETE/webhooks/{webhook_id}
Удалить webhook.
Path params:
webhook_id— integer
Response 200:
{"status":"ok"}
Ошибки:
- 404 — webhook не найден (или не принадлежит ключу)
GET/webhooks/{webhook_id}/deliveries
История доставок webhook’а (новые первыми).
Query parameters:
limit— int, по умолчанию 50, максимум 200
Response 200 (пример):
{
"status":"ok",
"deliveries":[
{
"id":501,
"event":"UPLOAD",
"status_code":500,
"success":false,
"error":"[attempt 1/3] HTTP 500: ...",
"created_at":"2026-10-02T15:10:28"
}
]
}
Каждая попытка доставки записывается отдельной строкой; в error указан номер попытки.
Ошибки:
- 404 — webhook не найден (или не принадлежит ключу)
POST/webhooks/{webhook_id}/test
Отправить на webhook тестовое событие PING (с подписью) и сразу получить результат. Запись о тесте попадает в историю доставок.
Response 200:
{"status":"ok","success":true,"status_code":200,"error":null}
При неудачной доставке success равен false, а в status_code/error — причина.
Ошибки:
- 400 — у webhook нет
secret - 403 — адрес webhook указывает на private/loopback адрес
- 404 — webhook не найден (или не принадлежит ключу)
Формат доставки
Сервер отправляет на URL webhook’а запрос:
- Method: POST
- Content-Type: application/json
- Таймаут: 10 секунд; успешной считается доставка с ответом 2xx
- При сетевой ошибке или ответах 408, 425, 429, 500, 502, 503, 504 доставка повторяется до 3 попыток (через 2 и 10 секунд); остальные ответы 4xx не повторяются
- Body:
{"event":"UPLOAD","payload":{...}}
Headers:
| Header | Описание |
|---|---|
| User-Agent | DP-CDN-API Webhooks/1.0 |
| X-DP-Event | имя события |
| X-DP-Timestamp | время отправки, unix‑секунды |
| X-DP-Signature | подпись в формате v1=<hex> |
События
| Событие | Когда | Поля payload |
|---|---|---|
| UPLOAD | файл загружен | url, size, domain, folder, source, name_mode (+ source_url для by_url, upload_id для multipart, discord_id/telegram_id для ботов) |
| UPLOAD_OVERWRITE | файл перезаписан через POST /upload с conflict_action=overwrite | как у UPLOAD |
| DELETE | файл удалён или перемещён в корзину | url, domain, folder, filename, deleted_size, source (+ trashed=true для корзины) |
| RESTORE | файл восстановлен из корзины | url, domain, folder, filename, size, source |
| EXPIRE | файл удалён по сроку жизни | url, domain, folder, filename, deleted_size, source=ttl |
| BULK_DELETE | массовое удаление | results (+ source, discord_id для ботов) |
| FILE_MOVE | файл перемещён или переименован | from, to, from_domain, to_domain, from_folder, to_folder |
| FILE_COPY | файл скопирован | from, to, size, domain, folder |
| FILE_SAVE | изменён текстовый файл | url, domain, folder, filename, new_size |
| SCAN_FINISHED | завершено сканирование диска (admin) | state |
| DOMAIN_VERIFIED | домен подтверждён (проверкой, через Cloudflare или администратором) | domain, method |
| DOMAIN_REMOVED | подтверждённый домен освобождён | domain |
| PING | тестовая отправка через POST /webhooks/{id}/test | message, webhook_id |
Поле source принимает значения api, by_url, multipart, bot (Discord), telegram_bot.
Подпись (HMAC)
Каждый запрос подписан секретом webhook’а. Webhook без секрета не получает события.
Алгоритм:
- base =
{timestamp}.{raw_body}(timestamp изX-DP-Timestamp, тело — байты запроса как есть) - signature =
"v1=" + HMAC_SHA256(secret, base)в hex - получатель сравнивает подпись (в постоянное время) и проверяет свежесть timestamp (рекомендуется ±300 секунд)
Пример проверки (Python):
import time, hmac, hashlib
def verify(headers, secret: str, body: bytes, tolerance=300) -> bool:
ts = int(headers["X-DP-Timestamp"])
if abs(time.time() - ts) > tolerance:
return False
msg = str(ts).encode() + b"." + body
expected = "v1=" + hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, headers["X-DP-Signature"])
Пример проверки (Node.js):
const crypto = require('crypto');
function verify(headers, secret, rawBody, tolerance = 300) {
const ts = Number(headers['x-dp-timestamp']);
if (Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
const expected = 'v1=' + crypto.createHmac('sha256', secret)
.update(`${ts}.`).update(rawBody).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(headers['x-dp-signature']);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Авторизация (сессии)
Эти эндпоинты используются веб‑интерфейсом: после входа сервер устанавливает cookie‑сессию, и запросы к CDN‑эндпоинтам работают без X-Api-Key (если к аккаунту привязан API ключ). Одновременно активна только одна сессия: вход через другой способ завершает предыдущую.
Вход через OAuth
GET/auth/google
Перенаправляет на страницу входа Google. После успешного входа браузер возвращается на https://cdn.denchikp.com с установленной сессией.
GET/auth/discord
Перенаправляет на страницу входа Discord (аналогично Google).
GET/auth/github
Перенаправляет на страницу входа GitHub (аналогично Google).
GET/auth/telegram
Перенаправляет на авторизацию Telegram (OpenID Connect, PKCE). Если пользователь отказал или произошла ошибка, возврат происходит на главную с параметром ?tg_error=<код>.
Служебные эндпоинты: GET /auth/google/callback, GET /auth/discord/callback, GET /auth/github/callback, GET /auth/telegram/callback вызываются самими провайдерами и напрямую не используются.
Сессия и привязка ключа
POST/auth/cdn/login
Вход по API ключу: создаёт cookie‑сессию.
Body (JSON):
{"api_key":"<API_KEY>"}
Response 200 (пример):
{
"status":"ok",
"message":"Вы успешно вошли через DenchikP CDN",
"user":{"name":"ClientName","auth_type":"cdn"},
"key_info":{"name":"ClientName","domains":["public"],"enabled":true,"is_admin":false}
}
Ошибки:
- 401 — недействительный ключ
- 403 — ключ отключён или IP не разрешён
POST/auth/cdn/logout
Завершить сессию входа по ключу.
Response 200:
{"status":"ok","message":"Вы вышли из DenchikP CDN"}
GET/auth/me
Текущая авторизация и привязанный ключ.
Response 200 (пример, вход через Discord):
{
"authenticated":true,
"provider":"discord",
"user":{"id":"1234567890","email":"[email protected]","name":"Display Name","username":"user","avatar":"..."},
"has_bound_key":true,
"key_info":{"name":"ClientName","domains":["public"],"enabled":true,"is_admin":false,"ips":[]}
}
Значения provider: cdn, google, discord, github, telegram. Состав поля user зависит от провайдера.
Response 401:
{"authenticated":false}
POST/auth/bind-api-key
Привязать API ключ к аккаунту, под которым выполнен вход (Google/Discord/GitHub/Telegram).
Body (JSON):
{"api_key":"<API_KEY>"}
Response 200:
{"status":"ok","message":"API ключ успешно привязан к Discord аккаунту"}
Ошибки:
- 400 — ключ не существует
- 401 — нет активной OAuth‑авторизации
POST/auth/unbind-api-key
Отвязать API ключ от текущего аккаунта.
Response 200:
{"status":"ok","message":"API ключ успешно отвязан от Discord аккаунта"}
Ошибки:
- 401 — нет активной OAuth‑авторизации
- 404 — привязка не найдена
GET/auth/logout
Завершить любую активную сессию (общий выход).
Response 200:
{"status":"ok","message":"Вы вышли из аккаунта"}
Telegram‑сессия
GET/auth/telegram/me
Текущий пользователь Telegram из сессии.
Response 200:
{
"status":"ok",
"user":{"id":"123456789","first_name":"Ivan","last_name":"","username":"ivan","photo_url":"https://...","auth_date":1790000000},
"bound_api_key":"AbCdEfGh...xYz9"
}
bound_api_key — маскированный ключ или null, если ключ не привязан.
POST/auth/telegram/bind
Привязать API ключ к Telegram‑аккаунту из сессии.
Body (JSON):
{"api_key":"<API_KEY>"}
Response 200:
{"status":"ok","message":"API ключ привязан"}
POST/auth/telegram/unbind
Отвязать ключ от Telegram‑аккаунта.
Response 200:
{"status":"ok","unbound":true}
POST/auth/telegram/logout
Завершить Telegram‑сессию.
Response 200:
{"status":"ok","message":"Telegram сессия завершена"}
DELETE/auth/telegram/delete-data/{telegram_id}
Удалить данные привязки Telegram‑пользователя (запрос на удаление данных, GDPR). Только admin‑ключ.
Headers:
X-Api-Key: <ADMIN_API_KEY>
Response 200:
{"status":"ok","deleted_rows":1}
Ошибки:
- 403 — не admin
Администрирование
Все эндпоинты этого раздела требуют ключ с is_admin=true (иначе 403 «Только админ»).
API ключи
GET/admin/api-keys
Список всех API ключей.
Query parameters:
show_full— bool, по умолчаниюfalse: приfalseключи маскируются
Response 200 (пример):
{
"status":"ok",
"api_keys":[
{
"id":10,
"name":"ClientName",
"api_key":"AbCdEfGh...xYz9",
"enabled":true,
"is_admin":false,
"kind":"personal",
"kind_label":"Личный",
"owned_domains":["example.net"],
"ips":["1.2.3.4"],
"domains":["public","example.com"],
"created_at":"2026-06-16T10:00:00",
"updated_at":"2026-06-16T10:00:00"
}
]
}
POST/admin/api-keys
Создать API ключ. Секрет генерируется сервером и возвращается один раз.
Body (JSON):
{
"name":"ClientName",
"enabled":true,
"is_admin":false,
"kind":"personal",
"ips":["1.2.3.4"],
"domains":["example.com"]
}
kind — тип ключа: personal (по умолчанию), work или service. Обязательно только name. Для не‑admin ключей создаётся квота по умолчанию.
Response 200:
{"status":"ok","id":10,"api_key":"<NEW_API_KEY>","kind":"personal"}
PATCH/admin/api-keys/{api_key_id}
Изменить имя, флаг enabled, is_admin или тип ключа kind (передавайте только нужные поля).
Body (JSON):
{"name":"NewName","enabled":false,"is_admin":false,"kind":"work"}
Response 200:
{"status":"ok","kind":"work","warning":null}
warning заполняется, если ключ стал сервисным, но уже привязан к аккаунтам: привязки сохраняются, новые создать нельзя.
Ошибки:
- 404 — ключ не найден
POST/admin/api-keys/{api_key_id}/rotate
Перевыпустить ключ: старое значение перестаёт работать, новое возвращается в ответе.
Response 200:
{"status":"ok","api_key":"<NEW_API_KEY>"}
DELETE/admin/api-keys/{api_key_id}
Удалить ключ.
Response 200:
{"status":"ok"}
Ошибки:
- 404 — ключ не найден
POST/admin/api-keys/{api_key_id}/ips
Добавить разрешённый IP.
Body (JSON):
{"value":"1.2.3.4"}
Response 200:
{"status":"ok"}
Если у ключа есть хотя бы один IP в списке, запросы с других адресов отклоняются (403).
DELETE/admin/api-keys/{api_key_id}/ips/{ip}
Удалить разрешённый IP.
Response 200:
{"status":"ok"}
POST/admin/api-keys/{api_key_id}/domains
Добавить разрешённый домен.
Body (JSON):
{"value":"example.com"}
Response 200:
{"status":"ok"}
Если список доменов пуст, ограничений по домену нет. Доступ к домену даёт и его поддомены.
DELETE/admin/api-keys/{api_key_id}/domains/{domain}
Удалить разрешённый домен.
Response 200:
{"status":"ok"}
Домены пользователей
Заявки на подтверждение и закреплённые домены всех ключей (см. «Типы ключей и свои домены»).
GET/admin/domains
Все заявки и подтверждённые домены. Необязательный параметр status — pending или verified. В каждой записи добавлены key_name и key_kind владельца.
POST/admin/domains/assign
Закрепить домен за ключом без проверки. Если домен уже закреплён, владелец меняется. Заявки на этот домен от других ключей удаляются.
Body (JSON):
{"api_key_id":10,"domain":"example.com"}
Ошибки: 400 — неверный домен, 404 — ключ не найден.
POST/admin/domains/{claim_id}/approve
Подтвердить заявку вручную (способ manual). 409 — домен уже закреплён за другим ключом.
DELETE/admin/domains/{claim_id}
Удалить заявку или снять закрепление домена.
Квоты ключа
GET/admin/api-keys/{api_key_id}/quota
Ручная квота ключа (без учёта тарифов).
Response 200 (пример):
{
"status":"ok",
"quota":{
"max_file_bytes":524288000,
"max_total_bytes":10737418240,
"max_total_files":50000,
"max_monthly_upload_bytes":5368709120,
"max_monthly_upload_files":null,
"updated_at":"2026-06-16T10:00:00"
}
}
Если квота не задана, quota равен null. null в поле лимита означает «без лимита».
PUT/admin/api-keys/{api_key_id}/quota
Обновить квоту (меняются только переданные поля; передача null снимает лимит).
Body (JSON):
{
"max_file_bytes":524288000,
"max_total_bytes":10737418240,
"max_total_files":50000,
"max_monthly_upload_bytes":5368709120,
"max_monthly_upload_files":null
}
Response 200:
{"status":"ok"}
Файлы, логи, события
GET/admin/files
Список файлов из базы (все владельцы).
Query parameters (все опциональны):
| Name | Type | Default | Description |
|---|---|---|---|
| domain | string | — | фильтр по домену |
| owner | string | — | фильтр по владельцу (имя ключа) |
| exists_on_disk | bool | — | фильтр по наличию на диске |
| q | string | — | поиск по filename/original_filename (без учёта регистра) |
| limit | int | 200 | от 1 до 500 |
| offset | int | 0 | смещение |
Response 200 (пример):
{
"status":"ok",
"total":123,
"limit":200,
"offset":0,
"files":[
{
"id":1,
"owner":"ClientName",
"domain":"public",
"folder":"img",
"filename":"a.png",
"original_filename":"cat.png",
"file_path":"<путь на сервере>",
"file_url":"https://cdn.denchikp.com/public/img/a.png",
"mime_type":"image/png",
"extension":"png",
"size":12345,
"exists_on_disk":true,
"uploader_ip":"1.2.3.4",
"created_at":"2026-06-16T10:00:00",
"updated_at":"2026-06-16T10:00:00"
}
]
}
GET/admin/logs
Журнал событий CDN.
Query parameters (все опциональны):
| Name | Type | Default | Description |
|---|---|---|---|
| action | string | — | фильтр по действию (например UPLOAD, BROWSE) |
| level | string | — | INFO | WARNING | ERROR |
| domain | string | — | фильтр по домену |
| owner | string | — | фильтр по владельцу |
| limit | int | 100 | максимум 1000 |
Response 200 (пример):
{
"status":"ok",
"total":1,
"logs":[
{
"id":1001,
"timestamp":"2026-06-16T10:00:00",
"level":"INFO",
"action":"UPLOAD",
"owner":"ClientName",
"ip":"1.2.3.4",
"domain":"public",
"filename":"a.png",
"message":"Файл загружен: cat.png → a.png",
"extra_json":"{...}"
}
]
}
GET/events
Поток логов в реальном времени (Server‑Sent Events).
Query parameters:
last_id— int, по умолчанию 0: отдавать записи сidбольше указанного
Response 200: text/event-stream. Каждая запись — событие cdn_log; данные — JSON:
id: 1001
event: cdn_log
data: {"id":1001,"timestamp":"2026-06-16T10:00:00","level":"INFO","action":"UPLOAD","owner":"ClientName","domain":"public","filename":"a.png","message":"...","extra_json":"{...}"}
Новые записи проверяются раз в секунду, до 200 записей за проход.
Сканирование диска
POST/admin/public/purge
Очистить домен public (с версии 6.16.4, только администратор). Удаляет все файлы и каталоги домена, помечает записи файлов удалёнными, снимает сроки жизни и записывает каждое удаление в статистику. Этот метод вызывает ежемесячный скрипт clean_public.ps1.
Query: dry_run=true — только посчитать, ничего не удаляя.
Response 200:
{"status":"ok","dry_run":false,"files":15,"bytes":1073343,"entries":8,"removed":15,"failed":[]}
POST/admin/scan/start
Запустить фоновое сканирование каталога хранилища и синхронизацию с базой.
Response 200:
{
"status":"ok",
"message":"scan запущен",
"state":{"running":true,"last_start":"2026-06-16T10:00:00","last_finish":null,"last_error":null},
"ignore":{"domains":[],"folders":[],"files":[],"file_globs":[],"notes":["..."]}
}
Если сканирование уже идёт, возвращается "message":"scan уже идет". По завершении отправляется webhook SCAN_FINISHED.
GET/admin/scan/status
Статус сканирования.
Response 200:
{
"status":"ok",
"state":{"running":false,"last_start":"2026-06-16T10:00:00","last_finish":"2026-06-16T10:03:21","last_error":null},
"ignore":{"domains":[],"folders":[],"files":[],"file_globs":[],"notes":["..."]}
}
Поле ignore — текущие правила исключения путей из сканирования (домены, папки, файлы и маски); каталоги, имена которых начинаются с . или _, считаются служебными и игнорируются всегда.
GitHub: деплой из репозитория
Деплой файлов из репозитория на CDN через GitHub App (с версии 6.18.0). Методы пользователя работают с ключом или сессией; сервисные ключи получают 403.
GET/github/status
Создано ли приложение, подключённые установки GitHub и настроенные деплои своего ключа.
{"status":"ok","app_configured":true,"app_slug":"denchikp-cdn",
"installations":[{"installation_id":51234567,"account":"demo-user","type":"User","suspended":false,"manage_url":"https://github.com/settings/installations/51234567"}],
"deploys":[{"id":1,"repo":"demo-user/landing","branch":"main","source_dir":"dist","domain":"example.com","enabled":true,
"last_sha":"9f2c4e1…","last_status":"success","last_message":"Новых 3, обновлено 2, без изменений 41","last_run_at":"…"}]}
last_status: success, neutral (часть файлов с ошибками или выкладывать нечего), failure.
POST/github/install-link
Ссылка на установку приложения на GitHub для своего ключа (действует 30 минут). После установки GitHub вернёт пользователя в панель, а установка привяжется к ключу; принадлежность установки проверяется через аккаунт GitHub.
GET/github/installations/{installation_id}/repos
Репозитории, к которым у установки есть доступ: id, full_name, default_branch, private.
POST/github/deploys
Добавить деплой. Нужно право загрузки в домен.
{"installation_id":51234567,"repo_id":1,"branch":"main","source_dir":"dist","domain":"example.com"}
Ошибки: 400 — неверная ветка или папка; 403 — нет доступа к домену; 404 — установка или репозиторий недоступны; 409 — для этой ветки деплой уже есть.
PATCH/github/deploys/{id}
Изменить branch, source_dir, domain или enabled.
DELETE/github/deploys/{id}
Удалить деплой. Уже выложенные файлы остаются на CDN.
POST/github/deploys/{id}/run
Выложить последний коммит ветки сейчас. Ответ: {"status":"ok","sha":"…","message":"Деплой запущен"}; итог появляется в GET /github/status и в GitHub Checks.
GET/admin/github/app
Состояние приложения (админ): configured, app_id, slug, install_url, число установок и деплоев.
POST/admin/github/app/manifest
Начать создание приложения (админ). Необязательное тело {"org":"my-org"} создаёт приложение в организации (нужны права её владельца), без него — в личном профиле. Возвращает url: открыв его в браузере, администратор попадает на GitHub с готовым manifest (права: contents read, checks write, metadata read; событие push). После подтверждения ключи приложения сохраняются на сервере автоматически.
POST/github/webhook
Приём событий GitHub (push, installation, installation_repositories). Подпись X-Hub-Signature-256 обязательна. При push в ветку с настроенным деплоем файлы выкладываются в фоне; событие GITHUB_DEPLOY уходит и в ваши webhooks.
Подписки (Billing)
GET/admin/billing/plans
Список тарифов и SKU. Ответ совпадает с GET /billing/plans.
POST/admin/billing/entitlements
Выдать пользователю тариф по SKU.
Body (JSON):
{
"owner_type":"discord",
"owner_id":"1234567890",
"sku":"PRO",
"api_key_id":null,
"starts_at":null,
"expires_at":null,
"source":"manual",
"external_transaction_id":null,
"metadata":{"note":"gift"}
}
Обязательные поля: owner_type (discord | telegram), owner_id, sku. Остальные необязательны.
Response 200:
{
"status":"ok",
"entitlement":{
"id":5,
"owner_type":"discord",
"owner_id":"1234567890",
"api_key_id":10,
"sku":"PRO",
"name":"Pro",
"kind":"base",
"status":"active",
"starts_at":"2026-06-16T10:00:00",
"expires_at":"2026-07-16T10:00:00",
"source":"manual",
"external_transaction_id":null,
"metadata":{"note":"gift"}
}
}
Ошибки:
- 400 —
owner_typeнеdiscord/telegram - 404 — SKU не найден или выключен
GET/admin/billing/entitlements/{owner_type}/{owner_id}
Подписки пользователя (новые первыми).
Response 200:
{"status":"ok","entitlements":[{"id":5,"sku":"PRO","status":"active", "...":"..."}]}
Структура элемента такая же, как в entitlement выше.
PATCH/admin/billing/entitlements/{entitlement_id}
Изменить или отозвать подписку.
Body (JSON, все поля опциональны):
{"status":"revoked","expires_at":"2026-08-01T00:00:00","metadata":{"reason":"refund"}}
status: active | revoked | expired | replaced.
Response 200:
{"status":"ok","entitlement":{...}}
Ошибки:
- 400 — недопустимый
status - 404 — подписка не найдена
Bot (internal)
Все эндпоинты /bot/* — внутренние (для Discord‑ и Telegram‑ботов) и требуют заголовок X-Bot-Token. Бот действует от имени пользователя: файл или аккаунт определяется по discord_id/telegram_id, а права берутся из привязанного к этому аккаунту API ключа. Если аккаунт не привязан, возвращается 401.
| Header | Обязательно | Описание |
|---|---|---|
| X-Bot-Token | да | внутренний токен бота; должен совпадать с токеном на сервере (см. таблицу ниже) |
Какой токен нужен:
| Эндпоинты | Токен на сервере |
|---|---|
Discord: /bot/* (кроме /bot/telegram/*) | DISCORD_BOT_INTERNAL_TOKEN |
Telegram: все /bot/telegram/* | TELEGRAM_BOT_INTERNAL_TOKEN |
Начиная с версии 6.7.0 все эндпоинты Telegram‑бота используют собственный токен. В версиях до 6.6.1 включительно эндпоинты quota, usage, domains, file/* и files/bulk-delete ошибочно проверяли токен Discord‑бота.
Ограничения доступа для не‑admin ключей: бот может работать только с файлами, принадлежащими привязанному ключу, иначе 403 «Файл не принадлежит вашему API ключу».
Discord binding
POST/bot/discord/bind
Привязать Discord пользователя к API‑ключу (для бота).
Headers:
X-Bot-Token: <BOT_TOKEN>
Body (JSON):
{
"discord_id": "1234567890",
"api_key": "<API_KEY>",
"username": "optional",
"global_name": "optional",
"email": "optional",
"avatar": "optional"
}
Response 200 (пример):
{
"status": "ok",
"message": "Discord аккаунт успешно привязан к API ключу",
"discord_id": "1234567890",
"key_info": {
"name": "ClientName",
"domains": ["public"],
"enabled": true,
"is_admin": false,
"ips": []
}
}
Ошибки:
- 403 — недействительный
X-Bot-Token - 400 — API ключ не существует
POST/bot/discord/unbind
Отвязать Discord пользователя от API‑ключа.
Headers:
X-Bot-Token: <BOT_TOKEN>
Body (JSON):
{ "discord_id": "1234567890" }
Response 200:
{ "status": "ok", "message": "Discord аккаунт успешно отвязан от API ключа" }
Ошибки:
- 403 — недействительный
X-Bot-Token - 404 — привязка не найдена
GET/bot/discord/me/{discord_id}
Получить информацию о привязке Discord пользователя.
Headers:
X-Bot-Token: <BOT_TOKEN>
Path params:
discord_id— Discord ID
Response 200 (пример, если привязан):
{
"authenticated": true,
"bound": true,
"discord_id": "1234567890",
"user": { "name": "Display Name", "email": "[email protected]", "avatar": "https://..." },
"key_info": { "name":"ClientName","domains":["public"],"enabled":true,"is_admin":false,"ips":[] }
}
Response 404 (если не привязан):
{ "authenticated": false, "bound": false }
Bot files
POST/bot/upload
Загрузка файла через Discord‑бота (multipart/form-data).
Headers:
X-Bot-Token: <BOT_TOKEN>
Content-Type: multipart/form-data
Max file size: 500 MB
Form fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| discord_id | string | да | — | Discord ID |
| file | file | да | — | файл |
| domain | string | нет | "" | "" → public |
| name_mode | string | нет | uuid | uuid | original | custom |
| custom_filename | string | нет | "" | используется при custom |
| conflict_action | string | нет | "" | overwrite → перезаписать (с проверками) |
Response 200 (пример):
{
"status":"ok",
"url":"https://cdn.denchikp.com/public/img/...",
"size":12345,
"owner":"ClientName",
"domain":"public",
"folder":"img",
"filename":"..."
}
Response 409 (если имя занято и overwrite не задан):
{
"status":"conflict",
"message":"FILE_EXISTS",
"requested_filename":"a.png",
"existing_url":"https://cdn.denchikp.com/public/img/a.png",
"domain":"public",
"folder":"img"
}
Ошибки (частые):
- 401 — Discord аккаунт не привязан к API ключу
- 403 — нет доступа к домену / квота / overwrite запрещён
- 413 — файл слишком большой
- 500 — внутренняя ошибка
Пример (curl):
curl -X POST "https://cdnapi.denchikp.com/bot/upload" \
-H "X-Bot-Token: <BOT_TOKEN>" \
-F "discord_id=1234567890" \
-F "domain=public" \
-F "name_mode=uuid" \
-F "file=@./cat.png"
DELETE/bot/delete
Удалить файл через бота.
Headers:
X-Bot-Token: <BOT_TOKEN>
Body (JSON):
{ "discord_id":"1234567890", "url":"https://cdn.denchikp.com/public/files/a.txt" }
Response 200 (пример):
{
"status":"ok",
"message":"Файл успешно удален",
"domain":"public",
"folder":"files",
"filename":"a.txt",
"url":"https://cdn.denchikp.com/public/files/a.txt",
"owner":"ClientName"
}
Ошибки:
- 400 — некорректный URL / путь не файл
- 401 — Discord аккаунт не привязан
- 403 — нет доступа к домену
- 404 — файл не найден
GET/bot/download/{discord_id}/{domain}/{folder}/{filename:path}
Получить ссылки на скачивание (CDN URL + API download URL).
Headers:
X-Bot-Token: <BOT_TOKEN>
Response 200 (пример):
{
"status":"ok",
"owner":"ClientName",
"domain":"public",
"folder":"img",
"filename":"a.png",
"size":12345,
"cdn_url":"https://cdn.denchikp.com/public/img/a.png",
"api_download_url":"https://cdnapi.denchikp.com/download/public/img/a.png"
}
Ошибки:
- 400 — недопустимое имя файла (.. или / или \)
- 401 — Discord аккаунт не привязан
- 403 — нет доступа к домену
- 404 — файл не найден
POST/bot/browse
Просмотр файлов домена через бота.
Headers:
X-Bot-Token: <BOT_TOKEN>
Body (JSON):
{ "discord_id":"1234567890", "domain":"public", "folder":"img", "filter":"cat" }
Response 200 (пример, укорочено):
{ "status":"ok", "domain":"public", "total_files":1, "files":[{"name":"a.png","path":"img/a.png","size":12345,"url":"..."}] }
GET/bot/folders/{discord_id}/{domain}
Папки домена через бота + статистика по папкам.
Headers:
X-Bot-Token: <BOT_TOKEN>
Response 200 (пример):
{
"status":"ok",
"domain":"public",
"total_files":10,
"total_size":1234567,
"folders":[{"name":"img","file_count":5,"size":1000000,"path":"public/img"}]
}
POST/bot/validate
Валидация ссылки через бота (HEAD/GET, статус и заголовки). Для CDN URL также возвращает parsed info и exists_on_disk.
Headers:
X-Bot-Token: <BOT_TOKEN>
Body (JSON):
{ "discord_id":"1234567890", "url":"https://example.com/file.png" }
Response 200 (схема, укорочено):
{
"status":"ok",
"checked_by":"ClientName",
"url":"https://example.com/file.png",
"is_valid_url":true,
"is_cdn_url":false,
"available":true,
"http_status":200,
"headers":{"content_type":"image/png","content_length":"12345"},
"file_info":{"filename":"file.png","extension":"png","host":"example.com","path":"/file.png"},
"cdn_info":null,
"error":null
}
POST/bot/file/content
Получить содержимое текстового файла (предпросмотр для бота).
Ограничения:
- только текстовые файлы
- размер ≤ 2 MB
Body (JSON):
{ "discord_id":"1234567890", "url":"https://cdn.denchikp.com/public/files/app.js" }
Response 200 (пример):
{
"status":"ok",
"url":"https://cdn.denchikp.com/public/files/app.js",
"domain":"public",
"folder":"files",
"filename":"app.js",
"mime_type":"application/javascript",
"size":1200,
"content":"console.log('hi');\n"
}
Bot: квоты, домены, тариф
GET/bot/quota/{discord_id}
Квота и использование пользователя. Ответ такой же, как у GET /quota (включая поле billing).
Headers:
X-Bot-Token: <BOT_TOKEN>
GET/bot/usage/{discord_id}
Текущее использование. Ответ такой же, как у GET /usage.
Headers:
X-Bot-Token: <BOT_TOKEN>
GET/bot/domains/{discord_id}
Домены, доступные пользователю. Ответ такой же, как у GET /domains.
Headers:
X-Bot-Token: <BOT_TOKEN>
GET/bot/billing/{discord_id}
Тариф и подписки пользователя.
Headers:
X-Bot-Token: <BOT_TOKEN>
Response 200 (пример):
{
"status":"ok",
"api_key_name":"ClientName",
"api_key_id":10,
"billing":{
"source":"billing",
"plan":{"sku":"PRO","name":"Pro","kind":"base","expires_at":"2026-07-16T10:00:00","entitlement_id":5},
"entitlements":[
{"id":5,"sku":"PRO","name":"Pro","kind":"base","status":"active","starts_at":"2026-06-16T10:00:00","expires_at":"2026-07-16T10:00:00","source":"discord"}
]
},
"entitlements":[
{"id":5,"owner_type":"discord","owner_id":"1234567890","api_key_id":10,"sku":"PRO","name":"Pro","kind":"base","status":"active","starts_at":"2026-06-16T10:00:00","expires_at":"2026-07-16T10:00:00","source":"discord","external_transaction_id":"1400000000000000000","metadata":{}}
]
}
Если активных подписок нет, billing = {"source":"legacy","plan":null,"entitlements":[]}.
Bot: операции с файлами
Во всех запросах этого раздела передаётся discord_id и CDN url файла. Заголовок: X-Bot-Token: <BOT_TOKEN>.
POST/bot/file/meta
Метаданные файла. Ответ такой же, как у POST /file/meta.
Body (JSON):
{"discord_id":"1234567890","url":"https://cdn.denchikp.com/public/img/a.png"}
POST/bot/file/exists
Проверка существования файла.
Body (JSON):
{"discord_id":"1234567890","url":"https://cdn.denchikp.com/public/img/a.png"}
Response 200:
{"status":"ok","exists_in_db":true,"exists_on_disk":true,"domain":"public","folder":"img","filename":"a.png"}
POST/bot/file/hash
Хэш файла.
Body (JSON):
{"discord_id":"1234567890","url":"https://cdn.denchikp.com/public/files/a.bin","algo":"sha256"}
Response 200:
{"status":"ok","algo":"sha256","hash":"<hex>","url":"https://...","size":12345}
Ошибки:
- 400 — алгоритм не sha256/sha1/md5
- 404 — файл не найден
POST/bot/file/rename
Переименовать файл (new_filename обязателен).
Body (JSON):
{"discord_id":"1234567890","url":"https://cdn.denchikp.com/public/img/a.png","new_filename":"b.png"}
Ответ — как у /bot/file/move.
POST/bot/file/move
Переместить файл (домен, папка, имя).
Body (JSON):
{
"discord_id":"1234567890",
"url":"https://cdn.denchikp.com/public/img/a.png",
"new_domain":"public",
"new_folder":"files",
"new_filename":"b.png"
}
Response 200:
{"status":"ok","from":"https://.../a.png","to":"https://.../b.png","domain":"public","folder":"files","filename":"b.png"}
Ошибки:
- 404 — исходный файл не найден
- 409 — целевой файл уже существует (в отличие от
POST /file/move, который перезаписывает)
Webhook event: FILE_MOVE (payload.source="bot")
POST/bot/file/copy
Скопировать файл (учитывается в квоте как новая загрузка).
Body (JSON):
{
"discord_id":"1234567890",
"url":"https://cdn.denchikp.com/public/img/a.png",
"target_domain":"public",
"target_folder":"img",
"target_filename":"copy.png"
}
Response 200:
{"status":"ok","from":"https://.../a.png","to":"https://.../copy.png","size":12345,"domain":"public","folder":"img","filename":"copy.png"}
Ошибки:
- 403 — квота превышена
- 404 — исходный файл не найден
- 409 — целевой файл уже существует
Webhook event: FILE_COPY (payload.source="bot")
POST/bot/files/bulk-delete
Массовое удаление (от 1 до 500 URL).
Body (JSON):
{"discord_id":"1234567890","urls":["https://cdn.denchikp.com/public/files/a.txt","..."]}
Response 200:
{
"status":"ok",
"results":[
{"url":"...","status":"ok"},
{"url":"...","status":"error","error":"Файл не принадлежит вашему API ключу"}
]
}
Ошибки:
- 400 — пустой список или больше 500 URL
Webhook event: BULK_DELETE (payload.source="bot")
Bot stats
GET/bot/public-summary
Публичная статистика CDN (вызов внутри бота). Фактически возвращает то же, что GET /stats/public-summary, но требует X-Bot-Token.
Response 200 (пример):
{
"status":"ok",
"stats":{
"uploaded_files":1000,
"current_files":900,
"total_size_bytes":123456789,
"total_size_mb":117.74,
"domains":10,
"clients":3,
"registered_api_clients":5
}
}
GET/bot/stats/{discord_id}
Админ‑статистика для бота: доступно только если привязанный к discord_id ключ имеет is_admin=true. Возвращает ту же структуру, что /bot/public-summary.
Ошибки:
- 403 — только администраторы могут просматривать статистику
- 401 — Discord аккаунт не привязан к API ключу
Telegram binding
POST/bot/telegram/bind
Привязать Telegram пользователя к API ключу.
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
Body (JSON):
{
"telegram_id": "123456789",
"api_key": "<API_KEY>",
"username": "optional",
"first_name": "optional",
"last_name": "optional"
}
Response 200:
{
"status": "ok",
"message": "Telegram аккаунт успешно привязан к API ключу",
"telegram_id": "123456789",
"key_info": {"name":"ClientName","domains":["public"],"enabled":true,"is_admin":false,"ips":[]}
}
Ошибки:
- 400 — API ключ не существует
- 403 — недействительный
X-Bot-Token
POST/bot/telegram/unbind
Отвязать Telegram пользователя от ключа.
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
Body (JSON):
{"telegram_id":"123456789"}
Response 200:
{"status":"ok","message":"Telegram аккаунт успешно отвязан от API ключа"}
Ошибки:
- 404 — привязка не найдена
GET/bot/telegram/me/{telegram_id}
Статус привязки Telegram пользователя.
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
Response 200 (если привязан):
{
"authenticated": true,
"bound": true,
"telegram_id": "123456789",
"user": {"username":"ivan","first_name":"Ivan","last_name":null},
"key_info": {"name":"ClientName","domains":["public"],"enabled":true,"is_admin":false,"ips":[]}
}
Response 404 (если не привязан):
{"authenticated": false, "bound": false}
Telegram: файлы
POST/bot/telegram/upload
Загрузка файла через Telegram‑бота (multipart/form-data, максимум 500 MB). Поля те же, что у /bot/upload, но вместо discord_id передаётся telegram_id.
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
Form fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| telegram_id | string | да | — | Telegram ID |
| file | file | да | — | файл |
| domain | string | нет | "" | "" → public |
| name_mode | string | нет | uuid | uuid | original | custom |
| custom_filename | string | нет | "" | используется при custom |
| conflict_action | string | нет | "" | overwrite → перезаписать (с проверками) |
Response 200:
{"status":"ok","url":"https://cdn.denchikp.com/public/img/...","size":12345,"owner":"ClientName","domain":"public","folder":"img","filename":"..."}
Response 409: как у /bot/upload ("status":"conflict","message":"FILE_EXISTS").
Ошибки (частые):
- 401 — Telegram аккаунт не привязан к API ключу
- 403 — нет доступа к домену / квота / перезапись запрещена
- 413 — файл слишком большой
Webhook event: UPLOAD (payload.source="telegram_bot")
DELETE/bot/telegram/delete
Удалить файл.
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
Body (JSON):
{"telegram_id":"123456789","url":"https://cdn.denchikp.com/public/files/a.txt"}
Response 200:
{"status":"ok","message":"Файл успешно удален","domain":"public","folder":"files","filename":"a.txt","url":"https://cdn.denchikp.com/public/files/a.txt","owner":"ClientName"}
Webhook event: DELETE (payload.source="telegram_bot")
GET/bot/telegram/download/{telegram_id}/{domain}/{folder}/{filename:path}
Ссылки для скачивания (CDN URL и API download URL). Ответ такой же, как у /bot/download/....
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
POST/bot/telegram/browse
Просмотр файлов домена.
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
Body (JSON):
{"telegram_id":"123456789","domain":"public","folder":"img","filter":"cat"}
Ответ такой же, как у /bot/browse.
GET/bot/telegram/folders/{telegram_id}/{domain}
Папки домена и статистика по ним. Ответ такой же, как у /bot/folders/....
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
POST/bot/telegram/validate
Валидация ссылки (HEAD/GET). Ответ такой же, как у /bot/validate.
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
Body (JSON):
{"telegram_id":"123456789","url":"https://example.com/file.png"}
POST/bot/telegram/file/content
Содержимое текстового файла (до 2 MB). Ответ такой же, как у /bot/file/content.
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
Body (JSON):
{"telegram_id":"123456789","url":"https://cdn.denchikp.com/public/files/app.js"}
Telegram: квоты, домены, тариф
GET/bot/telegram/quota/{telegram_id}
Квота и использование. Ответ такой же, как у GET /quota.
GET/bot/telegram/usage/{telegram_id}
Текущее использование. Ответ такой же, как у GET /usage.
GET/bot/telegram/domains/{telegram_id}
Доступные домены. Ответ такой же, как у GET /domains.
GET/bot/telegram/billing/{telegram_id}
Тариф и подписки. Ответ такой же, как у GET /bot/billing/{discord_id}.
Telegram: операции с файлами
Запросы такие же, как у одноимённых Discord‑эндпоинтов, но вместо discord_id передаётся telegram_id; ответы идентичны. Заголовок X-Bot-Token — токен Telegram‑бота.
| Метод и путь | Аналог | Body |
|---|---|---|
| POST /bot/telegram/file/meta | /bot/file/meta | telegram_id, url |
| POST /bot/telegram/file/exists | /bot/file/exists | telegram_id, url |
| POST /bot/telegram/file/hash | /bot/file/hash | telegram_id, url, algo |
| POST /bot/telegram/file/rename | /bot/file/rename | telegram_id, url, new_filename |
| POST /bot/telegram/file/move | /bot/file/move | telegram_id, url, new_domain, new_folder, new_filename |
| POST /bot/telegram/file/copy | /bot/file/copy | telegram_id, url, target_domain, target_folder, target_filename |
| POST /bot/telegram/files/bulk-delete | /bot/files/bulk-delete | telegram_id, urls |
Для событий FILE_MOVE, FILE_COPY и BULK_DELETE этих эндпоинтов в payload указывается "source":"bot", а идентификатор пользователя передаётся в поле discord_id.
Telegram: статистика
GET/bot/telegram/public-summary
Публичная статистика CDN. Ответ такой же, как у GET /stats/public-summary.
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
GET/bot/telegram/stats/{telegram_id}
Админ‑статистика: только если привязанный ключ имеет is_admin=true. Ответ такой же, как у /bot/public-summary.
Headers:
X-Bot-Token: <TELEGRAM_BOT_TOKEN>
Ошибки:
- 401 — Telegram аккаунт не привязан к API ключу
- 403 — только администраторы
Discord Premium Apps webhook
POST/webhooks/discord/entitlements
Приёмник событий Discord Premium Apps: зеркалирует покупки в подписки CDN (см. «Тарифы»). Вызывается серверами Discord, подпись проверяется по публичному ключу приложения.
Headers:
X-Signature-Ed25519: <hex>X-Signature-Timestamp: <unix_seconds>
Подпись — Ed25519 над строкой {timestamp}{raw_body}; допустимое расхождение времени — 300 секунд.
Обрабатываемые события: ENTITLEMENT_CREATE, ENTITLEMENT_UPDATE, ENTITLEMENT_DELETE. SKU Discord сопоставляется с внутренним SKU (PRO, PRO_PLUS, LIFETIME_PRO_PLUS, STORAGE_50, STORAGE_100). Новая базовая подписка заменяет предыдущую платную базовую (статус replaced); удаление отзывает подписку (revoked).
Response:
- 204 — событие принято или проигнорировано (пинг, неизвестный SKU или тип события)
Ошибки:
- 400 — некорректный JSON или нет
user_id/idв данных подписки - 401 — нет, просрочена или неверна подпись; неверный
application_id - 404 — внутренний SKU не найден или выключен
- 503 — на сервере не настроен публичный ключ Discord