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.
Two 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.
What 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?
Answer the check above to continue.
Build 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.
Audit 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.
What'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.