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".
invalid_key platform 401 The X-Api-Key header is missing, malformed, revoked or expired. Send an active key. Repeated bad keys from one address are throttled, so fix the header rather than retrying.
forbidden_scope platform 403 The key is valid but not scoped to this API, or it is a test key on an API without sandbox support. Add this API to the key's scopes on the Keys page, or use a key scoped to all APIs.
insufficient_credits platform 402 The balance cannot cover the call. Top up. Nothing is charged and the request never reaches the API, so no work is half-done.
rate_limited platform 429 The key exceeded its per-minute limit. Back off and retry. The limit is per key, so a second key does not share the budget.
upstream_error platform 502 The API failed unexpectedly after being charged. Safe to retry — credits are refunded automatically whenever this is returned.

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-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-Request-Id Identifier for this call. It appears in your usage log and in every error body — quote it in support requests.