# 📖 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.5.6-beta`