From nothing to a live endpoint
Seven steps, and only the first two are about the studio itself. Everything after that is you describing the API you wish existed.
GET /users/42 → the status, headers and JSON body you wrote for that route, after the delay you set
Create a workspace
A workspace is the container for everything else — your mock servers, their routes, their variables and their request log. There is no email and no password: a secret written into an HttpOnly cookie is what proves this browser owns it.
Save the recovery key
It is shown once, on the screen straight after creation, and only a one-way digest of it is stored — so nobody, us included, can show it to you again. It is how you reach the same workspace from another machine, and the only way back after you clear this browser's cookies.
Create a server inside it
A server owns a group of routes and answers on its own address. The key is the part callers type; leave it blank and one is drawn from the name. The address starts answering the moment the server has a route.
Add a route
Pick a method and a path. A segment written as :name becomes a parameter, a trailing star matches everything below a prefix, and the query string is not part of the path — a route on /game answers /game?id=7 as well.
Shape what comes back
The response is built as a tree of fields rather than typed as JSON. Each field says where its value comes from: a fixed literal, something out of the request, a variable, or generated data such as a name, an email address, or a fresh UUID on every call.
Put logic in front of it, if you need any
A route with no flow simply returns its body. Open the flow editor to add authentication, conditions, delays, weighted branches, variables and logging between the request arriving and the reply going out.
Point your app at the address
Copy the address, call it from your code or from curl, and the calls appear in the workspace's request log. It answers whether or not this page is open, so a deployed front end can reach it too.
Nothing here is private. Anybody holding the address of a mock endpoint can call it, so treat everything in a response as public and never put a real credential in one.
Ownership without an account
There is no account here. Creating a workspace writes a random secret into an HttpOnly cookie on this browser and stores only a digest of it, so owning the workspace means holding that secret. Nothing about you is recorded and nothing has to be verified by email.
The recovery key is the portable half. It is shown once, on the screen that follows creation, and it is stored the same one-way way — so if it is lost, no support request can bring it back. Importing a workspace with its key adds the new browser alongside the ones that already have access rather than replacing them; nothing is signed out.
Forget here and Delete permanently are different actions, and the difference matters. Forgetting removes the workspace from this browser only — its endpoints keep answering, other browsers keep their access, and the recovery key brings it back. Deleting destroys every server, endpoint, variable and log under it, for everyone, and no key recovers it.
Creating and importing both sit behind a Cloudflare Turnstile challenge, and creation is metered per address per hour on top of that. The per-browser count is an affordance rather than a wall — the cookie belongs to you and clearing it buys more — so the limit that actually holds is the per-address one.
Routes: methods, paths and how one is chosen
A route is a method and a path. The path is matched against whatever follows the server key in the address, and the query string is not part of it — a route on /game answers /game?id=7 too, and you read id in the response through From the request, then Query.
| Pattern | Matches | What you get |
|---|---|---|
/users | GET /users | That exact path and nothing else. A static segment is the most specific thing a path can hold. |
/users/:id | GET /users/42 | Captures one segment. The value arrives in the response builder under the name you gave it, as From the request, then Path parameter. |
/files/* | GET /files/logos/dark.png | Every path below the prefix, however many segments follow. This is the catch-all to put underneath more specific routes. |
When more than one route could match, the most specific wins: a static segment beats a parameter and a parameter beats a wildcard, read left to right. That ranking is computed when the route is saved, so which route answers never depends on the order rows come back from the database.
A request matching no path gets 404. A request whose path exists under a different method gets 405 with an Allow header naming the methods that do exist — a different fact from the path being wrong, and the one that tells you it is your verb rather than your URL. HEAD falls through to GET with the body stripped, and an OPTIONS you have not defined is answered from what the path supports, because it is a preflight.
Two switches turn things off without deleting them. A disabled route answers 404 as though it had never been created. A paused server answers 503 on every route under it, and tells the caller the server exists but is not answering — which is what you want while you are half-way through changing something.
Building the body
The body is built as a tree rather than typed as JSON, because most of what makes a mock useful has no JSON spelling — there is no literal that means a different name on every call. Every field picks a value type, and these are the ones on offer.
| Value type | What it puts in the response |
|---|---|
| Fixed value | Exactly what you type: text, a number, true, false or null. |
| From the request | Something out of the incoming call — a body field, a header, a cookie, a query parameter or a path parameter, addressed by name. |
| Variable | The value of a variable set on this workspace or server, or one written earlier in the flow by a Set variable node. |
| Environment variable | The value in the environment this call is running under, so one route can answer differently for a staging set and a production set. |
| Fake data | Generated data of a named kind — a full name, an email address, a company, a city, a price. A different one on every call. |
| UUID | A fresh v4 UUID each time the route is called. |
| Current time | The time of the call, in the format you pick. |
| Text template | Text with other values interpolated into it, for the cases where a string has to be assembled rather than chosen. |
| Object | A nested object. Its fields are values of these same types, so the tree goes as deep as the response needs. |
| List | A list built from one item template, repeated a fixed number of times or a random count between two numbers. |
| One of | One of several options, drawn per call. This is how you model a field that is sometimes present and sometimes null, or one of a small set of statuses. |
There is a JSON tab beside the builder, and it is honest about when it can be an editor. A response made only of fixed values has an exact JSON spelling, so there the tab is an editor and pasting into it replaces the tree. A response holding generated values has no such spelling, so the tab becomes a viewer and says so — applying JSON there would silently freeze every generated field into one sample of itself.
The content type comes from an allowlist: JSON, plain text, XML and CSV. A value outside it is served as plain text rather than refused. That is not fussiness — mock endpoints answer on this site's own origin, so a response able to declare itself HTML would be a page anybody could publish under our name. Every response also carries nosniff and a sandbox policy.
Request bodies are read too, and not only JSON. Form-encoded and multipart posts become fields you address the same way, and an uploaded file arrives as a value carrying its filename, contentType and size — enough for a condition to branch on the type or refuse anything over a size. The file itself is never stored.
The flow editor
Every route has a flow, even when it is only two nodes: the request arrives, the response goes out. The flow editor is where anything in between goes. Nodes run left to right along the connections you draw, and the path taken decides which response node is reached — which is how one route answers 200 to one caller and 401 to another.
| Node | What it does |
|---|---|
| Authentication | Checks for a credential of a given shape — API key, bearer token, basic or JWT — and sends the call down its Pass or Fail output. It checks shape, never validity: there is no identity provider behind a mock, so a JWT signature is not verified. |
| Validate request | Checks that a request carries every field on a list — headers, query keys, body paths — and sends it down Pass or Fail. The names it found missing go into a variable, so the refusal can say which ones. An import writes one of these from whatever the document marked required. |
| Condition | Compares two values and branches True or False. Either side can be a fixed value or something out of the request, and the tests include equals, contains, starts with, greater than, is present, is missing and matches pattern. |
| Switch | The same idea with more than two outcomes: one value, a list of cases, and a Default for everything unmatched. |
| Delay | Waits before continuing, so you can see what your loading states actually look like. |
| Random branch | Sends calls down one of several outputs by weight — the way to make a route fail one time in twenty rather than never or always. |
| Set variable | Writes a value that later nodes and the response can read, under a name you choose. |
| Log | Writes a line into this call's trace in the request log. It never reaches the server's own logs. |
| HTTP request | Calls a real service and continues with its reply on OK or its failure on Error. Addresses are checked before every hop and redirects are followed one guarded step at a time, so a mock cannot be pointed at a private network. |
| TransformNot runnable yet | Reshapes a value between two nodes. It can be placed and wired, and a request that reaches it answers 500 rather than pretending it worked. |
| Response | Ends the flow and sends a reply. A flow can hold several, and each one owns its own status, content type and body. |
The canvas has the shortcuts you would expect: drag from a node's right edge to connect it, double-click a connection to remove it, ⌘Z and ⇧⌘Z to undo and redo, ⌘C, ⌘V and ⌘D to copy, paste and duplicate, and the bracket keys to fold the panels either side. Tidy up re-lays a graph that has grown tangled.
A route whose flow branches has more than one response, so the single response box on the route screen stops being the whole answer and says which one it is showing. That is deliberate: one box could only ever show one branch, and quietly showing the first would be worse than pointing at the editor.
Variables and environments
Variables are values your routes read instead of hard-coding them — a base URL, a fixture id, a token your own tests expect. Set one on the workspace and every server in it sees the value; set the same name on a server and that server's value wins.
Environments are separate sets of the same names. Overriding happens inside one environment, never across two, so a staging set and a production set cannot leak into each other.
A variable marked as a secret is never sent back to the browser — the list shows dots, and saving over it replaces the value rather than revealing it. It is still stored on our servers and still readable by anything your mock returns it in, so secret here means hidden from the screen, not safe to publish.
Bringing an API in, and taking it away
You do not have to build from nothing. The importer takes a Swagger or OpenAPI document, in JSON or YAML, or a Postman collection, and turns every operation in it into a working route — paths, methods, status codes, and the headers, query keys and body fields it declares. A specification supplies its response through a schema; a collection supplies it through the responses somebody saved beside each request, and a collection saved without any produces routes that answer a placeholder until you edit them. Internal pointers are resolved; remote ones are left alone rather than fetched.
Export this server gives you a JSON bundle holding everything the studio knows: every route, its response and the flow behind it. That is the one to commit next to your code or keep as a backup, and it is the format that comes back in whole.
There is an OpenAPI export as well, for describing the API to other tools. It cannot carry generated data — a field that returns a different name on every call goes out as one example of a name — so treat it as a description rather than as a backup.
Limits, logs and what is public
Every ceiling here is either about stopping one workspace from making the service unusable for everybody else, or about a mock staying a mock. These are the numbers the server actually enforces.
| Limit | Value | Why |
|---|---|---|
| Workspaces | 3 per browser | An affordance rather than a wall — the cookie is yours and clearing it buys more. Creation is metered per address instead. |
| Servers | 10 per workspace | Enough to split a project by service, few enough that one workspace stays navigable. |
| Routes | 200 per server | Comfortably above what an imported document brings across. |
| Delay node | 5,000 ms | An uncapped delay is a denial of service anybody could configure in two clicks. |
| Flow run time | 10 seconds | Whatever is unfinished at this point reports an error instead of hanging. |
| Response body | 1 MB | A mock is a fixture. Anything larger is a file host. |
| Imported document | 4 MB | Larger than any specification or collection anyone has pasted in, small enough to parse without pinning a server. |
| Logged requests | 500 per workspace | Newest first; the oldest are trimmed after each write. |
| Log retention | 7 days | Anything older is dropped, and a large body is truncated and marked rather than kept whole. |
Credentials are stripped before a log row is written rather than filtered when it is read, so an Authorization header your code sent by accident was never on disk here. Everything else about the call is: path, query, headers, both bodies, and the nodes that ran.
