From 6186b0446be60d3d3d3469b8f7235d165f4ee4ca Mon Sep 17 00:00:00 2001 From: gsr Date: Sat, 10 Oct 2026 21:06:18 +0300 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- markclean-api-en.md | 1012 +++++++++++++++++++++++++++++++++ api.md => markclean-api-ru.md | 0 2 files changed, 1012 insertions(+) create mode 100644 markclean-api-en.md rename api.md => markclean-api-ru.md (100%) diff --git a/markclean-api-en.md b/markclean-api-en.md new file mode 100644 index 0000000..9a2213f --- /dev/null +++ b/markclean-api-en.md @@ -0,0 +1,1012 @@ +# ๐Ÿ“– MarkClean API โ€” API Key Usage Documentation + +> **API Version:** `v1` +> **Base URL:** `https://markclean.online/api` +> **Data Format:** JSON +> **Authentication:** API key (`X-API-Key` / `Authorization: Bearer`) for `/convert` and `/converter/history`; HTTP session (cookie `session_id`) for account, API-key and billing endpoints + +--- + +## ๐Ÿ“‘ Table of Contents + +1. [Overview](<#๐Ÿ” Overview>) +2. [Getting an API Key](<#๐Ÿ”‘ Getting an API Key>) +3. [Authentication](<#๐Ÿ›ก๏ธ Authentication>) +4. [Converting URL to Markdown](<#๐Ÿ”„ Converting URL to Markdown>) +5. [Conversion History](<#๐Ÿ“š Conversion History>) +6. [Conversion Options](<#๐ŸŽ›๏ธ Conversion Options>) +7. [API Keys](<#๐Ÿ”‘ API Keys>) +8. [Billing: Credit Packs & Subscriptions](<#๐Ÿ’ณ Billing: Credit Packs & Subscriptions>) +9. [Balance Check](<#๐Ÿ“Š Balance Check>) +10. [Error Handling](<#โš ๏ธ Error Handling>) +11. [Quotas, Limits & Caching](<#๐Ÿ“Š Quotas, Limits & Caching>) +12. [Code Examples](<#๐Ÿ’ป Code Examples>) +13. [FAQ](<#โ“ FAQ>) + +--- + +## ๐Ÿ” Overview + +The MarkClean API allows you to programmatically convert web pages into clean Markdown, optimized for LLMs, RAG systems, and documentation. + +**Key Features:** +- ๐ŸŒ Convert any HTML page to Markdown +- ๐Ÿงน Automatic junk removal (ads, navigation, footers) via Readability +- โšก Two-tier caching โ€” repeated requests do not consume credits +- ๐ŸŽ›๏ธ Flexible output options (title, frontmatter, table of contents, image/link stripping) +- ๐Ÿ“Š Full history of all conversions with de-duplication +- ๐Ÿ’ณ Credit system + daily limit + subscriptions +- ๐Ÿ—๏ธ API key management (create, read, delete) +- ๐Ÿ“ฆ Credit packs and subscriptions + +--- + +## ๐Ÿ”‘ Getting an API Key + +An API key can be created **only** through the web interface (session authentication required). + +### Steps: + +1. Log in to your account on [markclean.online](https://markclean.online) +2. Navigate to the **API Keys** page (or open `/api-keys`) +3. Click **"Generate Key"** +4. **โš ๏ธ IMPORTANT:** Save the key immediately โ€” it will be shown **only once!** + +### Key Format + +``` +mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ +โ”‚ โ””โ”€โ”€ 32 base64url characters (cryptographically random, 24 bytes) +โ””โ”€โ”€ prefix (8 characters) +``` + +**Total length:** 40 characters + +> ๐Ÿ”’ **Security:** Only the SHA-256 hash of the key is stored in the database. If you lose the key โ€” it cannot be recovered, only a new one can be created. + +--- + +## ๐Ÿ›ก๏ธ Authentication + +The API supports two independent authentication methods: + +### 1. API Key (for API clients) + +All requests to protected endpoints must include an API key in one of the following headers: + +#### Method 1: `X-API-Key` (recommended) + +```http +X-API-Key: mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ +``` + +#### Method 2: `Authorization: Bearer` + +```http +Authorization: Bearer mk_live_Xk9mP2vQ7rT4wL8nZ3sY6tB1cD5fG0hJ +``` + +Both methods are equivalent. If the key is missing or invalid, the server returns `401 Unauthorized` with error `invalid_api_key`. + +### 2. Session (for the web interface and protected endpoints) + +For the web interface and protected endpoints (API keys, billing, history, profile), an HTTP session is used: + +- Cookie: `session_id` (UUID) +- Set on registration/login +- Expires after 30 days +- Sent automatically by the browser + +--- + +## ๐Ÿ”„ Converting URL to Markdown + +The main API endpoint. Available to **anonymous users** and **users with an API key**. + +### Request + +```http +POST /api/convert +Content-Type: application/json +X-API-Key: mk_live_YOUR_KEY_HERE +``` + +> **Note:** Anonymous users do not need an API key, but in that case the IP limit applies (3 requests / 24 hours). + +### Request Body + +```json +{ + "url": "https://example.com/article", + "include_title": true, + "include_frontmatter": false, + "include_toc": false, + "strip_images": false, + "strip_links": false +} +``` + +| Field | Type | Required | Default | Description | +|-------|------|:---------:|:-------:|-------------| +| `url` | string | โœ… | โ€” | URL of the web page to convert (http/https) | +| `include_title` | bool | โŒ | `false` | Add the page title as an H1 (`# Title`) | +| `include_frontmatter` | bool | โŒ | `false` | Add a YAML frontmatter with metadata | +| `include_toc` | bool | โŒ | `false` | Add a table of contents (TOC) | +| `strip_images` | bool | โŒ | `false` | Remove all `` tags | +| `strip_links` | bool | โŒ | `false` | Remove `` tags (text is preserved) | + +### Successful Response (200 OK) + +```json +{ + "title": "Example Article", + "content": "# Example Article\n\nThis is the converted content...", + "metadata": { + "words": 4521, + "tokens": 1234, + "bytes": 4521 + }, + "quota_incremented": true +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `title` | string | Page title (may be absent) | +| `content` | string | Markdown conversion result | +| `metadata.words` | int | Number of words in the content | +| `metadata.tokens` | int | Number of tokens (for LLM, cl100k) | +| `metadata.bytes` | int | Size in bytes | +| `quota_incremented` | bool | `true` โ€” a credit was consumed; `false` โ€” cache hit or limit reached | + +### cURL Example + +```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, + "include_frontmatter": true, + "strip_images": true, + "strip_links": false + }' +``` + +--- + +## ๐Ÿ“š Conversion History + +Retrieve the list of all conversions for the current user with pagination. + +**Authentication required** (API key or session). + +### Request + +```http +GET /api/converter/history?page=1&page_size=20 +X-API-Key: mk_live_YOUR_KEY_HERE +``` + +| Parameter | Type | Default | Description | +|-----------|------|:--------:|-------------| +| `page` | int | `1` | Page number (โ‰ฅ 1) | +| `page_size` | int | `20` | Number of records (1โ€“100) | + +### Successful Response (200 OK) + +```json +{ + "items": [ + { + "id": "27c1e865-a77e-4d99-87d0-1179aef2a3e6", + "title": "Example Article", + "target_url": "https://example.com/article", + "updated_at": "2026-09-27T14:30:00Z" + } + ], + "total_count": 42, + "page": 1, + "page_size": 20, + "has_more": true +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `items` | array | List of conversions (sorted by `updated_at` DESC) | +| `items[].id` | string | Record UUID | +| `items[].title` | string | Page title (or empty string) | +| `items[].target_url` | string | Source URL | +| `items[].updated_at` | string | Last conversion (ISO 8601) | +| `total_count` | int | Total number of records (successful only, status_code = 200) | +| `page` | int | Current page | +| `page_size` | int | Page size | +| `has_more` | bool | Whether there are more pages | + +> ๐Ÿ’ก **Deduplication:** One URL = one record. Repeated conversions update `updated_at` and move the record to the top. + +--- + +## ๐ŸŽ›๏ธ Conversion Options + +### `include_title: true` + +Adds the page title as an H1 at the beginning of the document: + +```markdown +# Example Article + +This is the content... +``` + +### `include_frontmatter: true` + +Adds a YAML frontmatter with metadata (convenient for static site generators such as Hugo/Jekyll): + +```markdown +--- +title: "Example Article" +source: "https://example.com/article" +language: "English" +words: 4521 +tokens: 1234 +bytes: 4521 +reading_time: "23 min" +extracted_at: "2026-09-27" +--- + +# Example Article + +This is the content... +``` + +The `language` field is detected automatically via `whatlanggo` (only when `include_toc` or `include_frontmatter` is enabled). + +### `include_toc: true` + +Adds a table of contents (TOC) from H1โ€“H3 headings: + +```markdown +## Table of Contents + +- [Main Section](<#Main Section>) +- [Subsection](<#Subsection>) + +# Main Section + +Content... +``` + +> โš ๏ธ The TOC is added only if there are **at least 2** H1โ€“H3 headings. Otherwise, the TOC is not included. + +### `strip_images: true` + +Completely removes all `` tags. Useful for: +- ๐Ÿค– Preparing text for LLMs (token savings) +- ๐Ÿ“„ Creating text-only versions of articles +- ๐Ÿ’พ Reducing the result size + +### `strip_links: true` + +Removes `` tags, but **preserves the link text**: + +**Before:** +```html +Visit our site for more info. +``` + +**After:** +```markdown +Visit our site for more info. +``` + +### ๐ŸŽฏ Recommended Combinations + +| Scenario | Options | +|----------|---------| +| **For LLM / RAG** | `strip_images: true`, `strip_links: true` | +| **For documentation** | `include_title: true`, `include_frontmatter: true` | +| **For archiving** | `include_title: true`, `include_frontmatter: true`, `strip_images: false` | +| **Minimal size** | `strip_images: true`, `strip_links: true`, `include_title: false` | + +--- + +## ๐Ÿ”‘ API Keys + +Managing API keys is **session-only** (web interface). API keys cannot be managed via the API. + +### Creating a Key + +**POST** `/api/keys` + +**Request:** +```json +{ + "name": "My Key" +} +``` +`name` is optional (max. 50 characters). If not specified, `"Default Key"` is used. + +**Response (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." +} +``` + +> โญ The raw key is returned **only in this response**. No one will ever show it again. + +**Errors:** +- `409 Conflict` + `key_already_exists` โ€” a key already exists (a user has only 1 key) +- `500 Internal Server Error` + `internal_error` + +### Getting Key Metadata + +**GET** `/api/keys` + +**Response (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" +} +``` + +**Errors:** +- `404 Not Found` + `key_not_found` โ€” the user has no key + +### Deleting a Key + +**DELETE** `/api/keys/:key_id` + +**Response (200 OK):** +```json +{ + "message": "API key deleted successfully" +} +``` + +**Errors:** +- `400 Bad Request` + `invalid_key_id` โ€” invalid UUID +- `404 Not Found` + `key_not_found` โ€” key not found +- `500 Internal Server Error` + `internal_error` + +--- + +## ๐Ÿ’ณ Billing: Credit Packs & Subscriptions + +### Getting Available Packages + +**GET** `/api/billing/packages` โ€” **public** endpoint (no authentication required). + +**Response (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 + } + ] +} +``` + +| Field | Description | +|-------|-------------| +| `free` | Free plan: daily limit | +| `credits[]` | Credit packs (from cheapest to most expensive) | +| `credits[].code` | Package code (`micro`, `start`, `optimal`, `pro`, `ultra`) | +| `credits[].credits` | Number of credits in the pack | +| `credits[].price` | Price in rubles | +| `credits[].price_per_100_requests` | Cost of 100 requests | +| `credits[].discount` | Discount percentage (compared to the base Micro rate) | +| `credits[].savings_rub` | Savings in rubles | +| `credits[].is_popular` | "Hit" flag | +| `subscriptions[]` | Subscription packs (from shortest to longest) | + +### Purchasing a Pack + +**POST** `/api/billing/purchase` โ€” **authentication required** (API key or session). + +> โš ๏ธ **Temporary endpoint!** In production it will be replaced with a real payment system integration. + +**Request:** +```json +{ + "package_code": "optimal" +} +``` + +**Response (200 OK):** +```json +{ + "message": "Credits added successfully", + "package": "optimal", + "credits_added": 3000, + "new_balance": 3000 +} +``` + +**Errors:** +- `400 Bad Request` + `invalid_package` โ€” invalid package code +- `401 Unauthorized` + `unauthorized` โ€” no authentication +- `500 Internal Server Error` + `purchase_failed` โ€” error while crediting + +--- + +## ๐Ÿ“Š Balance Check + +### Getting Credits & Subscription Balance + +**GET** `/api/billing/usage` โ€” **authentication required** (API key or session). + +**Response (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 +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `credits` | object | Credit balance | +| `credits.credits` | int | Paid credits (do not expire) | +| `credits.daily_used` | int | Requests used today | +| `credits.daily_limit` | int | Daily limit (10) | +| `credits.daily_remaining` | int | Requests remaining today | +| `credits.has_credits` | bool | Whether paid credits are available | +| `credits.daily_reset_at` | time | Next daily limit reset time | +| `subscription` | object/null | Active subscription info | +| `subscription.is_active` | bool | Subscription is active | +| `subscription.ends_at` | time | Subscription end date | +| `subscription.days_remaining` | int | Days remaining | +| `subscription.is_unlimited` | bool | Unlimited (always true) | +| `is_subscriber` | bool | Whether there is an active subscription | + +--- + +## โš ๏ธ Error Handling + +All errors are returned in a unified format: + +```json +{ + "error": "machine_readable_code", + "message": "Human-readable description" +} +``` + +### HTTP Statuses + +| Status | Error Code | Description | Action | +|:------:|------------|-------------|--------| +| `400` | `invalid_payload` | Invalid JSON or missing `url` | Check the request body | +| `400` | `invalid_url` | Invalid URL format | Must start with `http://` or `https://` | +| `400` | `invalid_request` | Invalid request (billing) | Check the parameters | +| `400` | `invalid_package` | Invalid package code | Use a valid code | +| `401` | `invalid_api_key` | Key missing, invalid, or deleted | Check the `X-API-Key` header | +| `401` | `unauthorized` | No authentication | Pass an API key or cookie | +| `402` | `quota_exceeded` | Credits exhausted + daily limit reached | Purchase credits in the web interface | +| `404` | `key_not_found` | Key not found | Create a new key | +| `404` | `key_not_found` | User's key not found (GET /api/keys) | Create a key | +| `404` | `user_not_found` | User not found | Check your credentials | +| `409` | `key_already_exists` | User already has an API key | Delete the existing key | +| `409` | `user_exists` | A user with this email is already registered | Use a different email | +| `409` | `account_already_deleted` | Account already deleted | โ€” | +| `410` | `email_previously_deleted` | Email was used for a deleted account | Use a different email | +| `415` | `unsupported_content` | URL does not return HTML | The API works only with web pages | +| `422` | `conversion_failed` | Could not recognize the content | Try another URL | +| `429` | `rate_limit_exceeded` | Request limit exceeded | Wait or log in to your account | +| `429` | `registration_ip_limit` | Registration limit from the IP exceeded | Wait | +| `429` | `resend_cooldown` | OTP requested too frequently | Wait | +| `429` | `otp_blocked` | Too many failed OTP attempts | Request a new code | +| `429` | `registration_ip_limit` | Registration limit from the IP exceeded | Wait | +| `429` | `registration_ip_limit` | Registration limit from the IP exceeded | Wait | +| `500` | `internal_server_error` | Server internal error | Retry later | +| `502` | `fetch_failed` | Could not load the page | Site unavailable or blocked the request | + +### Error Handling Example + +```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" +} +``` + +--- + +## ๐Ÿ“Š Quotas, Limits & Caching + +### Credit System + +- ๐Ÿ’ณ **Paid credits** โ€” do not expire, no expiration date +- ๐Ÿ“… **Daily limit** (10 requests / day) โ€” resets every 24 hours +- โšก **Debit priority:** paid credits first, then the daily limit +- ๐Ÿ“Š **Maximum balance:** 500,000 credits + +### Caching & Savings + +The API uses **two-tier caching**: + +| Level | What is cached | TTL | Savings | +|-------|----------------|-----|---------| +| **L1** | Raw HTML | 30 min | No HTTP request to the external site | +| **L2** | Final Markdown | 15 min | Instant response | + +**Important:** Credits are debited **only** when an actual HTTP request is made to the external site. Repeated conversions of the same URL (even with different options) are **free** if the HTML is still in the cache. + +### Scenario Table + +| Scenario | Fetch | Credits | Speed | +|----------|:-----:|:-------:|-------| +| First request for a URL | โœ… | โˆ’1 | ~2-5 sec | +| Repeat with the same options | โŒ | 0 | ~50 ms | +| Repeat with different options | โŒ | 0 | ~200 ms | +| After 30+ minutes | โœ… | โˆ’1 | ~2-5 sec | + +### IP Limits (for anonymous users) + +- **Conversion:** 3 requests / 24 hours per IP +- **Registration:** 3 registrations / 1 hour per IP (globally), 3 registrations / 2 minutes per IP +- **Login:** 10 attempts / 30 minutes per IP, 5 attempts / 15 minutes per email +- **Recovery OTP:** 10 attempts / 1 hour per IP +- **OTP Verify:** 20 attempts / 5 minutes per IP +- **Password change:** 5 attempts / 1 hour per IP, 3 attempts / 1 hour per user + +### Balance Check + +To check your credit balance, use: +- Web interface (page **Billing**) +- API: **GET** `/api/billing/usage` (authentication required) + +--- + +## ๐Ÿ’ป Code Examples + +### 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, +} + +# Conversion +response = requests.post( + f"{API_BASE}/convert", + headers=headers, + json={ + "url": "https://example.com/article", + "include_title": True, + "include_frontmatter": True, + "strip_images": True, + "strip_links": False, + }, +) + +if response.ok: + data = response.json() + 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(data["content"][:500]) +else: + error = response.json() + print(f"Error {response.status_code}: {error['error']} - {error['message']}") +``` + +### Python (batch conversion) + +```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) # Polite delay + +# Save to a single file +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, + include_toc: options.toc ?? 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(); +} + +// Usage +try { + const result = await convertUrl('https://example.com/article', { + title: true, + frontmatter: 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"` + IncludeTOC bool `json:"include_toc"` + StripImages bool `json:"strip_images"` + StripLinks bool `json:"strip_links"` +} + +type Metadata struct { + Words int `json:"words"` + Tokens int `json:"tokens"` + Bytes int `json:"bytes"` +} + +type ConvertResponse struct { + Title string `json:"title"` + Content string `json:"content"` + Metadata Metadata `json:"metadata"` + QuotaIncremented bool `json:"quota_incremented"` +} + +func main() { + reqBody, _ := json.Marshal(ConvertRequest{ + URL: "https://example.com/article", + IncludeTitle: true, + IncludeFrontmatter: true, + StripImages: true, + StripLinks: false, + }) + + 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 (all scenarios) + +```bash +# Basic conversion +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"}' + +# For LLM (plain text) +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 + }' + +# For documentation (with metadata) +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 + }' + +# For documentation (with TOC) +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 + }' + +# Conversion history +curl -X GET "https://markclean.online/api/converter/history?page=1&page_size=10" \ + -H "X-API-Key: mk_live_YOUR_KEY" + +# Balance check +curl -X GET "https://markclean.online/api/billing/usage" \ + -H "X-API-Key: mk_live_YOUR_KEY" + +# Get available packages +curl -X GET "https://markclean.online/api/billing/packages" +``` + +--- + +## โ“ FAQ + +### ๐Ÿ” Is it safe to pass an API key? + +**Yes**, as long as you follow these rules: +- โœ… Use HTTPS (always) +- โœ… Store the key in environment variables, not in code +- โœ… Do not log the key +- โŒ Do not commit the key to Git +- โŒ Do not pass the key in URL parameters + +### ๐Ÿ”„ What to do if the key is compromised? + +1. Open `/api-keys` in the web interface +2. Click **"Regenerate"** (the old key is deleted and a new one is created) +3. Update the key in all of your applications + +> โš ๏ธ The old key stops working immediately after regeneration. + +### ๐Ÿ’ณ Can I use the API without credits? + +Yes, every user has a **free daily limit** (10 requests / day) that resets every 24 hours. After both the daily limit and credits are exhausted, the API will return `402 quota_exceeded`. + +### โšก Why is `quota_incremented: false`? + +This is normal in three cases: +1. **Cache hit** โ€” this URL was recently converted (free) +2. **Anonymous request** โ€” without an API key (IP limit) +3. **Subscription** โ€” the user has an active subscription, no credits are consumed + +### ๐ŸŒ Does the API work with any site? + +The API works with any **publicly accessible HTML page**. Limitations: +- โŒ Sites requiring authentication +- โŒ Sites with strict anti-bot protection (Cloudflare challenges, etc.) +- โŒ PDF, images, and other non-HTML formats +- โŒ Sites that block by User-Agent + +### ๐Ÿ“ฆ Is there a page size limit? + +- Maximum: **5 MB** of content after conversion +- Typical articles: 10-100 KB +- Long technical documents: 200-500 KB + +Pages larger than 5 MB are not cached but are still converted. + +### ๐Ÿ”— How to use an API key with other tools? + +**Postman:** +- **Authorization** tab โ†’ Type: **API Key** +- Key: `X-API-Key`, Value: your key, 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" +} +``` + +### ๐Ÿ—‘๏ธ What happens to the API key after account deletion? + +**The key continues to work** until the credits run out. After account deletion: +- โœ… The API key remains active +- โœ… Existing credits can still be used +- โŒ You cannot top up credits (no UI access) +- โŒ You cannot create a new key + +This is for fairness: you paid for the credits โ€” you use them. + +### ๐Ÿ› ๏ธ Is there an SDK? + +No official SDK yet, but the API is simple enough to use via standard HTTP clients in any language. The examples above cover the most popular languages. + +--- + +## ๐Ÿ“ž Support + +- ๐Ÿ“ง **Email:** support@markclean.online +- ๐Ÿ“ง **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) diff --git a/api.md b/markclean-api-ru.md similarity index 100% rename from api.md rename to markclean-api-ru.md