You want a word count next to an editor, or a rule like "at least 100 words" on a form. Here's the honest version of how to build that: what to count in the browser, what to leave to the Text Analyzer API, and how the API counts, so the two agree.
Do you need an API for this?
For the raw number, no. Counting words is one line:
const words = text.trim().split(/\s+/).filter(Boolean).length;
That's pretty much what the API does too. It splits on whitespace and counts the pieces. If a number under a textarea is all you need, use the line above and stop reading.
The trouble starts when two places have to agree. The editor counts in JavaScript, the server checks the limit in PHP or Python, and the user gets told "98 words, minimum 100" by one and "101 words" by the other. I ran the usual local counters over a few inputs:
| Input | JS split(" ") |
JS split(/\s+/) |
PHP str_word_count |
Python split() |
API |
|---|---|---|---|---|---|
hello world (two spaces) |
3 | 2 | 2 | 2 | 2 |
café résumé |
2 | 2 | 3 | 2 | 2 |
It costs 3.14 in 2026 |
5 | 5 | 3 | 5 | 5 |
10 km away (non-breaking space after 10) |
2 | 3 | 2 | 3 | 3 |
Ship it 🚀 |
3 | 3 | 2 | 3 | 3 |
str_word_count is the odd one out. It skips numbers and emoji, and it split café résumé into three on my machine. Nobody is wrong here, they just define "word" differently.
So the API earns its place when you want one definition everywhere: a PHP backend, a JavaScript frontend and a Python import script all getting the same count, plus sentences, paragraphs, reading time and top words in the same response, with nothing to install. It doesn't earn its place as a live counter. More on that below.
How the API counts
curl -X POST "https://apixies.io/api/v1/analyze-text" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "This is a sample blog post. It has two sentences."}'
{
"status": "success",
"data": {
"characters": 49,
"characters_no_spaces": 40,
"words": 10,
"sentences": 2,
"paragraphs": 1,
"lines": 1,
"avg_word_length": 4,
"reading_time_min": 0.1,
"speaking_time_min": 0.1,
"top_words": { "this": 1, "is": 1, "a": 1, "sample": 1, "blog": 1, "post.": 1 }
}
}
(top_words is cut short here. It holds up to ten entries.)
The rules for words:
- Whitespace separates words: spaces, tabs, line breaks. Several in a row count once.
- A non-breaking space separates too. Rich text editors love to insert these, and a counter that misses them (PHP's
str_word_count, a plainsplit(" ")) comes up short. - Hyphens don't separate.
well-knownis one word. - Punctuation belongs to the word next to it for counting. A lone
-or##from Markdown is a word of its own. - Numbers, URLs and emoji are words.
- Nothing is stripped.
<strong>big</strong>is one word, tags and all. - Text without spaces is one word, however long. A Japanese sentence of 22 characters came back as
words: 1, so this isn't the tool for Chinese, Japanese or Thai.
If you want the browser to match the API, the one-liner at the top does it. JavaScript's \s and the API agree on what whitespace is, non-breaking spaces included.
Don't count on every keystroke
The old advice was to debounce the input and call the API half a second after the user stops typing. Don't. The free tier is 75 requests a day. One person writing one blog post will pause more often than that. And a counter that lags behind the text by a network round trip feels broken.
Count locally while they type. Call the API once, when they save:
const editor = document.getElementById("editor");
const counter = document.getElementById("word-count");
// Live: local, instant, free.
editor.addEventListener("input", () => {
const words = editor.value.trim().split(/\s+/).filter(Boolean).length;
counter.textContent = `${words} words`;
});
The API call belongs on your server, in the save handler. That keeps your key out of the page source as well. Anything in a <script> tag is public.
Check a word limit on save
This runs on the server when a post or form is submitted. It returns the count along with the verdict, so you can store it.
Python
import os
import requests
def check_length(text, min_words=100, max_words=5000):
res = requests.post(
"https://apixies.io/api/v1/analyze-text",
json={"text": text},
headers={"X-API-Key": os.environ["APIXIES_API_KEY"]},
timeout=15,
)
body = res.json()
if body["status"] != "success":
raise RuntimeError(f"{body['code']}: {body['message']}")
words = body["data"]["words"]
errors = []
if words < min_words:
errors.append(f"Too short: {words} words, minimum {min_words}")
if words > max_words:
errors.append(f"Too long: {words} words, maximum {max_words}")
return {"valid": not errors, "words": words, "errors": errors}
print(check_length("Short.", min_words=50))
# {'valid': False, 'words': 1, 'errors': ['Too short: 1 words, minimum 50']}
PHP
function wordCount(string $html): int
{
// The API counts what it's given, so tags have to go first.
$text = trim(html_entity_decode(strip_tags($html)));
$context = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\nX-API-Key: " . getenv('APIXIES_API_KEY'),
'content' => json_encode(['text' => $text]),
'ignore_errors' => true,
]]);
$body = json_decode(file_get_contents('https://apixies.io/api/v1/analyze-text', false, $context), true);
if (($body['status'] ?? '') !== 'success') {
throw new RuntimeException(($body['code'] ?? 'ERROR') . ': ' . ($body['message'] ?? ''));
}
return $body['data']['words'];
}
echo wordCount('<p>Hello <strong>big</strong> world</p>'), "\n";
// 3
Two things to decide before you ship a limit. What happens when the API can't be reached? For a word limit I'd let the post through and count later, not block someone's submission over a timeout. And empty text never reaches the counter: the API answers a blank text with a 422, so check for that first.
Strip the markup first
This is the mistake that makes counts look wrong. <p>Hello <strong>big</strong> world</p> sent as it is comes back as 3 words with an average length of 12.3, because <strong>big</strong> is one long "word". Here the count happens to be right and the average gives it away. With real HTML, attributes and inline styles add words that nobody wrote.
Most rich text editors will hand you plain text if you ask. In TinyMCE it's getContent({ format: "text" }), in Quill it's getText(). On the server, strip_tags in PHP does the job. For Markdown, at least drop the code blocks. One of our own guides went from 1,123 words to 880 when I did.
Next steps
- Text Analysis API tutorial: every field in the response and what it really measures
- Estimate reading time: turn the word count into a "min read" badge
- Text Analyzer API reference: parameters and error codes
- Text Analyzer tool: paste some text and see the numbers
- All guides