# ๐Ÿ“– MarkClean API 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)