Comparison · mock tools vs. a replica
Mock and stub tools and SandboxAPIs solve adjacent problems, and the difference is structural rather than a matter of degree. A mock serves the endpoints you define, with the bodies you supply. SandboxAPIs serves 21 real APIs across 7 categories — git host, issue tracker, AI telemetry, messaging, CI/CD, observability, CRM/support — already populated with one simulated company, all telling the same engineering story in each provider's own dialect.
Everything claimed below is a command you can run right now, without an account. Where a mock tool is the better choice, this page says so.
01 / The structural difference
The three commands below follow one canonical backlog item across three hosts. The first two fetch the same issue from two different trackers — nobody wrote two response bodies; there is one simulated backlog underneath, and each host renders it the way that provider really would. The third follows the branch Linear names straight to the merged pull request that resolved it on the GitHub host. All three are pinned snapshot hosts, so every value printed here is a value you can still get back years from now.
# One canonical backlog item, three hosts. No setup, no key. Every host is a
# pinned snapshot: these bytes are frozen, so the values you get back today are
# the values you get back in two years.
# 1 — Jira REST: the item as a Jira ticket
curl -s "https://jira-v3.snap.sandboxapis.dev/rest/api/3/issue/ARGO-59?fields=summary,status,customfield_10016,labels"
# 2 — Linear GraphQL: the same item as a Linear issue — note branchName
curl -s https://linear-2026-08.snap.sandboxapis.dev/graphql -H 'content-type: application/json' \
-d '{"query":"{ issue(id:\"ARGO-59\"){ identifier title estimate state{name} branchName labels{nodes{name}} } }"}'
# 3 — GitHub REST: that branch is the head of the PR that resolved it
curl -s "https://gh-2026-03.snap.sandboxapis.dev/repos/olympus-labs/parthenon/pulls/58"| Field | Jira REST · jira-v3 pin | Linear GraphQL · linear-2026-08 pin |
|---|---|---|
| Identity | key: ARGO-59 | identifier: ARGO-59 |
| Title | Support per-tenant backpressure | Support per-tenant backpressure |
| Estimate | customfield_10016: 1 | estimate: 1 |
| Status | status.name: Done | state.name: Done |
| Labels | ["enhancement"] | ["enhancement"] |
Then the reference resolves across categories. Linear reports branchName: add-per-tenant-backpressure; fetch that branch on the GitHub host and it is the head of pull request #58, "WIP: Add per-tenant backpressure", merged into main on 2026-06-28. The tracker item and the git history are one event, rendered by three providers — not three fixtures kept in sync by hand.
Pinned hosts · frozen bytesThese are not a capture with a date on it. A pin regenerates byte-identically on every request, so the commands above return exactly these values today, next month, and in two years. The live hosts re-roll every day — same org, new activity — which is why the frozen demo runs against pins.
Azure DevOps renders the same canonical backlog as work items too — on its live host, https://ado.sandboxapis.dev, which re-rolls daily, and — because every provider is pinned in the same frozen generation — on its pinned host as well. The frozen demo above stays with Jira and Linear because they line up as the two cleanest dialects side by side.
Note what is not identical: the shapes are each provider's own. Story points arrive as customfield_10016 on Jira and estimate on Linear. Status is a Jira status object carrying a statusCategory on one host and a Linear workflow state with its own type on the other — both read Done here, but neither is the other's field. The dev branch is a first-class branchName on the Linear issue; the Jira issue resource doesn't carry it at all. Each provider's own vocabulary is preserved, because your client expects that vocabulary.
A mock tool is not wrong here — it is simply not in this business. To get the table above from stubs, you would author two response bodies, invent a shared identity for them, and then keep both in sync by hand every time the scenario changed.
02 / Determinism, demonstrated
A pinned *.snap. host serves a frozen universe. Not "stable" — byte-identical, and testable as such. Run this yourself.
# Same request, twice, against a pinned snapshot host.
curl -s https://jira-v3.snap.sandboxapis.dev/rest/api/3/issue/ARGO-59 | shasum -a 256
curl -s https://jira-v3.snap.sandboxapis.dev/rest/api/3/issue/ARGO-59 | shasum -a 256
37e26e28b1b7cb65baafa7d4982bfc50a3f38b00161cb4cb24e6d762c91e96b1 -
37e26e28b1b7cb65baafa7d4982bfc50a3f38b00161cb4cb24e6d762c91e96b1 -The property that makes this hold is that a snapshot is a recipe, not stored bytes. Nothing is persisted per snapshot; given the template version, seed, and anchor time, the entire dataset regenerates identically on demand — after a cache eviction, on another machine, a year from now. CI enforces it on every build by generating, deleting, regenerating, and byte-comparing the artifacts. A determinism break would silently corrupt pinned CI, so it is treated as a sev-1, not a nice-to-have.
Because the values are frozen, they can be printed here and stay true. This is what the pinned host returns for ARGO-59 — abridged to the fields worth looking at, but every value is verbatim:
{
"key": "ARGO-59",
"fields": {
"summary": "Support per-tenant backpressure",
"status": { "name": "Done", "statusCategory": { "key": "done" } },
"customfield_10016": 1,
"labels": [ "enhancement" ]
}
}03 / Capabilities, side by side
Mock tools win more rows in this table than we do, and that is the honest result: they are a general-purpose instrument and we are a specific one. The rows we win are the rows you cannot get by authoring stubs harder. The third column is what most teams reach for first — a real account with a real token — and the rows it loses are the ones that end in a leaked credential or a mutated repo.
| Capability | Single-endpoint mock tools (WireMock Cloud, Microcks, Beeceptor) | SandboxAPIs | Your own account (a real tenant, a real token) |
|---|---|---|---|
| Define custom endpoints and response bodies | YesYes — this is the product, and it is the right tool for it. | NoNo. The surface is fixed to what the 21 real APIs actually publish. | NoNo. It is the real API; it does what it does. |
| Mock an API we don't serve — or one that doesn't exist yet | YesYes, from a spec or by hand. Any API, including your own. | NoNo. 21 APIs, and only their real surfaces. | NoNot one that doesn't exist yet. Any real API you can get an account on — at that account's setup cost. |
| Verify the requests your code made | YesYes — request journals and assertions are core to the category. | NoNo. We answer requests; we don't hand you a record to assert against. | NoNo. The provider's audit log, if it keeps one — hours later and without request bodies. |
| Inject latency, faults, and malformed payloads | YesYes — arbitrary delays, statuses, and broken bodies on demand. | NoNo. You get the provider's real success and error shapes, not synthetic failure. | NoNo. Real outages arrive on their own schedule, never on your test's. |
| Stateful scenarios and scripted behaviour | YesYes — sequences, state machines, and per-call branching. | NoNo. The universe is read-only and already written. | YesYes — by mutating real data, which is the risk you were trying to avoid. |
| A pre-existing data set whose every reference resolves | NoBring your own fixtures — and keep them consistent yourself. | YesYes. A PR's author is a fetchable user; its commits exist with matching SHAs; reviewers belong to real teams. | NoOnly what your own team has actually done: thin, private, and impossible to share in a repro. |
| The same story across APIs | NoOut of category — each stub is independent by design. | YesYes. One canonical issue renders as a Jira issue, a Linear issue, and an ADO work item — and the incident thread discussing it reads byte-identically through Slack's dialect and Microsoft Graph's. | NoOnly if your team already uses every one of those tools, and you link the records by hand. |
| Official client libraries drop in unmodified | NoAs far as you author the surface each client touches — including the paths it probes first. | YesYes. Octokit, python-gitlab, jira.js, @linear/sdk, azure-devops-node-api, @slack/web-api, @microsoft/microsoft-graph-client — a base-URL swap, nothing else. | YesYes — it is the real thing. No swap at all. |
| Provider-exact pagination, rate-limit headers, and error bodies | NoYou author them, and you keep them current when the provider changes. | YesYes. Validated against the vendored official specs by the conformance suite, not by hand. | YesYes, by definition — including the quota you burn getting them. |
| Deterministic responses | YesYes — a hand-authored stub is static by construction. Nothing to drift. | YesYes, across a whole generated data set. Pinned hosts regenerate byte-identically rather than storing bytes. | NoNo. A real account changes under you; yesterday's fixture 404s tomorrow. |
| MCP server for agents | YesYes. The category converged here in 2026 — typically exposing stub management. | YesYes — orientation plus query tools over the dataset, answered through the same renderers. | YesOften — the vendor's own, with a real token in the agent's hands. |
| Nothing real to leak or break | YesYes — a local stub holds nothing real. | YesYes. Any token is accepted and every host is read-only, so there is no real credential and no real data anywhere in the loop. | NoNo. A real token with real scopes — in .env files, CI secrets and agent configs — with real data behind it. |
Vendors are named to identify the category, not to characterise any product's specifics. Each row describes what the category does by definition; check any vendor's own docs for their current feature set.
04 / Credit where it's due
These are not gaps on a roadmap. They follow from being a replica instead of a mocker, and if you need one of them, a mock tool is the correct answer.
Arbitrary custom behaviour
If the endpoint you need doesn't exist — because it's your own API, or a provider surface nobody serves — a mock tool gives you one in minutes. We can't do that at all.
Request verification
"Did my client call PUT /issues/7 exactly once, with this body?" is a question a mock tool answers and we don't. For contract tests on the request side, reach for a mock.
Latency and fault injection
Timeouts, 503 storms, truncated JSON, slow first byte. We serve the provider's real behaviour, which by definition doesn't include the failures you want to rehearse.
Spec-driven scaffolding
Point a mock tool at an OpenAPI document and get a server. That workflow is the category's home ground and it stays the fastest way to unblock a client against an unbuilt API.
05 / Picking one
Reach for a mock tool
You need behaviour you control.
Reach for SandboxAPIs
You need data you didn't write.
Or use both
They compose cleanly.
06 / Don't take our word for it
shasum in section 02, against a pinned host.Link header with rel="next|first|last" and as GitLab's X-Total / X-Page / X-Total-Pages family; rate limits as X-RateLimit-Limit/Remaining/Reset.403 carrying that provider's error body: message + documentation_url on GitHub, errorMessages on Jira.404 plus an X-SandboxAPIs-Coverage header; an uncovered GraphQL field returns an explicit NOT_COVERED error, never a silent null. Statuses in the manifest are synced from conformance results, never hand-set.