Record a real API and replay it
The API already exists. Writing mocks for it by hand is the wrong way to spend an afternoon, and anything you write by hand is a guess about what it returns.
apifae record proxies the real thing, captures what actually came back, and
turns it into mocks you can serve offline.
Record
You need a workspace first — record stores its sessions inside one:
apifae init -N
Then point the proxy at the API and send your traffic through it:
$ apifae record http://localhost:9100 -p 4555 -n demo
● Recording → http://localhost:9100 (proxy on :4555)
Session: demo
Intercept: off (recording works; --init-ca adds HTTPS proxy interception)
[11:46:15] GET /orders → 200 (2ms, 576b)
[11:46:16] GET /reports/daily → 500 (0ms, 68b)
2 requests captured | Press Ctrl+C to stop
Send requests to the proxy port rather than to the API, and it forwards them on:
curl http://127.0.0.1:4555/orders
-n names the session. Name it — the default is a timestamp, and you will want
to say which capture you mean when you convert it.
On Ctrl+C it summarises what it has:
● Recording stopped.
Session: demo
Target: http://localhost:9100
Duration: 2s
Captured: 3 requests (2 unique endpoints)
Stored: …/.apifae/recordings/demo
Run apifae record --list to see all sessions.
Nothing is a mock yet. The session is raw capture, one file per request:
.apifae/recordings/demo/
├── 001_GET_orders.yaml
├── 002_GET_orders.yaml
├── 003_GET_reports_daily.yaml
└── metadata.yaml
Convert
$ apifae record --convert -n demo --non-interactive
i Converting session "demo"...
Generated mocks:
+ …/mocks/reports/daily.yaml (1 endpoints, 1 responses) [NEW]
+ …/mocks/orders.yaml (1 endpoints, 1 responses) [NEW]
Summary: 2 endpoints, 2 responses, 2 created, 0 updated, 0 skipped
EXIT: 0
Three requests became two endpoints, because two of them hit the same path.
--non-interactive makes this scriptable. With more than one session present and
no -n, it refuses and lists what is available rather than guessing — so a CI
step cannot silently convert the wrong capture.
Now stop the real API and serve the capture:
apifae up
The recorded bodies come back, byte for byte.
Recording HTTPS
Most real APIs are HTTPS, and a proxy cannot read that traffic without being
trusted. --init-ca generates a certificate authority for the purpose:
apifae record --init-ca https://api.example.com
Try it without touching your system trust store first. curl will trust a CA
for exactly one invocation, which is enough to prove the setup works and costs
nothing to undo:
curl --proxy http://127.0.0.1:4001 \
--cacert ~/.apifae/ca/apifae-ca.pem \
https://api.example.com/some/path
That is the machine-wide CA, which is where record writes one today. If this
workspace has a .apifae/ca/ directory of its own, that one is used instead —
record prefers a workspace-local CA when it finds one, which is how workspaces
created before the machine-wide store keep working. Run apifae ca list if you
are not sure which you have; it prints the path in use.
Without --cacert the same request fails to connect — that failure is the CA
doing its job, not a bug. Only once that works is it worth installing the CA
system-wide, and only if you need browsers or an SDK to go through the proxy too.
When you are done, take it back out:
apifae ca list
apifae ca remove
ca remove deletes what APIFae wrote. It cannot untrust a certificate you
installed into the system store — that needs your own tooling, and the command
prints the steps rather than pretending it has done them.
Committing a capture as a baseline
A converted capture is deterministic: convert the same session twice and you get byte-identical files, with headers and matchers in a stable order. That is what makes this workflow work:
apifae record --convert -n demo --non-interactive
git add mocks/ && git commit -m "baseline: orders API, 2026-08-27"
Re-record against production later, convert again, and git diff shows exactly
what the API changed — nothing else. A converter whose output re-ordered itself
on every run would bury one real change in fifty spurious ones and the diff would
be unreadable.
For the same question asked continuously rather than at capture time, use
diff, which compares without re-recording.
A note on what you capture
Recorded traffic is real traffic. It may contain tokens, cookies, personal data
and anything else the API returned to you. Read a capture before you commit it —
.apifae/recordings/ holds the raw exchange, and the generated mocks carry the
response headers and bodies.
Values in recorded content that contain {{ or {% are wrapped in
{% raw %}…{% endraw %} automatically, so captured braces replay as themselves
rather than being interpreted as templates.