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://mockbird.mockbird.workers.dev/m/demo/products?limit=3"
curl "https://mockbird.mockbird.workers.dev/m/demo/orders?status=shipped&_expand=customer"
curl -X POST "https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/m/httpbin/get?x=1"        # httpbin-shaped echo
curl "https://mockbird.mockbird.workers.dev/m/httpbin/status/500"     # any status
curl "https://mockbird.mockbird.workers.dev/m/httpbin/delay/3"        # seconds, max 10

GET /m/httpbin lists every supported endpoint. Full details + what's deliberately omitted: httpbin alternative guide.

randomuser.me-compatible surface

Code that hardcodes randomuser.me can also just swap the host: /m/randomuser/api serves the same response shape (name.title, location.street.number, login.uuid, dob.date, picture.large…) with the same params β€” results (max 100), seed, page, gender, nat, inc/exc, noinfo=1. Fully deterministic: same seed+page β†’ identical users, forever. Picture URLs are deterministic SVG initials avatars served under /m/randomuser/api/portraits/… (same path shape as theirs); the md5/sha1/sha256 login fields are stable dummy hex, not real digests. JSON only.

curl "https://mockbird.mockbird.workers.dev/m/randomuser/api/?results=3&seed=demo&nat=us,gb"
curl "https://mockbird.mockbird.workers.dev/m/randomuser/api/?inc=name,email,picture&noinfo=1"

GET /m/randomuser is the self-describing index. Want fake users you can write to? See the randomuser.me alternative guide.

FakerAPI-compatible surface

fakerapi.it has been answering 502 since Aug 29, 2026 (live status). Code that hardcodes it can swap the host: /m/fakerapi/api/v2/<resource> serves the same ten resources (persons, users, addresses, companies, books, products, texts, images, places, credit_cards) plus the custom builder (/api/v2/custom?id=counter&name=firstName&mail=email), with the same params (_quantity β€” max 100, theirs 1000 β€” _locale, _seed, _gender, _birthday_start/_end, _price_min/_max, _taxes, _categories_type, _characters, _type/_width/_height) and the same {status, code, locale, seed, total, data} envelope (/api/v1/… omits locale/seed, like the original). _seed is deterministic β€” the same seed returns the same rows forever. Honest notes: _locale is echoed but data stays English (their 40+ real locales are their win), text fields are generated word text, and image URLs point at our deterministic SVG placeholders β€” which actually load, unlike FakerAPI's dead placeimg.com/placekitten.com hosts.

curl "https://mockbird.mockbird.workers.dev/m/fakerapi/api/v2/persons?_quantity=3"
curl "https://mockbird.mockbird.workers.dev/m/fakerapi/api/v2/persons?_quantity=2&_seed=42"
curl "https://mockbird.mockbird.workers.dev/m/fakerapi/api/v2/custom?_quantity=3&id=counter&name=firstName&mail=email"

GET /m/fakerapi is the self-describing index. Want fake data that persists (single-record GETs, filters, writes)? See the FakerAPI 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://mockbird.mockbird.workers.dev/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
sortBy=randomshuffled order β€” with limit=1 you get one random record (a quotable-style /random endpoint; caveat: a field literally named random can't be sorted on)?sortBy=random&limit=1
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. Repeat the param to OR values β€” ?id=3&id=5 returns both records (json-server-style batch fetch)?city=Tokyo&inStock=true, ?id=3&id=5
<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
mock_ratelimitrate-limit simulation: allow N requests per 60-second window per client IP, then 429 with Retry-After. Every response (success too) carries x-ratelimit-limit/x-ratelimit-remaining/x-ratelimit-reset headers β€” test your backoff and countdown UI without getting banned by a real API. Simulated 429s carry x-mockbird-ratelimit: simulated. Parallel workers on one IP (CI runners): add ?mock_ratelimit_key=w1 β€” each key gets its own counter, like mock_seq_key. Rate-limit guide β†’?mock_ratelimit=5
mock_seqdeterministic response sequence: comma-separated statuses served in order, one per request β€” 503,503,200 means the first request gets a 503, the second a 503, the third (and every later one) the real response. Sticks on the last entry once exhausted. Statuses <400 serve the real response; β‰₯400 return a simulated error, and failed writes are not applied (the "server" died before processing β€” safe to point retry logic at). The counter is per project + path + sequence; add mock_seq_key=w1 to give parallel workers isolated counters, and mock_seq_reset=1 to restart (that request counts as #1). Every response carries x-mockbird-seq: pos/len. The deterministic alternative to mock_chaos for retry tests: no flaky randomness β€” fail exactly twice, then succeed. On the shared demo project, counters are additionally scoped per client IP, so a sequence link shared publicly always starts fresh for each visitor.?mock_seq=503,503,200
_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
mock_cursorcursor pagination (Stripe/Slack-style): the response becomes {"data":[…],"next_cursor":"…","has_more":true}. limit is the page size; page is ignored. Follow next_cursor with ?cursor=<token> (implies cursor mode, so mock_cursor=1 is only needed on the first request). Filters, sort, q, select, _expand/_embed, nested routes and snapshot pinning all compose; an invalid token is a 400 β€” exactly what your infinite-scroll retry logic should see. next_cursor is also sent as the x-mockbird-next-cursor header. Doesn't combine with CSV/streaming modes, and takes precedence over mock_envelope. Note: a field literally named cursor can't be used as an exact-match filter. Infinite-scroll guide β†’?mock_cursor=1&limit=10
mock_ssestream the list as Server-Sent Events β€” one event per record (event name = resource, incrementing id:), then a final done event. See streaming?mock_sse=1
mock_streamstream the list as NDJSON (one record per line, application/x-ndjson). =sse is an alias for mock_sse=1?mock_stream=1
mock_stream_intervalms between streamed records (default 500, 0–5000; total stream time capped at 60 s)?mock_stream_interval=200
mock_validatemake writes validate like a real API: POST/PUT/PATCH bodies are type-checked against the resource schema β€” a bad payload gets 422 {"error":"validation_failed","fields":{"price":"expected number, got string"}}. =strict also rejects unknown fields and requires every schema field (PATCH stays partial). =off overrides a project default. See write validation?mock_validate=1
mock_formatreturn any list or single-record GET as CSV (RFC 4180) instead of JSON β€” sending Accept: text/csv works too. Columns = the record's fields (id first); ?select= picks the columns; nested values are JSON-stringified into their cell; filters/sort/pagination apply first. See CSV export?mock_format=csv

Empty-valued params are ignored. ?category=&cursor=&mock_status= is treated as if those params weren't there at all. API clients that import our OpenAPI spec (Hoppscotch, Postman, Insomnia) often pre-fill every documented query param with an empty value β€” imported requests work as-is instead of filtering the list down to nothing. (Bare flags like ?mock_seq_reset keep working.)

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

Write validation β€” 422s like a real API

Every other mock accepts any payload, so the sad path of your form β€” the 422, the field errors, the red text under the input β€” goes untested until the real backend arrives. Mockbird can validate writes against the resource schema you already defined:

# price is a number field β€” send a string, get a proper 422
curl -X POST 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_validate=1' \
  -H 'content-type: application/json' \
  -d '{"name":"Widget","price":"cheap"}'
# β†’ 422 {"error":"validation_failed","fields":{"price":"expected number, got string"}}

What's checked per field type: numbers (number/price/rating/…) must be JSON numbers, boolean must be a boolean, email/uuid/url/ip/date fields must be well-formed strings, oneOf values must be in the enum. Other string fields just need to be strings.

The response shape is stable β€” fields maps each bad field to a human-readable reason β€” so you can build your error-rendering against it. Combine with ?mock_status=500 and ?mock_chaos= for the full sad-path matrix. Full walkthrough: test your form's sad path β†’

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://mockbird.mockbird.workers.dev/m/demo/products?mock_envelope=data&limit=2'
β†’ {"data":[{…},{…}]}

# or a JSON template β€” "$data" becomes the records; URL-encode it
curl 'https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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.

Streaming β€” SSE & NDJSON

Any list endpoint can stream its records instead of returning one JSON array β€” test EventSource clients, fetch-stream readers, progressive rendering and skeleton screens against a real hosted URL:

# Server-Sent Events: one event per record, then a final `done` event
curl -N 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_sse=1&limit=5&mock_stream_interval=300'

# NDJSON: one record per line
curl -N 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_stream=1&limit=5'

In the browser:

const es = new EventSource("https://mockbird.mockbird.workers.dev/m/demo/products?mock_sse=1&limit=10");
es.addEventListener("products", (e) => render(JSON.parse(e.data)));  // event name = resource
es.addEventListener("done", () => es.close());  // {"total":30,"sent":10}

Everything composes: filters, sortBy, pagination, q, select, nested routes, and snapshot mode all apply before streaming. mock_stream_interval paces the records (default 500 ms; total stream time is capped at 60 s, so the interval shrinks automatically for large pages). mock_delay delays the first byte; mock_status/mock_chaos still win (they return a normal JSON error, handy for testing EventSource onerror). Envelopes are ignored β€” streams have their own framing.

Protected-mode note: EventSource can't send an Authorization header, so protected projects also accept the token as ?mock_token=<jwt> on any mock endpoint (mock data only; the value is redacted in the request inspector).

Full walkthrough with EventSource + fetch-stream client code: the SSE mocking guide.

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. Every operation carries a clean operationId (listProducts, createProduct, getProduct, …), so codegen tools produce readable method names β€” including Apple's swift-openapi-generator, which refuses specs without them. 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://mockbird.mockbird.workers.dev/m/demo/types.ts"

# Zod schemas + z.infer types instead (works with zod v3.20+ and v4)
curl -o schemas.ts "https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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.

.http request file (VS Code / JetBrains / Thunder Client)

Every project also serves a ready-to-run .http request file at /m/<project>/requests.http (public, CORS on, no auth β€” schema only, no records). Save it and click Send Request:

curl -o requests.http "https://HOST/m/demo/requests.http"

The file works in the VS Code REST Client extension (humao.rest-client), the JetBrains HTTP Client built into IntelliJ / WebStorm / PyCharm, and Thunder Client β€” anything that speaks the .http format. You get full CRUD per resource with example bodies matching your field types, a @baseUrl variable, simulation examples (mock_status, mock_delay) and a GraphQL request. In protected mode the file starts with a named login request that later requests reference via {{login.response.body.$.token}} (VS Code REST Client syntax). Re-download any time to sync with your current schema. Full .http guide β†’

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://mockbird.mockbird.workers.dev/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.

WebSocket endpoint β€” echo server + mock realtime feed

Every project serves a WebSocket endpoint at wss://mockbird.mockbird.workers.dev/m/<project>/ws. Out of the box it's an echo server β€” any message you send (text or binary) comes straight back β€” so it's a drop-in for the dead echo endpoints old tutorials still point at (echo.websocket.events and friends). Add ?quiet=1 to suppress the welcome frame for byte-exact echo tests.

npx wscat -c "wss://mockbird.mockbird.workers.dev/m/demo/ws"
> hello
< hello

The part nobody else has: subscribe to your own resources and the records stream back as timed events β€” a mock realtime feed (ticker, notifications, order stream) with your schema, no backend:

# auto-subscribe from the URL…
npx wscat -c "wss://mockbird.mockbird.workers.dev/m/demo/ws?subscribe=products&interval=500&limit=5"
# …or send a command on any open connection:
> {"subscribe":"products","interval":500,"limit":5,"repeat":true,"jitter":300}
< {"type":"subscribed","resource":"products","count":5,"interval":500,"repeat":true}
< {"type":"record","resource":"products","seq":1,"of":5,"data":{"id":1,...}}
< ...
< {"type":"complete","resource":"products","count":5,"repeat":true}
OptionWhat it does
intervalms between records (100–10000, default 1000)
jitteradds 0–jitter random ms per record β€” irregular, realistic pacing (max 5000)
limitrecords per cycle (max 100)
repeatloop the feed until you disconnect (true/1)
{"unsubscribe":true}stop the stream, keep the connection
{"ping":1}β†’ {"type":"pong","ts":…}

In protected mode pass ?token=<jwt> on the URL (the browser WebSocket API can't send an Authorization header). Limits: a connection counts as one request against the daily cap; sessions close after 5 minutes (reconnect any time); 1000 messages per connection; 64 KB max echo.

Server-Sent Events β€” the same feed for EventSource

Prefer EventSource to WebSockets? Every project also serves GET /m/<project>/sse (text/event-stream, CORS on). Bare /sse emits timed tick events β€” the live test signal every SSE tutorial needs β€” and ?subscribe=<resource> streams that resource's own records as record events, same options as the WebSocket feed (interval, limit, repeat, jitter; ?quiet=1 skips the welcome event):

curl -N "https://mockbird.mockbird.workers.dev/m/demo/sse?subscribe=products&interval=500&limit=3"
const es = new EventSource("https://mockbird.mockbird.workers.dev/m/demo/sse?subscribe=products&interval=500");
es.addEventListener("record", (e) => {
  const { seq, of, data } = JSON.parse(e.data);   // data = the record itself
  console.log(`${seq}/${of}`, data);
});
es.addEventListener("complete", () => es.close()); // omit with &repeat=1

Streams are reconnect-aware: every event carries an id:, so when EventSource auto-reconnects it sends Last-Event-ID and the stream resumes at the next position instead of starting over (you can also pass ?lastEventId=N by hand). In protected mode pass ?token=<jwt> (EventSource can't send headers either). Limits: one daily-cap request per connection; sessions close cleanly after 5 minutes with a bye event (EventSource reconnects on its own; retry: 3000 is pre-set); 1000 events per connection.

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://mockbird.mockbird.workers.dev/api/projects/import --data-binary @openapi.yaml
# β†’ {"id":"abc123","adminKey":"...","baseUrl":"https://mockbird.mockbird.workers.dev/m/abc123",
#    "dashboard":"https://mockbird.mockbird.workers.dev/app#open=abc123:KEY",
#    "resources":[{"name":"pets","seeded":20,"url":"https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/api/projects/import --data-binary @db.json
# β†’ {"id":"abc123","kind":"db.json","baseUrl":"https://mockbird.mockbird.workers.dev/m/abc123",
#    "resources":[{"name":"posts","records":42,"url":"https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/m/demo/db.json
npx json-server db.json   # the same API, running locally

Prefer in-process mocking? GET /m/<project>/msw.js returns a self-contained Mock Service Worker (MSW v2) handlers module with your current data baked in β€” CRUD, exact filters, q search, _sort/_order, _page/_limit + X-Total-Count, all in memory. Drop it into setupServer(...handlers) (Node tests) or setupWorker(...handlers) (browser) and your mock keeps working offline with zero Mockbird dependency. Relations, operator suffixes, chaos/latency and GraphQL stay hosted-only β€” they compose:

curl -o msw.js https://mockbird.mockbird.workers.dev/m/demo/msw.js
# import { setupServer } from 'msw/node'
# import { handlers } from './msw.js'
# setupServer(...handlers).listen()

Host any JSON file as an API

Don't have a db.json? Any JSON array of objects works. POST it to the same endpoint and it becomes a live collection β€” your records verbatim, integer ids preserved (missing ones filled in), field types inferred from the data. Name the collection with ?resource= (default items):

curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/import?resource=todos" \
  --data-binary @todos.json
# todos.json = [{"id":1,"title":"buy milk","done":false}, ...]
# β†’ {"baseUrl":"https://mockbird.mockbird.workers.dev/m/abc123", "resources":[{"name":"todos","records":3,...}]}

You instantly get GET/POST/PUT/PATCH/DELETE, filters (?done=true, _gte/_lte/_ne/_like), pagination, search, select=, GraphQL and every export on that data β€” a full read-write API, not just a hosted file. A plain JSON object works too: object-of-collections is treated as a db.json; singular object values become 1-record collections (handy for config blobs β€” GET /m/<id>/config/1).

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://mockbird.mockbird.workers.dev/api/projects/import --data-binary @collection.json
# β†’ {"id":"abc123","kind":"postman","baseUrl":"https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/api/projects/import --data-binary @myapp.har
# β†’ {"id":"abc123","kind":"har","baseUrl":"https://mockbird.mockbird.workers.dev/m/abc123",
#    "resources":[{"name":"products","records":34,...},{"name":"cart-items","records":3,...}]}

Notes:

Import a VCR / vcrpy cassette

The same endpoint accepts VCR (Ruby) and vcrpy (Python) cassettes β€” the YAML files those libraries record (http_interactions: / interactions:; the JSON serializers work too). The 2xx JSON response bodies recorded in the cassette become hosted records, served back verbatim β€” your cassette suite's data, now behind a URL that curl, Postman, a browser, or a teammate's machine can hit (cassette replay is normally visible only inside the recording process).

curl -X POST https://mockbird.mockbird.workers.dev/api/projects/import --data-binary @cassettes/checkout.yml
# β†’ {"id":"abc123","kind":"cassette","baseUrl":"https://mockbird.mockbird.workers.dev/m/abc123", ...}

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://mockbird.mockbird.workers.dev/api/projects/import?resource=people' \
  -H 'content-type: text/csv' --data-binary @people.csv
# β†’ {"id":"abc123","kind":"csv","baseUrl":"https://mockbird.mockbird.workers.dev/m/abc123",
#    "resources":[{"name":"people","records":124,...}]}

Notes:

Export as CSV

Every list and single-record GET can come back as CSV instead of JSON β€” add ?mock_format=csv or send Accept: text/csv. Filters, sort, search, pagination and ?select= all apply first, so a filtered download is one URL (the response carries a content-disposition filename, so a browser visit saves products.csv):

# the whole (paged) list as a spreadsheet
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_format=csv&limit=100'

# only some columns, sorted β€” ?select= picks the CSV columns
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_format=csv&select=name,price&sortBy=price&order=desc'

# via content negotiation
curl -H 'accept: text/csv' 'https://mockbird.mockbird.workers.dev/m/demo/products'

Rules: RFC 4180 output (CRLF rows, quotes doubled, fields quoted when they contain commas/quotes/newlines). Columns are the union of the returned records' fields with id hoisted first; nested objects/arrays are JSON-stringified into their cell. Works on nested routes and snapshot mode too. Envelope and streaming params don't combine with CSV, and writes/GraphQL are JSON-only. Together with CSV import this is a full round-trip: spreadsheet in β†’ live API β†’ spreadsheet out.

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. Try every parameter live in the playground β†’

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
GET /m/<project>/img/300/200                # picsum.photos-shaped path β€” same image
GET /m/<project>/img/seed/abc/300/200       # picsum seed path (= ?seed=abc)
GET /m/<project>/img/300x200.png            # PNG output (also ?format=png)
GET /m/<project>/img/seed/abc/300/200.jpg   # .jpg/.jpeg paths work too (serve PNG bytes)
GET /m/<project>/img/400x300?grayscale       # picsum-style effects …
GET /m/<project>/img/400x300?blur=5          # … work on SVG and PNG output alike

Initials avatars

There's also a dedicated avatar endpoint with ui-avatars.com-compatible params (swapping the host is a drop-in):

GET /m/<project>/avatar?name=John+Doe            # 128px square, "JD"
GET /m/<project>/avatar/64?name=Ada+Lovelace&rounded=true
GET /m/<project>/avatar?name=John+Doe&background=0D8ABC&color=fff&rounded=true
GET /m/<project>/avatar/150?u=a042581f4e29        # pravatar-style unique id

Open Graph / social card images

Every project also gets a dynamic Open Graph card endpoint β€” an og:image URL you can paste into a <meta> tag today, no image editor, no deploy. It's PNG by default because that's what social scrapers require (Facebook, Twitter/X, Slack, Discord and Bluesky don't render SVG cards):

GET /m/<project>/og?title=My+launch+post                    # 1200Γ—630 PNG
GET /m/<project>/og?title=Hello&subtitle=shipped+today&site=example.com
GET /m/<project>/og?title=Dark+or+light&theme=light
GET /m/<project>/og?title=Brand+colors&bg=0b1220&accent=22d3ee
GET /m/<project>/og?title=With+a+mark&logo=myapp            # identicon logo, bottom-right
GET /m/<project>/og/800x418?title=Other+sizes               # /og/:WxH override
GET /m/<project>/og.svg?title=Vector+preview                # SVG twin (?format=svg too)

QR codes

Every project also serves scannable QR codes from a URL β€” no key, no library, no signup. PNG by default (what <img> tags, print stylesheets and most tooling want); SVG twin on request:

GET /m/<project>/qr?data=https%3A%2F%2Fexample.com      # 256Γ—256 PNG
GET /m/<project>/qr/500?data=hello                      # /qr/:size override (64–2000)
GET /m/<project>/qr.svg?data=hello                      # vector twin (?format=svg too)
GET /m/<project>/qr?data=hello&ecc=H&margin=2           # error correction + quiet zone
GET /m/<project>/qr?data=WIFI%3AT%3AWPA%3BS%3Amynet%3BP%3Asecret%3B%3B   # wifi payload

Chart images

Every project also serves chart images from numbers in the URL β€” no key, no chart library, no build step. PNG by default (README/markdown embeds and chat previews want raster); SVG twin on request:

GET /m/<project>/chart?data=3,7,4,9,6                        # 600Γ—300 line chart PNG
GET /m/<project>/chart?data=12,19,7,24&type=bar&labels=q1,q2,q3,q4&title=Signups
GET /m/<project>/chart/300x80?data=3,7,4,9,6,2,8&type=spark  # README sparkline
GET /m/<project>/chart.svg?data=3,7,4,9,6&type=area          # vector twin (?format=svg too)
GET /m/<project>/chart?data=3,7,4&theme=dark&color=34d399    # dark theme + custom series color

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.

No preset? New projects still work instantly: a plain POST /api/projects (or bare /app#new) includes a seeded starter resource items β€” 20 fake product-ish records, live at /m/<id>/items the moment the create call returns. Reshape or delete it, or pass {"blank": true} (dashboard: pick "empty project") to start with nothing.

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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/api/projects/demo/requests.

HAR export

The same window is available as a standard HAR 1.2 file β€” the format DevTools and every HTTP-debugging tool already speak:

curl -o trace.har 'https://mockbird.mockbird.workers.dev/api/projects/abc123/requests.har' -H 'x-admin-key: KEY'
# or in the dashboard: Recent requests β†’ β€œ.har ↗”

Open it in Chrome or Firefox DevTools (Network tab β†’ Import HAR), a HAR viewer, or diff two runs with jq. It takes the same trajectory filters as the JSON inspector, so requests.har?method=POST&since=<episode-start> exports exactly one agent episode's writes β€” handy for attaching an eval run's traffic to a bug report or review. Honest caveats: entries are what your client sent plus the response status β€” the inspector doesn't store response bodies β€” and request bodies over 2 KB are truncated. Works on share links too (/api/share/<token>/requests.har), and the demo's is public: /api/projects/demo/requests.har. (The reverse direction β€” turning a DevTools-recorded HAR into a mock β€” is HAR import.)

Trajectory assertions (filters + count)

The inspector accepts filters, and every response includes count β€” the number of matching requests in the retained window β€” so a test or eval grader can assert on behavior, not just final state:

ParamMeaning
method=DELETEHTTP method; comma-list ok (PUT,PATCH,DELETE)
path=/taskssegment-aware path prefix β€” matches /tasks and /tasks/5, not /tasksomething
status=404exact response status; comma-list ok
status_gte=400 / status_lte=status range (status_gte=400 = any error)
since=<epoch ms | ISO-8601>only requests at/after that time β€” record the episode start, assert about only that episode
limit=Ntruncate the returned array (count still reflects all matches)
# the agent must not have deleted anything:
curl -s '.../api/projects/ID/requests?method=DELETE' -H 'x-admin-key: KEY' | jq -e '.count == 0'
# no request ever errored:
curl -s '.../api/projects/ID/requests?status_gte=400' -H 'x-admin-key: KEY' | jq -e '.count == 0'
# it DID write exactly one order:
curl -s '.../api/projects/ID/requests?method=POST&path=/orders' -H 'x-admin-key: KEY' | jq -e '.count == 1'

Honest caveat: filters apply within the retained window (window: 50 is echoed in the response). For eval runs, fork per run β€” a fresh fork's log contains exactly one episode's traffic, so the window is never a problem. The same filters work on share-link inspector URLs (/api/share/<token>/requests?method=DELETE) and on the MCP inspect_requests tool.

Read-only share links

Mint a read-only share link for a project and hand it to anyone who should see it but not touch it β€” a teammate reviewing your mock, a client checking the API shape, or (if you're an AI agent) the human supervising your sandbox:

curl -X POST https://mockbird.mockbird.workers.dev/api/projects/abc123/share -H 'x-admin-key: KEY'
# → {"shareUrl": "https://mockbird.mockbird.workers.dev/share/<token>", ...}

Anyone opening the shareUrl in a browser gets a live read-only view: resources with record counts, a data browser (works even when the project is in protected mode), custom routes, snapshots, exports, and the request inspector β€” refreshable, so they can watch traffic arrive. No writes are possible through the link and the admin key never appears. For scripts there's a JSON flavor: GET /api/share/<token> (overview), /api/share/<token>/requests (plus /requests.har β€” the HAR export), /api/share/<token>/data/<resource>?page=&limit= β€” and /api/share/<token>/verdict/<name> runs a saved verdict keylessly (grading a run is a read).

One link per project. POST .../share with {"rotate":true} invalidates the old link and mints a fresh one; DELETE .../share revokes it; GET .../share shows the current link. Also available in the dashboard (Share card) and over MCP as the share_project tool.

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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/api/projects/abc123/webhook/test -H 'x-admin-key: KEY'

# see the last 20 delivery attempts (status, error, duration)
curl https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/api/projects/abc123/snapshots/baseline/restore \
  -H 'x-admin-key: KEY'

# list / inspect / delete
curl https://mockbird.mockbird.workers.dev/api/projects/abc123/snapshots -H 'x-admin-key: KEY'
curl https://mockbird.mockbird.workers.dev/api/projects/abc123/snapshots/baseline -H 'x-admin-key: KEY'   # includes full data
curl -X DELETE https://mockbird.mockbird.workers.dev/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.

Authored snapshots (write the expected state by hand)

A snapshot doesn't have to be captured from live data β€” pass a data object and the snapshot is built from the collections you send, exactly as written:

curl -X POST https://mockbird.mockbird.workers.dev/api/projects/abc123/snapshots \
  -H 'x-admin-key: KEY' -H 'content-type: application/json' -d '{
    "name": "expected",
    "data": {
      "tasks":  [ {"id":1,"title":"write tests","done":true},
                  {"id":2,"title":"ship it","done":true} ],
      "labels": [] }}'

Records are stored verbatim (ids preserved; missing ids get filled in sequentially), an empty array means "this resource, with zero records" β€” perfect for expected: emptied assertions β€” and you can name resources that don't exist live yet (restore rebuilds them). Resources that do exist live keep their live field schema, so a restore never changes your types.ts/GraphQL output. This is the natural way to author an eval answer key: write the expected end-state inline, run the agent, then diff against it β€” no need to mutate live data into the expected shape first just to capture it. Same 64 KB management-API body limit applies; for bigger states, set the data up via import and capture normally.

Fork a project (isolated copies for parallel runs)

Snapshots pin reads; forking isolates writes. POST /api/projects/:id/fork copies the whole project β€” resources, every record verbatim (ids preserved), custom routes, and behavior settings (protected mode, envelope, validation) β€” into a brand-new project with its own id and adminKey:

# fork your project into an independent copy
curl -X POST https://mockbird.mockbird.workers.dev/api/projects/abc123/fork \
  -H 'x-admin-key: KEY' -H 'content-type: application/json' -d '{"name":"run-42"}'
# β†’ {"id":"xyz789","adminKey":"...","forkedFrom":"abc123","baseUrl":".../m/xyz789", ...}

# the demo can be forked by anyone, no auth β€” the demo dataset as your own project
curl -X POST https://mockbird.mockbird.workers.dev/api/projects/demo/fork

Typical use: parallel test workers or AI-agent eval runs that all need to write. Keep one template project, fork it per run (each run gets a private URL β€” no shared-state races, no restore coordination), and DELETE /api/projects/:id the fork when done β€” or skip the cleanup entirely with a ttl. Not copied: webhooks, snapshots, the request log, and (for demo forks) the demo's showcase routes. Forks count toward your project limits like any create. Mock-auth note: tokens are signed per-project, so JWTs issued by the source project are rejected by the fork β€” log in against the fork's own /auth/login.

Self-expiring projects (ttl)

Pass "ttl": <seconds> (or ?ttl=) on create, import, or fork and the project deletes itself β€” all records, routes, snapshots, logs β€” when the time is up. Range 60 s to 7 days. Built for CI and agent-eval runs: fork with a ttl and a crashed run can't leak sandboxes; there is no cleanup step to forget.

# a sandbox that cleans up after itself, even if your CI dies mid-run
curl -X POST 'https://mockbird.mockbird.workers.dev/api/projects/demo/fork?ttl=900'
# β†’ { ..., "expiresAt":"2026-09-09T17:00:00.000Z", "expiresIn":900 }

# changed your mind β€” keep it (or push the deadline back)
curl -X PUT .../api/projects/xyz789/settings -H 'x-admin-key: KEY'   -H 'content-type: application/json' -d '{"ttl": null}'      # keep forever
#   ... -d '{"ttl": 3600}'                                    # new deadline: 1h from now

Semantics: expiresAt is returned on create and shown on project GET (with a live expiresIn countdown). Expiry is enforced immediately on the REST mock endpoints and the management API (requests return 404 and the data is deleted on the spot); idle expired projects are fully swept within ~30 minutes by a background job. ttl in settings is always counted from now β€” sending {"ttl": 3600} twice moves the deadline, it doesn't stack. Projects without a ttl behave exactly as before: they never expire.

Diff a snapshot against live data (machine-checkable grading)

GET /api/projects/:id/snapshots/:name/diff compares a snapshot (expected) with the project's current data (actual) and returns a structured verdict β€” no dump-both-and-eyeball:

curl "https://mockbird.mockbird.workers.dev/api/projects/abc123/snapshots/expected/diff" \
  -H 'x-admin-key: KEY'
# β†’ {"identical": false,
#    "summary": {"resourcesAdded":[], "resourcesRemoved":[],
#                "records": {"added":1, "removed":1, "changed":2, "unchanged":22}},
#    "resources": [{"resource":"tasks", "added":[6], "removed":[2],
#      "changed":[{"id":3,"fields":{"done":{"expected":false,"actual":true}}}], "unchanged":2}]}

# one-liner pass/fail (also sent as the x-mockbird-identical response header)
curl -s ".../snapshots/expected/diff" -H 'x-admin-key: KEY' | jq -e '.identical'

Semantics: added = records present now but not in the snapshot; removed = in the snapshot but gone now; changed lists per-field {expected, actual} pairs (canonical comparison β€” key order never causes a false diff). Options: ?ignore=updatedAt,createdAt excludes volatile field names from comparison everywhere; ?against=other-snapshot compares two snapshots instead of snapshot-vs-live. Resource-level drift shows up as resourcesAdded/resourcesRemoved and schemaChanged. Detail lists cap at 200 ids / 100 changed records per resource (counts stay exact; changedTruncated: true flags the cap).

Built for eval/CI grading: save the expected end-state as a snapshot once, run the agent or test against a fork, then diff and assert identical β€” the grader checks state, not the transcript. Recipe: AI-agent sandbox guide.

Verdict: the whole grade in one call

POST /api/projects/:id/verdict composes the state check (snapshot diff) and any number of trajectory constraints on the request log into a single {pass, checks[]}:

curl -s https://mockbird.mockbird.workers.dev/api/projects/abc123/verdict \
  -H 'x-admin-key: KEY' -H 'content-type: application/json' -d '{
    "snapshot": "expected",
    "ignore": ["updatedAt"],
    "trajectory": [
      {"method": "DELETE", "count": 0},
      {"method": "POST", "path": "/orders", "count": 1},
      {"status_gte": 400, "count": 0}
    ]
  }'
# snapshot: live data must match it Β· ignore: volatile fields excluded from the state check
# trajectory: never deleted / exactly one order created / nothing errored
# β†’ {"pass": true, "checks": [
#      {"kind":"state","snapshot":"expected","pass":true,"summary":{...}},
#      {"kind":"trajectory","filter":{"method":"DELETE"},"expect":{"count":0},"count":0,"pass":true},
#      ...], "window": 50}

# one-liner gate (pass is also sent as the x-mockbird-pass response header)
curl -s .../verdict -H 'x-admin-key: KEY' -d '{"snapshot":"expected"}' | jq -e '.pass'

Both parts are optional β€” send only snapshot for a pure state grade, or only trajectory for pure behavior constraints. Each trajectory constraint takes the same filters as the request-inspector (method comma-list, path segment-aware prefix, status, status_gte/status_lte, since) plus an expectation: count (exact), min and/or max (max 20 constraints). Failed checks carry a human-readable detail; the state check includes the full diff resources breakdown.

Want the HTTP status itself to be the gate? Add "failStatus": 422 and a failing verdict returns that status instead of 200 β€” so curl -sf, a Playwright expect(res.ok), or any http-client error path is the CI gate, no JSON parsing needed. Trajectory counts see the retained window (last 50 requests) β€” fork per run so the log is exactly one episode. Over MCP: the verdict tool.

Saved verdicts: name the spec, grade from a share link

Sending the whole spec on every run means every grader script carries the answer key. Instead, save the spec once, by name, on the project:

# author the grading spec on your TEMPLATE project (max 10 per project)
curl -X PUT https://mockbird.mockbird.workers.dev/api/projects/abc123/verdicts/final \
  -H 'content-type: application/json' -H 'x-admin-key: KEY' \
  -d '{"snapshot":"expected","trajectory":[{"status_gte":400,"count":0}],"failStatus":422}'

# run it by name (admin)
curl -s .../api/projects/abc123/verdict -H 'x-admin-key: KEY' -d '{"name":"final"}'

# GET /verdicts lists them; GET/DELETE /verdicts/final reads/removes one

Saved verdicts are copied by fork (pass {"withSnapshots":true} too if the spec references a snapshot), and β€” the payoff β€” they run keylessly from the share link:

# the grader/CI needs ONLY the share token and the name β€” no admin key, no spec
curl -sf https://mockbird.mockbird.workers.dev/api/share/<token>/verdict/final

That completes the eval-harness handoff: the agent under test gets the fork's mock URL (can't read the grading spec, which lives behind the admin API), the grader gets the share link and a name, and with failStatus set, curl -sf on that URL is the whole CI gate. Running a check is a pure read β€” it can't mutate data, so handing out the link risks nothing. The share page also shows saved checks as one-click Run buttons for human reviewers. Over MCP: verdict with name.

Live pass/fail badge

Every saved verdict also renders as a live SVG badge β€” append /badge.svg to the keyless run URL and embed it in a README, PR description, or dashboard:

![final](https://mockbird.mockbird.workers.dev/api/share/<token>/verdict/final/badge.svg)

Each render runs the verdict against the live data + request log β€” green pass / red fail, no stale build artifacts. The badge is always HTTP 200 (a red badge still has to render through image proxies like GitHub's camo β€” the curl -sf CI gate stays on the JSON route), caches for ~60s, and takes ?label= to override the left-hand text. Unknown names render a gray unknown badge. The share page shows each check's badge next to its Run button β€” click it to copy the Markdown.

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://mockbird.mockbird.workers.dev/m/abc123/products

# same URL, answered from the "empty" snapshot β€” live data untouched
curl https://mockbird.mockbird.workers.dev/m/abc123/products -H 'X-Mockbird-Snapshot: empty'

# query-param variant
curl "https://mockbird.mockbird.workers.dev/m/abc123/products?mock_snapshot=edge-cases"

# GraphQL queries honor it too (schema + data come from the snapshot)
curl https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/m/demo/products?mock_snapshot=empty"        # β†’ []
curl "https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/m/abc123/health
# β†’ {"status":"ok","time":"2026-07-25T21:53:18.105Z"}

# an echo endpoint with path params + request data
curl -X POST https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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 request body β€” JSON (dot paths work) and HTML form posts (application/x-www-form-urlencoded or multipart/form-data, the default for htmx and plain <form> submits, and what Twilio/Slack-style webhooks send)
{{headers.x}}request header (case-insensitive; cookie, x-admin-key and client-IP infra headers are never exposed to templates)
{{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 (200–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.

Serving HTML: set contentType: "text/html" and a browser navigation renders your route as a real page β€” mock whole HTML fragments for htmx frontends or host a tiny test page next to its mock API. For safety on this shared origin, every custom-route response carries Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-modals: scripts in your mocked page run normally and can fetch() your mock endpoints (CORS is open), but the page gets an opaque origin β€” it can never read Mockbird cookies or localStorage. This header can't be overridden.

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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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

Proxy fallback β€” mock some endpoints, pass the rest through

Point a project at your real API and Mockbird becomes a partial mock: requests that match one of your resources, custom routes or built-in endpoints are served from the mock; everything else is forwarded to the upstream β€” method, path, query string, request body and Authorization header included β€” and the real response comes back. It's Mirage's passthrough / Mockoon's proxy mode, as a hosted URL your whole team (and CI) can share.

# turn it on (admin key required; null clears it)
curl -X PUT https://mockbird.mockbird.workers.dev/api/projects/abc123/settings \
  -H 'X-Admin-Key: YOUR_ADMIN_KEY' -H 'content-type: application/json' \
  -d '{"proxyBase":"https://api.example.com/v1"}'

curl https://mockbird.mockbird.workers.dev/m/abc123/products     # your resource β†’ mock data
curl https://mockbird.mockbird.workers.dev/m/abc123/payments/42  # no such resource β†’ GET https://api.example.com/v1/payments/42

Useful when the endpoint you need to fake doesn't exist yet (or misbehaves) but the rest of the API is fine β€” mock the one endpoint, proxy the rest, change one base URL in your app.

Record & replay

Flip on recording and every proxied 2xx response is saved as a custom route β€” the next request to that method + path is served locally, byte-for-byte, without touching the upstream. Browse your app against the mock URL once, and the endpoints you exercised become stubs you can keep, edit, or delete. (This is WireMock's record mode / Mockoon's "record from proxy", hosted and free.)

curl -X PUT https://mockbird.mockbird.workers.dev/api/projects/abc123/settings \
  -H 'X-Admin-Key: YOUR_ADMIN_KEY' -H 'content-type: application/json' \
  -d '{"proxyRecord":true}'

curl https://mockbird.mockbird.workers.dev/m/abc123/payments/42   # proxied β†’ real API, response saved (x-mockbird-recorded: 1)
curl https://mockbird.mockbird.workers.dev/m/abc123/payments/42   # replayed locally β€” no x-mockbird-proxied header, upstream never hit

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 (no preset = includes a seeded starter resource "items",
# so the API serves data immediately; pass {"blank": true} to start empty)
curl -X POST https://mockbird.mockbird.workers.dev/api/projects -H 'content-type: application/json' -d '{"name":"shop"}'
# β†’ {"id":"abc123","adminKey":"...","baseUrl":"https://mockbird.mockbird.workers.dev/m/abc123",
#    "dashboard":"https://mockbird.mockbird.workers.dev/app#open=abc123:KEY","resources":[{"name":"items",...}],"try":"curl ..."}

# add a resource from a template, seeded with 50 records
curl -X POST https://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/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

The create response's dashboard URL (/app#open=<id>:<adminKey>) opens the project in the web dashboard on any machine β€” data browser/editor, request inspector, snapshots, webhooks β€” and remembers it in that browser for next time. Made the project from a script or an AI agent? That link is the hand-off. It embeds the admin key (in the URL fragment, which never reaches a server or a log) β€” share it only with the project's owner.

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://mockbird.mockbird.workers.dev/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://mockbird.mockbird.workers.dev/mcp

# Cursor / Windsurf / generic JSON config (mcp.json)
{ "mcpServers": { "mockbird": { "url": "https://mockbird.mockbird.workers.dev/mcp" } } }

# VS Code
code --add-mcp '{"name":"mockbird","type":"http","url":"https://mockbird.mockbird.workers.dev/mcp"}'

Eighteen tools: create_project, import_data, fork_project (copy a template project per eval/CI run β€” pass ttl for a self-expiring fork that cleans itself up even when the run crashes; withSnapshots carries answer keys along), add_resource, project_info, query_records, write_record, generate_fake_data (instant realistic fake data β€” ready-made shapes like persons / products / credit cards or a custom field map, deterministic with a seed, stateless, no project needed), custom_route, snapshots (save / restore / diff β€” action:"diff" grades an eval run against an expected snapshot), verdict (the whole eval grade in one call β€” state diff + trajectory constraints β†’ {pass, checks[]}, see verdict), inspect_requests (read the request inspector β€” verify what your app or webhook sender actually sent), share_project (mint a read-only share link β€” hand your human reviewer a browser URL to inspect the sandbox, no key handover), delete_project (clean up short-lived test projects), check_api_status (live health of the ~37 public mock/testing APIs on our status tracker β€” check whether httpbin or JSONPlaceholder is down before your agent relies on it), uptime_monitor (free downtime alerts for any public URL β€” checked every 30 minutes, webhook on down/recovery, public status page + badge), heartbeat (a dead man's switch for cron jobs and recurring agent runs β€” the job pings Mockbird; a missed ping fires the alert webhook), and watch_service_status (subscribe a webhook to down/recovered events for any tracked public API β€” or omit the webhook to get a pollable subscription your agent checks on its own schedule). 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.

It also ships MCP prompts β€” ready-made workflows your client surfaces as slash commands: mock_an_api (plain-English description β†’ live API), mock_from_spec (paste an OpenAPI spec / db.json / Postman collection / HAR / CSV), simulate_failures (point retry/timeout handling at a mock that misbehaves on demand), monitor_uptime (arm downtime alerts for a URL or a cron job in one go), and is_it_down (live status of a public mock API + a drop-in replacement if it's failing). In Claude Code that's e.g. /mockbird:mock_an_api (MCP) from the slash-command picker.

The server also exposes MCP resources: mockbird://docs/llms.txt (the full agent-oriented API guide) and mockbird://api/index (machine-readable JSON endpoint index) β€” readable via resources/list + resources/read, so a connected agent never needs a separate HTTP fetch to learn the API. A discovery manifest lives at /.well-known/mcp.json.

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.