Frequently asked questions
Questions that come up when you are deciding whether APIFae fits, rather than when a command has already gone wrong. For the latter, see Troubleshooting.
Where the answer is "no", it says no and points at the ticket. A manual that implies a capability you then cannot find is worse than one that admits the gap.
Can I fail my build only on breaking changes, and let additive ones through?
Not with the exit code alone. diff has one severity: any disagreement
between your mocks and the live API exits 1, including a purely additive one.
A field the API gained and your mock does not have is reported and fails the
run, exactly like a field whose type changed.
Two things soften that before you reach for a filter:
--valuesis off by default. Same-type value changes —"1.0"becoming"2.0", a total of4250becoming4300— are ignored unless you ask for them. That is the one severity distinction built in, and it is the one that would otherwise fail your build every day.- Values the spec forbids are always reported, with or without
--values, because a value outside a declared enum is a contract breach rather than a number that moved.
For anything finer, use --json and decide for yourself. Every finding is
typed, so a filter over the finding names is a real gate:
$ apifae diff https://api.example.com --json > drift.json || true
$ jq -e '[.endpoints[].result.Success.diffs[]? | keys[0]]
| map(select(. == "FieldRemoved" or . == "TypeChanged" or . == "StatusCodeChanged"))
| length == 0' drift.json
|| true on the first line is deliberate: diff has already exited 1 on the
additive change you are choosing to allow, and you want jq to be the judge.
Keep exit 2 separate
though — an unreachable API must not be filtered into a pass.
What the finding names mean
These are the keys --json writes, and the ones the filter above matches on.
Whether a change breaks your clients is a judgement about your clients; the
last column is the common case, not a rule.
--json key |
What changed | Breaks a client? |
|---|---|---|
TypeChanged |
a field kept its name and changed type — id from string to integer |
yes |
FieldRemoved |
a field your mock declares is gone from the response | yes |
StatusCodeChanged |
the endpoint answers with a different status | yes |
HeaderMissing |
a response header your mock declares is gone | sometimes — only if a client reads it |
FieldAdded |
the response gained a field your mock does not have | no |
HeaderAdded |
the response gained a header your mock does not have | no |
ValueChanged |
same type, different value (needs --values) |
no |
ArrayBecameEmpty |
an array that had items came back empty | no — that is data, not shape |
ArrayBecameNonEmpty |
an empty array came back with items | no — that is data, not shape |
There is no --fail-on <kind> flag. Adding one means deciding, for everybody,
whether a removed header or a widened enum is breaking, and that is a decision
worth making deliberately rather than in a docs page.
Findings against your spec are reported separately as violations, and are
a different question — see
Keep a mock honest, which walks a drift run
where the two oracles disagree.
What do the exit codes mean, and how do I tell "the API changed" from "the API is down"?
| Exit | Meaning | What CI should do |
|---|---|---|
| 0 | clean | pass |
| 1 | drift and/or a contract violation | fail, open a ticket |
| 2 | the run could not complete — connection refused, timeout, 401, 403 | retry the job; the contract is unproven, not broken |
The split matters more than it looks. Before it existed, a documented 500 on
one endpoint made a clean contract exit 1, and an unreachable staging box was
indistinguishable from a genuine breaking change.
Point diff at nothing and it says so:
- path: /orders
method: GET
responses:
- status: 200
headers:
content-type: application/json
body:
orders: []
$ apifae diff http://localhost:9199
Comparing against http://localhost:9199
GET /orders
⚠ Connection failed: error sending request for url (http://localhost:9199/orders)
Summary: 1 checked, 1 errors
EXIT: 2
An endpoint that could not be checked at all — an unresolved {id}, no response
defined — is reported and counted, but does not fail the run. It is reduced
coverage rather than a broken contract. Pass --fail-on-skip if a gap in the
gate should be fatal in your pipeline; it is worth turning on once your mocks
are complete enough that a skip is a surprise.
Can I get the result as a report rather than terminal output?
--json writes the whole run: every endpoint, its findings, its spec
violations, how long it took, and a summary block your pipeline can read
without parsing prose.
$ apifae diff https://api.example.com --json
{
"base_url": "https://api.example.com",
"endpoints": [
{
"method": "GET",
"path": "/orders",
"result": {
"Success": {
"diffs": [
{ "FieldAdded": { "path": "$.nickname", "value_type": "String" } }
],
"violations": []
}
},
"duration_ms": 2
}
],
"summary": {
"endpoints_checked": 1,
"endpoints_drifted": 1,
"endpoints_violating": 0,
"endpoints_errored": 0,
"endpoints_unchanged": 0,
"endpoints_skipped": 0
}
}
diffs is disagreement with your mock. violations is disagreement with
your spec. They are independent: an API can match your mock exactly and
still be breaking its published contract.
Add --samples N to issue several requests per endpoint. The report then
carries how often each response shape was seen, which turns "it failed once in
CI" into "a fourth value on 12% of calls".
How do I mock an API that is versioned?
All three common strategies work, because a response can declare the conditions
it answers under. when: matches on path.*, query.*, header.* and
body.*, and a ~ suffix on the key makes the value a regular expression.
URL-based needs nothing special — the version is part of the path:
- path: /v2/orders
method: GET
responses:
- status: 200
body:
data: []
Header-based and query-based use a matcher. Put the versioned responses first and leave one response unconditioned as the default:
- path: /orders
method: GET
responses:
- when:
header.api-version: "2026-01-01"
status: 200
body:
data: []
page: 1
- when:
query.version: "1"
status: 200
body:
orders: []
- status: 200
body:
orders: []
$ curl -s -H "api-version: 2026-01-01" http://localhost:4000/orders
{"data":[],"page":1}
$ curl -s http://localhost:4000/orders
{"orders":[]}
Header names are matched lowercased, so API-Version and api-version are the
same key. When several responses match, the one with the most matchers wins.
Does diff understand my versioned mock?
Yes. It probes every response you declared, building each probe from that
response's own when: block — the header, the query parameter, the path
parameter that response answers to — and comparing the live answer against that
response. Each one is reported under its own label:
$ apifae diff https://api.example.com
GET /orders
[header.api-version=2026-01-01]
✓ No drift
[default]
✗ Drift detected:
~ $.total: Number → String
Summary: 1 checked (2 variants), 1 drifted
The label carries the full matcher key, so query.version=2 and
header.version=2 are never confused for one another.
What it cannot probe
Three kinds of response are reported as skipped rather than guessed at, because no single request satisfies them:
| Response | Why |
|---|---|
a body.* matcher |
the probe's request body comes from your spec, and diff will not reach in and set a field in it |
any ~ regex matcher |
^Bearer says which values are acceptable, not which one to send |
a scenario: response |
a state of your mock — there is no request that means "be rate limited" |
A skip is reduced coverage, not a failure: it is counted and printed, and the
run still exits 0. Add --fail-on-skip if a variant nothing probed should stop
your pipeline — it covers skipped variants, not just skipped endpoints.
If a variant you care about falls into one of those rows, give it a matcher
diff can satisfy — a header or a query parameter usually says the same thing
a body field does — or send the value on every probe with auth.headers:
auth:
headers:
api-version: "2026-01-01"
How do I give diff and patch credentials for a live API?
Through auth.headers in your workspace config. Values expand ${VAR} from the
environment, so the config is safe to commit and the secret comes from your CI:
auth:
headers:
Authorization: "Bearer ${API_TOKEN}"
X-Api-Key: "${API_KEY}"
Or set one without editing the file:
$ apifae config set auth.headers.X-Api-Key '${API_KEY}'
These headers are sent on every request diff and patch make. They are never
written into your mocks.
An unset variable is left as the literal ${API_TOKEN} rather than blanked, so
a missing secret in CI shows up as a 401 and exit 2 — "could not check" —
instead of a silent pass. That is deliberate: the opposite behaviour turns a
misconfigured pipeline into a green build.
The mock server itself does no authentication. If you want a mock that rejects an unauthenticated call, say so with a matcher:
- path: /orders
method: GET
responses:
- when:
header.authorization~: "^Bearer "
status: 200
body:
data: []
- status: 401
body:
error: unauthorized
What happens to my credentials when I record a live API?
They are written to disk in the clear. record is a proxy, and it captures
each request as it saw it — every header except the hop-by-hop ones. A session
under .apifae/recordings/ therefore looks like this:
request:
method: GET
path: /orders
headers:
authorization: Bearer supersecret123
x-api-key: key_abc
There is no redaction and no encryption, and apifae init does not write a
.gitignore. Add one before your first recording:
echo '.apifae/' >> .gitignore
The recordings are worth keeping locally — record --convert reads them, and
you will want to re-convert with a different --responses strategy — so
ignoring the directory is the right move rather than deleting it.
Do credentials end up in the mocks I commit?
Request headers do not. record --convert keeps only response headers, and
filters those through a deny-list that already drops set-cookie,
strict-transport-security, the caching and CDN noise, and anything prefixed
x-amz-, x-goog- or x-ms-. Extend it for your own:
convert:
denied_headers:
- x-internal-trace
Response bodies are kept verbatim, and that is the case to watch. If you
recorded a login call, the token in its response body is now in your mock, and
the deny-list does not look at bodies. Read what --convert generated before
committing it — the same as you would any other generated file.
Is there anything a mock cannot serve?
Very little. Beyond JSON, a response can carry raw_body for text, XML or HTML
served verbatim with no content-type inference, raw_body_base64 for bytes that
are not valid UTF-8 — images, gzip, protobuf — and body_file for a payload too
large to sit comfortably in the YAML.
What is not there is the hosted side: login, logout, push and pull
name a service that does not exist yet, and they exit non-zero saying so rather
than pretending. Sharing a workspace today means committing it to git.