diff --git a/api.md b/api.md
new file mode 100644
index 0000000..39beaf4
--- /dev/null
+++ b/api.md
@@ -0,0 +1,648 @@
+# 📖 MarkClean API — Документация по использованию API-ключа
+
+> **Версия API:** `v1`
+> **Базовый URL:** `https://markclean.online/api`
+> **Формат данных:** JSON
+> **Аутентификация:** API-ключ (Bearer token / X-API-Key)
+
+---
+
+## 📑 Содержание
+
+1. [Обзор](#-обзор)
+2. [Получение API-ключа](#-получение-api-ключа)
+3. [Аутентификация](#-аутентификация)
+4. [Конвертация URL в Markdown](#-конвертация-url-в-markdown)
+5. [История конвертаций](#-история-конвертаций)
+6. [Опции конвертации](#-опции-конвертации)
+7. [Обработка ошибок](#-обработка-ошибок)
+8. [Квоты и лимиты](#-квоты-и-лимиты)
+9. [Примеры кода](#-примеры-кода)
+10. [FAQ](#-faq)
+
+---
+
+## 🔍 Обзор
+
+MarkClean API позволяет программно конвертировать веб-страницы в чистый Markdown, оптимизированный для LLM, RAG-систем и документации.
+
+**Ключевые возможности:**
+- 🌐 Конвертация любой HTML-страницы в Markdown
+- 🧹 Автоматическая очистка от мусора (реклама, навигация, футеры) через Readability
+- ⚡ Двухуровневое кэширование — повторные запросы не списывают кредиты
+- 🎛️ Гибкие опции вывода (заголовок, frontmatter, удаление картинок/ссылок)
+- 📊 История всех конвертаций с дедупликацией
+
+---
+
+## 🔑 Получение API-ключа
+
+API-ключ можно создать **только** через веб-интерфейс (требует авторизации по сессии).
+
+### Шаги:
+
+1. Войдите в аккаунт на [markclean.online](https://markclean.online)
+2. Перейдите на страницу **API Keys** (или откройте `/api-keys`)
+3. Нажмите **"Сгенерировать ключ"**
+4. **⚠️ ВАЖНО:** Сохраните ключ немедленно — он будет показан **только один раз!**
+
+### Формат ключа
+
+```
+mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ
+│ └── 32 символа base64url (криптографически случайные)
+└── префикс (8 символов)
+```
+
+**Общая длина:** 40 символов
+
+> 🔒 **Безопасность:** В базе данных хранится только SHA-256 хеш ключа. Если вы потеряете ключ — восстановить его невозможно, только создать новый.
+
+---
+
+## 🛡️ Аутентификация
+
+Все запросы к защищённым эндпоинтам должны содержать API-ключ в одном из заголовков:
+
+### Способ 1: `X-API-Key` (рекомендуется)
+
+```http
+X-API-Key: mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ
+```
+
+### Способ 2: `Authorization: Bearer`
+
+```http
+Authorization: Bearer mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ
+```
+
+Оба способа равнозначны. При отсутствии или невалидности ключа сервер вернёт `401 Unauthorized`.
+
+---
+
+## 🔄 Конвертация URL в Markdown
+
+Основной эндпоинт API.
+
+### Запрос
+
+```http
+POST /api/convert
+Content-Type: application/json
+X-API-Key: mk_live_YOUR_KEY_HERE
+```
+
+### Тело запроса
+
+```json
+{
+ "url": "https://example.com/article",
+ "include_title": true,
+ "include_frontmatter": false,
+ "strip_images": false,
+ "strip_links": false
+}
+```
+
+| Поле | Тип | Обязательное | По умолчанию | Описание |
+|------|-----|:------------:|:------------:|----------|
+| `url` | string | ✅ | — | URL веб-страницы для конвертации (http/https) |
+| `include_title` | bool | ❌ | `false` | Добавить заголовок страницы как H1 (`# Title`) |
+| `include_frontmatter` | bool | ❌ | `false` | Добавить YAML-шапку с метаданными |
+| `strip_images` | bool | ❌ | `false` | Удалить все `
` теги |
+| `strip_links` | bool | ❌ | `false` | Удалить `` теги (текст сохраняется) |
+
+### Успешный ответ (200 OK)
+
+```json
+{
+ "title": "Example Article",
+ "content": "# Example Article\n\nThis is the converted content...",
+ "quota_incremented": true
+}
+```
+
+| Поле | Тип | Описание |
+|------|-----|----------|
+| `title` | string | Заголовок страницы (может отсутствовать) |
+| `content` | string | Результат конвертации в Markdown |
+| `quota_incremented` | bool | `true` — списан кредит; `false` — cache hit или лимит |
+
+### Пример cURL
+
+```bash
+curl -X POST https://markclean.online/api/convert \
+ -H "Content-Type: application/json" \
+ -H "X-API-Key: mk_live_YOUR_KEY_HERE" \
+ -d '{
+ "url": "https://metanit.com/web/html5/2.6.php",
+ "include_title": true,
+ "strip_images": true
+ }'
+```
+
+---
+
+## 📚 История конвертаций
+
+Получить список всех конвертаций текущего пользователя с пагинацией.
+
+### Запрос
+
+```http
+GET /api/converter/history?page=1&page_size=20
+X-API-Key: mk_live_YOUR_KEY_HERE
+```
+
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|:------------:|----------|
+| `page` | int | `1` | Номер страницы (≥ 1) |
+| `page_size` | int | `20` | Количество записей (1–100) |
+
+### Успешный ответ (200 OK)
+
+```json
+{
+ "items": [
+ {
+ "id": "27c1e865-a77e-4d99-87d0-1179aef2a3e6",
+ "title": "Example Article",
+ "target_url": "https://example.com/article",
+ "count_usage": 3,
+ "created_at": "2026-09-27T10:00:00Z",
+ "updated_at": "2026-09-27T14:30:00Z"
+ }
+ ],
+ "total_count": 42,
+ "page": 1,
+ "page_size": 20,
+ "has_more": true
+}
+```
+
+| Поле | Тип | Описание |
+|------|-----|----------|
+| `items` | array | Список конвертаций (отсортированы по `updated_at` DESC) |
+| `items[].id` | string | UUID записи |
+| `items[].title` | string | Заголовок страницы (или пустая строка) |
+| `items[].target_url` | string | Исходный URL |
+| `items[].count_usage` | int | Сколько раз этот URL конвертировался |
+| `items[].created_at` | string | Первая конвертация (ISO 8601) |
+| `items[].updated_at` | string | Последняя конвертация (ISO 8601) |
+| `total_count` | int | Общее количество записей |
+| `page` | int | Текущая страница |
+| `page_size` | int | Размер страницы |
+| `has_more` | bool | Есть ли следующие страницы |
+
+> 💡 **Дедупликация:** Один URL = одна запись. Повторные конвертации увеличивают `count_usage` и обновляют `updated_at`, перемещая запись наверх.
+
+---
+
+## 🎛️ Опции конвертации
+
+### `include_title: true`
+
+Добавляет заголовок страницы как H1 в начало документа:
+
+```markdown
+# Example Article
+
+This is the content...
+```
+
+### `include_frontmatter: true`
+
+Добавляет YAML-шапку с метаданными (удобно для статических генераторов типа Hugo/Jekyll):
+
+```markdown
+---
+title: "Example Article"
+source: "https://example.com/article"
+bytes: 4521
+tokens: 1234
+---
+
+# Example Article
+
+This is the content...
+```
+
+### `strip_images: true`
+
+Полностью удаляет все `
` теги. Полезно для:
+- 🤖 Подготовки текста для LLM (экономия токенов)
+- 📄 Создания текстовых версий статей
+- 💾 Уменьшения размера результата
+
+### `strip_links: true`
+
+Удаляет `` теги, но **сохраняет текст ссылок**:
+
+**Было:**
+```html
+Visit our site for more info.
+```
+
+**Стало:**
+```markdown
+Visit our site for more info.
+```
+
+### 🎯 Рекомендуемые комбинации
+
+| Сценарий | Опции |
+|----------|-------|
+| **Для LLM / RAG** | `strip_images: true`, `strip_links: true` |
+| **Для документации** | `include_title: true`, `include_frontmatter: true` |
+| **Для архивирования** | `include_title: true`, `include_frontmatter: true`, `strip_images: false` |
+| **Минимальный размер** | `strip_images: true`, `strip_links: true`, `include_title: false` |
+
+---
+
+## ⚠️ Обработка ошибок
+
+Все ошибки возвращаются в едином формате:
+
+```json
+{
+ "error": "machine_readable_code",
+ "message": "Human-readable description"
+}
+```
+
+### HTTP-статусы
+
+| Статус | Код ошибки | Описание | Действие |
+|:------:|------------|----------|----------|
+| `400` | `invalid_payload` | Битый JSON или отсутствует `url` | Проверьте тело запроса |
+| `400` | `invalid_url` | Некорректный формат URL | Должен начинаться с `http://` или `https://` |
+| `401` | `invalid_api_key` | Ключ отсутствует, невалиден или удалён | Проверьте заголовок `X-API-Key` |
+| `401` | `unauthorized` | Нет аутентификации | Передайте API-ключ |
+| `402` | `quota_exceeded` | Кредиты закончились + дневной лимит | Купите кредиты в веб-интерфейсе |
+| `404` | `key_not_found` | Ключ не найден (для истории) | Создайте ключ заново |
+| `415` | `unsupported_content` | URL не возвращает HTML | API работает только с веб-страницами |
+| `422` | `conversion_failed` | Не удалось распознать контент | Попробуйте другой URL |
+| `429` | `rate_limit_exceeded` | Превышен лимит запросов | Подождите или войдите в аккаунт |
+| `502` | `fetch_failed` | Не удалось загрузить страницу | Сайт недоступен или заблокировал запрос |
+| `500` | `internal_server_error` | Внутренняя ошибка сервера | Повторите позже |
+
+### Пример обработки ошибки
+
+```bash
+$ curl -X POST https://markclean.online/api/convert \
+ -H "X-API-Key: mk_live_INVALID" \
+ -H "Content-Type: application/json" \
+ -d '{"url": "https://example.com"}'
+
+HTTP/1.1 401 Unauthorized
+{
+ "error": "invalid_api_key",
+ "message": "Invalid or expired API key"
+}
+```
+
+---
+
+## 📊 Квоты и лимиты
+
+### Система кредитов
+
+- 💳 **Платные кредиты** — не сгорают, не имеют срока действия
+- 📅 **Дневной лимит** (5 запросов/сутки) — сбрасывается каждые 24 часа
+- ⚡ **Приоритет списания:** сначала платные кредиты, затем дневной лимит
+
+### Кэширование и экономия
+
+API использует **двухуровневое кэширование**:
+
+| Уровень | Что кэшируется | TTL | Экономия |
+|---------|----------------|-----|----------|
+| **L1** | Сырой HTML | 30 мин | Нет HTTP-запроса к внешнему сайту |
+| **L2** | Финальный Markdown | 5 мин | Мгновенный ответ |
+
+**Важно:** Кредиты списываются **только** при реальном HTTP-запросе к внешнему сайту. Повторные конвертации того же URL (даже с другими опциями) **бесплатны**, если HTML ещё в кэше.
+
+### Таблица сценариев
+
+| Сценарий | Fetch | Кредиты | Скорость |
+|----------|:-----:|:-------:|----------|
+| Первый запрос URL | ✅ | −1 | ~2-5 сек |
+| Повтор с теми же опциями | ❌ | 0 | ~50 мс |
+| Повтор с другими опциями | ❌ | 0 | ~200 мс |
+| Спустя 30+ минут | ✅ | −1 | ~2-5 сек |
+
+### Проверка баланса
+
+Для проверки баланса кредитов используйте веб-интерфейс (эндпоинт `GET /api/billing/credits` доступен только через сессию, не через API-ключ).
+
+---
+
+## 💻 Примеры кода
+
+### Python (requests)
+
+```python
+import requests
+
+API_KEY = "mk_live_YOUR_KEY_HERE"
+API_BASE = "https://markclean.online/api"
+
+headers = {
+ "Content-Type": "application/json",
+ "X-API-Key": API_KEY,
+}
+
+# Конвертация
+response = requests.post(
+ f"{API_BASE}/convert",
+ headers=headers,
+ json={
+ "url": "https://example.com/article",
+ "include_title": True,
+ "strip_images": True,
+ },
+)
+
+if response.ok:
+ data = response.json()
+ print(f"Title: {data.get('title')}")
+ print(f"Credits used: {data['quota_incremented']}")
+ print(data["content"][:500])
+else:
+ error = response.json()
+ print(f"Error {response.status_code}: {error['error']} - {error['message']}")
+```
+
+### Python (пакетная конвертация)
+
+```python
+import requests
+import time
+
+API_KEY = "mk_live_YOUR_KEY_HERE"
+urls = [
+ "https://example.com/page1",
+ "https://example.com/page2",
+ "https://example.com/page3",
+]
+
+headers = {
+ "Content-Type": "application/json",
+ "X-API-Key": API_KEY,
+}
+
+results = []
+for url in urls:
+ resp = requests.post(
+ "https://markclean.online/api/convert",
+ headers=headers,
+ json={"url": url, "include_title": True},
+ )
+ if resp.ok:
+ results.append(resp.json())
+ print(f"✅ {url}")
+ else:
+ print(f"❌ {url}: {resp.json()['error']}")
+ time.sleep(0.5) # Вежливый delay
+
+# Сохраняем в один файл
+with open("output.md", "w", encoding="utf-8") as f:
+ for r in results:
+ f.write(r["content"] + "\n\n---\n\n")
+```
+
+### JavaScript / Node.js (fetch)
+
+```javascript
+const API_KEY = 'mk_live_YOUR_KEY_HERE';
+const API_BASE = 'https://markclean.online/api';
+
+async function convertUrl(url, options = {}) {
+ const response = await fetch(`${API_BASE}/convert`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-API-Key': API_KEY,
+ },
+ body: JSON.stringify({
+ url,
+ include_title: options.title ?? false,
+ include_frontmatter: options.frontmatter ?? false,
+ strip_images: options.stripImages ?? false,
+ strip_links: options.stripLinks ?? false,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(`${error.error}: ${error.message}`);
+ }
+
+ return response.json();
+}
+
+// Использование
+try {
+ const result = await convertUrl('https://example.com/article', {
+ title: true,
+ stripImages: true,
+ });
+ console.log(result.content);
+} catch (err) {
+ console.error('Conversion failed:', err.message);
+}
+```
+
+### Go
+
+```go
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+)
+
+const (
+ apiKey = "mk_live_YOUR_KEY_HERE"
+ apiBase = "https://markclean.online/api"
+)
+
+type ConvertRequest struct {
+ URL string `json:"url"`
+ IncludeTitle bool `json:"include_title"`
+ IncludeFrontmatter bool `json:"include_frontmatter"`
+ StripImages bool `json:"strip_images"`
+ StripLinks bool `json:"strip_links"`
+}
+
+type ConvertResponse struct {
+ Title string `json:"title"`
+ Content string `json:"content"`
+ QuotaIncremented bool `json:"quota_incremented"`
+}
+
+func main() {
+ reqBody, _ := json.Marshal(ConvertRequest{
+ URL: "https://example.com/article",
+ IncludeTitle: true,
+ StripImages: true,
+ })
+
+ req, _ := http.NewRequest("POST", apiBase+"/convert", bytes.NewReader(reqBody))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("X-API-Key", apiKey)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ panic(err)
+ }
+ defer resp.Body.Close()
+
+ body, _ := io.ReadAll(resp.Body)
+
+ if resp.StatusCode != http.StatusOK {
+ fmt.Printf("Error %d: %s\n", resp.StatusCode, body)
+ return
+ }
+
+ var result ConvertResponse
+ json.Unmarshal(body, &result)
+ fmt.Println(result.Content)
+}
+```
+
+### cURL (все сценарии)
+
+```bash
+# Базовая конвертация
+curl -X POST https://markclean.online/api/convert \
+ -H "X-API-Key: mk_live_YOUR_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"url": "https://example.com"}'
+
+# Для LLM (чистый текст)
+curl -X POST https://markclean.online/api/convert \
+ -H "X-API-Key: mk_live_YOUR_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "url": "https://example.com",
+ "strip_images": true,
+ "strip_links": true
+ }'
+
+# Для документации (с метаданными)
+curl -X POST https://markclean.online/api/convert \
+ -H "X-API-Key: mk_live_YOUR_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "url": "https://example.com",
+ "include_title": true,
+ "include_frontmatter": true
+ }'
+
+# История конвертаций
+curl -X GET "https://markclean.online/api/converter/history?page=1&page_size=10" \
+ -H "X-API-Key: mk_live_YOUR_KEY"
+```
+
+---
+
+## ❓ FAQ
+
+### 🔐 Безопасно ли передавать API-ключ?
+
+**Да**, если соблюдаете правила:
+- ✅ Используйте HTTPS (всегда)
+- ✅ Храните ключ в переменных окружения, не в коде
+- ✅ Не логируйте ключ
+- ❌ Не коммитьте ключ в Git
+- ❌ Не передавайте ключ в URL-параметрах
+
+### 🔄 Что делать, если ключ скомпрометирован?
+
+1. Откройте `/api-keys` в веб-интерфейсе
+2. Нажмите **"Пересоздать"** (старый ключ будет удалён, создан новый)
+3. Обновите ключ во всех ваших приложениях
+
+> ⚠️ Старый ключ перестаёт работать мгновенно после пересоздания.
+
+### 💳 Можно ли использовать API без кредитов?
+
+Да, у каждого пользователя есть **бесплатный дневной лимит** (5 запросов/сутки), который сбрасывается каждые 24 часа. После исчерпания и дневного лимита, и кредитов — API вернёт `402 quota_exceeded`.
+
+### ⚡ Почему `quota_incremented: false`?
+
+Это нормально в трёх случаях:
+1. **Cache hit** — этот URL уже конвертировался недавно (бесплатно)
+2. **Анонимный запрос** — без API-ключа (лимит по IP)
+3. **Ошибка инкремента** — редкая системная ошибка (запрос всё равно выполнен)
+
+### 🌍 Работает ли API с любым сайтом?
+
+API работает с любыми **публично доступными HTML-страницами**. Ограничения:
+- ❌ Сайты, требующие авторизации
+- ❌ Сайты с жёсткой анти-бот защитой (Cloudflare challenge и т.п.)
+- ❌ PDF, изображения и другие не-HTML форматы
+- ❌ Сайты, блокирующие по User-Agent
+
+### 📦 Есть ли лимит на размер страницы?
+
+- Максимум: **5 MB** контента после конвертации
+- Типичные статьи: 10-100 KB
+- Длинные технические документы: 200-500 KB
+
+Страницы больше 5 MB не кэшируются, но всё равно конвертируются.
+
+### 🔗 Как использовать API-ключ с другими инструментами?
+
+**Postman:**
+- Вкладка **Authorization** → Type: **API Key**
+- Key: `X-API-Key`, Value: ваш ключ, Add to: **Header**
+
+**Insomnia:**
+- Request → **Auth** → **API Key**
+- Header name: `X-API-Key`
+
+**VS Code (REST Client):**
+```http
+@apiKey = mk_live_YOUR_KEY
+
+POST https://markclean.online/api/convert
+Content-Type: application/json
+X-API-Key: {{apiKey}}
+
+{
+ "url": "https://example.com"
+}
+```
+
+### 🗑️ Что будет с API-ключом после удаления аккаунта?
+
+**Ключ продолжит работать**, пока не закончатся кредиты. После удаления аккаунта:
+- ✅ API-ключ остаётся активным
+- ✅ Существующие кредиты можно использовать
+- ❌ Пополнить кредиты нельзя (нет доступа к UI)
+- ❌ Создать новый ключ нельзя
+
+Это сделано для честности: вы оплатили кредиты — вы их используете.
+
+### 🛠️ Есть ли SDK?
+
+Официальных SDK пока нет, но API достаточно простой для использования через стандартные HTTP-клиенты в любом языке. Примеры выше покрывают самые популярные языки.
+
+---
+
+## 📞 Поддержка
+
+- 📧 **Email:** gnutenko.s.r@gmail.com
+- 💬 **Telegram:** [@Slayder12](https://t.me/Slayder12)
+- 📝 **Changelog:** [markclean-doc/changelog.md](https://gitea.gsrhost.ru/gsr/markclean-doc/src/branch/main/changelog.md)
+
+---
+
+**Последнее обновление:** 27 сентября 2026
+**Версия API:** `0.7.5-beta`
\ No newline at end of file