DEV Community

Cover image for 10 Free Facts, Jokes & Name APIs With No Key (2026)
Alex Spinov
Alex Spinov

Posted on • Originally published at blog.spinov.online

10 Free Facts, Jokes & Name APIs With No Key (2026)

On July 12, 2026 I asked a free API to guess the age of someone named Xzqwlptv. It answered in a few milliseconds: HTTP 200, valid JSON.

# runnable, read-only: no key needed
curl -s "https://api.agify.io?name=Xzqwlptv"
Enter fullscreen mode Exit fullscreen mode
{"count":0,"name":"Xzqwlptv","age":null}      # HTTP 200 OK
Enter fullscreen mode Exit fullscreen mode

Status 200. The JSON parses. The age key is present, exactly where a schema says it belongs. Its value is null. Every guard I usually reach for passes this response: if resp.ok, if "age" in data, even a JSON Schema that requires an age property. The null walks straight past all of them and into the dataset.

My earlier keyless-API posts kept circling one idea from different angles. HTTP 200 does not mean the read worked, because the body can be empty. HTTP 201 Created does not mean a write happened, because the read-back returns 404. This post moves the lie one level deeper than either of those. Here the status is 200, the body arrives, it parses, it matches your schema, and the field you came for is sitting right there. The value is just empty. The null that passes your schema check.

A free fun or facts API here means a public endpoint that returns a joke, a fact, or a guess about a name, with no API key, no signup, and no credit card. A URL you can paste into a terminal right now. Ten of them clear that bar, and I re-verified every response below with a live curl on July 12, 2026: real HTTP code, real body, trimmed but never reworded.

One scope note first, so the numbers stay honest. I curl-verified all ten APIs on July 12, 2026. I have not run any of them in production. My 2,190 production scraper runs (962 of them on a single Trustpilot scraper) are a different domain, and I cite them for one reason only: they are why I read a field's value and its confidence instead of its status line. That number is not a claim about these ten endpoints.

# API What it returns Example call The empty success to watch
1 agify.io Age guess from a first name GET api.agify.io?name=Xzqwlptv 200 with age: null
2 genderize.io Gender guess + probability GET api.genderize.io?name=Andrea 200 with gender at 0.69 confidence
3 nationalize.io Country guesses for a name GET api.nationalize.io?name=Xzqwlptv 200 with an empty country array
4 icanhazdadjoke Random dad joke GET icanhazdadjoke.com/ 200 as HTML or JSON, your header decides
5 Advice Slip Random advice GET api.adviceslip.com/advice/99999999 200, envelope shape silently changes
6 Useless Facts Random trivia fact GET uselessfacts.jsph.pl/api/v2/facts/random?language=xx 200, asked for xx, got English
7 Cat Facts Cat trivia + pagination GET catfact.ninja/facts?limit=99999 200, silently clamped to 1,000
8 yesno.wtf Random yes / no + gif GET yesno.wtf/api?force=yes 200, a hidden flag decides "random"
9 Chuck Norris Random Chuck Norris joke GET api.chucknorris.io/jokes/random None: an empty array here is normal
10 JokeAPI v2 Filtered jokes GET v2.jokeapi.dev/joke/Any None: it pairs its body flag with a real 400

Two APIs that a thousand tutorials still hardcode did not answer at all the day I checked. They get their own section after the ten.

1. agify.io: the null that passes your schema check

agify guesses a person's age from a first name, using a database of names it has seen with ages attached. It is keyless, fast, and it is the first API in a lot of "let's call an API" lessons. The happy path is clean.

# runnable, read-only
curl -s "https://api.agify.io?name=alex"
Enter fullscreen mode Exit fullscreen mode
{"count":426587,"name":"alex","age":45}
Enter fullscreen mode Exit fullscreen mode

Age 45, and a count of 426,587: the API saw the name "alex" attached to an age in 426,587 records. Now send it a name it has never seen, which is the transcript from the top of this post: ?name=Xzqwlptv returns HTTP 200 with {"count":0,"name":"Xzqwlptv","age":null}. The count drops to zero and age becomes null. The documentation is upfront that an unknown name yields a null age; the point is that nothing in the HTTP layer flags it. Status 200, valid JSON, age present. This guard, which I have shipped more than once, waves it through:

# toy, illustrative: this "safety" check does not protect you
r = requests.get("https://api.agify.io", params={"name": name})
d = r.json()
if r.ok and "age" in d:      # 200, and the key exists
    ages.append(d["age"])    # None goes into the list
Enter fullscreen mode Exit fullscreen mode

There is a second, quieter trap in the same response, and it is the one I care about more. count is the confidence, and almost nobody reads it.

# runnable, read-only
curl -s "https://api.agify.io?name=Kwabena"
Enter fullscreen mode Exit fullscreen mode
{"count":684,"name":"Kwabena","age":45}
Enter fullscreen mode Exit fullscreen mode

"alex" is backed by 426,587 samples. "Kwabena" by 684. Both answered age: 45. If your code reads only the age field, a guess drawn from 684 people and a guess drawn from 426,587 look identical. One is a solid estimate. The other is a shrug wearing a number. The truthful guard reads the value and the confidence together:

# toy, illustrative: the check the field actually needs
if d.get("age") is not None and d.get("count", 0) >= 100:
    ages.append(d["age"])
Enter fullscreen mode Exit fullscreen mode

When to use it: playful demos, and as the cheapest possible lesson that a present field is not an answered field. The threshold of 100 is mine, not the API's; pick your own and write it down.

2. genderize.io: the confidence field you threw away

genderize is agify's sibling from the same team: it guesses gender from a first name and, unlike agify, it labels its own uncertainty out loud.

# runnable, read-only
curl -s "https://api.genderize.io?name=alex"
Enter fullscreen mode Exit fullscreen mode
{"count":1668776,"name":"alex","gender":"male","probability":0.95}
Enter fullscreen mode Exit fullscreen mode

Unknown name, same empty success as agify, one field richer:

# runnable, read-only
curl -s "https://api.genderize.io?name=Xzqwlptv"
Enter fullscreen mode Exit fullscreen mode
{"count":0,"name":"Xzqwlptv","gender":null,"probability":0.0}
Enter fullscreen mode Exit fullscreen mode

gender: null and probability: 0.0. The API is being honest to a fault here; it hands you a machine-readable "I have no idea" and most code still checks if data["gender"]:, gets None, and moves on quietly. But the sharper case is not the null. It is the answer that is real and thin at the same time:

# runnable, read-only
curl -s "https://api.genderize.io?name=Andrea"
Enter fullscreen mode Exit fullscreen mode
{"count":1681597,"name":"Andrea","gender":"female","probability":0.69}
Enter fullscreen mode Exit fullscreen mode

gender: "female", backed by 1.68 million records, at a probability of 0.69. That is barely better than a coin flip, and "Andrea" is male in Italy and female in much of the English-speaking world, which is exactly why the number is low. Your if gender: passes on 0.69 the same way it passes on 0.95. The API did its job: it put the doubt in the payload. Reading it is on you.

# toy, illustrative
if d.get("gender") and d.get("probability", 0) >= 0.90:
    genders[name] = d["gender"]
Enter fullscreen mode Exit fullscreen mode

When to use it: rough demographic guesses where you will honor the probability. Contrast it with agify in your head: agify buries its confidence in a count, genderize spells it out as a probability, and both get ignored the same way.

3. nationalize.io: the empty array at HTTP 200

nationalize, third in the same family, guesses which country a name comes from and returns a ranked list of candidates.

# runnable, read-only
curl -s "https://api.nationalize.io?name=alex"
Enter fullscreen mode Exit fullscreen mode
{"count":1673580,"name":"alex","country":[
  {"country_id":"US","probability":0.095754},
  {"country_id":"GB","probability":0.071241},
  {"country_id":"ES","probability":0.052449}]}
Enter fullscreen mode Exit fullscreen mode

Note the top probability is 0.096. Even on a common name, the single best country guess is under 10 percent, which tells you how to weigh this data before you build anything on it. Now the unknown name:

# runnable, read-only
curl -s "https://api.nationalize.io?name=Xzqwlptv"
Enter fullscreen mode Exit fullscreen mode
{"count":0,"name":"Xzqwlptv","country":[]}
Enter fullscreen mode Exit fullscreen mode

An empty array. Not null, not an error, an empty list under a 200. This is the third form of "I don't know" from three sibling APIs on the same request: agify says null, genderize says null plus 0.0, nationalize says []. The failure mode is specific to the shape. This line looks defensive and is not:

top = r.json()["country"][0]["country_id"]   # IndexError on a 200
Enter fullscreen mode Exit fullscreen mode

data["country"] exists, so a key check passes. The list is empty, so indexing into it throws, on a response your status check already blessed. There is no single "is this empty" test that covers null, 0.0, and []; you write one per shape, per API. That is the whole lesson of this trio.

When to use it: guessing likely origin for a name, always past a guard that treats an empty country list as "no answer" rather than crashing on it.

4. icanhazdadjoke: the same URL, HTML or JSON, your header decides

icanhazdadjoke serves random dad jokes and it does something the previous three do not: it hands back a completely different body for the identical URL, depending on one request header.

# runnable, read-only: no Accept header
curl -s "https://icanhazdadjoke.com/"
Enter fullscreen mode Exit fullscreen mode
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    ...
Enter fullscreen mode Exit fullscreen mode

That is HTTP 200 with Content-Type: text/html, a full web page, not a joke you can parse. Call resp.json() on it and you get a decode error on a response your code just classified as a success. Add one header and the same URL turns into JSON:

# runnable, read-only
curl -s -H "Accept: application/json" "https://icanhazdadjoke.com/"
Enter fullscreen mode Exit fullscreen mode
{"id":"NCQfqHYgaFd","joke":"I'm glad I know sign language, it's pretty handy.","status":200}
Enter fullscreen mode Exit fullscreen mode

Same path, same 200, HTML or JSON purely by what you put in Accept. Forget the header and your parser chokes on a healthy response. There is a bonus tell in that JSON: "status":200, the HTTP code copied down into the body. It is the same habit I documented in the pop-culture APIs post, where Open Trivia DB tucks a response_code inside the envelope. When an API repeats its status inside the payload, that is usually a hint that the payload is where the truth lives, not the status line.

When to use it: any joke feature, with Accept: application/json sent explicitly and never assumed by default.

5. Advice Slip: the envelope that changes shape at 200

Advice Slip returns one piece of advice per call. The happy path is a tidy wrapper.

# runnable, read-only
curl -s "https://api.adviceslip.com/advice"
Enter fullscreen mode Exit fullscreen mode
{"slip":{"id":154,"advice":"State the problem in words as clearly as possible."}}
Enter fullscreen mode Exit fullscreen mode

You reach for data["slip"]["advice"] and move on. Then someone requests an id that does not exist:

# runnable, read-only
curl -s "https://api.adviceslip.com/advice/99999999"
Enter fullscreen mode Exit fullscreen mode
{"message":{"type":"error","text":"Advice slip not found."}}
Enter fullscreen mode Exit fullscreen mode

Still HTTP 200. Still application/json. But the top-level key is no longer slip, it is message, and the shape underneath is different too. data["slip"] throws a KeyError on a response that reported success. This is schema drift inside a single endpoint: the envelope silently swaps from {"slip": ...} to {"message": ...} based on whether the lookup hit, and the status code stays 200 through the switch. Your parser has to branch on which key is present, not on the HTTP code.

When to use it: a random advice string for filler content, with a check for the slip key before you read into it, because the miss path returns a valid 200 with a different structure.

6. Useless Facts: you asked for language xx, you got English

Useless Facts serves a random trivia fact with a source attached. It also takes a language parameter, and that parameter is where it gets quietly generous.

# runnable, read-only
curl -s "https://uselessfacts.jsph.pl/api/v2/facts/random"
Enter fullscreen mode Exit fullscreen mode
{"id":"b939f633...","text":"Until 1994, world maps and globes sold in Albania only had Albania on them.",
 "source":"djtech.net","language":"en","permalink":"https://uselessfacts.jsph.pl/api/v2/facts/b939f633..."}
Enter fullscreen mode Exit fullscreen mode

Now ask for a language that does not exist:

# runnable, read-only
curl -s "https://uselessfacts.jsph.pl/api/v2/facts/random?language=xx"
Enter fullscreen mode Exit fullscreen mode
{"id":"7e69818f...","text":"On a Canadian two dollar bill, the flag flying over the Parliament buildings is an American flag.",
 "source":"djtech.net","language":"en","permalink":"..."}
Enter fullscreen mode Exit fullscreen mode

I asked for language xx. I got HTTP 200 and an English fact. No error, no warning, no 400. The only evidence that my request was ignored is the language: "en" field in the response, which happens to disagree with what I sent. This is a silent fallback: the API decided that giving me something plausible beat telling me my input was wrong. If you localize content off a parameter like this and never compare the echoed language back to the one you sent, you will ship English into a French page and every request will report success.

When to use it: a random fact for a footer or a loading screen, with a check that the returned language matches what you requested if you care about the language at all.

7. Cat Facts: ask for 99,999, silently get 1,000

Cat Facts is exactly what it sounds like, plus a paginated /facts endpoint with a limit parameter. The single-fact call is clean.

# runnable, read-only
curl -s "https://catfact.ninja/fact"
Enter fullscreen mode Exit fullscreen mode
{"fact":"Unlike humans, cats are usually lefties. Studies indicate that their left paw is typically their dominant paw.","length":110}
Enter fullscreen mode Exit fullscreen mode

Now abuse the pagination. Ask for far more than can exist:

# runnable, read-only
curl -s "https://catfact.ninja/facts?limit=99999"
Enter fullscreen mode Exit fullscreen mode
{"current_page":1,"per_page":1000,"total":332,"last_page":1,
 "data":[ ... 332 facts ... ]}
Enter fullscreen mode Exit fullscreen mode

I requested limit=99999. The response reports per_page: 1000. The server clamped my limit to its own maximum of 1,000 and did it silently, HTTP 200, no field named "you asked for too much." Here the total is only 332 so I got everything anyway, but the clamp is the behavior that matters, and it is the same silent clamp I hit on Random User Generator in an earlier keyless-API post: you ask for N, the server caps at M, and your "full export" is a fraction of what you think. The tell is the per_page field in the response, which reports what actually happened rather than what you asked for.

When to use it: cat trivia, obviously, and as a cheap reminder to compare the per_page and total a paginator returns against the limit you sent, every time.

8. yesno.wtf: the flag that decides whether "random" was random

yesno.wtf answers a yes-or-no question at random and returns a matching gif. It is the simplest API on this list, and it still has a field that changes the meaning of the response.

# runnable, read-only
curl -s "https://yesno.wtf/api"
Enter fullscreen mode Exit fullscreen mode
{"answer":"yes","forced":false,"image":"https://yesno.wtf/assets/yes/11-a23cbde4....gif"}
Enter fullscreen mode Exit fullscreen mode

answer: "yes", forced: false. The forced field says this answer was genuinely random. But the endpoint accepts a ?force= parameter, and when you use it the answer stops being random while the status stays 200:

# runnable, read-only
curl -s "https://yesno.wtf/api?force=yes"
Enter fullscreen mode Exit fullscreen mode
{"answer":"yes","forced":true,"image":"https://yesno.wtf/assets/yes/9-6403270c....gif"}
Enter fullscreen mode Exit fullscreen mode

Same 200, same answer: "yes", but forced: true. If your code reads only answer, a real coin flip and a rigged one are indistinguishable. The single field that tells them apart, forced, is the one nobody bothers to read. It is a toy version of a real problem: a boolean deep in the payload that changes what the main value means, sitting quietly next to it at the same successful status.

When to use it: a fun random decision, and a two-line demonstration for a junior of why you read the flags in a body, not just the headline field.

9. Chuck Norris: when an empty array is the correct answer

The last two APIs on this list get it right, and they are here because "right" is worth seeing next to all of the above. api.chucknorris.io serves random Chuck Norris jokes.

# runnable, read-only
curl -s "https://api.chucknorris.io/jokes/random"
Enter fullscreen mode Exit fullscreen mode
{"categories":[],"id":"SPv79G_bQWuEBMXyCMHynA",
 "value":"Chuck Norris is the only person on earth who can rhyme 'orange' with 'lozenge'.",
 "url":"https://api.chucknorris.io/jokes/random"}
Enter fullscreen mode Exit fullscreen mode

Look at categories: []. An empty array, exactly the shape that meant "no answer" over on nationalize. Here it means the opposite: the joke simply has no category assigned, which is a perfectly normal state, and the value field is fully populated. Same JSON shape, opposite meaning, and the only way to know is to read each API's contract. There is no universal rule that an empty array signals failure. Then request a category that does not exist:

# runnable, read-only
curl -s "https://api.chucknorris.io/jokes/random?category=notreal"
Enter fullscreen mode Exit fullscreen mode
{"timestamp":"2026-07-11T21:39:17.715Z","status":404,"error":"Not Found","path":"/jokes/random"}
Enter fullscreen mode Exit fullscreen mode

An actual HTTP 404, with a clean error object. When the request cannot be served, Chuck Norris says so with the right status code instead of a 200 and an empty body. That is the good-citizen behavior the first eight entries kept failing at.

When to use it: random jokes with categories, and as the contrast case that teaches [] is ambiguous across APIs and honest 404s still exist.

10. JokeAPI v2: the body flag paired with the right status

JokeAPI serves jokes with filters (categories, safe-mode, blacklists). Its normal response carries a status flag right in the body, which is the pattern that usually precedes a lie on this list.

# runnable, read-only
curl -s "https://v2.jokeapi.dev/joke/Any?safe-mode"
Enter fullscreen mode Exit fullscreen mode
{"error":false,"category":"Pun","type":"single",
 "joke":"I was struggling to figure out how lightning works, but then it struck me.",
 "flags":{"nsfw":false,"religious":false,"...":"..."},"id":219,"safe":true,"lang":"en"}
Enter fullscreen mode Exit fullscreen mode

error: false, embedded in the payload. On this list, a status field inside the body has been a warning sign. So I forced a failure, blacklisting every category so nothing could match:

# runnable, read-only
curl -s "https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit&contains=zzzzxxxqqq"
Enter fullscreen mode Exit fullscreen mode
{"error":true,"internalError":false,"code":106,
 "message":"No matching joke found",
 "causedBy":["No jokes were found that match your provided filter(s)."]}
Enter fullscreen mode Exit fullscreen mode

Here is the part I did not expect until I ran it: that response came back HTTP 400, not 200. JokeAPI puts error: true in the body and sets a 4xx status. Both channels agree. That is how the body-flag pattern is supposed to work, and it is why JokeAPI is the counterexample rather than another entry in the "200 lies" column. The analysis I started from guessed this endpoint returned a 200 with error: true; my own curl on July 12 said 400, so the text you are reading follows the curl, not the guess.

When to use it: filtered jokes when you want categories and safe-mode, and as proof that an in-body status flag is fine as long as the HTTP code is telling the same story.

Two that stopped answering at all

The dead teach the lesson louder. Both checks below are from July 12, 2026.

Quotable (api.quotable.io/random) was the default free quotes API in an enormous number of tutorials. It did not answer: connection failed, no HTTP status at all, on repeated tries.

curl -si "https://api.quotable.io/random"
# -> curl: (couldn't connect), no HTTP status
Enter fullscreen mode Exit fullscreen mode

Bored API (boredapi.com/api/activity), the "suggest me something to do" API from a thousand beginner projects, did the same: no response, no status code.

I make a narrow claim here: on the day and hour I checked, from where I checked, both were unreachable. That is enough to matter, because "keyless" and "alive" are properties you re-verify, not facts you remember. A tutorial written in 2022 against Quotable produces a dead link today, and the reader gets no HTTP status to even diagnose it.

Two honesty notes on the flaky ones, because pretending my checks are the last word would be its own kind of lie. ZenQuotes (zenquotes.io/api/random) returned nothing when the analysis for this post was drafted, then answered me a clean 200 with a normal quote on July 12; it flaps, so I left it off the ten and I am not repeating the rate-limit gotcha I could not reproduce. Official Joke API (official-joke-api.appspot.com/random_joke) was reported dead in an earlier scan and answered me a healthy 200; a single curl from one machine is a snapshot, not an uptime measurement, and I did not measure uptime for anything here.

Why does a green schema check prove so little?

Here is the uncomfortable part, and the reason this is not a listicle. Every trap above survives the three checks most code actually runs. The status is 200, so resp.raise_for_status() is happy. The body is valid JSON, so resp.json() succeeds. The field you wanted is present, so a schema validator passes. And the value is still null, or [], or a guess at 0.69 confidence, or an English fact you did not ask for. Line these up and they are not novelties, they are named classes of production drift:

  • A null scalar at 200 (agify). Your real backend returns null for absent optional fields all the time. If your pipeline treats "field present" as "value present," it seeds nulls and reports success.
  • A confidence field nobody reads (agify's count, genderize's probability). Real ML and enrichment APIs attach confidence to every guess. Dropping it turns a 0.69 coin flip into a fact in your database.
  • An empty array at 200 (nationalize). Production search and recommendation endpoints return [] for "no results" constantly, and results[0] throws on a response that reported success.
  • An envelope that changes shape (Advice Slip). Real APIs return one shape on hit and another on miss under the same 200. Branch on the HTTP code and you read the wrong key.
  • A silent fallback or clamp (Useless Facts, Cat Facts). You ask for language xx or limit=99999; the server substitutes a default and reports success, and your export is quietly wrong.

A schema check that passes against one of these has proven one thing: the response has the right shape. Whether it has an answer is a separate question, and the status line will not answer it for you. The fix is not more validation libraries. It is a guard that reads the value and its confidence, not just the field's presence. Against the flagship:

# runnable local: a present field is not an answered field
import requests

def guess_age(name, min_count=100):
    r = requests.get("https://api.agify.io", params={"name": name}, timeout=15)
    r.raise_for_status()               # 200 sails through
    d = r.json()                       # valid JSON, "age" key present
    age, count = d.get("age"), d.get("count", 0)
    if age is None or count < min_count:
        raise ValueError(
            f"no usable age for {name!r}: age={age}, count={count}"
        )
    return age

print(guess_age("alex"))       # -> 45   (count 426,587)
print(guess_age("Xzqwlptv"))   # -> ValueError: no usable age for 'Xzqwlptv': age=None, count=0
Enter fullscreen mode Exit fullscreen mode
45
Traceback (most recent call last):
  ...
ValueError: no usable age for 'Xzqwlptv': age=None, count=0
Enter fullscreen mode Exit fullscreen mode

That is the live behavior from July 12, 2026 (traceback trimmed). raise_for_status() is satisfied, the JSON parses, the age key exists, and the extra two-line check catches the null and the zero-confidence case that all three earlier guards missed. The same skeleton covers the siblings: for genderize, gate on probability; for nationalize, treat an empty country list as "no answer" before you index into it.

For context, not a claim of scale: I build web-scraping and data-enrichment tool layers, currently 2,190 runs across 32 actors in production, and the read-the-value habit comes from there. The expensive incidents were never the 500s. A 500 pages you at 3am and you fix it. The quiet 200 with a null where a value should be does not page anyone; it just corrupts a column, and you find out weeks later when a report looks wrong. These toy APIs are the cheapest lab I know for that exact failure, because you can reproduce it in one curl. I wrote the same pattern up for pop-culture APIs and government data APIs; the fun ones turned out to teach it best, because a joke API has nothing to hide behind.

FAQ

Is agify free without an API key?
Yes. GET https://api.agify.io?name=alex works with no key, no signup, and no card, and returned HTTP 200 with {"count":426587,"name":"alex","age":45} on July 12, 2026. There is a free daily request limit for anonymous use and paid tiers above it, but casual and learning use needs no credentials. The same holds for its siblings genderize.io and nationalize.io.

Why does agify return null for a name?
Because agify guesses age from names it has seen with ages attached, and an unfamiliar name has no data behind it. For ?name=Xzqwlptv it returns HTTP 200 with {"count":0,"name":"Xzqwlptv","age":null}: count is how many records backed the guess, and when it is 0 the age is null. The status is still 200 and the age key is still present, so a schema check passes and the null enters your data unless you test the value.

What do count and probability mean in genderize and agify?
They are the confidence behind the guess. In agify, count is the number of records the age estimate is based on; "alex" returned count: 426587, a rare name might return count: 684, and both can show the same age. In genderize, probability is how sure the gender guess is: "alex" came back at 0.95, "Andrea" at 0.69. A non-null answer at 0.69 is barely better than a coin flip, so gate on the confidence, not just on the field being present.

Why does icanhazdadjoke return HTML instead of JSON?
Because it uses content negotiation. The same URL, https://icanhazdadjoke.com/, returns a full HTML page under the default Accept header and returns JSON only when you send Accept: application/json. Both are HTTP 200. If you call resp.json() without setting that header, it fails on a successful response. Send the header explicitly.

What happened to the Quotable and Bored APIs?
Both were unreachable when I checked on July 12, 2026: api.quotable.io/random and boredapi.com/api/activity returned no HTTP response at all on repeated tries from my machine. They were default APIs in years of tutorials, which means a lot of copy-pasted code now points at dead endpoints. That is a snapshot from one place and time, not an uptime claim, but it is why "keyless" is a property to re-verify rather than remember.

What is a good free joke or facts API with no key?
For jokes, icanhazdadjoke (send Accept: application/json), api.chucknorris.io, and v2.jokeapi.dev all worked keyless on July 12, 2026, and JokeAPI pairs its body error flag with a correct HTTP status. For facts, uselessfacts.jsph.pl and catfact.ninja both returned clean JSON. For name guesses, the agify / genderize / nationalize trio is the most useful, as long as you read the confidence fields rather than just the answer.


Written by Aleksei Spinov. I build web-scraping and data-enrichment tool layers, currently 2,190 runs across 32 actors in production. Every endpoint above was re-verified with a live curl (real HTTP code, real body) on July 12, 2026 before publishing; responses are trimmed, never paraphrased. I have not run these fun and facts APIs in production; the 2,190 runs are a different domain, cited only as the origin of the read-the-value habit. Drafted with an AI assistant, then fact-checked and edited by me against the raw responses.

Follow for the next keyless layer I verify. And tell me: what is the worst silent null or empty array you have had a "successful" API hand you, and how long did it take to notice? I read every comment.

Top comments (0)