flaky.

A mock REST API that fails on purpose.

Real-looking data at real URLs, like every other fake API. The difference: you decide how it behaves. Force a 503, add two seconds of latency, make a third of requests fail — on any endpoint, from the query string.

Free, no signup to read, and a key takes one request if you want a higher limit.

Coming from JSONPlaceholder? Same resources, same fields — here is the one-line migration.

Try it

status — time — tier — remaining —
Press Send.

The three parameters

They work on every endpoint, including nested routes and sandboxes. Combine them freely.

# a slow endpoint — test your loading state
GET /v1/posts?_delay=3000

# a hard failure — test your error boundary
GET /v1/posts?_status=503

# an intermittent one — test your retry logic
GET /v1/posts?_fail_rate=0.3

# slow AND flaky, because that is what production is
GET /v1/products?_delay=1500&_fail_rate=0.2

A failure that stops

The three above are a coin toss, a permanent outage, and a wait. None of them can test retry logic or a circuit breaker, because both need a sequence: fail, fail, then work — so a test can assert which attempt recovered rather than that one eventually did.

curl -X POST https://flakyapi.dev/v1/scenario -d '{"fail":2}'
# → {"id":"66b6…"}

GET /v1/posts?_scenario=66b6…   # 503 · x-scenario-attempt: 1
GET /v1/posts?_scenario=66b6…   # 503 · x-scenario-attempt: 2
GET /v1/posts?_scenario=66b6…   # 200 — your backoff worked

# rewind between tests, without spending an attempt
POST /v1/scenario/66b6…/reset

Or the other way round: it works, then it stops

A rate limit does not fail first and recover — it is fine, fine, fine, then 429, and it stays that way. So does a quota, a free trial, and an access token that expires mid-run. Send succeed instead of fail and the sequence inverts.

curl -X POST https://flakyapi.dev/v1/scenario -d '{"succeed":3}'
# → {"id":"9f2a…"}   status defaults to 429 here

GET /v1/posts?_scenario=9f2a…   # 200 · x-scenario-remaining-successes: 2
GET /v1/posts?_scenario=9f2a…   # 200
GET /v1/posts?_scenario=9f2a…   # 200 — allowance spent
GET /v1/posts?_scenario=9f2a…   # 429 · Retry-After: 1 — and stays 429

This is the one you cannot get from a real provider without burning real quota or waiting out a real window. It never recovers, because a quota that ran out does not refill halfway through your test.

Both directions are what people currently write a throwaway fake server for, and together they are the one control no other mock API has at all.

Three more, for failures nothing else reproduces

# a 429 that actually says when to come back
GET /v1/posts?_status=429                  # Retry-After: 5
GET /v1/posts?_status=503&_retry_after=30

# a body that stops mid-record — the .json() catch path nobody tests
GET /v1/posts?_malformed=1

# the CORS error, on purpose. only a browser will refuse it
GET /v1/posts?_cors=off
This is the part you would otherwise write a local mock server for. Here it is a query parameter, so it works from a browser console, a curl one-liner, a CodePen, or a colleague's machine with nothing installed.

Resources

Every resource answers at the same kinds of URL, and the method decides what happens there. Writes are echoed back, not stored, unless you use a sandbox.

URLMethodWhat it does
Loading…
ResourceRowsNested
Loading…
Need your own shapes instead? Paste any JSON and get a mock server for it, or paste your OpenAPI spec and get a mock of your own API — same query parameters, same chaos controls, and you can export either to run locally.

Filtering, search, sorting, paging

GET /v1/todos?userId=3&completed=true   # filter by any field
GET /v1/posts?_q=voluptate                # full-text search
GET /v1/products?_sort=price&_order=desc  # sort
GET /v1/photos?_page=2&_limit=50          # paginate
GET /v1/photos?_start=40&_limit=10        # or offset, json-server style
GET /v1/products?_select=title,price      # only these fields
GET /v1/posts/1/comments                  # nested

Writes that actually stick

POST, PUT, PATCH and DELETE are echoed, not stored — same as every other mock API, and the response says so in an x-mock-write header so nobody loses an afternoon to it. When you need writes that persist, take a sandbox.

# 1. a free key (no confirmation email)
curl -X POST https://flaky.dev/v1/keys \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com"}'

# 2. a sandbox — your own writable copy, 24 hours
curl -X POST https://flaky.dev/v1/sandbox \
  -H 'authorization: Bearer flk_...'

# 3. write to it. this one is real.
curl -X POST https://flaky.dev/v1/sandbox/<id>/posts \
  -H 'content-type: application/json' \
  -d '{"title":"this persists"}'

A sandbox is an overlay, not a copy — only records you change are stored, so the shared dataset stays cached at the edge and your sandbox stays fast.

A key, if you want one

Everything above works with no key and no signup. A key is not a gate — it raises the daily limit tenfold and unlocks sandboxes. One request, no confirmation email, no password, nothing to click.

curl -X POST https://flakyapi.dev/v1/keys \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com"}'

# → {"key":"flk_...", "tier":"free"}  — use it straight away

Send it on any request and the tier changes:

curl https://flakyapi.dev/v1/posts \
  -H 'authorization: Bearer flk_...'

The address is stored only so the key can be revoked or you can be contacted about it. Nothing is ever sent to it — no mailing list, no confirmation, no newsletter. The privacy page says exactly what is kept and for how long.

Watch x-ratelimit-remaining on any response. If you are getting close to zero, that is the moment a key is worth the one request.

Limits

TierRequests/dayMax pageSandboxes
Anonymous1,000100—
Free key10,0002501

That is all of it: flaky is free, and there is no paid tier. Limits reset at 00:00 UTC. Every response carries x-ratelimit-remaining. Machine-readable description at /v1/meta, and an OpenAPI 3.1 spec you can import straight into Postman, Insomnia or a code generator.