Log in Sign up
Docs/Content/Text Analysis

Text Analysis v1.0 1 credit / call

GET|POST /api/v1/text-analysis

Word counts, readability grades and language detection for any block of text — rule-based, no ML.

100 free/month Test keys supported OpenAPI spec Try it in the console
At a glance
Base path
/api/v1/text-analysis
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
100 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

Text Analysis API

Three things about a block of text, in one call: how big it is, how hard it is to read, and what language it is in.

Everything is computed from the text itself using published formulas and character statistics. No model is loaded, no external service is contacted. That has two consequences worth planning around: latency is flat and predictable regardless of load, and the same input always returns the same numbers — so you can store a score and compare it against one computed months later.

POST /api/v1/text-analysis
X-Api-Key: ksty_test_...
Content-Type: application/json

{"text": "The quick brown fox jumps over the lazy dog. It was a sunny day."}
{
  "counts": { "words": 14, "sentences": 2, "syllables": 17, "reading_time_seconds": 5 },
  "readability": {
    "reliable": true,
    "scores": { "flesch_reading_ease": 92.4, "flesch_kincaid_grade": 2.1 },
    "consensus_grade": 2.4,
    "interpretation": { "ease": "very easy", "school_level": "2nd grade" }
  },
  "language": { "language": "en", "name": "English", "confidence": 0.71 }
}

Counts

Words keep their internal apostrophes and hyphens — "don't" is one word, not two, because that is what a reader speaks and what the readability formulas assume when they divide by word count.

Sentence splitting masks common abbreviations first. Without that, "Dr. Smith went home." counts as two sentences, which shortens the average sentence length and makes the text look far simpler than it is. Every grade-level formula divides by sentence count, so that single mistake moves all of them at once.

Reading time assumes 200 words per minute silently; speaking time assumes 130.

Readability

Six formulas, not one:

Score Reads as
flesch_reading_ease 0–100, higher is easier. 60–70 is standard prose.
flesch_kincaid_grade US school grade
gunning_fog US school grade, weights 3+ syllable words
smog_index US school grade, common in health writing
coleman_liau_index US school grade, from letters rather than syllables
automated_readability_index US school grade, from characters

They weigh the same two ingredients — sentence length and word weight — but disagree on the weighting, so they routinely differ by a grade or two on the same text. consensus_grade averages the five grade-level scores. The spread between them is itself information: when they cluster, the text is consistently pitched; when they scatter, it mixes short sentences with heavy vocabulary (or the reverse), and no single number describes it well.

Below 10 words, reliable is false and scores is null. A formula built on averages over sentences cannot say anything trustworthy about a fragment, and returning a confident-looking number for "Hello there" would be worse than returning nothing.

These formulas are calibrated on English prose. Run them on French and you will get numbers, but not meaningful ones — which is why the language result is returned alongside, so you can check before trusting the grade.

Language detection

Two stages, cheapest and most decisive first.

Script. Most languages are settled by the Unicode block their letters occupy — Devanagari is Hindi, Hangul is Korean, Thai is Thai. Near-certain from a handful of characters, no statistics needed. Detected this way: Japanese, Korean, Chinese, Russian, Arabic, Hindi, Hebrew, Greek, Thai. The response reports "method": "script".

Function words and n-grams. English, Spanish, French, German, Italian, Portuguese and Dutch share an alphabet, so they need evidence. Three signals combine: function words (the, de, der, het), character trigrams that capture morphology (sch, ção, ijk), and diacritics (ß, ñ). Function words carry the most weight because they survive topic changes — a Spanish text about football and one about medicine share almost no vocabulary except de, la, que. Reported as "method": "ngram".

confidence is the winner's share of the total evidence, so it says how far clear of the runner-up it finished — not how certain the answer is in absolute terms. alternatives lists the next-best candidates, which is where you look when confidence is middling.

Accuracy & limitations

  • Short text is hard. Under 5 words, reliable is false. "No" is valid in several of these languages, and nothing can decide between them.
  • Related languages compete. Spanish and Portuguese, or Dutch and German, share function words and diacritics. Expect lower confidence on a single sentence and check alternatives.
  • One language per call. A text mixing two languages returns whichever dominates, not a breakdown.
  • Syllables are approximated. English syllable counting uses vowel-group heuristics; an exact count needs a pronunciation dictionary. It is within one syllable on the overwhelming majority of words, and the formulas themselves were calibrated on the same approximation.

When to use it

  • Enforcing a reading level on help-centre or policy copy before it ships.
  • Routing user-submitted content to the right language queue.
  • Showing "N min read" on articles, from a real word count.
  • Screening a CMS import for text that is far denser than the rest of the corpus.

Endpoints

GETPOST /api/v1/text-analysis 1 credit

Full analysis — counts, readability and language

Runs all three analyses and returns them together. The focused endpoints below cost the same, so use them only when you want a smaller payload.

Parameters
ParameterTypeDescription
text required string The text to analyse. Up to 100,000 characters.
Request
curl -X POST "https://ksty.ch/api/v1/text-analysis" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"The quick brown fox jumps over the lazy dog."}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/text-analysis');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "text": "The quick brown fox jumps over the lazy dog."}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/text-analysis', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "text": "The quick brown fox jumps over the lazy dog."
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/text-analysis',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "text": "The quick brown fox jumps over the lazy dog."
},
)
data = response.json()
Response 200 OK
{
    "counts": {
        "words": 9,
        "sentences": 1,
        "syllables": 11,
        "reading_time_seconds": 3
    },
    "readability": {
        "reliable": false,
        "note": "Fewer than 10 words — readability scores need a paragraph or so to mean anything, so they are omitted.",
        "scores": null
    },
    "language": {
        "language": "en",
        "name": "English",
        "confidence": 0.68,
        "reliable": true
    }
}
Response fields
FieldTypeDescription
counts object Every size measure — see the counts endpoint.
readability object Six formulas plus a consensus grade, or nulls with a note when the text is too short.
language object Detected language with confidence and alternatives.
GETPOST /api/v1/text-analysis.counts 1 credit

Word, sentence and character counts only

Parameters
ParameterTypeDescription
text required string The text to count.
Request
curl -X POST "https://ksty.ch/api/v1/text-analysis.counts" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Hello world. This is a test."}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/text-analysis.counts');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "text": "Hello world. This is a test."}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/text-analysis.counts', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "text": "Hello world. This is a test."
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/text-analysis.counts',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "text": "Hello world. This is a test."
},
)
data = response.json()
Response 200 OK
{
    "characters": 28,
    "characters_no_spaces": 23,
    "words": 6,
    "unique_words": 6,
    "sentences": 2,
    "paragraphs": 1,
    "syllables": 7,
    "avg_word_length": 3.83,
    "avg_sentence_length": 3,
    "reading_time_seconds": 2,
    "speaking_time_seconds": 3
}
Response fields
FieldTypeDescription
characters integer Total characters including whitespace.
characters_no_spaces integer Characters with all whitespace removed.
letters integer Letter characters only — the input to the Coleman-Liau and ARI formulas.
words integer Words, counting "don't" and "well-known" as one each.
unique_words integer Distinct words, compared case-insensitively.
sentences integer Sentences, with common abbreviations masked so "Dr." does not split one.
paragraphs integer Blocks separated by a blank line.
syllables integer Approximate total syllables (English vowel-group heuristic).
polysyllabic_words integer Words of 3+ syllables — what Gunning Fog and SMOG treat as complex.
long_words integer Words of 7+ characters.
avg_word_length number Mean characters per word.
avg_sentence_length number Mean words per sentence.
avg_syllables_per_word number Mean syllables per word.
reading_time_seconds integer At 200 words per minute, silent reading.
speaking_time_seconds integer At 130 words per minute, read aloud.
GETPOST /api/v1/text-analysis.readability 1 credit

Readability grades and a plain-language interpretation

Parameters
ParameterTypeDescription
text required string The text to score. Needs at least 10 words to return scores.
Request
curl -X POST "https://ksty.ch/api/v1/text-analysis.readability" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"The committee subsequently determined that the implementation of the proposed methodology would necessitate substantial additional expenditure."}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/text-analysis.readability');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "text": "The committee subsequently determined that the implementation of the proposed methodology would necessitate substantial additional expenditure."}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/text-analysis.readability', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "text": "The committee subsequently determined that the implementation of the proposed methodology would necessitate substantial additional expenditure."
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/text-analysis.readability',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "text": "The committee subsequently determined that the implementation of the proposed methodology would necessitate substantial additional expenditure."
},
)
data = response.json()
Response 200 OK
{
    "reliable": true,
    "scores": {
        "flesch_reading_ease": 8.2,
        "flesch_kincaid_grade": 19.8,
        "gunning_fog": 24.6,
        "smog_index": 19.1,
        "coleman_liau_index": 21.4,
        "automated_readability_index": 23.1
    },
    "consensus_grade": 21.6,
    "interpretation": {
        "ease": "very difficult",
        "school_level": "College graduate",
        "audience": "Academic or specialist register; expect general readers to struggle."
    }
}
Response fields
FieldTypeDescription
reliable boolean false when the text is under 10 words, in which case scores is null. A grade computed from two sentences is noise, so none is returned.
note string|null Why scores were omitted, when they were.
scores.flesch_reading_ease number 0–100, higher is easier. 60–70 is standard prose.
scores.flesch_kincaid_grade number US school grade level.
scores.gunning_fog number Grade level weighting words of 3+ syllables.
scores.smog_index number Grade level from polysyllable density; common in health writing.
scores.coleman_liau_index number Grade level from letters per word, avoiding syllable estimation entirely.
scores.automated_readability_index number Grade level from characters per word.
consensus_grade number Mean of the five grade-level scores. A wide spread between them means the text mixes registers.
interpretation.ease string very easy … very difficult, from the Flesch score.
interpretation.school_level string Consensus grade as a school level.
interpretation.audience string One sentence on who can comfortably read it.
GETPOST /api/v1/text-analysis.language 1 credit

Detect the language of the text

Parameters
ParameterTypeDescription
text required string The text to identify. At least 5 words for a reliable answer on Latin-script languages.
Request
curl -X POST "https://ksty.ch/api/v1/text-analysis.language" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Le chat noir dort sur le tapis dans le salon."}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/text-analysis.language');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "text": "Le chat noir dort sur le tapis dans le salon."}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/text-analysis.language', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "text": "Le chat noir dort sur le tapis dans le salon."
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/text-analysis.language',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "text": "Le chat noir dort sur le tapis dans le salon."
},
)
data = response.json()
Response 200 OK
{
    "language": "fr",
    "name": "French",
    "script": "Latin",
    "confidence": 0.42,
    "reliable": true,
    "method": "ngram",
    "alternatives": [
        {
            "language": "es",
            "name": "Spanish",
            "confidence": 0.21
        }
    ]
}
Response fields
FieldTypeDescription
language string|null ISO 639-1 code, or null when nothing could be determined.
name string|null English name of the language.
script string|null Writing system — Latin, Cyrillic, Han, Devanagari and so on.
confidence number 0–1. The winner's share of the total evidence, so it measures the margin over the runner-up rather than absolute certainty.
reliable boolean false for very short text or a close call. Check alternatives before acting on an unreliable result.
method string script when the Unicode block decided it (near-certain), ngram when function words and trigrams did, none when nothing matched.
alternatives array Next-best candidates with their confidence. Populated only for Latin-script scoring.

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 No "text" parameter was sent, or it was empty once trimmed. Send the text you want analysed. Note that unanalysable text is not an error — a two-word string returns 200 with "reliable": false, so "you called this wrong" stays distinguishable from "this text is too short to score".
text_too_large this API 413 The text exceeded 100,000 characters. Split the document and analyse it in parts. Counts add up across parts; readability scores do not — recompute those per section, or on a representative sample.
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.