DEV Community

Cover image for 9 Free Mock & Fake-Data APIs With No Key (2026)
Alex Spinov
Alex Spinov

Posted on • Originally published at blog.spinov.online

9 Free Mock & Fake-Data APIs With No Key (2026)

On July 10, 2026 I sent JSONPlaceholder a POST. It answered 201 Created and handed me the new resource's id. Then I asked it for that resource.

# runnable, harmless: nothing is saved
curl -si -X POST https://jsonplaceholder.typicode.com/posts \
  -H "Content-Type: application/json" -d '{"title":"hello"}'
Enter fullscreen mode Exit fullscreen mode
HTTP/2 201
{
  "title": "hello",
  "id": 101
}
Enter fullscreen mode Exit fullscreen mode
curl -si https://jsonplaceholder.typicode.com/posts/101
Enter fullscreen mode Exit fullscreen mode
HTTP/2 404
{}
Enter fullscreen mode Exit fullscreen mode

201 Created, id 101. Then a 404 for id 101, four seconds later. The API refuted its own receipt. Nothing was ever written, and the docs are upfront about that; it is a mock. But your HTTP client cannot tell the difference, and neither can a test suite that stops reading at the status line.

My earlier keyless-API posts kept hitting one lesson on the read path: HTTP 200 does not mean the read worked. The body can be empty, the entity can be wrong, the error can hide in a JSON field. Mock APIs move that lesson to the write path. A 201 Created is the server's claim that a write happened, not proof that it did. The only proof of a write is a read.

A free mock or fake-data API here means a public endpoint that returns test data (fake users, fake products, or an echo of your own request) with no API key, no signup, and no credit card. A real URL you can paste into a terminal right now. Nine services clear that bar, and I re-verified every endpoint below with a live curl on July 10, 2026: real HTTP code, real body, trimmed but never paraphrased.

One scope note before the list, so the numbers stay honest. I curl-verified every API here on July 10, 2026; I have not run these mock APIs 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 exactly one reason: they are why I read bodies instead of status lines. That number is not a claim about these nine endpoints.

# API What it fakes Example call The lie to watch
1 JSONPlaceholder Blog-style REST (posts, users, todos) POST jsonplaceholder.typicode.com/posts 201 Created, nothing created
2 DummyJSON Products, carts, users, quotes DELETE dummyjson.com/products/1 "Deleted" with a timestamp, still there
3 Fake Store API E-commerce catalog GET fakestoreapi.com/products/999 200 with a zero-byte body
4 Random User Generator Fake user profiles GET randomuser.me/api/?results=5001 Asks for 5,001, returns 1, no error
5 httpbin Echo + any status code on demand GET httpbin.org/status/404 None: you order the failures
6 httpbingo httpbin rewritten in Go GET httpbingo.org/get Same path, different JSON types
7 Postman Echo Echo of your request GET postman-echo.com/get A third envelope shape
8 Platzi Fake Store E-commerce with real CRUD GET api.escuelajs.co/api/v1/products/1 400 (not 404) for missing, shared DB
9 Beeceptor Echo Echo of your request GET echo.free.beeceptor.com/probe A fourth envelope shape

One famous name is missing on purpose. reqres.in, the default fake API in years of tutorials, now answers with a 401 key wall. It gets its own section after the nine, along with three services that stopped answering at all.

1. JSONPlaceholder: the 201 Created that created nothing

JSONPlaceholder is the most famous fake REST API on the internet: 100 posts, 10 users, comments, todos, all keyless. It is the first API in half the frontend tutorials ever written. The read path is spotless.

# runnable, read-only
curl "https://jsonplaceholder.typicode.com/posts/1"
Enter fullscreen mode Exit fullscreen mode
{"userId": 1, "id": 1,
 "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
 "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum..."}
Enter fullscreen mode Exit fullscreen mode

The write path is the transcript at the top of this post. Every POST /posts answers 201 with id: 101, because the collection ships with 100 posts and your new one is never stored. The guide says it plainly: the resource "will not be really updated on the server but it will be faked as if."

The miss contract has its own trap. Ask for a post that does not exist:

curl -si "https://jsonplaceholder.typicode.com/posts/9999"
# -> HTTP 404, content-type: application/json, body: {}   (2 bytes)
Enter fullscreen mode Exit fullscreen mode

An empty object, not an error message. Code that does resp.json().get("error") gets None and stays silent. The 404 itself is honest; the body tells you nothing about why.

When to use it: frontend prototypes and HTTP lessons, with the write path treated as theater. It is deterministic theater, which is exactly what you want for a demo.

2. DummyJSON: deleted at 23:52:01.722Z, still in stock

DummyJSON serves a richer fake world: 100+ products with reviews and stock levels, plus users, carts, and quotes. The catalog detail is genuinely good.

# runnable, read-only
curl "https://dummyjson.com/products/1"
Enter fullscreen mode Exit fullscreen mode
{"id":1,"title":"Essence Mascara Lash Princess","category":"beauty",
 "price":9.99,"discountPercentage":10.48,"rating":2.56,"stock":99}
Enter fullscreen mode Exit fullscreen mode

Now delete that product. The response is my favorite artifact in this whole list:

# runnable, harmless: nothing is actually deleted
curl -sX DELETE "https://dummyjson.com/products/1"
# -> HTTP 200, full product plus:
#    "isDeleted": true, "deletedOn": "2026-07-10T23:52:01.722Z"
Enter fullscreen mode Exit fullscreen mode

A deletion confirmation with a millisecond timestamp. Fetch /products/1 again and it is right back, HTTP 200, stock 99. The delete never happened; the API just generated paperwork that says it did. POST /products/add plays the same game and answered me 201 with {"id":195,"title":"probe-47"}.

Credit where due: the miss contract is the most honest of the three fake stores here. GET /products/9999 returns HTTP 404 with {"message":"Product with id '9999' not found"}. Machine-readable, status and body in agreement. Docs at dummyjson.com/docs.

When to use it: the best-stocked fake catalog for UI work, and a teaching example of why a "deleted" confirmation means nothing until a read confirms it.

3. Fake Store API: HTTP 200 with a zero-byte body

Fake Store API is the standard fake e-commerce backend in React and Vue shop tutorials: 20 products, carts, users.

# runnable, read-only
curl "https://fakestoreapi.com/products/1"
Enter fullscreen mode Exit fullscreen mode
{"id":1,"title":"Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
 "price":109.95,"category":"men's clothing","rating":{"rate":3.9,"count":120}}
Enter fullscreen mode Exit fullscreen mode

Ask for a product that does not exist and you get the sharpest read-path lie in this post:

curl -si "https://fakestoreapi.com/products/999"
# -> HTTP 200, content-type: application/json, body: 0 bytes
Enter fullscreen mode Exit fullscreen mode

Zero bytes. Not null, not {}, not an error object. The status says success and the declared content type says JSON, so resp.json() throws on a response your code just classified as fine, and if resp.ok: waves the emptiness straight into your pipeline. Writes are fake here too: my POST /products with "price": 1.0 came back 201 as {"id":21,"title":"probe-47","price":1}. Note the price: 1.0 went in, 1 came out. On paper that is the same JSON number, but json.loads now hands your test an int where you posted a float. Small, but if you round-trip money values through a mock like this, your assertions compare against a type you never sent. DELETE /products/1 returns 200 plus the full "deleted" product, which is still there. Docs at fakestoreapi.com/docs.

When to use it: quick shop UIs, with a guard that treats 200 + empty body as a failure, because this API will produce exactly that.

4. Random User Generator: ask for 5,001 users, get 1, no warning

Random User Generator fabricates user profiles (names, addresses, avatars) for seeding databases and UI lists. Clean, fast, keyless.

# runnable, read-only
curl "https://randomuser.me/api/?results=1&inc=name,email&noinfo"
Enter fullscreen mode Exit fullscreen mode
{"results":[{"name":{"title":"Mr","first":"Vitaliano","last":"Lima"},
             "email":"vitaliano.lima@example.com"}]}
Enter fullscreen mode Exit fullscreen mode

The documentation caps results at 5,000 per request. Cross that line and watch what happens:

curl "https://randomuser.me/api/?results=5001&inc=name&noinfo"
# -> HTTP 200, len(results) == 1
Enter fullscreen mode Exit fullscreen mode

I asked for 5,001 records and received one. Not 5,000, the documented maximum. One. No error field, no warning, HTTP 200. Send garbage (?results=abc) and you also get HTTP 200 with one result. The parameter validation is forgiving, which in practice means it lies: whatever you ask for, something plausible comes back.

The guard is one line and it is the whole lesson:

assert len(body["results"]) == requested, \
    f"asked for {requested}, got {len(body['results'])}"
Enter fullscreen mode Exit fullscreen mode

A seeding script without that line writes 1 row where it meant to write 5,001 and reports success.

When to use it: seeding users for demos and load tests, always counting what came back against what you asked for.

5. httpbin: the API where you order the failure yourself

httpbin flips the premise of this whole list. The other APIs fake data; httpbin reflects your own request back so you can test the client you wrote.

# runnable, read-only
curl "https://httpbin.org/get?probe=47"
Enter fullscreen mode Exit fullscreen mode
{"args": {"probe": "47"},
 "headers": {"Accept": "*/*", "Host": "httpbin.org", "...": "..."},
 "origin": "85.198.89.58",
 "url": "https://httpbin.org/get?probe=47"}
Enter fullscreen mode Exit fullscreen mode

Note args.probe is the string "47". Hold that thought for the next section.

The killer feature is /status/{code}: GET https://httpbin.org/status/404 returns an actual HTTP 404 (empty body) because you asked for one. After eight sections of APIs failing in ways nobody asked for, here is the one place you can order the failure on purpose and check that your retry logic, your error taxonomy, and your alerting actually fire. Most error-handling code I have read was never once executed before production. This is the free fix for that.

The caveat is reliability. httpbin is a long-running community service, it has a reputation for slow spells under load, and there is no SLA behind it. It answered all my probes on July 10, 2026; I would still not put it inside a CI job I care about without a timeout. Self-hosting it is one docker run away, per its homepage.

When to use it: exercising your client's error path deliberately, which almost nobody does until an outage does it for them.

6. httpbingo: the same request, different types

httpbingo is a Go reimplementation of httpbin, hosted on Fly.io, and it exists because of the previous paragraph: people wanted a faster, steadier httpbin. Same paths, same idea.

# runnable, read-only
curl "https://httpbingo.org/get?probe=47"
Enter fullscreen mode Exit fullscreen mode
{"args": {"probe": ["47"]},
 "headers": {"Accept": ["*/*"], "Host": ["httpbingo.org"], "...": "..."},
 "method": "GET",
 "url": "https://httpbingo.org/get?probe=47"}
Enter fullscreen mode Exit fullscreen mode

Same /get, same query. httpbin answered "probe": "47", a string. httpbingo answers "probe": ["47"], an array, and every header value is an array too. A client written against httpbin that does data["args"]["probe"].upper() explodes on httpbingo with an AttributeError, on a perfectly healthy HTTP 200.

This is schema drift between two services that describe themselves as the same tool. Not an edge case, the default response shape. If "just point it at the mirror" ever sounded safe, this pair is the two-curl proof it is not. Docs at httpbingo.org.

When to use it: the steadier echo for regular use, as long as your parser was written for httpbingo and not merely pointed at it.

7. Postman Echo: three echo services, three envelopes

Postman Echo is Postman's own echo service, the backend for their tutorials. Corporate hosting, been around for years.

# runnable, read-only
curl "https://postman-echo.com/get?probe=47"
Enter fullscreen mode Exit fullscreen mode
{"args":{"probe":"47"},
 "headers":{"host":"postman-echo.com","accept":"*/*","...":"..."},
 "url":"https://postman-echo.com/get?probe=47"}
Enter fullscreen mode Exit fullscreen mode

Line up all three echoes I probed on July 10 and the point makes itself:

  • httpbin: args values are strings, header keys are Title-Case ("Accept"), and there is an origin field with your IP.
  • httpbingo: args and header values are all arrays.
  • Postman Echo: args values are strings again, but header keys are lower-case ("accept") and there is no origin field at all.

None of this violates a spec, by the way. HTTP header names are case-insensitive on the wire, so each echo renders them into JSON however it likes. The spec does not care about the casing. Your parser does.

Three services, one job, three different JSON envelopes for the identical request. Anyone who has moved between "compatible" providers in production has met this exact failure, except there it costs a weekend. Here it costs two curls, which is why I would show a junior this trio before any architecture diagram. Docs at learning.postman.com/docs/developer/echo-api.

When to use it: a stable echo with corporate hosting behind it, and the third data point that "drop-in replacement" is a myth even in mock infrastructure.

8. Platzi Fake Store: real CRUD in a database everyone shares

Platzi Fake Store API is the opposite bet from JSONPlaceholder: writes are real. POST actually inserts into a database, with pagination and filters on top. The catch is that it is one database, shared by every stranger on the internet, and it shows immediately.

# runnable, read-only
curl "https://api.escuelajs.co/api/v1/products?offset=0&limit=1"
Enter fullscreen mode Exit fullscreen mode
[{"id":12,"title":"New Product 12","slug":"new-product-12","price":100500,
  "description":"A description 12","category":{"id":1,"name":"Updated Category Name"},
  "images":["https://placehold.co/600x400"]}]
Enter fullscreen mode Exit fullscreen mode

That is real production data from July 10, 2026: a product named "New Product 12" priced at 100,500, in a category someone renamed to "Updated Category Name". Now ask for product 1:

curl -si "https://api.escuelajs.co/api/v1/products/1"
# -> HTTP 400 (not 404!)
# {"path":"/api/v1/products/1","timestamp":"2026-07-10T23:53:05.903Z",
#  "name":"EntityNotFoundError",
#  "message":"Could not find any entity of type \"Product\" matching: {...\"id\": 1...}"}
Enter fullscreen mode Exit fullscreen mode

Two lessons in one response. First, not-found arrives as 400 Bad Request, not 404, with a raw ORM error name and the query dump in message. Your error handling, if it branches on 404 for missing entities, takes the wrong branch here. Second, why is product 1 missing at all? I cannot prove who did it, but the writes are real and the state is shared, so the safe assumption is that another user deleted it. The tutorial code you wrote on Monday fails on Tuesday when a stranger deletes "your" product.

Put this next to JSONPlaceholder and you get the actual trade-off of mock backends: fake writes are deterministic but lie to you; real writes in a shared sandbox tell the truth and then someone else edits it. Pick your poison consciously. Docs at fakeapi.platzi.com.

When to use it: practicing real CRUD flows and pagination, in tests that assume nothing about existing ids, because those assumptions rot within days here.

9. Beeceptor Echo: the fourth envelope shape

Beeceptor's free echo endpoint requires nothing: no key, no account. (Their actual product, a configurable mock server, sits behind a signup, so only the echo endpoint counts for this list.)

# runnable, read-only
curl "https://echo.free.beeceptor.com/probe?x=47"
Enter fullscreen mode Exit fullscreen mode
{"method": "GET", "protocol": "https", "host": "echo.free.beeceptor.com",
 "path": "/probe?x=47",
 "headers": {"Host": "echo.free.beeceptor.com", "Accept": "*/*", "...": "..."},
 "parsedQueryParams": {"x": "47"}}
Enter fullscreen mode Exit fullscreen mode

Envelope number four. Here path includes the query string as one raw lump ("/probe?x=47"), the parsed parameters live in their own parsedQueryParams object, and header keys keep their original case. Four echo services in this list, four incompatible answers to the same question: what request did I just send you? The free tier is rate limited; I did not push hard enough to measure the ceiling, so I will not quote one. Details at beeceptor.com.

When to use it: a quick request inspector when httpbin is slow, coded against its own envelope like every other entry here.

The one that grew a key wall: reqres.in

For years, reqres.in was the answer to "what fake API do I hit in tutorials?". Thousands of blog posts and course exercises hardcode it. Here is what those exercises get today:

curl -si "https://reqres.in/api/users/2"
# -> HTTP 401
# {"error":"missing_api_key",
#  "message":"The x-api-key header is required for this endpoint.",
#  "hint":"Create a free key at app.reqres.in and send it as x-api-key."}
Enter fullscreen mode Exit fullscreen mode

The key is free and takes a minute to get, so this is a speed bump rather than a paywall. But copy-paste the code from any of those tutorials today and you get a 401 out of the box, and most of them will never be updated. That is why reqres is not one of my nine: "keyless" is a property you re-verify, not a fact you remember. I checked every entry above on July 10, 2026, and I would re-check them before trusting this post a year from now.

Three that stopped answering at all

The dead teach the same lesson louder. All three checks below are from July 10, 2026.

httpstat.us was the beloved "give me any status code" service, the same job as httpbin.org/status/{code}. It was on my shortlist for this post right up until the curl:

curl -si "https://httpstat.us/200"
# -> curl: (52) Empty reply from server
Enter fullscreen mode Exit fullscreen mode

Three tries, both HTTP and HTTPS. The service whose one purpose was returning status codes now returns no status code at all, not even an error. If it comes back tomorrow, the point stands: it was gone the day I checked, and test suites all over GitHub still reference it.

FakerAPI (fakerapi.it) answered HTTP 502 from its own nginx on both probes that evening. I only have two probes a few minutes apart, so I make no claim about how long it has been down; on the day I checked, it was.

Random Data API (random-data-api.com) did not answer at all: connection timeout after 12 seconds, twice.

Two near-misses for completeness: mockapi.io gives you endpoints only per project, behind a signup, so there is no public URL to verify. Mockaroo requires an API key by design; I did not probe it and make no claim about its responses.

Why does a green test against a mock prove so little?

Here is the uncomfortable part, and the reason this list is not just trivia. Mock APIs are the only APIs that lie honestly: the fake write is documented behavior, not a bug. Yet every mismatch cataloged above is a real class of drift between mock and production:

  • 201 without a write (JSONPlaceholder). Your real backend returns 201 after an actual insert. The mock returns 201 after nothing. A test asserting status == 201 passes identically against both.
  • 200 with an empty body (Fake Store API). Production APIs produce this shape too, during partial outages and bad deploys. If your parser never met it in testing, it meets it at 3am.
  • A silent clamp (Random User). Production pagination does this constantly: you ask for 500, the server caps at 100, and your "full export" is 20% of the data.
  • 400 where you expected 404 (Platzi). Error taxonomies differ per backend. Branch on the wrong code and your retry logic retries a request that can never succeed.
  • Envelope drift between lookalikes (httpbin vs httpbingo vs Postman Echo vs Beeceptor). The same drift that hits when you switch payment providers, geocoders, or LLM vendors that are "API-compatible".

A test suite that is green against a mock has proven one thing: your code can talk to the mock. The integration with the real backend is exactly as untested as it was before. The fix is not to abandon mocks; it is to stop letting the status line close the loop. A write is confirmed by a read, not by a receipt. In code, against the flagship liar:

# runnable local -- a write is not done until a read confirms it
import requests

BASE = "https://jsonplaceholder.typicode.com"

def create_post(payload):
    r = requests.post(f"{BASE}/posts", json=payload, timeout=15)
    r.raise_for_status()              # 201 sails through this line
    created = r.json()
    new_id = created.get("id")

    # read back what the API claims it just created
    check = requests.get(f"{BASE}/posts/{new_id}", timeout=15)
    if check.status_code != 200:
        raise RuntimeError(
            f"API said {r.status_code} Created (id={new_id}) "
            f"but read-back returned {check.status_code}"
        )
    return created

create_post({"title": "hello"})
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  ...
RuntimeError: API said 201 Created (id=101) but read-back returned 404
Enter fullscreen mode Exit fullscreen mode

That is the live output from July 10, 2026 (traceback trimmed), not a mock-up of a mock. raise_for_status() is satisfied, the JSON parses, the id looks plausible, and the read-back catches the lie in one extra request. Against DummyJSON the same skeleton catches the fake delete (read back after DELETE, expect a 404, get a 200). Against Fake Store API, add a len(r.content) > 0 check before parsing, because that is the one that hands you zero bytes under a 200.

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-back habit comes from there, where the expensive incidents were never the 500s. The 500s page you. The quiet 200s and 201s with wrong bodies just corrupt your data. I have written the same pattern for food and nutrition APIs and pop-culture APIs; the mock world turned out to be the purest lab for it, because here the lying is the documented feature.

FAQ

Is JSONPlaceholder completely free?
Yes. No key, no signup, no card, and no request cap published for normal use. Reads are real JSON; writes (POST, PUT, DELETE) are simulated. The response pretends the operation succeeded, returns 201 with a plausible id, and stores nothing. The official guide states the resource "will not be really updated on the server but it will be faked as if."

Why does JSONPlaceholder return id 101 but not save my post?
The posts collection ships with exactly 100 items, and the API answers every create with the next id, 101, without persisting anything. That is by design: it gives frontend code a realistic 201 response to render. The proof is one request away: GET /posts/101 returns 404 immediately after the 201.

What happened to reqres.in?
It put its endpoints behind an API key. As of July 10, 2026, GET https://reqres.in/api/users/2 returns HTTP 401 with "error":"missing_api_key" and a hint to create a free key at app.reqres.in. The key costs nothing, but every older tutorial that hardcodes reqres without a key now fails out of the box.

What is the best free alternative to reqres.in?
For keyless fake CRUD, JSONPlaceholder (deterministic, writes simulated) and DummyJSON (richer data, writes simulated, honest 404s on misses) are the closest matches. If you specifically need writes that persist, Platzi Fake Store API inserts into a real shared database, with the caveat that other users mutate the same data under you.

Does Fake Store API actually save data?
No. POST /products returns 201 with a new id (21 on my July 10, 2026 probe) but nothing is stored, and DELETE /products/1 returns 200 with the "deleted" product that remains available. Watch its miss contract too: a request for a nonexistent product returns HTTP 200 with a completely empty body, zero bytes, which breaks resp.json() on a response that reports success.

What is the difference between httpbin and httpbingo?
httpbingo is a Go rewrite of httpbin hosted on Fly.io, generally steadier than the original. The response schema differs: httpbin returns query args and headers as plain strings, httpbingo wraps every value in an array ("probe": "47" vs "probe": ["47"]). Code written for one breaks on the other despite identical paths, so pin one and parse its exact shape.


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 10, 2026 before publishing; responses are trimmed, never paraphrased. I have not run these mock APIs in production; the 2,190 runs are a different domain, cited only as the origin of the read-back 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: which mock burned you when the real backend arrived, and what did your green tests miss? I read every comment.

Top comments (0)