Your Dictionary API 404s on Real Words. Here Are 9 Keyless.


I pointed a signup form’s “is this a real word” check at a free dictionary API. The first thing it rejected was webhook.

Then rizz. Then doomscrolling. Then kubernetes. All four are real English. The API answered HTTP 404 on every one, my code read 404 as “not a word”, and the user got bounced for typing a word that has been in major dictionaries for years.

webhook         -> REJECTED as not a word
rizz            -> REJECTED as not a word
doomscrolling   -> REJECTED as not a word
kubernetes      -> REJECTED as not a word
selfie          -> accepted
hello           -> accepted

That is real stdout from a six-line gate against api.dictionaryapi.dev, run on July 18, 2026. The gate is not buggy. It does exactly what its author told it to. The bug is the belief that a 404 is a fact about the English language.

The one idea in this post

A free dictionary API returns word data with no API key, no signup, no card. Nine of them clear that bar and I re-verified each with a live curl on July 18, 2026. The table is one screen down. Under it sits the actual lesson, and it is the escalation of a thing I keep hitting: the signals you gate on lie to you before you ever read the body. A 404 is a database-membership flag, not a verdict on the word. A confidence field is provenance, not correctness.

Earlier stops on this road argued about what a 200 body contained: a null that passed schema-check, a field whose name promised more than it counted, a timestamp in the wrong unit. This one is worse, because it attacks the two things a naive consumer trusts before reading the body. The status line. And a number the vendor labels confidence.

The 9, at a glance

Everything here returned HTTP 200 with no key from my host on July 18, 2026. I sent a plain User-Agent and nothing else: no Authorization, no ?key=.

#APIWhat it returnsThe gotcha
1Free Dictionary APIDefinitions, IPA, audio mp3Built on a frozen wordlist. 404s on webhook, rizz, doomscrolling, kubernetes
2FreeDictionaryAPIRicher entries: partOfSpeech, sensesNewer community project. The one that does know webhook
3DatamuseSynonyms, rhymes, means-like, spellingscore is not relevance. sp=hippo* returns hippopotomonstrosesquippedaliophobia as a scored word
4Urban Dictionary (unofficial)Crowd definitions200 with authoritative-looking JSON, content unvetted. NSFW risk. The top api entry had 0 upvotes
5RhymeBrainRhymes, portmanteausscore and freq are its own internal metrics. Throttle politely
6Random Word APIOne or more random wordsLiterally random. Three calls gave me cockney, turfy, mesenteric
7LanguageToolGrammar and spell check (POST)The free 200 admits it under-reports, in a premiumHint field
8MyMemoryTranslation memorymatch: 1 (100%) sat on a wrong translation
9PoetryDBPoems by author, title, linesA literary corpus, not a dictionary. No fuzzy match, exact keys only

Hosts 1, 2, 3 and 9 are the cleanest for code: a plain GET and JSON back. Number 7 is a POST form. Numbers 4 and 6 are crowd and unfiltered, keyless but not authoritative. Number 8 carries the sharpest trap, and I will get to it.

Why a real word gets a 404

A 404 from a keyless dictionary means “absent from my snapshot”, never “absent from English.” The word matrix makes it plain. Same word, two keyless hosts, opposite verdict:

# runnable, read-only: no key, no signup. does each API think these are words?
import urllib.request, urllib.error

WORDS = ["webhook", "rizz", "doomscrolling", "kubernetes", "hello"]
APIS = {
    "dictionaryapi.dev":     "https://api.dictionaryapi.dev/api/v2/entries/en/{}",
    "freedictionaryapi.com": "https://freedictionaryapi.com/api/v1/entries/en/{}",
}
UA = {"User-Agent": "keyless-audit/1.0"}

def status(url):
    try:
        with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=25) as r:
            return r.status
    except urllib.error.HTTPError as e:
        return e.code

print(f"{'word':<15}" + "".join(f"{name:<24}" for name in APIS))
for w in WORDS:
    row = "".join(f"{status(tmpl.format(w)):<24}" for tmpl in APIS.values())
    print(f"{w:<15}{row}")
word           dictionaryapi.dev       freedictionaryapi.com
webhook        404                     200
rizz           404                     200
doomscrolling  404                     200
kubernetes     404                     200
hello          200                     200

Four real words, four 404s on the first host, four 200s with real noun definitions on the second. Curl freedictionaryapi.com for webhook and you get back noun, with the sense “A method of augmenting or altering the behavior of a web application”. A real definition, keyless, for the word the other API swears does not exist.

This is not a fluke of four cherry-picked words. I ran twelve. The four tech and neologism tokens above split 404-then-200 on every one. The eight ordinary controls (selfie, unfollow, podcasting, cryptocurrency, hello, data, cats, running) returned 200 on both hosts, all eight. The line between them is not “is this English”. It is “was this in the dump that api.dictionaryapi.dev was built from”, which appears to be a wordset frozen years ago. rizz was Oxford’s word of the year for 2023. It still 404s.

It is not even English-specific. Ask api.dictionaryapi.dev for the Spanish hola and you get a 404, on a word in every first-week Spanish textbook.

So “does this word exist” has no authoritative keyless answer. It has a per-database answer. If your signup validator, your LLM input sanitizer, or your Scrabble-style checker treats 404 as “reject the user”, you will reject webhook, rizz and doomscrolling, and you will do it silently, because 404 is a perfectly ordinary thing for an HTTP client to see and shrug at.

When it IS a 200, the confidence number can be invented

The 404 problem has an obvious fix: stop trusting a single database. The confidence-field problem is nastier, because the response is a 200, the number looks like a probability, and it is wrong in a way no status code will warn you about.

MyMemory is a keyless translation-memory API. Ask it to translate “hello” into French and read what it hands back:

# runnable, read-only: no key. what does match=1 actually promise?
import json, urllib.request, urllib.parse

UA = {"User-Agent": "keyless-audit/1.0"}

def translate(q, pair):
    url = "https://api.mymemory.translated.net/get?" + urllib.parse.urlencode({"q": q, "langpair": pair})
    with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=25) as r:
        d = json.load(r)
    return d["responseData"]["translatedText"], d["responseData"]["match"]

for q, pair in [("hello", "en|fr"), ("hello", "en|es"), ("thank you", "en|fr")]:
    text, match = translate(q, pair)
    print(f"{q:<10} {pair}  ->  {text!r:<12} match={match}")
hello      en|fr  ->  'world'      match=1
hello      en|es  ->  'testvalue'  match=1
thank you  en|fr  ->  'merci'      match=1

Read those three lines together, because that is the whole point. English “hello” into French comes back as “world” with match=1, which is 100%. Into Spanish it comes back as “testvalue”, some crowd member’s junk, also match=1. And “thank you” into French comes back correctly as “merci”, also match=1. The score is identical whether the answer is wrong, garbage, or right. It is not measuring the answer at all.

Pull the ranked list and you can see the correct answers losing:

# runnable, read-only: the crowd segments behind the top answer
import json, urllib.request

UA = {"User-Agent": "keyless-audit/1.0"}
url = "https://api.mymemory.translated.net/get?q=hello&langpair=en|fr"
with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=25) as r:
    d = json.load(r)

print("top translatedText:", repr(d["responseData"]["translatedText"]), "match:", d["responseData"]["match"])
for m in d["matches"][:3]:
    print(f"   {m['translation']!r:<12} match={m['match']}  created-by={m.get('created-by')}")
top translatedText: 'world' match: 1
   'world'      match=1  created-by=MateCat
   'salut'      match=0.99  created-by=MateCat
   'bonjour'    match=0.99  created-by=MateCat

salut and bonjour, the two correct French greetings, rank below “world” at 0.99. MyMemory’s match is a translation-memory provenance score: how closely a crowd-submitted source segment matched your query string, not how good the translation is. Gate on match >= 0.9 and you will happily ship “hello” translated as “world”, stamped 100% confident, HTTP 200, no exception, no warning.

About those two runnable blocks: they will drift, and that is the lesson

I need to be honest about reproducibility here, because it is exactly the trap. The match=1 blocks above are live crowd data, and I cannot promise you the same strings tomorrow. MyMemory is a translation memory anyone can write to. “world” and “testvalue” are segments some human uploaded; a cleanup pass could delete them tonight. The random word API will obviously never repeat. Even the 404 matrix could shift if api.dictionaryapi.dev ever refreshes its wordlist, though it has been frozen for years.

So do not trust my exact stdout. Run the blocks. What is stable is not the value, it is the mechanism: a match number that stays pinned at 1 across a wrong answer, a garbage answer and a correct answer is telling you it was never a correctness score. If you get a different wrong translation than I did, you have reproduced the finding, not broken it. This is the rule I hold myself to: do not publish a number you cannot re-derive, and when the number is going to move, say so and hand over the code.

The takeaway, in one line

A 200 tells you the server found a record. A 404 tells you it did not find one in its table. A confidence field tells you how a record was scored, by whatever the vendor scores it on. None of the three is a fact about the word. Cross-check, or you will reject webhook and greet Paris with “world”.

The other keyless ones, honestly

The list is genuinely useful; I just will not pretend every host is a source of truth.

Datamuse is the one I reach for most. One host gives you synonyms (ml=), rhymes (rel_rhy=), autocomplete (sug) and spelling (sp=). But its own docs are clear that score is not a relevance or similarity measure, and it shows: sp=hippo* cheerfully returns hippopotomonstrosesquippedaliophobia and hippopathology as scored “words”. It is a corpus, not a validator.

LanguageTool is a grammar and spell checker over a POST form. Send it text=I has a apple. and it flags 2 matches, but look at what it also sends:

name= LanguageTool | version= 6.9-SNAPSHOT | premium= True
premiumHint= You might be missing errors only the Premium version can find. Contact us at sup...
n_matches= 2

The free 200 openly tells you it is a partial check. premium: true plus a premiumHint string is the API admitting, in-band, that the clean-looking result is not the whole result. I respect that more than a silent 200.

Urban Dictionary and Random Word are keyless and fun and unvetted. Urban Dictionary returns confident JSON for anything, the top entry for api today was written by user Nathanmx and had 0 upvotes, and plenty of terms are NSFW. Random Word is random: my three pulls were cockney, turfy, mesenteric. Neither is a place to check whether something is a word. PoetryDB is a delight for a different reason, it serves the full text of poems by author and title (Shall I compare thee to a summer's day? is one GET away), but it is a fixed literary corpus with exact-match keys, not a lookup you fuzz against.

Keyless attempt, key required

These five look free until you send the request. I tried each keyless on July 18, 2026 and logged what came back:

  • Merriam-Webster returned HTTP 200 with the plaintext body Key is required. A 200 that is a refusal, not data. Free key on signup.
  • Wordnik returned 401, No API key found in request. Free key.
  • WordsAPI (via RapidAPI) returned 401, Invalid API key. RapidAPI key, free tier.
  • API-Ninjas Dictionary returned 400, {"error": "Missing API Key."}. Free key.
  • Big Huge Thesaurus needs a key in the URL path itself, so there is no keyless call to make.

Merriam-Webster is the one to remember: a 200 does not even mean you got data. Read the body.

Dead, blocked, or moved (verified today, not on the list)

I do not list things I could not reach. For the record, on July 18, 2026:

  • ConceptNet (api.conceptnet.io): 502 Bad Gateway. Normally keyless and genuinely useful as a semantic graph; it was down every time I looked, so it does not get counted. Try it yourself, it may be back.
  • Random Word API (Vercel mirror): 402 Payment Required, DEPLOYMENT_DISABLED. Dead deploy. Use the Heroku host in the table.
  • FunTranslations: 403, Cloudflare block on my datacenter IP.
  • LibreTranslate public .de mirror: 301 redirect; the public instance is gone and libretranslate.com now wants a key.
  • Glosbe legacy gapi: 404. That endpoint no longer exists.

What I could not verify

I cannot prove why api.dictionaryapi.dev is frozen. Its behavior is consistent with a one-time wordset dump that never gets updated, and its 404s on 2020s vocabulary point that way, but I do not have its build process. That is an inference.

I also cannot promise the FreeDictionaryAPI host stays keyless. It is a newer community project, richer than the old one and the one that actually knows webhook, but “newer and generous today” is not “battle-tested”. Pin nothing critical to it without a fallback.

And a fair objection to me specifically: I run web scrapers in production, 2,190 runs across 32 published actors, 962 of them on one Trustpilot scraper. I have not built a dictionary pipeline in production, and nothing in this post rests on those runs. They are where the reflex comes from, “never gate real-world truth on a status code”, not the evidence. The evidence is the stdout above, and you can regenerate every line of it.

The 404 matrix and the match=1 result are not inferences. Curl them.


If you validate user input against a dictionary today, here is the question I actually want answered, because I do not have a clean method for it: how do you tell “this string is not a real word” apart from “this string is not in the one database I happened to query”? Every fix I can think of (query more sources, keep a local allowlist of neologisms, treat 404 as “unknown” rather than “invalid”) trades one blind spot for another.

What has worked for you?

Every endpoint above was verified with a live curl on July 18, 2026, from my own host, with no API key. Earlier in this series: free art and museum APIs, 15 keyless transit APIs and a timestamp their docs get wrong, HTTP 200 is a lie. Drafted with an AI assistant, then fact-checked and edited by me against the raw responses.


More production scraping tips: t.me/scraping_ai