Content teams rarely publish a page that is wrong. They publish pages that are hard to read: sentences that run for forty words, help articles written at a postgraduate level for people who just want to reset a password, landing pages that never actually mention the thing the visitor searched for. None of that fails a spell-check, and an editor with forty drafts in the queue will not catch all of it.
A machine can catch the measurable part. Readability formulas measure sentence length and word length. Reading time is word count divided by speed. Keyword extraction counts which terms dominate a text. Each of those is simple, and each misleads when you forget how simple it is. This lesson builds a content QA check from four APIs and spends as much time on what the numbers cannot tell you as on how to get them.
01Two ways in: a live page or the text itself
You can score content in two places, and the choice is mostly about cost.
Website Readability (websitereadability) takes a url, fetches the page, extracts its readable text and scores it. It is a POST and costs 25 credits.
curl -X POST "https://api.apiverve.com/v1/websitereadability" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'Only 27 words and 2 sentences: with this little text, the grade scores are close to meaningless.
Text Readability Score (readabilityscore) takes the text directly. Also a POST, and it costs 2 credits.
curl -X POST "https://api.apiverve.com/v1/readabilityscore" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Ship small changes. Test them well. Measure what users actually do after every release."}'Short sentences give a Flesch Reading Ease of 66.7 and an average grade level of 5.9.
That is a twelve-to-one price difference for very similar scores, and it should decide your architecture. If the content lives in your CMS, you already have the text. Score it before it is published, with the text API, while the author can still fix it. Use the URL-based API for pages you do not have the source for: competitors' articles, a partner's docs, a legacy site you are about to migrate, or a final check of how a page renders once templates, sidebars and footers wrap it.
The responses differ in shape, too. The text API returns everything on every plan, grouped: textCounts (syllableCount, lexiconCount, sentenceCount), readability (seven formula scores), readabilityText (plain-language labels), plus averageGradeLevel and targetAudience. The URL API returns a flat object where fleschReadingEase, fleschReadingEaseText, fleschKincaidGrade, wordCount, sentenceCount and readingTimeMinutes are available on every plan, and the other formulas, averageGradeLevel and targetAudience are premium.
02What the scores actually measure
Every formula in these responses is a variation on two ratios: words per sentence and syllables (or characters) per word. That is all.
- Flesch Reading Ease runs roughly 0 to 100, higher is easier. Plain consumer writing sits around 60-70. Our three-sentence example scored 66.7, labelled "standard". It can go above 100 for very simple text: "The cat sat on the mat. It was a sunny day. The dog ran in the park." scores 107.99.
- Flesch-Kincaid Grade, Gunning Fog, Coleman-Liau, SMOG and the Automated Readability Index each estimate a US school grade. They disagree with each other by a grade or two on the same text, which is why
averageGradeLevelexists. - Dale-Chall is the odd one out: it counts words outside a list of familiar words, rather than syllables.
Knowing the mechanism tells you where the numbers lie:
Short texts are noise. The example.com page above has 27 words in 2 sentences. One long word moves the grade by several points. Do not score anything under about 100 words, and do not compare a 200-word page with a 3,000-word guide.
Technical vocabulary inflates the grade. "Authentication", "configuration" and "idempotency" are long words, and your audience reads them without effort. A developer guide scoring grade 13 may be perfectly clear to its readers. Set thresholds per content type, not one number for the site.
Code, tables and lists distort sentences. A code block has no full stops, so the formula sees one enormous sentence. Strip code and tables before scoring your own text, and expect live-page scores for docs to look harder than they read.
Easy is not the same as good. You can push Flesch above 90 by writing in fragments. The formulas reward short sentences; they cannot tell whether the sentences say anything. A score is a prompt for an editor to look, never a verdict.
Your API reference pages all score around grade 14 and fail a site-wide 'grade 9 or lower' rule. What is the most sensible response?
03Reading time and keywords
Reading Time Calculator (readingtime) is a POST with text and an optional wpm (words per minute, default 250). It costs 1 credit.
curl -X POST "https://api.apiverve.com/v1/readingtime" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Readable pages keep visitors on the page longer, and short sentences help more than short words. Write for the reader you have, not the reader you wish you had.","wpm":120}'29 words at 120 wpm: reading_time_seconds is 15, reading_time_text is '< 1 min read'.
The response gives word_count, text_length, reading_time_seconds, reading_time_minutes, reading_time_text and the words_per_minute it used. Two behaviours are worth knowing before you display it. reading_time_minutes rounds up: 150 words at 100 wpm is 90 seconds and comes back as 2; a 17-word text at the default speed comes back as 1 with reading_time_text of < 1 min read. Show reading_time_text to readers and use reading_time_seconds for anything you compute or sort.
Set wpm to fit the content. 250 is a common figure for general prose. Dense technical material, anything read in a second language, and anything with code to type along with goes slower; 150-180 is more honest for a tutorial. An estimate that is consistently low teaches readers to ignore it.
Keyword Extractor (keywordextractor) is a POST with text, 10 credits. It returns keywords (a map of term to count, capped at 50 terms), topKeyword, totalOccurrences, keywordCount and topKeywords: the top five with count and percentage share.
curl -X POST "https://api.apiverve.com/v1/keywordextractor" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Certificate monitoring catches expiring certificates early. A certificate that expires takes the whole site offline, so monitoring every certificate weekly is cheap insurance."}'topKeyword is certificate (3), but certificates is counted separately: there is no stemming.
Run that example and two properties jump out. First, there is no stemming: certificate (3) and certificates (1) are separate entries, and in another test cache and caching were too. If you want to know how much a page is about certificates, add the variants yourself. Second, stop words and some short terms are dropped. In our tests, acronyms such as API, JSON and SDK and short words like key never appeared, even when they were the most frequent words in the text. That makes the extractor good for spotting the dominant topical words of a page and poor for confirming that a page mentions a specific acronym. For "does this page contain our target phrase?", a plain case-insensitive string search is more reliable and free.
What keyword counts are good for is a topic sanity check. If a page meant to rank for "invoice generator" has topKeywords dominated by pricing, plans and team, the copy drifted. You do not need the percentages to be any particular value; you need the intended topic to be in the top five.
A draft about 'webhooks' returns topKeywords of webhook (4), events (3), webhooks (3), retries (2), payload (2). What does this tell you?
04Build a pre-publish content check
Here is a CommonJS Express 4 service your CMS can call when an author presses "submit for review". It takes the draft text, the content type and the intended topic word, and returns warnings. It uses the 2- and 1-credit text APIs, and only calls the keyword extractor when a topic is given.
// Node 18+, CommonJS, Express 4: npm install express@4
const express = require('express');
const KEY = process.env.APIVERVE_API_KEY;
const LIMITS = {
help: { maxGrade: 8, wpm: 200 },
blog: { maxGrade: 10, wpm: 230 },
reference: { maxGrade: 14, wpm: 160 },
};
async function post(api, body) {
const res = await fetch(`https://api.apiverve.com/v1/${api}`, {
method: 'POST',
headers: { 'x-api-key': KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(10000),
});
const json = await res.json().catch(() => null);
if (!res.ok || json?.status !== 'ok') throw new Error(`${api} ${res.status}: ${json?.error}`);
return json.data;
}
function prose(markdown) {
return markdown
.replace(/`{3}[\s\S]*?`{3}/g, ' ') // code blocks are not sentences
.replace(/^\|.*\|$/gm, ' ') // nor are table rows
.replace(/\s+/g, ' ')
.trim();
}
const app = express();
app.use(express.json({ limit: '1mb' }));
app.post('/content-check', async (req, res) => {
const { body = '', type = 'blog', topic } = req.body;
const limits = LIMITS[type] ?? LIMITS.blog;
const text = prose(body);
const words = text.split(' ').filter(Boolean).length;
if (words < 100) return res.json({ words, warnings: ['too short to score reliably'] });
try {
const [score, time, keys] = await Promise.all([
post('readabilityscore', { text }),
post('readingtime', { text, wpm: limits.wpm }),
topic ? post('keywordextractor', { text }) : null,
]);
const warnings = [];
if (score.averageGradeLevel > limits.maxGrade) {
warnings.push(`grade ${score.averageGradeLevel} is above ${limits.maxGrade} for ${type} content`);
}
const perSentence = score.textCounts.lexiconCount / score.textCounts.sentenceCount;
if (perSentence > 22) warnings.push(`average sentence is ${perSentence.toFixed(1)} words`);
if (keys) {
const top = keys.topKeywords.map((k) => k.keyword);
if (!top.some((k) => k.startsWith(topic.toLowerCase()))) {
warnings.push(`"${topic}" is not among the top keywords: ${top.join(', ')}`);
}
}
res.json({ words, readingTime: time.reading_time_text, grade: score.averageGradeLevel, warnings });
} catch (err) {
// the check is advisory; never block publishing because it failed
res.json({ words, warnings: [], unavailable: String(err.message) });
}
});
app.listen(3000, () => console.log('content check on :3000'));Three decisions are worth defending in review. The check returns warnings, not rejections; readability is advisory, and an editor overrules it when the content needs the long words. When an API call fails, the response says the check was unavailable and lets the draft through, because a scoring outage must never stop a publish. And the topic match uses startsWith so that webhook matches webhooks, a small workaround for the missing stemming.
05Audit pages you don't own
For live pages, run the URL API in a batch job. This Python script scores a list of URLs and writes the ones that need attention. At 25 credits a page, keep the list deliberate: your top landing pages, a migration's before-and-after set, or a handful of competitor articles you are benchmarking against.
import os
import sys
import requests
KEY = os.environ["APIVERVE_API_KEY"]
def score(url):
r = requests.post("https://api.apiverve.com/v1/websitereadability",
json={"url": url}, headers={"x-api-key": KEY}, timeout=60)
body = r.json()
if body.get("status") != "ok":
return {"url": url, "error": body.get("error")}
d = body["data"]
return {
"url": url,
"words": d["wordCount"],
"ease": d["fleschReadingEase"],
"label": d["fleschReadingEaseText"],
"grade": d["fleschKincaidGrade"],
"minutes": d["readingTimeMinutes"],
}
for line in open(sys.argv[1]):
url = line.strip()
if not url:
continue
row = score(url)
if "error" in row:
print(f"{url}\tERROR\t{row['error']}")
elif row["words"] < 100:
print(f"{url}\tSKIP\tonly {row['words']} words")
else:
print(f"{url}\t{row['ease']}\t{row['label']}\tgrade {row['grade']}\t{row['minutes']} min")Remember what you are scoring. The URL API extracts the page's readable text, but templates differ, and a page with a heavy navigation or a long cookie notice can carry text you did not write into the score. When a number surprises you, look at wordCount first: if it is much higher or lower than the article you see, the extraction picked up something else, and the score describes that.
06What's next
That completes this course: you can audit a page's SEO, identify its stack, vet its domain and score its content. The final course of the Web Data Developer certification puts these pieces on a schedule: cleaning extracted text, monitoring pages for change and assembling it all into a pipeline that runs politely at scale. Continue with Build Data Pipelines.