Documentation

Try it in 5 seconds β€” the public playground

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.

httpbin-compatible surface

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.

The mock API

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

Query parameters (list endpoint)

ParamMeaningExample
page, limitpagination (limit ≀ 100); total in X-Total-Count header?page=2&limit=10
sortBy, ordersort by any field, asc/desc?sortBy=price&order=desc
searchsubstring match across all fields?search=lisbon
_page, _limit, _sort, _order, qjson-server-style aliases for the params above β€” paste json-server URLs unchanged?_sort=price&_order=desc&_limit=5
any field nameexact-match filter?city=Tokyo&inStock=true
<field>_gte / _lte / _gt / _ltrange 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>_nenot-equal filter (records missing the field match too)?status=pending β†’ ?status_ne=pending
<field>_likecase-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_delaysimulate slow network (ms, ≀5000)?mock_delay=2000
mock_statusforce an error response (400–599) to test error handling?mock_status=500
mock_chaoschaos 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_jitterrandom added latency in a ms range (800 = 0–800 ms); composes with mock_delay, total ≀5000 ms?mock_jitter=100-1500
_expandjoin the parent object via its <name>Id field?_expand=customer
_embedattach child records that point back at this record?_embed=comments
selectfield 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_envelopewrap 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).

Response envelopes β€” match your real API's shape

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.

Relations (_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.

Nested routes

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.

OpenAPI spec

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.

TypeScript types & Zod schemas

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.

Postman collection

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.

GraphQL endpoint

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:

FieldWhat 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 fieldsreview.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
_rawthe 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.

Import an OpenAPI spec

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:

Import a json-server db.json

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:

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

Import a Postman collection

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:

Import a HAR (record real traffic, replay it as a mock)

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:

Import a CSV (spreadsheet β†’ REST API)

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:

Field types

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.

Placeholder images

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

Templates

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.

Project presets β€” a whole backend in one click

Instead of adding resources one by one, create a project from a preset and get a full multi-resource fake backend instantly:

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.

Request inspector

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.

Webhooks

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).

Mock auth β€” fake JWT login flow

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.

Protected mode

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.

Scenario snapshots

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.

Serve a snapshot per-request (no restore needed)

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

Custom routes & response templating

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:

placeholdervalue
{{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.

Catch-all routes (request bin)

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

Limits (free beta)

Heads-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.

Management API

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

Scripts & AI agents

Mockbird is API-first β€” everything works without a browser, and AI agents are welcome as users. Two machine-friendly entry points:

Per-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.

MCP server (Model Context Protocol)

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.