FAQ

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:

  • --values is off by default. Same-type value changes — "1.0" becoming "2.0", a total of 4250 becoming 4300 — 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.

This manual is generated from the repository. Every command and flag in it is checked against the binary; the marked transcripts are executed.