Test HTTP APIs (.lnh)

File → New → New API test opens an HTTP client saved to a .lnh file. One file represents an endpoint (method, URL, headers and body) and keeps several tests in the list on the left — each test with its values for the {name} variables used in the URL, headers and body, plus the result of the last run (status, time, date, response headers and body). It also shows the equivalent curl command. The same one-off operation is available via CLI (http) and MCP (http_request). The tester uses curl, which ships with Windows 10 and 11; if it is missing, LightNote opens the install guide before sending.

Send and read the response

Ctrl+Enter sends the request. While the response has not come back, the same button becomes Stop (or Esc) and the wait shows the seconds. The response badge carries the status and the time; next to it come the type, the size and when the test ran. The curl line follows what you type, already with the selected test's variables: the button next to it copies the command, and its arrow offers the same in PowerShell, Python, JavaScript, C# and HTTPie. Pasting a curl command straight into the URL imports the method, headers and body without deleting the tests.

A variable with no value in the test is sent literally ({id} in the URL, and the server answers with an error that does not say why) — the Variables tab highlights it and names it. The dot next to each test in the list has the color of its last status, and the empty ring is a test that has not run yet. In the response, the bar above the body formats JSON/XML on screen only, wraps lines, copies the body and saves it to a file as it came.

Building the request. The Parameters tab shows the URL query as a list: editing a row changes the URL, and editing the URL changes the list. The Authentication tab builds the header for you — Basic, Bearer or API key, in a header or in the URL — and every field accepts a {variable}, so the secret lives in the environment and not in the file; if the Headers tab already has the same header, that one wins. In the Body, the Type picks JSON, XML, SOAP, form, multipart (text fields and files) or a file sent in binary, and the Content-Type is added when you did not write one. A relative file path counts from the .lnh folder. With multipart or a file, the arrow of the copy button grays out PowerShell and JavaScript, which cannot send that body. The Options tab changes the timeout (60 s by default) and makes the request follow redirects — without it, the response shown is the 3xx itself. Opened in an earlier version of LightNote, the file is sent without the authentication and the type, and never as a different request. For a local server with its own certificate, Do not verify the certificate (HTTPS) turns verification off: while it is on, the response line says certificate not verified, and lnote http-run says it in the report too.

Reading the response. The body opens in a read-only code editor, highlighted by the response type, with folding and the window search (Ctrl+F). The Cookies tab lists what the server asked to store, and the Timeline shows the exact request that went out — with the headers curl itself added — and the raw response headers. The timeline stays in memory only: it repeats the Authorization, and the file goes to Git.

Checks

The Checks tab tells whether the test passed. Each row compares a part of the response — the status, a header, the whole body, a JSONPath ($.user.id, $.items[0].name, $.items.length()), an XPath (//Price, count(//Item)) or the time in milliseconds — with the expected value: equal, different, greater, less, contains, matches a regular expression, exists or is of a type. In XPath the name prefix is ignored, so soap:Envelope and Envelope are the same step. With checks, they decide the result: an expected 404 passes, and a 200 with the wrong field fails. Each check's result appears in the last column, the summary sits next to the response badge and the test's dot in the list turns green or red. Unchecking a row keeps it in the file without counting toward the result.

The play button at the top of the list runs all tests in the file, one at a time, and says how many passed. Once a test has run twice, the compare button above the response body opens the previous and the current run side by side, with the body formatted on both sides. Only the previous run is kept: the file goes into Git, and a long history would fill every diff.

Environments, captures and the whole folder

Environments use the open JetBrains and VS Code format: an http-client.env.json in the folder (or above it) with one object per environment — {"dev": {"host": "http://localhost:8080"}} — and, next to it, an http-client.private.env.json with the secrets, to keep them out of Git. In the same environment the private file wins, and $shared applies to all of them. The selector next to the URL picks the environment, and the import and export menu creates the file. A variable comes from the test, then from a capture, then from the environment, and the Variables tab says where each one came from.

The Captures tab keeps a piece of the response as a variable — a login's token, a newly created resource's id — for the next tests in this file and the others in the same folder. That is how a login .lnh feeds another file's authenticated call. The captured value stays in memory only and never goes into the file.

The folder is the collection: lnote http-run --path <folder> --env dev runs all its .lnh files in natural order (01-login before 02-profile) and exits with an error when any test fails, which suits a CI. The same run is available over MCP as http_test_run, so the AI agent can run the tests you wrote. By default the files are left untouched; --save writes each test's last result into them.

Import and export

Importing and exporting OpenAPI. The arrows button at the top of the test list opens Import OpenAPI… and Export OpenAPI…. The import reads a specification in YAML or JSON across the three generations in use — Swagger 2.0, OpenAPI 3.0 and 3.1 — and builds the test on its own: the path {parameters} already use the same syntax as the {name} variables of the .lnh, the declared examples become the test values, the security scheme becomes the Authorization header, and each declared server becomes a test of its own (staging and production side by side). Since a .lnh holds one endpoint, a specification with several operations asks which one to import — or writes all of them at once, one .lnh file per operation.

The export goes the other way and produces an OpenAPI 3.0.3 specification from the file: servers, parameters and body come from the template, and the example responses come from the recorded runs. It is a skeleton, not a full contract — the .lnh describes calls, so types, descriptions and schemas are not reconstructed. Your tests travel in the x-lightnote-tests field, which is why they come back intact when you re-import the file.

Importing a curl command. In the same button, Import curl command… opens a paste box — that is where the command comes from, not from disk, and the field is pre-filled when there is a curl on the clipboard. It understands the three dialects the same command travels in: Copy as cURL from Chrome/Firefox (single quotes), Copy as cURL (cmd) on Windows (with ^) and the documentation example broken with \. The method comes from -X — or is inferred: POST when there is a body, GET when there is not.

The import is literal: no value turns into a {name} variable on its own, because guessing which token is a secret would get it wrong. A few cases warn instead of lying — a body coming from a file (-d @file) cannot be read, and a multipart upload (-F) is not faithfully reproduced, so the fields go into the body as text for you to adjust.

Import from another tool. Import from another tool… reads a .http/.rest file (REST Client for VS Code, JetBrains, Visual Studio), a Postman collection or environment, an Insomnia export (v4 in JSON or v5 in YAML), the HAR the browser saves with Save all as HAR, a Bruno .bru (or the whole folder, with Import Bruno folder…) and the WSDL of a SOAP service. The format is recognized by the content, not by the extension. Each request becomes a .lnh and the collection folders become folders: pick one for this file or save all of them at once. {{name}} becomes {name}, authentication becomes a header, environments go to http-client.env.json (what the source marks as secret goes to the .private file) without changing any value already there, and Bruno checks and captures — and Postman's pm.response.to.have.status — become checks and captures here. From a HAR only the API calls and the page come in, with the response the browser received as the last run; from a WSDL comes one SOAP envelope per operation, with the SOAPAction and each field as a variable.

What has no equivalent is kept as it is, and said: pre- and post-request scripts are not converted, dynamic variables ({{$guid}}) and chaining stay literal, and Basic with a variable, a multipart body and a file body go to the Authentication tab and the body Type — with the file path, which came from the other machine, listed so you can check it. When the collection is saved, this report becomes a note in the target folder. Export as .http… goes the other way with the selected test: the template with {{name}} and the values as @name = value.