Log in Sign up
Docs/Parsing/HTML Parser

HTML Parser v1.0 1 credit / call

GET|POST /api/v1/html-parser

Parse an HTML document into structured content — metadata, Open Graph, headings, links, images and readable text.

50 free/month Test keys supported OpenAPI spec Try it in the console
At a glance
Base path
/api/v1/html-parser
Methods
GET or POST — parameters are read identically from the query string or a JSON body
Auth
X-Api-Key header (never Authorization: Bearer)
Price
1 credit per call · about $0.0010 at 1,000 credits/USD
Free tier
50 credits, refilled every month — drawn down before your paid balance
Test mode
Supported — ksty_test_ keys call this API free of charge
Rate limit
60 calls/minute per key by default, raisable per key

HTML Parser API

Turns a raw HTML document into clean, structured JSON. Give it the HTML directly with html, or hand it a url and it fetches the page for you, and it returns the pieces you normally have to write a scraper for — the title and meta description, Open Graph and Twitter card tags, the canonical URL and favicon, every heading, link and image, and the readable plain-text body with a word count and reading-time estimate.

Parsing runs locally against a real HTML parser, so malformed markup, unclosed tags and messy real-world pages are handled the same way a browser would tolerate them — you get structure back, not an error.

Two ways to call it

Supply the markup directly:

POST /api/v1/html-parser
X-Api-Key: ksty_test_...
Content-Type: application/json

{"html": "<html><head><title>Hello</title></head><body><h1>Hi</h1><p>World</p></body></html>"}

…or let the API fetch the page (relative links are resolved against the fetched URL automatically):

{"url": "https://example.com"}
{
  "title": "Hello",
  "lang": "",
  "description": "",
  "canonical": "",
  "favicon": "",
  "meta": {},
  "opengraph": {},
  "twitter": {},
  "headings": { "h1": ["Hi"] },
  "links": [],
  "images": [],
  "text": "Hi World",
  "word_count": 2,
  "reading_time_min": 1
}

Endpoints

The root endpoint returns everything. Three focused endpoints return only one section each, so you pay for and transfer just the part you need:

  • html-parser — the full parse (all sections above).
  • html-parser.metadata — title, language, description, canonical, favicon, and the raw meta / opengraph / twitter maps. Ideal for building link previews.
  • html-parser.links — every <a href> as {url, text}, plus every <img> as {src, alt}, with relative URLs resolved to absolute.
  • html-parser.text — the readable plain-text body only, with word_count and reading_time_min.

Parameters

  • html — the HTML document to parse. Either this or url is required; if both are sent, html wins.
  • url — an http(s) URL to fetch and parse. The fetch is a single GET with an 8-second timeout and a 3 MB size cap; URLs that resolve to private or reserved IP addresses are refused (SSRF protection).
  • base_url — a base used to resolve relative links and images to absolute URLs. Defaults to url when you fetch, or the document's own <base href> when present.

Link resolution

Relative links (/about, ../pricing, logo.png) are turned into absolute URLs whenever a base is available — from base_url, the fetched url, or a <base href> in the document, in that order. When no base can be determined the original relative value is returned unchanged.

Readable text

text is the visible body text with <script>, <style> and <noscript> content removed and whitespace collapsed to single spaces — the same text a reader would see, not the raw markup. word_count counts whitespace-separated tokens and reading_time_min is ceil(word_count / 200).

Limits

  • No JavaScript. The parser reads the HTML as delivered; content injected by client-side scripts after load is not present. For script-rendered pages, fetch the rendered HTML yourself and pass it as html.
  • Fetch is best-effort. A URL that times out, is too large, or resolves to a blocked address returns a fetch_failed error and is not billed.
  • One document per call. There is no crawling or link-following; each call parses exactly the document you provide.

When to use it

  • Building link previews / unfurls from a URL (title, description, OG image).
  • Extracting the readable article text from a page for indexing or summarisation.
  • Auditing a page's links and images, or its SEO metadata, without writing a parser per site.

Endpoints

GETPOST /api/v1/html-parser 1 credit

Parse an HTML document into all structured sections

The root endpoint runs every extractor and merges the results, so one call gives you metadata, headings, links, images and readable text together. If you only need one of those, the dedicated endpoints below do the same work for the same price but return a much smaller payload — worth using when you are parsing at volume.

Parameters
ParameterTypeDescription
html optional string The HTML document to parse (required unless url is given). Wins over "url" when both are sent.
url optional string An http(s) URL to fetch and parse instead of passing html
base_url optional string Base URL relative links and images resolve against. Defaults to "url" when fetching, then to a <base href> in the document.
Request
curl -X POST "https://ksty.ch/api/v1/html-parser" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<html><head><title>Hello</title></head><body><h1>Hi</h1><p>Hi World</p></body></html>"}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/html-parser');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "html": "<html><head><title>Hello</title></head><body><h1>Hi</h1><p>Hi World</p></body></html>"}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/html-parser', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "html": "<html><head><title>Hello</title></head><body><h1>Hi</h1><p>Hi World</p></body></html>"
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/html-parser',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "html": "<html><head><title>Hello</title></head><body><h1>Hi</h1><p>Hi World</p></body></html>"
},
)
data = response.json()
Response 200 OK
{
    "title": "Hello",
    "lang": "en",
    "description": "",
    "canonical": "",
    "favicon": "",
    "meta": [],
    "opengraph": [],
    "twitter": [],
    "headings": {
        "h1": [
            "Hi"
        ]
    },
    "links": [],
    "images": [],
    "text": "Hi World",
    "word_count": 2,
    "reading_time_min": 1
}
Response fields
FieldTypeDescription
title string Contents of , whitespace-normalised.
lang string The attribute, empty when absent.
description string meta description, falling back to og:description.
canonical string Canonical URL, resolved to absolute.
favicon string First icon link, resolved to absolute.
meta object Every named tag as name → content. Tags with empty content are dropped.
opengraph object og:* properties with the prefix stripped, so og:image arrives as "image".
twitter object twitter:* tags with the prefix stripped.
headings object h1–h6 → array of heading texts, in document order. Levels with no headings are omitted rather than sent empty.
links array Every as {url, text}, absolutised. Fragment-only and javascript: links are skipped.
images array Every as {src, alt}, absolutised.
text string Readable body text with script, style, nav and other non-content nodes removed.
word_count integer Words in "text".
reading_time_min integer Estimated reading time in whole minutes, never less than 1.
GETPOST /api/v1/html-parser.metadata 1 credit

Extract page metadata only — title, description, canonical, favicon, Open Graph and Twitter tags

Parameters
ParameterTypeDescription
html optional string The HTML document to parse (required unless url is given)
url optional string An http(s) URL to fetch and parse instead of passing html
base_url optional string Base URL the canonical and favicon resolve against
Request
curl -X POST "https://ksty.ch/api/v1/html-parser.metadata" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<html><head><title>Hello</title><meta property=\"og:image\" content=\"/card.png\"></head></html>"}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/html-parser.metadata');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "html": "<html><head><title>Hello</title><meta property=\"og:image\" content=\"/card.png\"></head></html>"}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/html-parser.metadata', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "html": "<html><head><title>Hello</title><meta property=\"og:image\" content=\"/card.png\"></head></html>"
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/html-parser.metadata',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "html": "<html><head><title>Hello</title><meta property=\"og:image\" content=\"/card.png\"></head></html>"
},
)
data = response.json()
Response 200 OK
{
    "title": "Hello",
    "lang": "",
    "description": "",
    "canonical": "",
    "favicon": "",
    "meta": [],
    "opengraph": {
        "image": "/card.png"
    },
    "twitter": []
}
Response fields
FieldTypeDescription
title string Contents of , whitespace-normalised.
lang string The attribute, empty when absent.
description string meta description, falling back to og:description.
canonical string Canonical URL, resolved to absolute.
favicon string First icon link, resolved to absolute.
meta object Every named tag as name → content.
opengraph object og:* properties with the prefix stripped — the set social cards are built from.
twitter object twitter:* tags with the prefix stripped.
GETPOST /api/v1/html-parser.text 1 credit

Extract the readable plain-text body with word count and reading time

Parameters
ParameterTypeDescription
html optional string The HTML document to parse (required unless url is given)
url optional string An http(s) URL to fetch and parse instead of passing html
Request
curl -X POST "https://ksty.ch/api/v1/html-parser.text" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<body><script>ignored()</script><p>Hi World</p></body>"}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/html-parser.text');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "html": "<body><script>ignored()</script><p>Hi World</p></body>"}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/html-parser.text', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "html": "<body><script>ignored()</script><p>Hi World</p></body>"
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/html-parser.text',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "html": "<body><script>ignored()</script><p>Hi World</p></body>"
},
)
data = response.json()
Response 200 OK
{
    "text": "Hi World",
    "word_count": 2,
    "reading_time_min": 1
}
Response fields
FieldTypeDescription
text string Readable text with script, style and other non-content nodes stripped first, so nothing from the page chrome leaks in.
word_count integer Words in "text".
reading_time_min integer Estimated minutes to read, rounded up and never below 1.

Errors

Errors are JSON with a stable error.code — branch on the code, never on the message. Everything below carries the X-Request-Id of the failed call. Requests rejected by this API are not charged, and a 502 after a charge is refunded automatically.

CodeHTTPCause & what to do
invalid_request this API 400 Neither an "html" string nor a "url" was supplied (or "html" was blank). Send one of them. Malformed markup is not an error — the parser is deliberately tolerant and will extract what it can from broken HTML.
fetch_failed this API 502 A "url" was given but could not be retrieved: DNS failure, timeout, non-2xx status, a non-HTML content type, or a blocked address (private and loopback ranges are refused on every redirect hop). Check the URL is publicly reachable and returns HTML. Nothing is charged for a failed fetch, so a retry costs no credits. If the page needs cookies or JavaScript, fetch it yourself and post the markup as "html".
missing_key platform 401 No API key was sent. Send your key in the X-Api-Key header (or an api_key parameter).
invalid_key platform 401 The key is not recognised — mistyped, truncated or never issued. Copy the key again from the API keys page. Repeated bad keys from one address are throttled, so fix it rather than retrying.
insufficient_credits platform 402 The balance can't cover the call. Top up on the Billing page. Nothing is charged and the call never reaches the API.
key_suspended platform 403 The key is suspended (for example after an abuse review). The reason is shown on the API keys page; contact support to have it restored.
key_revoked platform 403 The key was revoked, or it belongs to a team you have left. Create or use another key.
key_expired platform 403 The key is past the expiry date set on it. Create a new key, or one without an expiry.
out_of_scope platform 403 The key is valid but not scoped to this API. Add this API to the key's scopes, or use a key scoped to all APIs.
sandbox_disabled platform 403 A test (ksty_test_) key was used on an API with no sandbox. Use a live key for this API.
ip_blocked platform 403 The key has an IP allowlist and the calling address isn't on it. Call from an allowed address, or edit the key's allowlist.
geo_blocked platform 403 Calls from this country aren't permitted for this API. Contact support if you believe this is wrong.
spend_cap_reached platform 403 The account has the credits, but this API's monthly spend cap is used up. Raise or clear the cap on the API's page in your dashboard — topping up won't help.
unknown_api platform 404 No public API has that slug. Check the slug against the catalog.
unknown_endpoint platform 404 The API has no endpoint with that name. Check the endpoint list on the API's reference page.
method_not_allowed platform 405 The endpoint accepts only GET or only POST. Use the method shown for the endpoint.
rate_limited platform 429 A rate limit was exceeded — per key, per account across all your keys, or the tighter per-account limit on AI APIs. Wait for Retry-After seconds. A second key shares the account limit, so it won't help.
api_unavailable platform 502 The API is temporarily unavailable. Nothing was charged. Retry later; check the status page.
upstream_error platform 502 The API failed while processing the call. Safe to retry — any credits taken are refunded automatically.

Response headers

Every successful call reports its own cost, so you never have to guess what a request spent or reconcile it later.

HeaderMeaning
X-Request-Id Identifier for this call — on every response, in every error body and in your usage log. Quote it in support requests.
X-Credits-Charged Credits taken for this call. Zero on test-mode calls and on errors raised before the work started.
X-Credits-Remaining Balance left afterwards, across per-API grants, promotional and paid credits.
X-RateLimit-Limit Requests allowed per minute by the limit you are closest to (key, account or AI tier).
X-RateLimit-Remaining Requests left in the current window of that limit.
X-RateLimit-Reset Unix time when that window resets.
Retry-After On a 429 only: seconds to wait before retrying.