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 error
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 response another response could answer instead the other response has more matchers (or as many, and comes first), and the request diff would send could satisfy them: a body matcher always could, and a path, query or header value only when the request carries it. The skip names that response
a {param} nothing supplies a value another response's when: selects on is never used, and neither is a template like "{{path.id}}". Pass --path-param or set diff.path_params

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

Does apifae record store my API keys?

No, not by default, and they do not reach the mocks record --convert writes either. Credentials come out at two points. The exact lists, and how to change them, are in Secrets and redaction.

When a request is captured. Request header values are replaced with <redacted> unless the header is on a short allow-list of harmless ones: accept, content-type, user-agent and similar. Query values whose name looks like a secret, such as api_key or token, are replaced the same way. The names survive, so you can still see which header carried the credential:

request:
  method: POST
  path: /login
  headers:
    accept: application/json
    authorization: <redacted>
    x-api-key: <redacted>
  query:
    api_key: <redacted>
    page: '2'

The API still receives the real request. Only the file on disk is changed.

When a mock is written. record --convert replaces string values in a JSON response body when their key looks like a credential, such as access_token, apiKey or private_key, at any depth. Numbers keep their value and their type, and keys nothing recognises are left alone:

- path: /login
  method: POST
  responses:
  - status: 200
    headers:
      content-type: application/json
    body:
      access_token: <redacted>
      expires_in: 3600
      refresh_token: <redacted>
      user:
        apiKey: <redacted>
        email: a@b.c

A body large enough to be written beside the mock as a body_file is redacted the same way. Response headers go through a deny-list that drops set-cookie, date, x-request-id and headers like them. A custom header is kept unless you add it:

convert:
  denied_headers:
    - x-internal-trace

What stays on your machine. The session under .apifae/recordings/ keeps each response body exactly as the API returned it, because a capture is meant to be a faithful record. apifae init adds .apifae/ to your .gitignore so git does not pick that copy up. If your workspace was created before init did that, add the line yourself.

What is not scanned. A body stored as raw_body or raw_body_base64 (CSV, XML, HTML, images) has no keys to match, so nothing in it is redacted. record --convert lists each of those responses in a warning under its summary. A secret under a key nothing recognises, or in a custom response header, is kept too. Read what --convert generated before you commit it, as you would any other generated file.

Turning it off. --no-redact on record writes header and query values verbatim, and record warns about it at the start and end of the session. On record --convert, --no-redact writes bodies verbatim. For a workspace, record.redact: false switches off capture redaction and convert.redact_bodies: false switches off body redaction. They are separate: record.redact: false leaves body redaction on. To adjust the lists rather than switch them off, use record.keep_headers, record.redact_query and convert.redact_body_keys.

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.

Last updated .