♻ update
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
> **Версия API:** `v1`
|
> **Версия API:** `v1`
|
||||||
> **Базовый URL:** `https://markclean.online/api`
|
> **Базовый URL:** `https://markclean.online/api`
|
||||||
> **Формат данных:** JSON
|
> **Формат данных:** JSON
|
||||||
> **Аутентификация:** API-ключ (Bearer token / X-API-Key)
|
> **Аутентификация:** API-ключ (`X-API-Key` / `Authorization: Bearer`) для `/convert` и `/converter/history`; HTTP-сессия (cookie `session_id`) для учётной, API-key и биллинга
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -15,10 +15,13 @@
|
|||||||
4. [Конвертация URL в Markdown](#-конвертация-url-в-markdown)
|
4. [Конвертация URL в Markdown](#-конвертация-url-в-markdown)
|
||||||
5. [История конвертаций](#-история-конвертаций)
|
5. [История конвертаций](#-история-конвертаций)
|
||||||
6. [Опции конвертации](#-опции-конвертации)
|
6. [Опции конвертации](#-опции-конвертации)
|
||||||
7. [Обработка ошибок](#-обработка-ошибок)
|
7. [API-ключи](#-api-ключи)
|
||||||
8. [Квоты и лимиты](#-квоты-и-лимиты)
|
8. [Биллинг: пакеты и подписки](#-биллингпакеты-и-подписки)
|
||||||
9. [Примеры кода](#-примеры-кода)
|
9. [Проверка баланса](#-проверка-баланса)
|
||||||
10. [FAQ](#-faq)
|
10. [Обработка ошибок](#-обработка-ошибок)
|
||||||
|
11. [Квоты, лимиты и кэширование](#-квотылимиты-и-кэширование)
|
||||||
|
12. [Примеры кода](#-примеры-кода)
|
||||||
|
13. [FAQ](#-faq)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,8 +33,11 @@ MarkClean API позволяет программно конвертироват
|
|||||||
- 🌐 Конвертация любой HTML-страницы в Markdown
|
- 🌐 Конвертация любой HTML-страницы в Markdown
|
||||||
- 🧹 Автоматическая очистка от мусора (реклама, навигация, футеры) через Readability
|
- 🧹 Автоматическая очистка от мусора (реклама, навигация, футеры) через Readability
|
||||||
- ⚡ Двухуровневое кэширование — повторные запросы не списывают кредиты
|
- ⚡ Двухуровневое кэширование — повторные запросы не списывают кредиты
|
||||||
- 🎛️ Гибкие опции вывода (заголовок, frontmatter, удаление картинок/ссылок)
|
- 🎛️ Гибкие опции вывода (заголовок, frontmatter, оглавление, удаление картинок/ссылок)
|
||||||
- 📊 История всех конвертаций с дедупликацией
|
- 📊 История всех конвертаций с дедупликацией
|
||||||
|
- 💳 Система кредитов + ежедневный лимит + подписка
|
||||||
|
- 🗝️ Управление API-ключами (создание, чтение, удаление)
|
||||||
|
- 📦 Пакеты кредитов и подписки
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -50,7 +56,7 @@ API-ключ можно создать **только** через веб-инт
|
|||||||
|
|
||||||
```
|
```
|
||||||
mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ
|
mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ
|
||||||
│ └── 32 символа base64url (криптографически случайные)
|
│ └── 32 символа base64url (криптографически случайные, 24 байта)
|
||||||
└── префикс (8 символов)
|
└── префикс (8 символов)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -62,27 +68,40 @@ mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ
|
|||||||
|
|
||||||
## 🛡️ Аутентификация
|
## 🛡️ Аутентификация
|
||||||
|
|
||||||
|
API поддерживает два независимых способа авторизации:
|
||||||
|
|
||||||
|
### 1. API-ключ (для API-клиентов)
|
||||||
|
|
||||||
Все запросы к защищённым эндпоинтам должны содержать API-ключ в одном из заголовков:
|
Все запросы к защищённым эндпоинтам должны содержать API-ключ в одном из заголовков:
|
||||||
|
|
||||||
### Способ 1: `X-API-Key` (рекомендуется)
|
#### Способ 1: `X-API-Key` (рекомендуется)
|
||||||
|
|
||||||
```http
|
```http
|
||||||
X-API-Key: mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ
|
X-API-Key: mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ
|
||||||
```
|
```
|
||||||
|
|
||||||
### Способ 2: `Authorization: Bearer`
|
#### Способ 2: `Authorization: Bearer`
|
||||||
|
|
||||||
```http
|
```http
|
||||||
Authorization: Bearer mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ
|
Authorization: Bearer mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ
|
||||||
```
|
```
|
||||||
|
|
||||||
Оба способа равнозначны. При отсутствии или невалидности ключа сервер вернёт `401 Unauthorized`.
|
Оба способа равнозначны. При отсутствии или невалидности ключа сервер вернёт `401 Unauthorized` с ошибкой `invalid_api_key`.
|
||||||
|
|
||||||
|
### 2. Сессия (для веб-интерфейса и защищённых эндпоинтов)
|
||||||
|
|
||||||
|
Для веб-интерфейса и защищённых эндпоинтов (API-ключи, биллинг, история, профиль) используется HTTP-сессия:
|
||||||
|
|
||||||
|
- Cookie: `session_id` (UUID)
|
||||||
|
- Устанавливается при регистрации/логине
|
||||||
|
- Живёт 30 дней
|
||||||
|
- Передаётся автоматически браузером
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔄 Конвертация URL в Markdown
|
## 🔄 Конвертация URL в Markdown
|
||||||
|
|
||||||
Основной эндпоинт API.
|
Основной эндпоинт API. Доступен **анонимам** и **пользователям с API-ключом**.
|
||||||
|
|
||||||
### Запрос
|
### Запрос
|
||||||
|
|
||||||
@@ -92,6 +111,8 @@ Content-Type: application/json
|
|||||||
X-API-Key: mk_live_YOUR_KEY_HERE
|
X-API-Key: mk_live_YOUR_KEY_HERE
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Примечание:** Для анонимов API-ключ не нужен, но в этом случае применяется IP-лимит (3 запроса/24 часа).
|
||||||
|
|
||||||
### Тело запроса
|
### Тело запроса
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -99,6 +120,7 @@ X-API-Key: mk_live_YOUR_KEY_HERE
|
|||||||
"url": "https://example.com/article",
|
"url": "https://example.com/article",
|
||||||
"include_title": true,
|
"include_title": true,
|
||||||
"include_frontmatter": false,
|
"include_frontmatter": false,
|
||||||
|
"include_toc": false,
|
||||||
"strip_images": false,
|
"strip_images": false,
|
||||||
"strip_links": false
|
"strip_links": false
|
||||||
}
|
}
|
||||||
@@ -109,6 +131,7 @@ X-API-Key: mk_live_YOUR_KEY_HERE
|
|||||||
| `url` | string | ✅ | — | URL веб-страницы для конвертации (http/https) |
|
| `url` | string | ✅ | — | URL веб-страницы для конвертации (http/https) |
|
||||||
| `include_title` | bool | ❌ | `false` | Добавить заголовок страницы как H1 (`# Title`) |
|
| `include_title` | bool | ❌ | `false` | Добавить заголовок страницы как H1 (`# Title`) |
|
||||||
| `include_frontmatter` | bool | ❌ | `false` | Добавить YAML-шапку с метаданными |
|
| `include_frontmatter` | bool | ❌ | `false` | Добавить YAML-шапку с метаданными |
|
||||||
|
| `include_toc` | bool | ❌ | `false` | Добавить оглавление (Table of Contents) |
|
||||||
| `strip_images` | bool | ❌ | `false` | Удалить все `<img>` теги |
|
| `strip_images` | bool | ❌ | `false` | Удалить все `<img>` теги |
|
||||||
| `strip_links` | bool | ❌ | `false` | Удалить `<a>` теги (текст сохраняется) |
|
| `strip_links` | bool | ❌ | `false` | Удалить `<a>` теги (текст сохраняется) |
|
||||||
|
|
||||||
@@ -118,6 +141,11 @@ X-API-Key: mk_live_YOUR_KEY_HERE
|
|||||||
{
|
{
|
||||||
"title": "Example Article",
|
"title": "Example Article",
|
||||||
"content": "# Example Article\n\nThis is the converted content...",
|
"content": "# Example Article\n\nThis is the converted content...",
|
||||||
|
"metadata": {
|
||||||
|
"words": 4521,
|
||||||
|
"tokens": 1234,
|
||||||
|
"bytes": 4521
|
||||||
|
},
|
||||||
"quota_incremented": true
|
"quota_incremented": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -126,6 +154,9 @@ X-API-Key: mk_live_YOUR_KEY_HERE
|
|||||||
|------|-----|----------|
|
|------|-----|----------|
|
||||||
| `title` | string | Заголовок страницы (может отсутствовать) |
|
| `title` | string | Заголовок страницы (может отсутствовать) |
|
||||||
| `content` | string | Результат конвертации в Markdown |
|
| `content` | string | Результат конвертации в Markdown |
|
||||||
|
| `metadata.words` | int | Количество слов в контенте |
|
||||||
|
| `metadata.tokens` | int | Количество токенов (для LLM, cl100k) |
|
||||||
|
| `metadata.bytes` | int | Размер в байтах |
|
||||||
| `quota_incremented` | bool | `true` — списан кредит; `false` — cache hit или лимит |
|
| `quota_incremented` | bool | `true` — списан кредит; `false` — cache hit или лимит |
|
||||||
|
|
||||||
### Пример cURL
|
### Пример cURL
|
||||||
@@ -137,7 +168,9 @@ curl -X POST https://markclean.online/api/convert \
|
|||||||
-d '{
|
-d '{
|
||||||
"url": "https://metanit.com/web/html5/2.6.php",
|
"url": "https://metanit.com/web/html5/2.6.php",
|
||||||
"include_title": true,
|
"include_title": true,
|
||||||
"strip_images": true
|
"include_frontmatter": true,
|
||||||
|
"strip_images": true,
|
||||||
|
"strip_links": false
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -147,6 +180,8 @@ curl -X POST https://markclean.online/api/convert \
|
|||||||
|
|
||||||
Получить список всех конвертаций текущего пользователя с пагинацией.
|
Получить список всех конвертаций текущего пользователя с пагинацией.
|
||||||
|
|
||||||
|
**Требует авторизации** (API-ключ или сессия).
|
||||||
|
|
||||||
### Запрос
|
### Запрос
|
||||||
|
|
||||||
```http
|
```http
|
||||||
@@ -168,8 +203,6 @@ X-API-Key: mk_live_YOUR_KEY_HERE
|
|||||||
"id": "27c1e865-a77e-4d99-87d0-1179aef2a3e6",
|
"id": "27c1e865-a77e-4d99-87d0-1179aef2a3e6",
|
||||||
"title": "Example Article",
|
"title": "Example Article",
|
||||||
"target_url": "https://example.com/article",
|
"target_url": "https://example.com/article",
|
||||||
"count_usage": 3,
|
|
||||||
"created_at": "2026-09-27T10:00:00Z",
|
|
||||||
"updated_at": "2026-09-27T14:30:00Z"
|
"updated_at": "2026-09-27T14:30:00Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -186,15 +219,13 @@ X-API-Key: mk_live_YOUR_KEY_HERE
|
|||||||
| `items[].id` | string | UUID записи |
|
| `items[].id` | string | UUID записи |
|
||||||
| `items[].title` | string | Заголовок страницы (или пустая строка) |
|
| `items[].title` | string | Заголовок страницы (или пустая строка) |
|
||||||
| `items[].target_url` | string | Исходный URL |
|
| `items[].target_url` | string | Исходный URL |
|
||||||
| `items[].count_usage` | int | Сколько раз этот URL конвертировался |
|
|
||||||
| `items[].created_at` | string | Первая конвертация (ISO 8601) |
|
|
||||||
| `items[].updated_at` | string | Последняя конвертация (ISO 8601) |
|
| `items[].updated_at` | string | Последняя конвертация (ISO 8601) |
|
||||||
| `total_count` | int | Общее количество записей |
|
| `total_count` | int | Общее количество записей (только успешные, status_code = 200) |
|
||||||
| `page` | int | Текущая страница |
|
| `page` | int | Текущая страница |
|
||||||
| `page_size` | int | Размер страницы |
|
| `page_size` | int | Размер страницы |
|
||||||
| `has_more` | bool | Есть ли следующие страницы |
|
| `has_more` | bool | Есть ли следующие страницы |
|
||||||
|
|
||||||
> 💡 **Дедупликация:** Один URL = одна запись. Повторные конвертации увеличивают `count_usage` и обновляют `updated_at`, перемещая запись наверх.
|
> 💡 **Дедупликация:** Один URL = одна запись. Повторные конвертации обновляют `updated_at` и перемещают запись наверх.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -218,8 +249,12 @@ This is the content...
|
|||||||
---
|
---
|
||||||
title: "Example Article"
|
title: "Example Article"
|
||||||
source: "https://example.com/article"
|
source: "https://example.com/article"
|
||||||
bytes: 4521
|
language: "English"
|
||||||
|
words: 4521
|
||||||
tokens: 1234
|
tokens: 1234
|
||||||
|
bytes: 4521
|
||||||
|
reading_time: "23 min"
|
||||||
|
extracted_at: "2026-09-27"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Example Article
|
# Example Article
|
||||||
@@ -227,6 +262,25 @@ tokens: 1234
|
|||||||
This is the content...
|
This is the content...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Поле `language` определяется автоматически через `whatlanggo` (только если включён `include_toc` или `include_frontmatter`).
|
||||||
|
|
||||||
|
### `include_toc: true`
|
||||||
|
|
||||||
|
Добавляет оглавление (Table of Contents) из заголовков H1–H3:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Main Section](<#Main Section>)
|
||||||
|
- [Subsection](<#Subsection>)
|
||||||
|
|
||||||
|
# Main Section
|
||||||
|
|
||||||
|
Content...
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Оглавление добавляется только если заголовков H1–H3 **не менее 2**. Иначе оглавление не включается.
|
||||||
|
|
||||||
### `strip_images: true`
|
### `strip_images: true`
|
||||||
|
|
||||||
Полностью удаляет все `<img>` теги. Полезно для:
|
Полностью удаляет все `<img>` теги. Полезно для:
|
||||||
@@ -259,6 +313,261 @@ Visit our site for more info.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔑 API-ключи
|
||||||
|
|
||||||
|
Управление API-ключами — **только через сессию** (веб-интерфейс). API-ключами нельзя управлять через API.
|
||||||
|
|
||||||
|
### Создание ключа
|
||||||
|
|
||||||
|
**POST** `/api/keys`
|
||||||
|
|
||||||
|
**Запрос:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "My Key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`name` необязательно (макс. 50 символов). Если не указан, используется `"Default Key"`.
|
||||||
|
|
||||||
|
**Ответ (201 Created):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "27c1e865-a77e-4d99-87d0-1179aef2a3e6",
|
||||||
|
"name": "My Key",
|
||||||
|
"api_key": "mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ",
|
||||||
|
"warning": "Save this key now. It will not be shown again."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⭐ Сырой ключ возвращается **только в этом ответе**. Больше его никто не покажет.
|
||||||
|
|
||||||
|
**Ошибки:**
|
||||||
|
- `409 Conflict` + `key_already_exists` — ключ уже существует (у пользователя только 1 ключ)
|
||||||
|
- `500 Internal Server Error` + `internal_error`
|
||||||
|
|
||||||
|
### Получение метаданных ключа
|
||||||
|
|
||||||
|
**GET** `/api/keys`
|
||||||
|
|
||||||
|
**Ответ (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "27c1e865-a77e-4d99-87d0-1179aef2a3e6",
|
||||||
|
"name": "My Key",
|
||||||
|
"key_prefix": "mk_live_a1b2c3d4",
|
||||||
|
"last_used_at": "2026-09-27 10:30:00",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2026-09-27 09:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ошибки:**
|
||||||
|
- `404 Not Found` + `key_not_found` — у пользователя нет ключа
|
||||||
|
|
||||||
|
### Удаление ключа
|
||||||
|
|
||||||
|
**DELETE** `/api/keys/:key_id`
|
||||||
|
|
||||||
|
**Ответ (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "API key deleted successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ошибки:**
|
||||||
|
- `400 Bad Request` + `invalid_key_id` — некорректный UUID
|
||||||
|
- `404 Not Found` + `key_not_found` — ключ не найден
|
||||||
|
- `500 Internal Server Error` + `internal_error`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💳 Биллинг: пакеты и подписки
|
||||||
|
|
||||||
|
### Получение доступных пакетов
|
||||||
|
|
||||||
|
**GET** `/api/billing/packages` — **публичный** эндпоинт (авторизация не требуется).
|
||||||
|
|
||||||
|
**Ответ (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"free": {
|
||||||
|
"name": "Free",
|
||||||
|
"daily_limit": 10
|
||||||
|
},
|
||||||
|
"credits": [
|
||||||
|
{
|
||||||
|
"code": "micro",
|
||||||
|
"name": "Micro",
|
||||||
|
"credits": 300,
|
||||||
|
"price": 49,
|
||||||
|
"price_per_100_requests": 17,
|
||||||
|
"discount": 0,
|
||||||
|
"savings_rub": 0,
|
||||||
|
"is_popular": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "start",
|
||||||
|
"name": "Start",
|
||||||
|
"credits": 1000,
|
||||||
|
"price": 99,
|
||||||
|
"price_per_100_requests": 9,
|
||||||
|
"discount": 32,
|
||||||
|
"savings_rub": 17,
|
||||||
|
"is_popular": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "optimal",
|
||||||
|
"name": "Optimal",
|
||||||
|
"credits": 3000,
|
||||||
|
"price": 199,
|
||||||
|
"price_per_100_requests": 7,
|
||||||
|
"discount": 42,
|
||||||
|
"savings_rub": 27,
|
||||||
|
"is_popular": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pro",
|
||||||
|
"name": "Pro",
|
||||||
|
"credits": 8000,
|
||||||
|
"price": 399,
|
||||||
|
"price_per_100_requests": 5,
|
||||||
|
"discount": 51,
|
||||||
|
"savings_rub": 57,
|
||||||
|
"is_popular": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ultra",
|
||||||
|
"name": "Ultra",
|
||||||
|
"credits": 20000,
|
||||||
|
"price": 799,
|
||||||
|
"price_per_100_requests": 4,
|
||||||
|
"discount": 63,
|
||||||
|
"savings_rub": 124,
|
||||||
|
"is_popular": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subscriptions": [
|
||||||
|
{
|
||||||
|
"months": 1,
|
||||||
|
"discount_percent": 0,
|
||||||
|
"total_price": 499,
|
||||||
|
"monthly_price": 499,
|
||||||
|
"savings_rub": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"months": 3,
|
||||||
|
"discount_percent": 5,
|
||||||
|
"total_price": 1419,
|
||||||
|
"monthly_price": 473,
|
||||||
|
"savings_rub": 78
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"months": 6,
|
||||||
|
"discount_percent": 10,
|
||||||
|
"total_price": 2690,
|
||||||
|
"monthly_price": 448,
|
||||||
|
"savings_rub": 304
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"months": 12,
|
||||||
|
"discount_percent": 19,
|
||||||
|
"total_price": 4790,
|
||||||
|
"monthly_price": 399,
|
||||||
|
"savings_rub": 1198
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Поле | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `free` | Бесплатный план: дневной лимит |
|
||||||
|
| `credits[]` | Пакеты кредитов (от дешёвого к дорогому) |
|
||||||
|
| `credits[].code` | Код пакета (`micro`, `start`, `optimal`, `pro`, `ultra`) |
|
||||||
|
| `credits[].credits` | Количество кредитов в пакете |
|
||||||
|
| `credits[].price` | Цена в рублях |
|
||||||
|
| `credits[].price_per_100_requests` | Стоимость 100 запросов |
|
||||||
|
| `credits[].discount` | Процент выгоды (от базового тарифа Micro) |
|
||||||
|
| `credits[].savings_rub` | Экономия в рублях |
|
||||||
|
| `credits[].is_popular` | Флаг «Хит» |
|
||||||
|
| `subscriptions[]` | Пакеты подписки (от короткого к длинному) |
|
||||||
|
|
||||||
|
### Покупка пакета
|
||||||
|
|
||||||
|
**POST** `/api/billing/purchase` — **требует авторизации** (API-ключ или сессия).
|
||||||
|
|
||||||
|
> ⚠️ **Временный endpoint!** В продакшене будет заменён на реальную интеграцию с платёжной системой.
|
||||||
|
|
||||||
|
**Запрос:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"package_code": "optimal"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Credits added successfully",
|
||||||
|
"package": "optimal",
|
||||||
|
"credits_added": 3000,
|
||||||
|
"new_balance": 3000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ошибки:**
|
||||||
|
- `400 Bad Request` + `invalid_package` — некорректный код пакета
|
||||||
|
- `401 Unauthorized` + `unauthorized` — нет аутентификации
|
||||||
|
- `500 Internal Server Error` + `purchase_failed` — ошибка начисления кредитов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Проверка баланса
|
||||||
|
|
||||||
|
### Получение баланса кредитов и подписки
|
||||||
|
|
||||||
|
**GET** `/api/billing/usage` — **требует авторизации** (API-ключ или сессия).
|
||||||
|
|
||||||
|
**Ответ (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"credits": {
|
||||||
|
"credits": 2500,
|
||||||
|
"daily_used": 3,
|
||||||
|
"daily_limit": 10,
|
||||||
|
"daily_remaining": 7,
|
||||||
|
"has_credits": true,
|
||||||
|
"daily_reset_at": "2026-09-27T14:00:00Z"
|
||||||
|
},
|
||||||
|
"subscription": {
|
||||||
|
"is_active": true,
|
||||||
|
"ends_at": "2026-10-01T10:00:00Z",
|
||||||
|
"days_remaining": 4,
|
||||||
|
"is_unlimited": true
|
||||||
|
},
|
||||||
|
"is_subscriber": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Поле | Тип | Описание |
|
||||||
|
|------|-----|----------|
|
||||||
|
| `credits` | object | Баланс кредитов |
|
||||||
|
| `credits.credits` | int | Платные кредиты (не сгорают) |
|
||||||
|
| `credits.daily_used` | int | Использовано запросов сегодня |
|
||||||
|
| `credits.daily_limit` | int | Дневной лимит (10) |
|
||||||
|
| `credits.daily_remaining` | int | Осталось запросов сегодня |
|
||||||
|
| `credits.has_credits` | bool | Есть ли платные кредиты |
|
||||||
|
| `credits.daily_reset_at` | time | Время следующего сброса дневного лимита |
|
||||||
|
| `subscription` | object/null | Информация об активной подписке |
|
||||||
|
| `subscription.is_active` | bool | Подписка активна |
|
||||||
|
| `subscription.ends_at` | time | Дата окончания подписки |
|
||||||
|
| `subscription.days_remaining` | int | Осталось дней |
|
||||||
|
| `subscription.is_unlimited` | bool | Безлимит (всегда true) |
|
||||||
|
| `is_subscriber` | bool | Есть ли активная подписка |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ⚠️ Обработка ошибок
|
## ⚠️ Обработка ошибок
|
||||||
|
|
||||||
Все ошибки возвращаются в едином формате:
|
Все ошибки возвращаются в едином формате:
|
||||||
@@ -276,15 +585,28 @@ Visit our site for more info.
|
|||||||
|:------:|------------|----------|----------|
|
|:------:|------------|----------|----------|
|
||||||
| `400` | `invalid_payload` | Битый JSON или отсутствует `url` | Проверьте тело запроса |
|
| `400` | `invalid_payload` | Битый JSON или отсутствует `url` | Проверьте тело запроса |
|
||||||
| `400` | `invalid_url` | Некорректный формат URL | Должен начинаться с `http://` или `https://` |
|
| `400` | `invalid_url` | Некорректный формат URL | Должен начинаться с `http://` или `https://` |
|
||||||
|
| `400` | `invalid_request` | Некорректный запрос (биллинг) | Проверьте параметры |
|
||||||
|
| `400` | `invalid_package` | Неверный код пакета | Используйте валидный код |
|
||||||
| `401` | `invalid_api_key` | Ключ отсутствует, невалиден или удалён | Проверьте заголовок `X-API-Key` |
|
| `401` | `invalid_api_key` | Ключ отсутствует, невалиден или удалён | Проверьте заголовок `X-API-Key` |
|
||||||
| `401` | `unauthorized` | Нет аутентификации | Передайте API-ключ |
|
| `401` | `unauthorized` | Нет аутентификации | Передайте API-ключ или cookie |
|
||||||
| `402` | `quota_exceeded` | Кредиты закончились + дневной лимит | Купите кредиты в веб-интерфейсе |
|
| `402` | `quota_exceeded` | Кредиты закончились + дневной лимит | Купите кредиты в веб-интерфейсе |
|
||||||
| `404` | `key_not_found` | Ключ не найден (для истории) | Создайте ключ заново |
|
| `404` | `key_not_found` | Ключ не найден | Создайте ключ заново |
|
||||||
|
| `404` | `key_not_found` | Ключ пользователя не найден (GET /api/keys) | Создайте ключ |
|
||||||
|
| `404` | `user_not_found` | Пользователь не найден | Проверьте данные |
|
||||||
|
| `409` | `key_already_exists` | У пользователя уже есть API-ключ | Удалите существующий ключ |
|
||||||
|
| `409` | `user_exists` | Пользователь с этим email уже зарегистрирован | Используйте другой email |
|
||||||
|
| `409` | `account_already_deleted` | Аккаунт уже удалён | — |
|
||||||
|
| `410` | `email_previously_deleted` | Email был использован для удалённого аккаунта | Используйте другой email |
|
||||||
| `415` | `unsupported_content` | URL не возвращает HTML | API работает только с веб-страницами |
|
| `415` | `unsupported_content` | URL не возвращает HTML | API работает только с веб-страницами |
|
||||||
| `422` | `conversion_failed` | Не удалось распознать контент | Попробуйте другой URL |
|
| `422` | `conversion_failed` | Не удалось распознать контент | Попробуйте другой URL |
|
||||||
| `429` | `rate_limit_exceeded` | Превышен лимит запросов | Подождите или войдите в аккаунт |
|
| `429` | `rate_limit_exceeded` | Превышен лимит запросов | Подождите или войдите в аккаунт |
|
||||||
| `502` | `fetch_failed` | Не удалось загрузить страницу | Сайт недоступен или заблокировал запрос |
|
| `429` | `registration_ip_limit` | Превышен лимит регистраций с IP | Подождите |
|
||||||
|
| `429` | `resend_cooldown` | Слишком часто запрашивается OTP | Подождите |
|
||||||
|
| `429` | `otp_blocked` | Слишком много неудачных попыток OTP | Запросите новый код |
|
||||||
|
| `429` | `registration_ip_limit` | Превышен лимит регистраций с IP | Подождите |
|
||||||
|
| `429` | `registration_ip_limit` | Превышен лимит регистраций с IP | Подождите |
|
||||||
| `500` | `internal_server_error` | Внутренняя ошибка сервера | Повторите позже |
|
| `500` | `internal_server_error` | Внутренняя ошибка сервера | Повторите позже |
|
||||||
|
| `502` | `fetch_failed` | Не удалось загрузить страницу | Сайт недоступен или заблокировал запрос |
|
||||||
|
|
||||||
### Пример обработки ошибки
|
### Пример обработки ошибки
|
||||||
|
|
||||||
@@ -303,13 +625,14 @@ HTTP/1.1 401 Unauthorized
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 Квоты и лимиты
|
## 📊 Квоты, лимиты и кэширование
|
||||||
|
|
||||||
### Система кредитов
|
### Система кредитов
|
||||||
|
|
||||||
- 💳 **Платные кредиты** — не сгорают, не имеют срока действия
|
- 💳 **Платные кредиты** — не сгорают, не имеют срока действия
|
||||||
- 📅 **Дневной лимит** (5 запросов/сутки) — сбрасывается каждые 24 часа
|
- 📅 **Дневной лимит** (10 запросов/сутки) — сбрасывается каждые 24 часа
|
||||||
- ⚡ **Приоритет списания:** сначала платные кредиты, затем дневной лимит
|
- ⚡ **Приоритет списания:** сначала платные кредиты, затем дневной лимит
|
||||||
|
- 📊 **Максимальный баланс:** 500 000 кредитов
|
||||||
|
|
||||||
### Кэширование и экономия
|
### Кэширование и экономия
|
||||||
|
|
||||||
@@ -318,7 +641,7 @@ API использует **двухуровневое кэширование**:
|
|||||||
| Уровень | Что кэшируется | TTL | Экономия |
|
| Уровень | Что кэшируется | TTL | Экономия |
|
||||||
|---------|----------------|-----|----------|
|
|---------|----------------|-----|----------|
|
||||||
| **L1** | Сырой HTML | 30 мин | Нет HTTP-запроса к внешнему сайту |
|
| **L1** | Сырой HTML | 30 мин | Нет HTTP-запроса к внешнему сайту |
|
||||||
| **L2** | Финальный Markdown | 5 мин | Мгновенный ответ |
|
| **L2** | Финальный Markdown | 15 мин | Мгновенный ответ |
|
||||||
|
|
||||||
**Важно:** Кредиты списываются **только** при реальном HTTP-запросе к внешнему сайту. Повторные конвертации того же URL (даже с другими опциями) **бесплатны**, если HTML ещё в кэше.
|
**Важно:** Кредиты списываются **только** при реальном HTTP-запросе к внешнему сайту. Повторные конвертации того же URL (даже с другими опциями) **бесплатны**, если HTML ещё в кэше.
|
||||||
|
|
||||||
@@ -331,9 +654,20 @@ API использует **двухуровневое кэширование**:
|
|||||||
| Повтор с другими опциями | ❌ | 0 | ~200 мс |
|
| Повтор с другими опциями | ❌ | 0 | ~200 мс |
|
||||||
| Спустя 30+ минут | ✅ | −1 | ~2-5 сек |
|
| Спустя 30+ минут | ✅ | −1 | ~2-5 сек |
|
||||||
|
|
||||||
|
### IP-лимиты (для анонимов)
|
||||||
|
|
||||||
|
- **Конвертация:** 3 запроса / 24 часа с одного IP
|
||||||
|
- **Регистрация:** 3 регистрации / 1 час с одного IP (глобально), 3 регистрации / 2 минуты с одного IP
|
||||||
|
- **Логин:** 10 попыток / 30 минут с одного IP, 5 попыток / 15 минут на email
|
||||||
|
- **Recovery OTP:** 10 попыток / 1 час с одного IP
|
||||||
|
- **OTP Verify:** 20 попыток / 5 минут с одного IP
|
||||||
|
- **Смена пароля:** 5 попыток / 1 час с одного IP, 3 попытки / 1 час на пользователя
|
||||||
|
|
||||||
### Проверка баланса
|
### Проверка баланса
|
||||||
|
|
||||||
Для проверки баланса кредитов используйте веб-интерфейс (эндпоинт `GET /api/billing/credits` доступен только через сессию, не через API-ключ).
|
Для проверки баланса кредитов используйте:
|
||||||
|
- Веб-интерфейс (страница **Billing**)
|
||||||
|
- API: **GET** `/api/billing/usage` (требует авторизации)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -359,13 +693,17 @@ response = requests.post(
|
|||||||
json={
|
json={
|
||||||
"url": "https://example.com/article",
|
"url": "https://example.com/article",
|
||||||
"include_title": True,
|
"include_title": True,
|
||||||
|
"include_frontmatter": True,
|
||||||
"strip_images": True,
|
"strip_images": True,
|
||||||
|
"strip_links": False,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.ok:
|
if response.ok:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
print(f"Title: {data.get('title')}")
|
print(f"Title: {data.get('title')}")
|
||||||
|
print(f"Words: {data['metadata']['words']}")
|
||||||
|
print(f"Tokens: {data['metadata']['tokens']}")
|
||||||
print(f"Credits used: {data['quota_incremented']}")
|
print(f"Credits used: {data['quota_incremented']}")
|
||||||
print(data["content"][:500])
|
print(data["content"][:500])
|
||||||
else:
|
else:
|
||||||
@@ -428,6 +766,7 @@ async function convertUrl(url, options = {}) {
|
|||||||
url,
|
url,
|
||||||
include_title: options.title ?? false,
|
include_title: options.title ?? false,
|
||||||
include_frontmatter: options.frontmatter ?? false,
|
include_frontmatter: options.frontmatter ?? false,
|
||||||
|
include_toc: options.toc ?? false,
|
||||||
strip_images: options.stripImages ?? false,
|
strip_images: options.stripImages ?? false,
|
||||||
strip_links: options.stripLinks ?? false,
|
strip_links: options.stripLinks ?? false,
|
||||||
}),
|
}),
|
||||||
@@ -445,6 +784,7 @@ async function convertUrl(url, options = {}) {
|
|||||||
try {
|
try {
|
||||||
const result = await convertUrl('https://example.com/article', {
|
const result = await convertUrl('https://example.com/article', {
|
||||||
title: true,
|
title: true,
|
||||||
|
frontmatter: true,
|
||||||
stripImages: true,
|
stripImages: true,
|
||||||
});
|
});
|
||||||
console.log(result.content);
|
console.log(result.content);
|
||||||
@@ -475,13 +815,21 @@ type ConvertRequest struct {
|
|||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
IncludeTitle bool `json:"include_title"`
|
IncludeTitle bool `json:"include_title"`
|
||||||
IncludeFrontmatter bool `json:"include_frontmatter"`
|
IncludeFrontmatter bool `json:"include_frontmatter"`
|
||||||
|
IncludeTOC bool `json:"include_toc"`
|
||||||
StripImages bool `json:"strip_images"`
|
StripImages bool `json:"strip_images"`
|
||||||
StripLinks bool `json:"strip_links"`
|
StripLinks bool `json:"strip_links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Words int `json:"words"`
|
||||||
|
Tokens int `json:"tokens"`
|
||||||
|
Bytes int `json:"bytes"`
|
||||||
|
}
|
||||||
|
|
||||||
type ConvertResponse struct {
|
type ConvertResponse struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Metadata Metadata `json:"metadata"`
|
||||||
QuotaIncremented bool `json:"quota_incremented"`
|
QuotaIncremented bool `json:"quota_incremented"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,7 +837,9 @@ func main() {
|
|||||||
reqBody, _ := json.Marshal(ConvertRequest{
|
reqBody, _ := json.Marshal(ConvertRequest{
|
||||||
URL: "https://example.com/article",
|
URL: "https://example.com/article",
|
||||||
IncludeTitle: true,
|
IncludeTitle: true,
|
||||||
|
IncludeFrontmatter: true,
|
||||||
StripImages: true,
|
StripImages: true,
|
||||||
|
StripLinks: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
req, _ := http.NewRequest("POST", apiBase+"/convert", bytes.NewReader(reqBody))
|
req, _ := http.NewRequest("POST", apiBase+"/convert", bytes.NewReader(reqBody))
|
||||||
@@ -544,9 +894,27 @@ curl -X POST https://markclean.online/api/convert \
|
|||||||
"include_frontmatter": true
|
"include_frontmatter": 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,
|
||||||
|
"include_toc": true
|
||||||
|
}'
|
||||||
|
|
||||||
# История конвертаций
|
# История конвертаций
|
||||||
curl -X GET "https://markclean.online/api/converter/history?page=1&page_size=10" \
|
curl -X GET "https://markclean.online/api/converter/history?page=1&page_size=10" \
|
||||||
-H "X-API-Key: mk_live_YOUR_KEY"
|
-H "X-API-Key: mk_live_YOUR_KEY"
|
||||||
|
|
||||||
|
# Проверка баланса
|
||||||
|
curl -X GET "https://markclean.online/api/billing/usage" \
|
||||||
|
-H "X-API-Key: mk_live_YOUR_KEY"
|
||||||
|
|
||||||
|
# Получить доступные пакеты
|
||||||
|
curl -X GET "https://markclean.online/api/billing/packages"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -572,14 +940,14 @@ curl -X GET "https://markclean.online/api/converter/history?page=1&page_size=10"
|
|||||||
|
|
||||||
### 💳 Можно ли использовать API без кредитов?
|
### 💳 Можно ли использовать API без кредитов?
|
||||||
|
|
||||||
Да, у каждого пользователя есть **бесплатный дневной лимит** (5 запросов/сутки), который сбрасывается каждые 24 часа. После исчерпания и дневного лимита, и кредитов — API вернёт `402 quota_exceeded`.
|
Да, у каждого пользователя есть **бесплатный дневной лимит** (10 запросов/сутки), который сбрасывается каждые 24 часа. После исчерпания и дневного лимита, и кредитов — API вернёт `402 quota_exceeded`.
|
||||||
|
|
||||||
### ⚡ Почему `quota_incremented: false`?
|
### ⚡ Почему `quota_incremented: false`?
|
||||||
|
|
||||||
Это нормально в трёх случаях:
|
Это нормально в трёх случаях:
|
||||||
1. **Cache hit** — этот URL уже конвертировался недавно (бесплатно)
|
1. **Cache hit** — этот URL уже конвертировался недавно (бесплатно)
|
||||||
2. **Анонимный запрос** — без API-ключа (лимит по IP)
|
2. **Анонимный запрос** — без API-ключа (лимит по IP)
|
||||||
3. **Ошибка инкремента** — редкая системная ошибка (запрос всё равно выполнен)
|
3. **Подписка** — у пользователя активна подписка, кредиты не списываются
|
||||||
|
|
||||||
### 🌍 Работает ли API с любым сайтом?
|
### 🌍 Работает ли API с любым сайтом?
|
||||||
|
|
||||||
@@ -644,5 +1012,5 @@ X-API-Key: {{apiKey}}
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Последнее обновление:** 27 сентября 2026
|
**Последнее обновление:** 10 октября 2026
|
||||||
**Версия API:** `0.5.6-beta`
|
**Версия API:** `v1`
|
||||||
|
|||||||
Reference in New Issue
Block a user