Commands
Every command the binary answers to, what it is for, and when to reach for it.
This page is an index, not a reference: it says what each command is for. The
exhaustive flag-by-flag reference — every option, its default and its value name,
plus what each exit code means — is generated from the binary's own argument
declaration and lives in reference.md.
APIFae ships sixteen commands. Twelve do work. Four name a hosted service that does not exist yet and refuse rather than pretending — they are listed at the bottom, under Commands that refuse, and are the honest edge of the tool rather than a gap in this page.
Exit codes follow one rule everywhere: 0 succeeded, 1 the command ran and
found a problem, 2 you asked for something impossible — an unknown flag, a
missing argument. Where a command means something more specific by 1, its entry
says so.
The daily loop
init
Turns a directory into an APIFae workspace. It looks for an OpenAPI 3.x or
Swagger spec where you are — openapi.yaml, swagger.json, api/*.yaml and
friends — copies it to schemas/, and writes one mock per documented path,
populated from the spec's own example: values.
Reach for it once, first. The point is that init then up serves the
documented API immediately, rather than leaving you a directory of stubs to fill
in.
$ apifae init -N
EXIT: 0
-N is the non-interactive form and imports the spec exactly as the prompt
does. It is what a Dockerfile or a CI step runs.
With no spec in the directory, init writes a single commented example mock
instead, and the workspace still serves.
up
Starts the mock server, watching for changes. This is the command you leave running in a terminal all day; edit a mock and the next request reflects it.
$ apifae up
up does not return: it serves until you stop it. There is no exit status to
gate on, which is why a pipeline runs validate or
diff rather than this.
The startup line names the port it bound, not the port it was asked for.
--port 0 uses that: the operating system picks a free port, and the line tells
you which one.
$ apifae up --port 0
✓ APIFae mock server running at http://127.0.0.1:53201
That is the way to start a server in a test or a script without choosing a port number and hoping nothing else took it. Read the port out of that line.
One consequence: status looks for a server on the configured port,
so it will not find one started with --port 0 unless you give it the port —
apifae status --port 53201.
serve
The same mock server as up. up is the quick-start spelling and
serve the explicit one; they run the same code.
Reach for serve in scripts and CI, where the more descriptive name reads
better, and for up when you are typing it yourself.
validate
Checks every mock in the workspace: that it loads, that its templates render, and that what it renders agrees with the spec.
Reach for it before you commit, and in CI. It exits 1 on an error — a mock that
disagrees with the spec, or a file that will not load. Mocks the spec does not
describe are warnings and exit 0, unless you pass --strict.
$ apifae validate
EXIT: 0
If there is no spec in the workspace, validate says so and checks the mocks on
their own terms rather than refusing.
status
Reads the workspace you are standing in and reports it: project, mock and schema counts, defined scenarios, and whether a server is currently serving this workspace.
Reach for it when you are not sure what a directory contains, or from a script —
--json gives the same reading in a machine-readable form.
The "is a server running" half looks at the workspace's configured port. A
server started with --port 0 is on a port no config names, so pass it:
apifae status --port 53201.
$ apifae status
EXIT: 0
Keeping a mock honest
diff
Compares your mocks against a live API, and the live API against your spec. Two independent questions, which is the point: a mock can match production perfectly while both have drifted away from the contract the spec describes.
Reach for it as a CI gate, and whenever you suspect the real API has moved.
$ apifae diff https://api.example.com
Its exit codes carry more meaning than the general rule:
| Exit | Meaning |
|---|---|
| 0 | clean |
| 1 | drift, a contract violation, or both |
| 2 | the run could not complete — connection refused, timeout, 401, 403 |
Separating 1 from 2 is what makes it usable as a gate: a network problem must not read as a passing contract, and it does not.
Every response variant is probed. A response carrying a when: block is
compared against a request built to satisfy that block, and reported under its
own label. A request is never built from another response's when: values. A
response that another response could answer instead is skipped, and the skip
names that response. See the FAQ
for what it cannot build a request for.
Endpoints and variants that could not be checked are reported but do not fail
the run unless you pass --fail-on-skip.
patch
Applies what diff found, rewriting only the fields that actually changed.
Reach for it after a diff you believe. It edits in place and preserves the file
around the change — comments, quoting and key order all survive — so the
resulting commit shows the drift and nothing else. --dry-run changes nothing;
every applied change leaves a timestamped .bak.
It sends the same requests diff does, one per response variant, and writes
each change only to the response whose request found it. A variant diff would
skip is reported and counted, and never written.
$ apifae patch https://api.example.com --dry-run
Under --dry-run it exits 1 when it finds drift — finding it is the whole
point, and the exit code is what a CI gate reads. A real run exits 0 once it has
applied everything it found, and 1 only when something it found could not be
written. Either way, a 1 from patch is not a failure to run; that is 2.
It will not write a value the spec forbids unless you pass
--accept-violations.
record
Proxies a real API and captures the traffic through it into mock definitions, including HTTPS by way of a generated CA certificate.
Reach for it when the API already exists and writing mocks by hand is the wrong way to spend an afternoon — and when you want a replayable capture of what production actually returned.
$ apifae record https://api.example.com
By default a recorded session carries no credentials. Request header values are
replaced with <redacted> unless the header is one record knows is harmless
(accept, content-type, user-agent and a handful of others), and query
values whose name looks like a secret go the same way. What is sent to the API
is unchanged — redaction applies only to what is written to disk.
--no-redact turns that off for one run and writes the captured headers and
query values verbatim. record.redact: false does the same for a workspace, and
the flag wins over the config. Either way the session directory then holds live
credentials.
$ apifae record https://api.example.com --no-redact
Response bodies are redacted too, as --convert writes the mock: string values
under credential-shaped keys become <redacted>. Raw and binary bodies cannot be
scanned and are named in the conversion summary instead. See
Secrets and redaction for the whole
picture.
ca
Inspects and removes the recording CA certificates APIFae has written on this
machine. record asks you to trust a root certificate; this is how you see what
that left behind and take it back out.
It reads what APIFae wrote on disk. It cannot see your system trust store, and says so rather than implying otherwise.
ca list
Lists the recording CAs APIFae has written on this machine.
$ apifae ca list
EXIT: 0
ca remove
Deletes a recording CA and prints how to untrust it. Removing the file does not untrust the certificate — that needs your system's own tooling, which the command spells out for you.
Scenarios
scenario
Named states one workspace can be in — empty, forbidden, slow — so the same
mocks can stand in for several API conditions without a second workspace.
Reach for scenarios when you are testing what your client does when things go wrong, which is most of the interesting testing.
scenario list
Lists the scenarios this workspace defines.
$ apifae scenario list
EXIT: 0
scenario set
Activates a scenario on a running server.
scenario reset
Returns to the default state, with no scenario active.
scenario create
Writes a new scenario file.
Configuration
config
Reads and writes the workspace's apifae.yaml.
Reach for config rather than editing the file when you want the effective
value — the one the tool will actually use once defaults, the config file and any
--config override have been resolved.
config list
Shows the effective configuration, annotated with where each value came from.
$ apifae config list
EXIT: 0
config get
Shows one key's effective value and its source.
config set
Sets a value.
config add
Appends to a list key, such as schemas.
config remove
Removes a list item, or deletes a map entry such as auth.headers.X-Api-Key.
Commands that refuse
login, logout, push and pull name APIFae's hosted service. That service
does not exist yet. These commands are on the surface because the names are
real intentions and part of the intended workflow, but each exits non-zero with a
message saying so.
They used to print a ✓ and exit 0 having done nothing. That was worse than
useless, so it is gone: a command that refuses honestly is not a false claim.
Nothing in the local workflow needs an account. init, up, record, diff,
patch, validate and scenario all work against files in your workspace.
Sharing a workspace with your team today means committing it to git.
login
Would authenticate against APIFae cloud.
$ apifae login
EXIT: 1
logout
Would end that session.
push
Would upload local schemas to a hosted workspace.
pull
Would download them.
When something goes wrong
If apifae does something you did not expect, report opens a prefilled issue
with the details a maintainer would ask for anyway.
report
$ apifae report --print
https://github.com/apifae/cli/issues/new?title=apifae+bug+report&body=…
EXIT: 0
It collects your version, OS, architecture and install channel. It does not collect flag values, file contents, environment variables or your username. This command sends nothing on its own: the URL opens a filled-in issue form and you decide whether to submit it.
Without --print it opens your browser, and falls back to printing the URL when
there is no browser to open — CI, SSH, a headless machine.
If apifae crashed, it writes a report to ~/.apifae/crash/ and prints the
path. Send it with:
apifae report --crash ~/.apifae/crash/<file>.md
A crash report carries two things a bare report does not: the command you ran — its name and its flag names, never a flag value — and where apifae panicked. The location is trimmed to a workspace- or crate-relative path, so it names a file inside apifae or inside one of its dependencies and never a directory on your machine.
The file is yours to read and edit first. The panic message is not in it — a panic message can quote a header value or a response body, so apifae shows it in your terminal and leaves it out of the file. If it matters, paste it in yourself.
The update check
Once a day, at most, apifae asks GitHub whether a newer release exists —
one HTTP HEAD request to a release URL, nothing more. That is the only
network request the tool ever makes on its own, without a command asking it
to; it never reaches apifae.com, and nothing about the request is counted.
When a newer version is cached from a previous check, a command prints a two-line notice naming it, on stderr, after its own output:
A newer apifae is available: 0.4.0 → 0.5.0
Upgrade: brew upgrade apifae (turn this off: APIFAE_NO_UPDATE_CHECK=1)
The upgrade command matches how you installed apifae. It is silent under
--json, when output is not a terminal, in CI, and for report — turn it
off everywhere with APIFAE_NO_UPDATE_CHECK=1, DO_NOT_TRACK=1, or
update_check: false in apifae.yaml or ~/.apifae/config.yaml. See
the config reference for the key.