DenchikP CDN
Руководство Политика конфиденциальности Условия использования API Docs
← К панели
Содержание
  1. Общие требования
  2. Мониторинг
  3. GET/healthz
  4. GET/readyz
  5. GET/version
  6. GET/status
  7. Ключ / домены / квоты
  8. GET/api-key/me
  9. GET/domains
  10. GET/usage
  11. GET/quota
  12. Типы ключей и свои домены
  13. GET/domains/claims
  14. POST/domains/claims
  15. POST/domains/claims/{claim_id}/check
  16. POST/domains/claims/{claim_id}/cloudflare
  17. DELETE/domains/claims/{claim_id}
  18. Тарифы (Billing)
  19. GET/billing/plans
  20. Файлы (БД / meta / exists)
  21. GET/files
  22. POST/file/meta
  23. POST/file/details
  24. POST/file/exists
  25. GET/file/redirect
  26. Загрузка
  27. POST/upload
  28. POST/upload/by-url
  29. Multipart Upload
  30. POST/upload/session/start
  31. PUT/upload/session/{upload_id}/part/{part_no}
  32. POST/upload/session/{upload_id}/complete
  33. POST/upload/session/{upload_id}/abort
  34. Операции с файлами
  35. POST/files/bulk-delete
  36. POST/file/move
  37. POST/file/rename
  38. POST/file/copy
  39. POST/file/hash
  40. POST/files/zip
  41. Browse / Folders / Download
  42. POST/browse
  43. GET/folders/{domain}
  44. GET/download/{domain}/{folder}/{filename:path}
  45. Изображения: размер и формат
  46. GET/file/transform/{domain}/{folder}/{filename}
  47. Validate URL
  48. POST/validate
  49. Delete file
  50. DELETE/delete
  51. Токены доступа и лимиты
  52. POST/tokens
  53. GET/tokens
  54. PATCH/tokens/{token_id}
  55. DELETE/tokens/{token_id}
  56. Приватные файлы и защита от хотлинка
  57. POST/file/sign
  58. GET/file/signed/{domain}/{folder}/{filename}
  59. POST/files/visibility
  60. Публичные ссылки (шаринг)
  61. GET/s/{code}
  62. GET/s/{code}/download, POST /s/{code}/download
  63. GET/s/{code}/raw
  64. POST/shares
  65. GET/shares
  66. PATCH/shares/{share_id}
  67. DELETE/shares/{share_id}
  68. Дубликаты
  69. POST/dedupe/scan
  70. GET/dedupe/groups
  71. POST/dedupe/resolve
  72. Антивирус (ClamAV)
  73. GET/av/status
  74. POST/av/scan
  75. Метрики (Prometheus)
  76. GET/metrics
  77. OpenAPI
  78. Bot: расширенные операции
  79. POST/bot/ops/{op}
  80. POST/bot/telegram/ops/{op}
  81. Корзина и срок жизни
  82. POST/trash
  83. GET/trash
  84. POST/trash/restore
  85. DELETE/trash
  86. POST/files/expire
  87. GET/files/expiring
  88. Text Editor (Read/Save)
  89. POST/file/content
  90. POST/file/save
  91. Статистика
  92. GET/stats/public-summary
  93. GET/stats/me
  94. GET/stats/top-files
  95. GET/stats/export
  96. Webhooks (Reference)
  97. Создание / управление
  98. POST/webhooks
  99. GET/webhooks
  100. DELETE/webhooks/{webhook_id}
  101. GET/webhooks/{webhook_id}/deliveries
  102. POST/webhooks/{webhook_id}/test
  103. Формат доставки
  104. События
  105. Подпись (HMAC)
  106. Авторизация (сессии)
  107. Вход через OAuth
  108. GET/auth/google
  109. GET/auth/discord
  110. GET/auth/github
  111. GET/auth/telegram
  112. Сессия и привязка ключа
  113. POST/auth/cdn/login
  114. POST/auth/cdn/logout
  115. GET/auth/me
  116. POST/auth/bind-api-key
  117. POST/auth/unbind-api-key
  118. GET/auth/logout
  119. Telegram‑сессия
  120. GET/auth/telegram/me
  121. POST/auth/telegram/bind
  122. POST/auth/telegram/unbind
  123. POST/auth/telegram/logout
  124. DELETE/auth/telegram/delete-data/{telegram_id}
  125. Администрирование
  126. API ключи
  127. GET/admin/api-keys
  128. POST/admin/api-keys
  129. PATCH/admin/api-keys/{api_key_id}
  130. POST/admin/api-keys/{api_key_id}/rotate
  131. DELETE/admin/api-keys/{api_key_id}
  132. POST/admin/api-keys/{api_key_id}/ips
  133. DELETE/admin/api-keys/{api_key_id}/ips/{ip}
  134. POST/admin/api-keys/{api_key_id}/domains
  135. DELETE/admin/api-keys/{api_key_id}/domains/{domain}
  136. Домены пользователей
  137. GET/admin/domains
  138. POST/admin/domains/assign
  139. POST/admin/domains/{claim_id}/approve
  140. DELETE/admin/domains/{claim_id}
  141. Квоты ключа
  142. GET/admin/api-keys/{api_key_id}/quota
  143. PUT/admin/api-keys/{api_key_id}/quota
  144. Файлы, логи, события
  145. GET/admin/files
  146. GET/admin/logs
  147. GET/events
  148. Сканирование диска
  149. POST/admin/public/purge
  150. POST/admin/scan/start
  151. GET/admin/scan/status
  152. GitHub: деплой из репозитория
  153. GET/github/status
  154. POST/github/install-link
  155. GET/github/installations/{installation_id}/repos
  156. POST/github/deploys
  157. PATCH/github/deploys/{id}
  158. DELETE/github/deploys/{id}
  159. POST/github/deploys/{id}/run
  160. GET/admin/github/app
  161. POST/admin/github/app/manifest
  162. POST/github/webhook
  163. Подписки (Billing)
  164. GET/admin/billing/plans
  165. POST/admin/billing/entitlements
  166. GET/admin/billing/entitlements/{owner_type}/{owner_id}
  167. PATCH/admin/billing/entitlements/{entitlement_id}
  168. Bot (internal)
  169. Discord binding
  170. POST/bot/discord/bind
  171. POST/bot/discord/unbind
  172. GET/bot/discord/me/{discord_id}
  173. Bot files
  174. POST/bot/upload
  175. DELETE/bot/delete
  176. GET/bot/download/{discord_id}/{domain}/{folder}/{filename:path}
  177. POST/bot/browse
  178. GET/bot/folders/{discord_id}/{domain}
  179. POST/bot/validate
  180. POST/bot/file/content
  181. Bot: квоты, домены, тариф
  182. GET/bot/quota/{discord_id}
  183. GET/bot/usage/{discord_id}
  184. GET/bot/domains/{discord_id}
  185. GET/bot/billing/{discord_id}
  186. Bot: операции с файлами
  187. POST/bot/file/meta
  188. POST/bot/file/exists
  189. POST/bot/file/hash
  190. POST/bot/file/rename
  191. POST/bot/file/move
  192. POST/bot/file/copy
  193. POST/bot/files/bulk-delete
  194. Bot stats
  195. GET/bot/public-summary
  196. GET/bot/stats/{discord_id}
  197. Telegram binding
  198. POST/bot/telegram/bind
  199. POST/bot/telegram/unbind
  200. GET/bot/telegram/me/{telegram_id}
  201. Telegram: файлы
  202. POST/bot/telegram/upload
  203. DELETE/bot/telegram/delete
  204. GET/bot/telegram/download/{telegram_id}/{domain}/{folder}/{filename:path}
  205. POST/bot/telegram/browse
  206. GET/bot/telegram/folders/{telegram_id}/{domain}
  207. POST/bot/telegram/validate
  208. POST/bot/telegram/file/content
  209. Telegram: квоты, домены, тариф
  210. GET/bot/telegram/quota/{telegram_id}
  211. GET/bot/telegram/usage/{telegram_id}
  212. GET/bot/telegram/domains/{telegram_id}
  213. GET/bot/telegram/billing/{telegram_id}
  214. Telegram: операции с файлами
  215. Telegram: статистика
  216. GET/bot/telegram/public-summary
  217. GET/bot/telegram/stats/{telegram_id}
  218. Discord Premium Apps webhook
  219. POST/webhooks/discord/entitlements

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‑типу файла:

ПапкаРасширения
imgjpg, jpeg, jfif, pjpeg, png, apng, webp, avif, svg, gif, ico, cur, bmp, tif, tiff, heic, heif, jxl
videomp4, m4v, webm, mov, mkv, avi, ogv, wmv, flv, 3gp, mpg, mpeg, ts, m2ts
audiomp3, wav, ogg, oga, opus, flac, m4a, aac, weba, wma, aif, aiff, mid, midi
docspdf, doc, docx, xls, xlsx, ppt, pptx, odt, ods, odp, rtf, txt, csv, md, epub
csscss
jsjs, mjs, cjs
fontswoff, 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_cnameCNAME-запись _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ТипХранилищеФайловЗагрузка в месяцЦенаСрок
FREEbase10 GB50 0005 GB0 €—
PRObase25 GB50 000100 GB, 10 000 файлов2.99 €30 дней
PRO_PLUSbase100 GB100 000500 GB, 50 000 файлов5.99 €30 дней
LIFETIME_PRO_PLUSbase100 GB100 000500 GB, 50 000 файлов49.99 €бессрочно
STORAGE_50addon+50 GB——1.99 €бессрочно
STORAGE_100addon+100 GB——2.99 €бессрочно

Максимальный размер одного файла во всех тарифах — 500 MB. Значения могут быть изменены администратором; актуальный список всегда отдаёт этот эндпоинт.

Файлы (БД / meta / exists)

GET/files

Список «моих» файлов из БД.

Headers:

  • X-Api-Key: <API_KEY>

Query parameters (все опциональны):

NameTypeDefaultDescription
qstring—поиск по filename/original_filename (LIKE)
domainstring—фильтр домена (нормализуется)
folderstring—фильтр папки
extstring—фильтр расширения
exists_on_diskbooltrueфильтр по наличию на диске
limitint50максимум 200
offsetint0смещение

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:

FieldTypeRequiredDefaultDescription
filefileда—загружаемый файл
domainstringнет"""" → public
name_modestringнетuuiduuid | original | custom
custom_filenamestringнет""используется при name_mode=custom
conflict_actionstringнет""overwrite → перезаписать (с проверками прав)
dedupeboolнетfalsetrue → не сохранять файл, если в домене уже есть такой же (см. раздел «Дубликаты»), и вернуть адрес существующего с deduplicated=true
ttlintнет—срок жизни файла в секундах (от 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/files
  • new_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 или files
  • filename — имя файла: jpg, jpeg, png, webp или gif

Query parameters (все опциональны):

NameTypeDefaultDescription
wint—максимальная ширина, 1–4096 px
hint—максимальная высота, 1–4096 px
fitstringscale-downscale-down (вписать, без увеличения) | contain (то же) | cover (обрезать точно по рамке w×h, нужны оба размера)
formatstringautoauto | webp | avif | jpeg | png
qint80качество 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):

FieldTypeRequiredDescription
namestringданазвание, 1–100 символов
scopesstring[]данепустой список из read, upload, delete, write
domainsstring[]нетограничить токен доменами (должны быть доступны вашему ключу)
rpmintнетлимит запросов в минуту, 1–6000 (по умолчанию 120)
expires_in_daysintнетсрок действия, 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):

FieldTypeRequiredDefaultDescription
urlstringда—CDN URL файла (в том числе из папки private)
ttl_secondsintнет3600срок действия: от 10 секунд до 30 дней
downloadboolнетfalsetrue → отдавать как вложение (скачивание)

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):

FieldTypeRequiredDescription
urlstringдаCDN URL файла
ttl_secondsintнетсрок действия ссылки: от 60 секунд до 10 лет (по умолчанию бессрочно)
passwordstringнетпароль, 4–128 символов
max_downloadsintнетлимит скачиваний, 1–1000000
namestringнетназвание на странице (до 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), все поля необязательны:

FieldTypeDescription
enabledboolвключить или временно отключить
ttl_secondsintновый срок действия (отсчёт от текущего момента)
clear_expiryboolсделать бессрочной
max_downloadsintновый лимит (счётчик скачиваний не сбрасывается)
clear_max_downloadsboolснять лимит
passwordstringустановить или сменить пароль
clear_passwordboolубрать пароль
namestringназвание на странице

Ошибки:

  • 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_PORT127.0.0.1, 3310адрес clamd
CLAMAV_MAX_SCAN_BYTES26214400 (25 МБ)файлы больше этого размера не проверяются; значение должно совпадать с StreamMaxLength в clamd.conf
CLAMAV_FAIL_MODEopenчто делать, если 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_secondsgaugeвремя работы процесса
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_totalcounterнеудачные попытки авторизации
dpcdn_token_rate_limited_totalcounterзапросы токенов, отклонённые лимитом (429)
dpcdn_av_scanned_total, _infected_total, _errors_total, _skipped_totalcounterантивирусные проверки
dpcdn_files_total, dpcdn_files_bytesgaugeфайлы на диске по данным БД
dpcdn_trash_items, dpcdn_trash_bytesgaugeкорзина
dpcdn_files_expiringgaugeфайлов со сроком жизни
dpcdn_share_links_active, dpcdn_tokens_total, dpcdn_api_keys_enabled, dpcdn_webhooks_enabled, dpcdn_file_hashesgaugeактивные публичные ссылки, токены, ключи, webhook’и, закешированные хэши
dpcdn_image_cache_bytesgaugeразмер кеша миниатюр
dpcdn_clamav_enabledgauge1, если антивирусная проверка включена

Счётчики запросов хранятся в памяти и обнуляются при перезапуске 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):'''

opparamsАналог
trashurls[]POST /trash
trash_listdomain, limit, offsetGET /trash
trash_restoreids[]POST /trash/restore
trash_purgeids[] или all=trueDELETE /trash
expireurl, ttl_seconds или clear=truePOST /files/expire
expiringdomain, limitGET /files/expiring
share_createurl, ttl_seconds, password, max_downloads, namePOST /shares
share_listdomain, limitGET /shares
share_revokeshare_idDELETE /shares/{id}
signurl, ttl_seconds, downloadPOST /file/sign
visibilityurls[], visibility (private или public)POST /files/visibility
dedupe_scandomain, budget_secondsPOST /dedupe/scan
dedupe_groupsdomain, min_sizeGET /dedupe/groups
dedupe_resolvekeep, remove[]POST /dedupe/resolve
dedupe_lookupdomain, size, sha256поиск файла с таким же содержимым перед загрузкой: {"found":true,"duplicate":{"url":"…"}}
av_status—GET /av/status
av_scanurl, action (report или trash)POST /av/scan
image_urlurl, w, h, fit, format, qсобирает ссылку на изображение нужного размера и формата (проверяет доступ и существование файла)
my_statsperiod, domainGET /stats/me
domain_list—GET /domains/claims
domain_adddomain, method (dns_txt, dns_cname, file, meta)POST /domains/claims (способ cloudflare в ботах запрещён: токен нельзя передавать в чат)
domain_checkclaim_id, methodPOST /domains/claims/{id}/check
domain_removeclaim_idDELETE /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, максимум 500
  • offset — смещение

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:

NameTypeDefaultDescription
periodstringmonthday | week | month | year; другое значение заменяется на month
domainstring—ограничить статистику одним доменом

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:

NameTypeDefaultDescription
periodstringmonthday | week | month (30 дней) | year (365 дней); любое другое значение — за всё время
bystringdownloadsdownloads | size
limitint50максимум 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:

NameTypeDefaultDescription
periodstringmonthкак у /stats/top-files
kindstringuploadsuploads | downloads | deletions | browses
formatstringcsvподдерживается только 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-AgentDP-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}/testmessage, 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 (все опциональны):

NameTypeDefaultDescription
domainstring—фильтр по домену
ownerstring—фильтр по владельцу (имя ключа)
exists_on_diskbool—фильтр по наличию на диске
qstring—поиск по filename/original_filename (без учёта регистра)
limitint200от 1 до 500
offsetint0смещение

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 (все опциональны):

NameTypeDefaultDescription
actionstring—фильтр по действию (например UPLOAD, BROWSE)
levelstring—INFO | WARNING | ERROR
domainstring—фильтр по домену
ownerstring—фильтр по владельцу
limitint100максимум 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:

FieldTypeRequiredDefaultDescription
discord_idstringда—Discord ID
filefileда—файл
domainstringнет"""" → public
name_modestringнетuuiduuid | original | custom
custom_filenamestringнет""используется при custom
conflict_actionstringнет""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:

FieldTypeRequiredDefaultDescription
telegram_idstringда—Telegram ID
filefileда—файл
domainstringнет"""" → public
name_modestringнетuuiduuid | original | custom
custom_filenamestringнет""используется при custom
conflict_actionstringнет""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/metatelegram_id, url
POST /bot/telegram/file/exists/bot/file/existstelegram_id, url
POST /bot/telegram/file/hash/bot/file/hashtelegram_id, url, algo
POST /bot/telegram/file/rename/bot/file/renametelegram_id, url, new_filename
POST /bot/telegram/file/move/bot/file/movetelegram_id, url, new_domain, new_folder, new_filename
POST /bot/telegram/file/copy/bot/file/copytelegram_id, url, target_domain, target_folder, target_filename
POST /bot/telegram/files/bulk-delete/bot/files/bulk-deletetelegram_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
© 2024 – 2026 DenchikP. Все права защищены.