Documentation
How to get ReqForge running on your machine, how to build your own binary if you would rather not use somebody else's, and how to use the parts of it that are not obvious. The application also carries its own guide: press F1 inside it.
What ReqForge is
A desktop API client and test runner: a Tauri v2 shell with a SolidJS front end and a Rust back end. The interface is a web view; everything underneath it, including every byte that goes on the wire, is native Rust.
| Runs on | macOS, Windows, Linux |
|---|---|
| Needs an account | A seat, assigned to your work email by your administrator |
| Needs a network | Only to reach the APIs you are testing |
| Stores your work as | One JSON file per collection, in a folder you can back up, diff and commit |
| Sends telemetry | None. No analytics and no crash reporting. It checks your licence once a day and sends nothing else — see privacy |
| Costs | £96 per seat per year — pricing |
| HTTP engine | Rust (reqwest over rustls) |
| Scripting | JavaScript, Postman-compatible pm API, in an embedded engine |
| Also ships as | reqforge-cli, a headless binary that runs the same collections in CI |
Installing it
ReqForge is a licensed product: you need a seat before it will send a request. Start a trial and you will be sent a build for each platform your team uses, along with the seat that runs it.
| Platform | What you get | Notes |
|---|---|---|
| macOS 12 or newer | .dmg, Apple silicon and Intel |
Not yet notarised — see the warning below |
| Windows 10 or 11 | .msi installer |
Not yet signed with an EV certificate — see below |
| Linux | .AppImage and .deb |
Needs GTK 3 and WebKitGTK 4.1, which most desktops already have |
| CI | reqforge-cli, one binary per platform |
Runs your collections on a build agent under one of your seats |
Activating your seat
The first time you open ReqForge it asks for the work email address your administrator gave the seat to. Type it, and a six-digit code arrives by email; type that, and the machine is activated.
- Open ReqForge. The activation screen is the first thing it shows.
- Enter your work email address and press Send code.
- Type the six digits from the email. It is good for ten minutes.
- That is it — the app opens, and stays open.
A seat runs on one machine at a time. Moving to a new laptop takes one click: Deactivate this machine in app settings on the old one, then activate on the new one. If the old machine is gone — lost, wiped, or in a skip — your administrator releases it from the console instead.
| What the app says | What it means | What to do |
|---|---|---|
| You do not hold a seat | The address is not assigned to a seat in any organisation | Ask your administrator to assign you one |
| That seat is already active on another machine | One seat, one machine | Deactivate the other machine, or ask for it to be released |
| Offline — ReqForge locks in n days | It has not reached the licence service for a while | Connect to a network for a moment; it refreshes by itself |
| The trial has ended | Fourteen days are up | Buy seats, and the same install carries on |
What activation sends us: your email address, a one-way hash identifying the machine, a label for it, and the app version. Never your requests, collections, URLs or responses — those stay on your disk. The privacy policy lists it exactly.
Running it offline
A licence is good for fourteen days away from the network, and the app tells you how many are left. Longer than that by design — an air-gapped lab, a customer site with no outbound access — is something we can set per organisation: ask.
The command line in CI
reqforge-cli reads a licence token from
REQFORGE_LICENCE_TOKEN, so a build agent needs no
interactive activation. Your administrator issues the token from the
console; treat it as a secret, and put it in your CI secret store
rather than in the repository.
Your first five minutes
-
Press
Create my first requestin the middle of the window. On a fresh install that makes a collection and a request together. If you already have collections, press+in the Collections header instead. -
Type a URL and pick a method beside it. Try
https://httpbin.org/get. -
Press
Send(Ctrl/⌘+Enter). The response appears with its status, timing and size. -
Make it reusable. Open
Managein the top bar, create an environment calledLocal, addbaseUrl=https://httpbin.org, select it in the top-bar dropdown, and change the URL to{{baseUrl}}/get. Hover the URL to see what it resolves to and where the value came from. -
Assert something. In the Tests tab:
Send again and read the Tests tab in the response panel.pm.test("status is 200", () => { pm.response.to.have.status(200); }); - Run the lot. Press on the collection row in the sidebar to run every request in it.
- Press F1 for the full guide.
Building a request
GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS. The URL bar and the Params table stay in sync in both directions: edit the query string and the rows follow, edit the rows and the URL follows.
-
Six body modes:
none,raw(JSON, Text, XML, HTML or JavaScript, with syntax highlighting),formdata,urlencoded,binaryandgraphql. -
Bulk edit params and headers as
key: valuelines when a table is slower than typing. - Rows can be disabled rather than deleted, and dragged to reorder.
- An in-flight request can be cancelled instead of waiting out the timeout.
-
Code ▾beside Save copies the request as a cURL command, a JavaScriptfetchcall, or Pythonrequests.
Variables and scopes
Write {{name}} in a URL, param, header, body or auth field
and it is substituted when the request is sent.
{{baseUrl}}/users/{{userId}}?token={{apiKey}}
Names resolve from the innermost scope outwards. Where two scopes define the same name, the innermost wins:
Environment (wins)
Folder
Collection
App globals (fallback)
Two things make this usable rather than a guessing game. Typing
{{ offers the names in scope along with what each currently
resolves to and which scope supplies it. And hovering — or tabbing into —
any field shows the fully resolved text, the value behind each token, and
which scopes it overrode.
A variable's value may itself contain {{tokens}}, which are
resolved in turn. That is how one collection covers every environment:
the collection owns the shape of the URL, each environment owns only the
piece that changes.
Collection baseUrl = https://{{env}}.api.example.com/v{{version}}
version = 2
Environment env = staging
Request {{baseUrl}}/users -> https://staging.api.example.com/v2/users
Environments
An environment is a named set of variables — one per place you run
against. Pick the active one in the top bar; No Environment
is a valid choice and means only collection, folder and global variables
resolve.
Mark a value secret and it is masked in the interface, kept out of exports unless you explicitly ask for it, and never written to the log. A secret still lives in a plain file on your disk: secrecy here is about not leaking it into an export or a bug report, not encryption at rest.
Inheritance
App defaults, then the collection, then each folder, then the request. Headers, auth, scripts, variables and transport settings all cascade field by field, so a folder can raise a timeout without restating everything else. Each field shows what it inherits and from which level.
The practical effect: set auth once on a collection and every request inside it is authenticated; add a header on a folder and only that folder's requests send it.
Authentication
| Type | Notes |
|---|---|
| Basic | Username and password |
| Bearer | A token, usually from a variable |
| API key | Sent as a header or a query parameter |
| OAuth 2.0 | Client credentials, with the token cached until it expires |
| AWS Signature V4 | Access key, secret, session token, region and service |
Set it on the collection and leave every request on
Inherit, which is the default.
Scripts and tests
Two scripts per request, both JavaScript with a Postman-compatible
pm API: Pre-request runs before the send,
Tests runs after the response arrives. Both cascade, so
a collection-level script runs for every request inside it.
pm.test("status is 200", () => {
pm.response.to.have.status(200);
});
const body = pm.response.json();
pm.test("returns a page of invoices", () => {
pm.expect(body.data).to.be.an("array");
pm.expect(body.data.length).to.be.above(0);
});
// Hand the first invoice to the requests that come after this one.
pm.environment.set("invoiceId", body.data[0].id);
console.log("first invoice", body.data[0].id);
pm.sendRequest is available when a call needs a token
first, and pm.iterationData.get() reads the current row when
the CLI is running with a data file. When the response is a page rather
than JSON — a sign-in policy, a SAML post — cheerio.load(pm.response.text())
reads it with the same CSS selectors a Postman collection uses, so the
code on a redirect link or a hidden form field is one
.attr() away. require serves the sandbox
packages a Postman script is written against — lodash,
moment, crypto-js, uuid,
chai, ajv, tv4,
xml2js, csv-parse and the Node modules
beside them — so an imported collection's scripts run unedited.
Scripts run in an embedded engine with time limits, so a runaway loop
cannot hang the app.
Reading responses
- Pretty, Raw and Preview views, with live search and a match count.
- Prettify for servers that mislabel JSON as
text/plain. - Redirect chains hop by hop, with what changed on each hop.
- A console showing exactly what was sent, recorded even when the request failed.
- Per-request history you can click to replay.
-
WebSockets and SSE in the Stream tab:
ws://,wss://and Server-Sent Events render as a timestamped conversation, with binary frames decoded to text when they are text and hex-dumped when they are not.
Running a collection
Press on a collection or folder row.
Requests run in tree order, results stream in as they finish, and each
row expands to the URL that was reached, the assertions and any error.
Stop at the first failure ends the run on the first failing
request. There is an inter-request delay for APIs that rate-limit.
The runner, the Send button and reqforge-cli all call the
same execution core, so a run in CI cannot behave differently from a run
at your desk.
The command line
reqforge-cli runs collections headlessly, through the same
engine: same variables, same scripts, same settings, same cascade. Its
option surface mirrors Newman's and it reads the same files, so an
existing pipeline usually only needs the command name changed.
$ reqforge-cli run ./billing.postman_collection.json
ReqForge · Billing API · 3 request(s)
Billing API / Invoices / List invoices GET http://127.0.0.1:8099/v1/invoices
200 OK · 3 ms · 123 B
✓ status is 200
✓ returns a page of invoices
Billing API / Invoices / Create invoice POST http://127.0.0.1:8099/v1/invoices
201 Created · 1 ms · 30 B
✓ status is 201
Billing API / Health check GET http://127.0.0.1:8099/healthz
200 OK · 0 ms · 55 B
✓ status is 200
✓ every dependency is up
3 request(s) · 0 failed · 5 assertion(s) · 0 failed · 46 ms
Commands
| Command | What it does |
|---|---|
run <collection> | Run a collection, a folder, or a single request |
list [what] | collections, environments, or requests <collection> |
logs | Print the log file path, or its tail with --tail N |
doctor | Paths, versions, counts and tool availability in one block |
help / version | What they say |
What the arguments accept
Format is decided by the content, not the file name, so the same command
works whichever tool produced the file. A file the Postman API wrapped in
a collection or environment key is unwrapped on
the way in.
| Argument | Accepts |
|---|---|
<collection> | A ReqForge or Postman v2.x collection file, or the name or id of a collection in this workspace |
-e, --environment | A ReqForge or Postman environment file, or the name of an environment in this workspace |
-g, --globals | A ReqForge environment or Postman globals file |
-d, --iteration-data | A CSV whose first row names the columns, or a JSON array of objects |
Run options
| Flag | Meaning |
|---|---|
-e, --environment <name|file> | Activate an environment |
-g, --globals <file> | Global variables — the outermost layer |
-d, --iteration-data <file> | Run once per row, binding that row's columns as variables |
-n, --iteration-count <n> | Run the selection this many times |
--folder <name> | Only run this folder (Name or Parent/Child). Repeatable |
--request <name> | Only run requests matching this. Repeatable |
--env-var <key=value> | Set a variable above the environment. Repeatable |
--global-var <key=value> | Set a variable at the globals layer. Repeatable |
-r, --reporters <list> | cli (default), json, junit — comma separated |
--reporter-json-export <file> | Where the json reporter writes |
--reporter-junit-export <file> | Where the junit reporter writes |
-o, --output <file> | Where to write, when exactly one reporter produces a file |
--timeout-request <ms> | Override the request timeout for this run |
--delay-request <ms> | Wait between requests |
-k, --insecure | Skip TLS certificate verification |
--ignore-redirects | Do not follow redirects |
--bail | Stop at the first failing request |
-x, --suppress-exit-code | Exit 0 even when something failed |
--color <on|off|auto> | Colour the cli reporter |
--silent | Print nothing; file reporters still write |
--verbose | Print the request as sent, redirect hops, bodies, script logs |
--data-dir <path> | Where this workspace's collections and environments live |
The pre-Newman spellings — --env, --var,
--timeout, --delay, --reporter and
--no-color — still work.
Exit codes
| Code | Meaning |
|---|---|
0 | Everything passed |
1 | A request failed, or an assertion failed |
2 | Bad usage, or a file or collection that could not be read |
Examples
reqforge-cli doctor
reqforge-cli list collections
reqforge-cli run ./api.postman_collection.json -e ./staging.postman_environment.json
reqforge-cli run "My API" --folder Users --bail
reqforge-cli run "My API" -d users.csv --delay-request 200
reqforge-cli run "My API" -r cli,junit --reporter-junit-export results.xml
reqforge-cli logs --tail 100
In CI
A build agent cannot type a code into an activation screen, so it
reads a licence token from REQFORGE_LICENCE_TOKEN
instead. Your administrator issues one from the console; it is a
secret, so it belongs in your CI secret store and nowhere else. The
agent uses one of your organisation's seats.
Point REQFORGE_DATA_DIR at a throwaway directory, or hand
the CLI an exported collection file and skip the data directory
altogether.
# .github/workflows/api-tests.yml
name: API tests
on: [push]
env:
REQFORGE_LICENCE_TOKEN: ${{ secrets.REQFORGE_LICENCE_TOKEN }}
jobs:
api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run the collection
run: |
reqforge-cli \
run ./tests/api.postman_collection.json \
-e ./tests/staging.postman_environment.json \
-r cli,junit --reporter-junit-export results.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: api-test-results
path: results.xml
The job fails on exit code 1, which covers both a request
that errored and an assertion that did not hold. Add
--verbose when a CI-only failure needs diagnosing, or read
reqforge-cli logs --tail 200.
Copy CLI command button that writes the exact
invocation for that scope to your clipboard.
Load testing
The Load Test tab turns the request you have open into a
k6 script and runs it locally. Choose the
number of virtual users, then either a duration (30s,
2m) or a fixed iteration count. k6's output streams while
the test runs; at the end you get average, p90, p95, p99 and max latency,
total requests, request rate, failure rate and peak VUs.
k6 is a separate tool and must be installed. Restart ReqForge after installing it; the tab says so if it cannot find k6.
brew install k6 # macOS
winget install k6 # Windows
snap install k6 # Linux
Security scanning
Security in the top bar drives
OWASP ZAP against your APIs.
ReqForge can start and stop a bundled ZAP for you, talk to a daemon you
already run, or use an executable path you give it.
Scan the active request, every request in the current collection, or
every request in every collection. Targets are resolved URLs, so
{{variables}} are substituted first; anything still
unresolved or not http(s) is skipped and counted so you know
it was left out. Spidering always runs; the active scan is opt-in,
because it sends attack traffic. Findings are grouped by risk with
description, evidence and a suggested fix, and export as JSON.
Workspaces and git
A workspace is a folder holding collections/,
environments/ and settings.json. ReqForge works
out of one at a time; until you open another, that is the default data
directory.
- Open any folder from the workspace chip in the top-left, or start on one with
reqforge . - ReqForge lists the API files it finds there — its own, Postman exports, OpenAPI documents — and imports them on a press.
- More than one window on the same folder, and tabs you can drag out into their own window.
- Opening a different folder reopens the app, because the collections, history and open tabs all live in the folder being left.
Because a collection is one JSON file, git tracks it with no translation.
Press Git in the header to clone a repository as a
workspace, then commit, branch, pull and push from inside the app.
libgit2 is bundled, so this works whether or not git is
installed.
Changesshows the branch, how far ahead or behind the remote you are, and every file that differs from the last commit.Branchesswitches or creates. Switching is refused while you have uncommitted changes, because the working tree is your collections.Pullfast-forwards only. If both sides have moved it says so rather than attempting a merge.- Access tokens go into your operating system's keychain, keyed by host — never into a collection or a settings file.
- Run history and logs are excluded by a
.gitignorewritten on clone: they are this machine's cache, and full of response bodies.
Import and export
Import in the top bar takes four formats, pasted as text or
picked as a file.
| Format | Becomes |
|---|---|
| Postman Collection v2.1 | A new collection, folders and all |
| Postman Environment | A new environment |
| cURL command | A single request in the collection you choose |
| OpenAPI 3.x (JSON) |
Paths become requests in document order, grouped by tag. The first
server becomes a {{baseUrl}} variable; query and header
parameters and JSON body examples come across; each
{path} parameter is seeded as an empty collection
variable
|
Export with on a collection row:
Postman v2.1 JSON that other tools, and ReqForge's own CLI, can read. If
the collection holds credentials, ReqForge asks before writing them out.
Choosing to blank them keeps every key, header and auth type with the
credential half empty and marks the filename .redacted.
Where your data lives
| Platform | Path |
|---|---|
| Windows | %APPDATA%\com.adrian.reqforge |
| macOS | ~/Library/Application Support/com.adrian.reqforge |
| Linux | ~/.local/share/com.adrian.reqforge |
<data-dir>/
collections/<id>.json one file per collection
environments/<id>.json one file per environment
settings.json app-wide defaults, globals and headers
history/ per-request run history, pruned automatically
logs/reqforge.log rotating at 5 MB, keeping three previous files
Back it up by copying the folder. Open ReqForge on another folder and all of that comes from there instead.
| Variable | Effect |
|---|---|
REQFORGE_DATA_DIR | Use this directory instead of the platform default |
REQFORGE_LOG | Log level: error, warn, info (default), debug, trace |
RUST_BACKTRACE=1 | Add backtraces to ordinary errors |
Cookies are handled for you — a Set-Cookie is stored and
sent back to the same site — and live in memory only, so quitting forgets
them. Defaults → Cookies lists what is stored and lets
you forget one or all of them.
Keyboard shortcuts
| Keys | Action |
|---|---|
| Ctrl/⌘+Enter | Send the active request |
| Ctrl/⌘+S | Save the active request |
| Ctrl/⌘+T | New request |
| Ctrl/⌘+W | Close the active tab |
| Ctrl/⌘+K | Jump to the sidebar filter |
| Ctrl/⌘+⇧+N | Open another window on the same folder |
| Ctrl/⌘+⇧+O | Open the workspace panel |
| F1 or Ctrl/⌘+/ | Open the user guide |
| Esc | Close a dialog, or clear the sidebar filter |
| ↑ ↓ | Sidebar: move between rows |
| → ← | Sidebar: open a folder, or close it and step back out |
| ← → | Tab strip: previous or next tab |
| Enter | Param and header tables: key → value → next row |
The collection tree and the tab strip are each a single tab stop, implementing the WAI-ARIA tree and tablist patterns, so Tab carries on rather than walking you through every row.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
npm error enoent ... /package.json |
Wrong directory. Run commands from reqforge/, not the repository root |
cargo metadata ... No such file or directory |
Rust is not installed or not on PATH. See rustup.rs |
./start-mac not found |
Use the full filename, ./start-mac.command |
tauri dev fails on Linux with a missing .pc file |
A missing system package — see the Linux prerequisites above |
| A variable is not substituting | Hover the field. The preview says whether it is defined and which scope would supply it |
| A request fails with a TLS error | Check Verify TLS in Settings, remembering it may be inherited from a folder or collection |
| The Load Test tab says k6 is missing | Install k6 and restart ReqForge |
| A request is authenticated as the wrong user | Defaults → Cookies. Forget the stale one |
| The build warns about an unknown publisher | Code signing is not configured yet |
| Anything else | Defaults → Diagnostics, or reqforge-cli logs --tail 100. Raise the detail with REQFORGE_LOG=debug |
Getting the builds
Builds go to the organisations that hold seats, by email, along with the seat itself. There is no public download page while the installers are unsigned: an unsigned installer means a SmartScreen warning on Windows and a Gatekeeper refusal on macOS, and a first run that looks like malware is not something to hand a stranger. This page gets a download table the day the certificates are in place.
Trialling ReqForge takes one email, and the builds come back with the trial.