The project id demo is a shared, always-on sandbox seeded with the e-commerce preset (products, orders, customers, reviews). Full CRUD works on it and it resets every 24 hours, so feel free to write to it:
curl "https://HOST/m/demo/products?limit=3"
curl "https://HOST/m/demo/orders?status=shipped&_expand=customer"
curl -X POST "https://HOST/m/demo/products" -H 'content-type: application/json' -d '{"name":"My Widget","price":9.99}'
The demo also ships three showcase custom routes: GET /m/demo/health, ANY /m/demo/echo (reflects method, query and body), and GET /m/demo/config/:key.
Opening a /m/demo/β¦ URL in a browser tab shows a friendly endpoint explorer (live status + body + copyable curl) instead of raw JSON. Code is never affected β curl, fetch() and crawlers always get the raw response β and your own projects always serve raw JSON everywhere. Append ?mock_raw=1 to see the raw body in a browser.
Scripts that hardcode httpbin.org can just swap the host: /m/httpbin/* serves the httpbin greatest hits with the same paths and response shapes β /get, /post (+ put/patch/delete), /anything/*, /headers, /ip, /user-agent, /uuid, /status/:codes, /delay/:n, /base64, /json//xml//html, /basic-auth, /bearer, /redirect/:n, /response-headers, /cookies, /stream/:n. No signup, no key, CORS on, shared 50k/day cap.
curl "https://HOST/m/httpbin/get?x=1" # httpbin-shaped echo
curl "https://HOST/m/httpbin/status/500" # any status
curl "https://HOST/m/httpbin/delay/3" # seconds, max 10
GET /m/httpbin lists every supported endpoint. Full details + what's deliberately omitted: httpbin alternative guide.
Every resource lives at /m/<project>/<resource>. All endpoints are CORS-enabled and need no API key. Every read endpoint also answers HEAD with the same status and headers (incl. X-Total-Count) and no body β friendly to curl -I, link checkers and uptime monitors.
GET /m/abc123/users # list (paginated, default 20)
GET /m/abc123/users/5 # one record
POST /m/abc123/users # create (JSON body) β assigns next id
PUT /m/abc123/users/5 # replace
PATCH /m/abc123/users/5 # merge fields
DELETE /m/abc123/users/5 # delete
Lost? GET /m/<project> (the bare project URL) returns a JSON index of the
whole API: every resource with record counts and URLs, custom routes, auth status, and
export links (OpenAPI, GraphQL, Postman, TypeScript). Try it:
curl https://HOST/m/demo
| Param | Meaning | Example |
|---|---|---|
page, limit | pagination (limit β€ 100); total in X-Total-Count header | ?page=2&limit=10 |
sortBy, order | sort by any field, asc/desc | ?sortBy=price&order=desc |
search | substring match across all fields | ?search=lisbon |
_page, _limit, _sort, _order, q | json-server-style aliases for the params above β paste json-server URLs unchanged | ?_sort=price&_order=desc&_limit=5 |
| any field name | exact-match filter | ?city=Tokyo&inStock=true |
<field>_gte / _lte / _gt / _lt | range filters (json-server style). Numbers compare numerically; strings and ISO dates compare lexicographically (which is correct for ISO dates) | ?price_gte=100&price_lt=500, ?placedAt_gte=2026-01-01 |
<field>_ne | not-equal filter (records missing the field match too) | ?status=pending β ?status_ne=pending |
<field>_like | case-insensitive substring match (note: json-server treats _like as a regex; ours is a plain substring β safer, and what mockapi.io's search does) | ?name_like=chair |
mock_delay | simulate slow network (ms, β€5000) | ?mock_delay=2000 |
mock_status | force an error response (400β599) to test error handling | ?mock_status=500 |
mock_chaos | chaos injection: fail that fraction of requests with a random 500/502/503/504/429 β test retry & backoff logic. Injected failures carry a x-mockbird-chaos: injected response header; override the status pool with mock_chaos_status=500,503 | ?mock_chaos=0.3 |
mock_jitter | random added latency in a ms range (800 = 0β800 ms); composes with mock_delay, total β€5000 ms | ?mock_jitter=100-1500 |
_expand | join the parent object via its <name>Id field | ?_expand=customer |
_embed | attach child records that point back at this record | ?_embed=comments |
select | field projection: return only the named fields (id is always included). Works on single-record GETs too, and can pick _expand/_embed-joined fields. Same syntax as DummyJSON. Alias: _select. Note: a field literally named select can't be used as an exact-match filter. | ?select=title,price |
mock_envelope | wrap the response in your real API's shape: a plain key (data β {"data":[β¦]}) or a URL-encoded JSON template containing "$data". none disables a project default. See response envelopes | ?mock_envelope=data |
Simulation flags work on every mock endpoint and method β test your spinners and error states without touching your code. See the loading & error states guide for recipes (race conditions, retry loops, Playwright integration).
Chaos note: when mock_chaos injects a failure on a write (POST/PUT/PATCH/DELETE), the write is not applied β exactly like a server that fell over before processing your request. Perfect for verifying your retry logic is safe to re-send. GraphQL requests honor mock_chaos/mock_jitter too (failures come back as an HTTP error with a GraphQL errors array).
Real APIs rarely return bare arrays. If your backend responds with {"data":[β¦],"total":42} or {"items":[β¦],"meta":{β¦}}, make the mock match β so your frontend's response parsing is exercised for real, and switching to the real API later is a no-op. (mockapi.io sells response reshaping on its paid plan; here it's a query param.)
# per request: a plain key wraps everything under that key
curl 'https://HOST/m/demo/products?mock_envelope=data&limit=2'
β {"data":[{β¦},{β¦}]}
# or a JSON template β "$data" becomes the records; URL-encode it
curl 'https://HOST/m/demo/products?limit=5&page=2&mock_envelope=%7B%22items%22%3A%22%24data%22%2C%22meta%22%3A%7B%22total%22%3A%22%24total%22%2C%22hasMore%22%3A%22%24hasMore%22%7D%7D'
β {"items":[β¦5 recordsβ¦],"meta":{"total":30,"hasMore":true}}
Template placeholders (strings, replaced anywhere in the template): "$data" (the records β required), "$total" (pre-pagination count), "$page", "$limit", "$count" (items on this page), "$hasMore" (boolean).
Project-wide default: set it once and every list and single-record GET is wrapped β no query param needed, so the code you're building never has to know it's talking to a mock:
curl -X PUT https://HOST/api/projects/PROJECT/settings \
-H 'x-admin-key: KEY' -H 'content-type: application/json' \
-d '{"envelope": {"data": "$data", "total": "$total"}}'
Rules: applies to successful GETs only β lists (incl. nested routes and snapshot mode), and single records (where $total/$count are 1). Errors, writes (POST/PUT/PATCH/DELETE responses), and GraphQL (which has native shaping) are never wrapped. ?mock_envelope=β¦ overrides the project default per request; ?mock_envelope=none disables it. The X-Total-Count header is still sent either way. Clear the default with {"envelope": null}. Also settable in the dashboard's Response envelope card.
_expand / _embed)If a record has a foreign-key-style field like postId or customerId
(the refId field type generates these), you can join related resources in one
request β the same way json-server does it:
# each comment gains a "post" object, looked up from the posts resource via postId
GET /m/abc123/comments?_expand=post
# each post gains a "comments" array of comments whose postId matches
GET /m/abc123/posts/7?_embed=comments
# combine, repeat, or comma-separate for multiple relations
GET /m/abc123/orders?_expand=customer&status=shipped
GET /m/abc123/customers/3?_embed=orders
Works on both list and single-record GETs, alongside filters, sorting and pagination.
_expand=post finds the resource named post, posts (or
-ies plural); _embed matches children by
<singular-parent>Id. Unknown names are ignored, and a dangling id expands
to null β no errors, just data. The blog / ecommerce / saas presets are wired
with these relations out of the box.
Prefer path-style relations? Every parent record exposes its children directly:
# comments whose postId == 1 β same as /m/abc123/comments?postId=1
GET /m/abc123/posts/1/comments
# create a comment with postId=1 set automatically
POST /m/abc123/posts/1/comments
{"body": "Nice post!"}
Nested GETs support everything list endpoints do β filters, search,
sortBy, pagination, even _expand. The parent must exist
(404 otherwise), and the child's <singular-parent>Id field does the
matching. POST fills the foreign key in for you.
Every project publishes a live OpenAPI 3.0 document at
/m/<project>/openapi.json β no auth required. It describes every
resource, field type, query parameter and CRUD operation, so you can import your mock
API straight into Swagger UI, Postman, Insomnia, or generate a typed client with
openapi-generator. The spec updates automatically as you add or change resources.
Prefer a native Postman collection? See Postman collection below.
Skip codegen tooling entirely: every project also serves ready-to-paste
TypeScript interfaces at /m/<project>/types.ts
(public, CORS on, no auth β like the OpenAPI spec):
# TypeScript interfaces + Input types for every resource
curl -o api.ts "https://HOST/m/demo/types.ts"
# Zod schemas + z.infer types instead (works with zod v3.20+ and v4)
curl -o schemas.ts "https://HOST/m/demo/types.ts?format=zod"
The TS flavor gives you one interface per resource (fields typed from the
schema β numbers, booleans, and oneOf fields become literal unions like
"low" | "med" | "high") plus a <Name>Input type for POST/PUT
bodies. The Zod flavor gives you <Name>Schema / <Name>ListSchema
with format refinements (.email(), .uuid(), .url(),
.datetime()) and z.infer type exports β drop it into your app and the
data your mock returns is guaranteed to parse. Regenerates on every request, so it always
matches your current resources.
Every project also serves a ready-to-import Postman Collection v2.1 at
/m/<project>/postman.json (public, CORS on, no auth). In Postman choose
Import β Link and paste the URL β you get one folder per resource with List /
Get one / Create / Update / Delete requests, real example bodies matching your field
types, the useful query params (pagination, sort, search, _expand,
mock_delayβ¦) pre-filled but disabled, and a {{baseUrl}}
collection variable. Works in Insomnia and Hoppscotch too (both import Postman format).
# Import from link in Postman β or download it:
curl -o demo.postman_collection.json "https://HOST/m/demo/postman.json"
If the project is in protected mode, the collection gains an
auth (mock) folder whose Login request stores the JWT in a {{token}}
variable via a test script β every other request then authenticates automatically.
Re-import any time to sync with your current schema.
Every project also serves GraphQL at
/m/<project>/graphql β same records as the REST API, no extra setup.
Open that URL in a browser and you get an embedded GraphiQL IDE with
autocomplete and schema docs (full introspection is on, so Apollo/urql codegen and
Postman work too).
curl https://HOST/m/demo/graphql \
-H 'content-type: application/json' \
-d '{"query":"{ products(limit: 2, sortBy: \"price\", order: \"desc\") { id name price reviews { rating } } }"}'
What the schema gives you, generated from your resources:
| Field | What it does |
|---|---|
products(limit, page, sortBy, order, q, where) | list with pagination, sort, search, and typed exact-match filters (where: {category: "books"}) |
product(id: 3) | single record |
productsCount(where, q) | total matching (ignores pagination) |
| relation fields | review.product (via productId) and product.reviews (children) β same conventions as _expand/_embed |
createProduct(input), updateProduct(id, input), deleteProduct(id) | mutations β they write the same records REST serves, and fire your webhooks |
_raw | the full stored record as JSON (handy for db.json imports with nested data) |
GET works too: /m/demo/graphql?query={productsCount}. Requests share the
project's daily cap; queries are limited to depth 8 and 8 KB. mock_delay
is honored as a query parameter on the endpoint URL.
Already have an OpenAPI 3.x or Swagger 2.0 spec? Post it (JSON or YAML) and get a
live mock of your real API's shape β resources found from your REST paths, field types
inferred from schemas (formats like email, uuid,
date-time are honored), enums keep your exact values, and
everything is seeded with realistic fake data:
curl -X POST https://HOST/api/projects/import --data-binary @openapi.yaml
# β {"id":"abc123","adminKey":"...","baseUrl":"https://HOST/m/abc123",
# "resources":[{"name":"pets","seeded":20,"url":"https://HOST/m/abc123/pets"}, ...],
# "warnings":[]}
Options: ?seed=50 records per resource (max 100), ?name=my-mock
project name (defaults to the spec's info.title). Or use the
Import button in the dashboard β /app#import
opens the panel directly β and paste or upload the file. Notes:
/users, /users/{id}, nested /users/{id}/posts β posts). $ref and allOf are resolved; wrapper objects like {"data": [...]} are unwrapped.price, avatar, city, createdAt, *Idβ¦ become matching realistic generators.warnings)./m/<project>/openapi.json) re-import losslessly.Coming from json-server? POST your db.json to the same
endpoint and Mockbird hosts it β your exact records, not fake data. Each
top-level key becomes a resource; numeric ids are preserved; nested
objects/arrays inside records are kept verbatim. Filtering, pagination, nested routes,
_expand/_embed, webhooks and the request inspector all work on
your data immediately:
curl -X POST https://HOST/api/projects/import --data-binary @db.json
# β {"id":"abc123","kind":"db.json","baseUrl":"https://HOST/m/abc123",
# "resources":[{"name":"posts","records":42,"url":"https://HOST/m/abc123/posts"}, ...]}
Notes:
paths/openapi/swagger is treated as a spec, anything shaped like {"posts": [...]} as a db. YAML works too.ids are renumbered to integers (listed in warnings); singular objects (json-server's profile) become 1-record collections.And it round-trips β eject anytime. GET /m/<project>/db.json
returns your entire live dataset in json-server's native format (data only, so the
file works verbatim). No lock-in:
curl -o db.json https://HOST/m/demo/db.json
npx json-server db.json # the same API, running locally
Moving off Postman mock servers? POST an exported Postman Collection v2.x
to the same endpoint. Requests become resources (GET /users,
/users/:id β users) and your saved example responses become
the hosted records, verbatim β wrappers like {"data": [...]} are
unwrapped, duplicate ids deduplicated. Resources without saved examples fall back to a
schema inferred from raw JSON request bodies and get seeded fake data
(?seed=N):
curl -X POST https://HOST/api/projects/import --data-binary @collection.json
# β {"id":"abc123","kind":"postman","baseUrl":"https://HOST/m/abc123",
# "resources":[{"name":"users","records":12,...},{"name":"orders","seeded":20,...}]}
Notes:
:id, {{userId}}, numbers and UUIDs are treated as parameters; a leading {{baseUrl}} and /api/v1-style prefixes are ignored.warnings) β in Postman, save a response as an example and re-export./m/<project>/postman.json) re-import cleanly (built-in auth/graphql folders are skipped).Open DevTools on any page of your app, use it for a minute, then Network tab β Export HAR. POST that file to the same endpoint and the JSON API traffic the page actually received becomes a hosted mock β the recorded response bodies are served back verbatim as records. It's record & replay without a proxy: your frontend can now run against yesterday's real data, offline backends and all.
curl -X POST https://HOST/api/projects/import --data-binary @myapp.har
# β {"id":"abc123","kind":"har","baseUrl":"https://HOST/m/abc123",
# "resources":[{"name":"products","records":34,...},{"name":"cart-items","records":3,...}]}
Notes:
/api/v2/users?page=1 and /api/v2/users/42 both land in a users collection; numeric/UUID segments are treated as parameters; repeated responses from polling are deduplicated (by id when present).{"data": [...]} are unwrapped. Endpoints that only appear as JSON writes (a POST with a body but no useful response) get a schema inferred from the request body and seeded fake data instead.text/plain still counts.POST a CSV or TSV file to the same endpoint and every row becomes a
record. The header row names the fields, the delimiter (comma, semicolon, or tab) is
auto-detected, quoted values with embedded commas/newlines work (RFC 4180), and
values are typed per column: whole-number columns become numbers,
true/false columns become booleans, zero-padded codes like
01234 stay strings, and an id column keeps your ids.
curl -X POST 'https://HOST/api/projects/import?resource=people' \
-H 'content-type: text/csv' --data-binary @people.csv
# β {"id":"abc123","kind":"csv","baseUrl":"https://HOST/m/abc123",
# "resources":[{"name":"people","records":124,...}]}
Notes:
?resource=<name> names the collection (default items); ?name= names the project. In the dashboard (/app#import) the file name is used automatically.?format=csv or a content-type: text/csv header. Headers with spaces are sanitized (First Name β First_Name); empty cells import as null; cells containing JSON (["a","b"]) are parsed._page/_limit, q search, select=, GraphQL, OpenAPI/types.ts exports, and db.json to eject. Full walkthrough + SheetDB/Sheety comparison: CSV β REST API guide.When defining a resource you give each field a type. Available types:
id uuid firstName lastName fullName username email avatar image
word words title sentence paragraph number price percent boolean
date pastDate futureDate url domain ip phone city country address
zipCode company jobTitle color latitude longitude rating age slug
status category refId
refId generates foreign-key-style ids (1β20, matching the default seed
range) β name the field postId, userId, etc. and it plugs straight
into _expand / _embed.
Every project gets a built-in placeholder image endpoint, so your mock data ships with images that actually render β no third-party image service involved:
GET /m/<project>/img/300x200 # SVG, shows "300Γ200"
GET /m/<project>/img/300 # square shorthand
GET /m/<project>/img/640x480?text=Product+7
GET /m/<project>/img/300x200?bg=1e3a8a&fg=fff
GET /m/<project>/img/128?round=1&text=OS&seed=olivia # avatar-style circle
text (β€64 chars), bg/fg (3- or 6-digit hex, no #), seed (deterministic palette color), round=1 (circle).Cache-Control: public. Counts toward the project's daily request cap.image and avatar fields point at this endpoint automatically (avatars get initials + round=1), so a fresh ecommerce preset renders product grids with zero setup.<img> tags can't send Authorization headers.One-click resource schemas: users, posts, products, todos, comments, orders, reviews, customers, events. Pick one in the dashboard or pass {"template":"products"} to the management API.
Instead of adding resources one by one, create a project from a preset and get a full multi-resource fake backend instantly:
postId), authorsproductId / customerId)In the dashboard, pick a preset next to "New project". Via API: POST /api/projects with {"name":"shop","preset":"ecommerce"} β the response lists every created endpoint. Reference fields use the refId type: a plausible foreign key in the 1β20 range so joins against the default seed data just work. One-click shortcut: opening /app#new=ecommerce (or #new=blog, #new=saas) creates an anonymous project with that preset and drops you straight into its dashboard.
Every project keeps a log of its last 50 requests β method, path, query string, response status, Origin header, (for writes) the request body, and captured request headers (content-type, user-agent, accept, referer, and every x-* header β so webhook signatures like x-hub-signature-256 are right there; authorization is logged with its value redacted). See it in the dashboard under Recent requests (tick βliveβ to watch requests stream in, click a row to expand headers + full body), or fetch it from the management API:
curl https://HOST/api/projects/abc123/requests -H 'x-admin-key: KEY'
Great for answering βis my app actually calling the API, and with what?β β wrong paths, missing bodies and CORS origins show up instantly. Combined with a catch-all custom route it doubles as a request bin (a free webhook.site alternative β see the guide). The shared demo playground's log is public, no key needed: https://HOST/api/projects/demo/requests.
Point a webhook at your app and Mockbird fires a signed POST every time a record is created, updated or deleted in your project β perfect for building and testing webhook consumers before the real event source exists. Set it in the dashboard (Webhook card) or via API:
# set (or update) the webhook β returns the signing secret
curl -X PUT https://HOST/api/projects/abc123/webhook \
-H 'content-type: application/json' -H 'x-admin-key: KEY' \
-d '{"url":"https://your-app.example.com/hooks/mockbird","events":["created","deleted"]}'
# fire a test delivery right now
curl -X POST https://HOST/api/projects/abc123/webhook/test -H 'x-admin-key: KEY'
# see the last 20 delivery attempts (status, error, duration)
curl https://HOST/api/projects/abc123/webhook/deliveries -H 'x-admin-key: KEY'
Each delivery is a JSON POST like:
{
"id": "evt_1a2b3c4d",
"event": "products.created", // <resource>.<created|updated|deleted>, or "test"
"project": "abc123",
"resource": "products",
"action": "created",
"record": { "id": 31, "name": "..." }, // deletes include the removed record
"ts": 1753430000000
}
Headers: X-Mockbird-Event, X-Mockbird-Delivery (event id), and X-Mockbird-Signature: sha256=<hex> β an HMAC-SHA256 of the raw body with your signing secret, so you can practice real signature verification:
// Node.js
const crypto = require('node:crypto');
function verify(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return signatureHeader.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}
Details: 5-second timeout, redirects are not followed, no retries, 100 deliveries per project per day. Non-2xx responses and errors are visible in the delivery log. URLs must be public http(s) β localhost and private IPs are rejected (use a tunnel like cloudflared or ngrok to reach your dev machine).
Every project has built-in auth endpoints so you can build real login flows against a fake backend: login forms, token storage, Authorization headers, 401 redirects, token-expiry refresh logic β all testable before your real auth exists.
# any email + password logs in (it's a mock) β a real signed JWT comes back
curl -X POST https://HOST/m/abc123/auth/login \
-H 'content-type: application/json' \
-d '{"email":"dev@example.com","password":"anything"}'
# β {"token":"eyJhbGciOi...","tokenType":"Bearer","expiresIn":3600,"user":{...}}
# who am I? (validates signature + expiry)
curl https://HOST/m/abc123/auth/me -H "Authorization: Bearer $TOKEN"
# register works too β if the project has a users resource, it creates a real record
curl -X POST https://HOST/m/abc123/auth/register \
-H 'content-type: application/json' \
-d '{"email":"new@example.com","password":"x","name":"New Person"}'
The tokens are genuine HS256 JWTs (signed with a per-project secret): they carry iat/exp claims, verify, and expire for real. Pass "expiresIn": 5 at login to get a token that dies in 5 seconds β the easiest way to test your app's expiry handling (min 5s, max 7 days, default 1 hour).
Users are real data. If the project has a users (or customers/accounts/members) resource, logging in with a record's email returns that record as the user (password-ish fields stripped), and /auth/register inserts a new record β visible instantly via REST and GraphQL. No users resource? A user object is synthesized from the email.
Flip Protected mode in the dashboard (Mock auth card) β or via API β and every endpoint of the project (REST, nested routes, GraphQL) starts requiring Authorization: Bearer <token>, returning proper 401s with WWW-Authenticate otherwise. Your auth guards, interceptors and redirect-to-login logic get a realistic workout:
# toggle via API
curl -X PUT https://HOST/api/projects/abc123/settings \
-H 'content-type: application/json' -H 'x-admin-key: KEY' \
-d '{"authMode":"protected"}' # or "none"
# now unauthenticated requests fail like production would
curl -i https://HOST/m/abc123/products # β 401 {"error":"missing bearer token", ...}
The /auth/* endpoints and openapi.json stay open (so you can always log in), and the exported OpenAPI spec gains a bearerAuth security scheme automatically. Expired or tampered tokens get 401 token expired / invalid signature β exactly what your interceptor needs to see. Full walkthrough with a client-side interceptor example: mock JWT auth guide.
Save a named copy of a project's entire data state β every record in every resource β then restore it whenever you want. This turns a mock project into a deterministic test fixture: your tests can create/update/delete freely, and one call puts everything back exactly as it was.
# save the current state as "baseline"
curl -X POST https://HOST/api/projects/abc123/snapshots \
-H 'content-type: application/json' -H 'x-admin-key: KEY' \
-d '{"name":"baseline"}'
# ...your test suite trashes the data via the mock API...
# put it all back β records, ids, everything, exactly as saved
curl -X POST https://HOST/api/projects/abc123/snapshots/baseline/restore \
-H 'x-admin-key: KEY'
# list / inspect / delete
curl https://HOST/api/projects/abc123/snapshots -H 'x-admin-key: KEY'
curl https://HOST/api/projects/abc123/snapshots/baseline -H 'x-admin-key: KEY' # includes full data
curl -X DELETE https://HOST/api/projects/abc123/snapshots/baseline -H 'x-admin-key: KEY'
Typical uses: a beforeEach/globalSetup hook in Playwright or Cypress that restores baseline so every run starts identical; an empty-state snapshot (resources with zero records) vs a full one for demoing both UI states; a known dataset for reproducing a bug report. Saving with an existing name overwrites it. Restore also rebuilds resources you deleted since the snapshot. Snapshots don't fire webhooks. Limits: 10 snapshots per project, 1 MB each. The dashboard has a Snapshots card with one-click save/restore. Full Playwright/Cypress walkthrough: deterministic test data guide.
Any GET request to the REST endpoints β and any GraphQL query β can be answered directly from a saved snapshot β without changing the project's live data β by sending the X-Mockbird-Snapshot header (or ?mock_snapshot= for places where you can't set headers, like an <img> tag or a browser address bar):
# live data
curl https://HOST/m/abc123/products
# same URL, answered from the "empty" snapshot β live data untouched
curl https://HOST/m/abc123/products -H 'X-Mockbird-Snapshot: empty'
# query-param variant
curl "https://HOST/m/abc123/products?mock_snapshot=edge-cases"
# GraphQL queries honor it too (schema + data come from the snapshot)
curl https://HOST/m/abc123/graphql -H 'X-Mockbird-Snapshot: empty' \
-H 'content-type: application/json' -d '{"query":"{ productsCount }"}'
Filters, search, sorting, pagination, _expand/_embed, and nested routes all work against the snapshot's records. This is built for parallel test workers: worker A pins empty, worker B pins edge-cases, worker C uses live data β same project, same URLs, zero interference, no restore-order headaches. Snapshot mode is read-only: REST writes return 405 and GraphQL mutations error (restore the snapshot if you want to mutate that state). No header β live data, exactly as before.
Try it right now on the demo β it ships with two built-in snapshots, empty (every resource, zero records β test your empty states) and edge-cases (hand-crafted nasty records: a 100-char product name, unicode + HTML-ish characters, price 0, price 1999999.99, an empty description, an out-of-stock one-star product, and id 9999 after id 6):
curl "https://HOST/m/demo/products?mock_snapshot=empty" # β []
curl "https://HOST/m/demo/products?mock_snapshot=edge-cases" # β 7 deliberately nasty records
Not everything is CRUD. Define your own endpoints β any method, any path β with a templated response body. Custom routes take precedence over resource routes, so you can also override a generated endpoint with a fixed payload (trailing-* catch-alls are the one exception β they fall back behind your resources). Walkthrough with live no-signup examples: mock any HTTP endpoint.
# a health endpoint
curl -X POST https://HOST/api/projects/abc123/routes \
-H 'X-Admin-Key: YOUR_ADMIN_KEY' -H 'content-type: application/json' \
-d '{"path":"/health","body":{"status":"ok","time":"{{now}}"}}'
curl https://HOST/m/abc123/health
# β {"status":"ok","time":"2026-07-25T21:53:18.105Z"}
# an echo endpoint with path params + request data
curl -X POST https://HOST/api/projects/abc123/routes \
-H 'X-Admin-Key: YOUR_ADMIN_KEY' -H 'content-type: application/json' \
-d '{"method":"POST","path":"/config/:key","status":201,
"body":"{\"key\":\"{{params.key}}\",\"got\":{{{body}}},\"id\":\"{{uuid}}\"}"}'
curl -X POST "https://HOST/m/abc123/config/theme" \
-H 'content-type: application/json' -d '{"dark":true}'
# β {"key":"theme","got":{"dark":true},"id":"82b14067-β¦"}
Template placeholders, resolved per request:
| placeholder | value |
|---|---|
{{query.x}} | query-string parameter x |
{{params.x}} | path parameter from a :x segment |
{{params.splat}} | the rest of the path matched by a trailing * |
{{body.x}} / {{body.a.b}} | field from the JSON request body (dot paths work) |
{{headers.x}} | request header (case-insensitive) |
{{method}} / {{path}} | request method / full path |
{{now}} / {{ts}} | ISO timestamp / epoch millis |
{{uuid}} / {{rand}} | random UUID / random integer |
Double braces are JSON-string-escaped β safe to drop inside "quotes" even when the value contains quotes. Triple braces ({{{body.count}}}, {{{body}}}) insert the raw JSON value β numbers stay unquoted, objects inline. Missing values render as an empty string (or null in triple braces).
Options per route: method (GET/POST/PUT/PATCH/DELETE/ANY), status (100β599 β make /legacy always 410, or a /teapot 418), contentType (default JSON β serve text/plain, XML, CSVβ¦), headers (up to 10 extra response headers, templated too), delayMs (built-in latency up to 5 s). Same method+path saves overwrite. ?mock_delay= and ?mock_status= still work on custom routes, protected mode guards them, and hits show up in the request inspector.
A trailing * segment matches any remaining path (available as {{params.splat}}). Unlike exact and :param routes, catch-alls are a fallback: your resource endpoints and more specific routes always win, and the * route picks up everything else. One more carve-out: a plain GET on the bare project root serves the project index β but POST/PUT/etc. to the root still land in your catch-all, so webhook receivers keep working.
# turn a project into a request bin: catch every method on every path
curl -X POST https://HOST/api/projects/abc123/routes \
-H 'X-Admin-Key: YOUR_ADMIN_KEY' -H 'content-type: application/json' \
-d '{"method":"ANY","path":"/*","body":{"ok":true,"caught":"{{method}} /{{params.splat}}"}}'
curl -X POST https://HOST/m/abc123/anything/at/all -d '{"hello":1}' -H 'content-type: application/json'
# β {"ok":true,"caught":"POST /anything/at/all"} β¦and the request β headers,
# body, everything β is now in the request inspector
Point a webhook sender at https://HOST/m/abc123/hooks, watch deliveries (with their x-* signature headers) arrive in the inspector, and reply with whatever status/body the sender expects. That's the webhook.site workflow, with no 100-request cap and no 7-day expiry. Scoped bins like /webhooks/* work too, and the catch-all coexists with your mocked resources.
# manage
GET /api/projects/:id/routes # list
POST /api/projects/:id/routes # create or overwrite
DELETE /api/projects/:id/routes/:routeId # remove
Retry-After when exceeded); the shared demo playground gets 50,000Heads-up for Python stdlib users: the hosting platform's edge protection blocks the default Python-urllib/x.y User-Agent with a 403 (this is platform-wide, not a Mockbird rule). Fix: send any User-Agent header, e.g. urllib.request.Request(url, headers={"User-Agent": "my-app"}). The requests library, curl, fetch, axios, and Go are unaffected.
Everything the dashboard does is also an API. Create a project, then use its adminKey via X-Admin-Key header:
# create project
curl -X POST https://HOST/api/projects -H 'content-type: application/json' -d '{"name":"shop"}'
# β {"id":"abc123","adminKey":"...","baseUrl":"https://HOST/m/abc123"}
# add a resource from a template, seeded with 50 records
curl -X POST https://HOST/api/projects/abc123/resources \
-H 'content-type: application/json' -H 'x-admin-key: KEY' \
-d '{"name":"products","template":"products","seed":50}'
# or with custom fields
curl -X POST https://HOST/api/projects/abc123/resources \
-H 'content-type: application/json' -H 'x-admin-key: KEY' \
-d '{"name":"gyms","fields":[{"name":"name","type":"company"},{"name":"city","type":"city"},{"name":"rating","type":"rating"}],"seed":25}'
# x-admin-key, "Authorization: Bearer KEY", or ?key=KEY all work
Mockbird is API-first β everything works without a browser, and AI agents are welcome as users. Two machine-friendly entry points:
GET /api β JSON index of every endpoint, with a copy-paste quickstart/llms.txt β plain-markdown guide for LLM-based tools and agentsPer-project OpenAPI 3.0 specs (/m/<project>/openapi.json) are public, so code
generators and agents can introspect any mock API they have the URL for.
Mockbird is also a hosted MCP server β point Claude Code, Cursor, VS Code, or any
MCP client at https://HOST/mcp and your coding agent can spin up and drive mock
backends as tools: create projects, import an OpenAPI spec / db.json / Postman collection / CSV,
add seeded resources, query and write records, define custom routes, save/restore snapshots.
No signup, no API key, no OAuth β it just works (Streamable HTTP transport, stateless).
# Claude Code
claude mcp add --transport http mockbird https://HOST/mcp
# Cursor / Windsurf / generic JSON config (mcp.json)
{ "mcpServers": { "mockbird": { "url": "https://HOST/mcp" } } }
# VS Code
code --add-mcp '{"name":"mockbird","type":"http","url":"https://HOST/mcp"}'
Eight tools: create_project, import_data, add_resource,
project_info, query_records, write_record,
custom_route, snapshots. They call the exact same API as the
dashboard and curl, so the request inspector, daily caps, and anonymous per-IP limits all apply;
the project adminKey returned by create_project flows through tool
arguments. A typical agent session: βcreate an ecommerce mock and point my frontend's
.env at itβ β one tool call, done. The mock API itself stays plain HTTP, so the
code your agent writes ships with a working URL, not a mock library.
Prefer raw JSON-RPC? POST /mcp with initialize /
tools/list / tools/call messages works from curl too.
Full walkthrough with a sample agent session: the MCP guide.