Test your API
Contract checking
Since 0.4.0 Requires Cloak Proxy 0.4.0 or later.
Bind an OpenAPI or Swagger spec to your API's host and Cloak Proxy checks every captured request and response against it as the app runs — undocumented endpoints and status codes, missing parameters, and bodies that don't match their schemas — with coverage, a report, MCP and CI.
An OpenAPI spec says what your API does. The app’s real traffic says what it actually does. Over time the two drift apart: a field becomes a string, a new status code appears, an endpoint ships that nobody documented. API Contracts puts a spec next to the traffic Cloak Proxy captures and checks every request and response against it, live, as you use the app.
Tools like Postman validate the requests you send yourself. Contract checking validates the requests your app sends, and the responses your backend really returns — which is where drift shows up.
Each row is an endpoint the app called; red rows broke the contract. The detail pane lists the endpoint's problems by JSON path.
- A spec, its hosts and an on/off switch
- Endpoints, Problems and Coverage
- Requests the spec doesn't document
- The selected endpoint's problems
Add a spec
Section titled “Add a spec”-
Open Tools › API Contracts… (⇧ ⌥ ⌘ K), or right-click a request or a host and choose Check Against Spec…
-
Click Add Spec and choose where the spec comes from:
Source How it’s kept up to date File A .yaml,.ymlor.jsonfile. Cloak Proxy watches it and reloads it when you save it in your editor.URL Fetched with a plain GETwhen you add it and when you choose Reload. It’s sent directly, not through the proxy, without cookies; add one header (for exampleAuthorization: Bearer …) if the URL needs it. The last good copy is kept, so it works offline.API Client A spec from API Client › Specs, read when it loads. The sheet reads the spec straight away and shows its title, version, OpenAPI version and number of operations, or what’s wrong with it.
-
Under Hosts, bind the spec to the host your app calls, such as
api.example.com. Cloak Proxy suggests captured hosts that look like this API: the hosts the spec’sserversname, then hosts whose requests match its paths. Use*.example.comfor every subdomain, orlocalhost:8080for one port. -
Click Add Spec. Every request already captured for those hosts is checked in the background, then each new one as it arrives.
Adding a spec file: its summary appears as soon as it's read, and the host is suggested from captured traffic.
Supported: OpenAPI 3.1 and 3.0, in YAML or JSON, and Swagger 2.0 on a best-effort basis (its body and formData parameters, consumes/produces and collectionFormat are converted). Local $refs are followed anywhere in the document, including nested and circular ones. References to other files can’t be followed; the spec’s row lists them, and values there aren’t checked.
The specs
Section titled “The specs”Each spec on the left shows its name and version, the OpenAPI version and number of operations, where it comes from and when it was last loaded, its bound hosts, and how many requests it has checked and how many had errors. The switch pauses and resumes it. If the spec can’t be read, its row says why in red; problems inside a readable spec (an unresolvable $ref, an invalid pattern) appear as an orange count you can hover.
Right-click a spec for Edit Hosts…, Reload, Open Spec File or Show in Finder (files), Open in API Client (workspace specs), Pause Checking and Remove. With more than one spec, All specs shows their results together (its badge says how many are on, such as 1 of 2 on); select one to see just its own. The + below the list adds another.
How requests are matched
Section titled “How requests are matched”A request is checked only when its host is bound to a spec. Then:
- The spec’s
serversgive the base path. Withhttps://api.example.com/v2, a request to/v2/orders/42is matched against/orders/{orderId}. Server variables (https://{region}.example.com/{version}) are expanded from theirenumanddefault. A request outside every base path, such as/health, isn’t this spec’s concern and isn’t checked. - Path templates match one segment per parameter, including mixed segments like
/files/{name}.{ext}. When several templates match, the most specific wins:/orders/summarybeats/orders/{orderId}, left to right. A trailing slash is ignored, andHEADis checked againstGET. - Several specs can share a host (services behind one gateway): the first that documents the request is used.
- Not checked: static files (images, scripts, styles, fonts), tunnels, WebSockets, and CORS preflight
OPTIONSrequests.
The traffic list
Section titled “The traffic list”While a spec is on, the traffic list shows a Contract column:
| Shows | Means |
|---|---|
| A red ✕ 2 | The request broke the contract: two errors |
| An orange ! 1 | Warnings only |
| ✓ | It matches the spec |
| Nothing | Not checked (another host, a static file, outside the base path) |
Hover a marker for the first problem. The column appears by itself, next to Status, when a spec starts checking; right-click the column headers to hide or move it. Click Violations in the filter bar (it shows how many requests have errors) to show only requests with problems — it adds a Contract contains violation condition to Filters, where you can narrow it further, for example Contract contains wrong type or Contract contains $.items.
The Contract column, the Violations filter, and the request's Contract tab.
- The Contract column
- Show only requests with problems
- The request's Contract tab
The Contract tab
Section titled “The Contract tab”Select a checked request and open the Contract tab in the inspector (or click the marker in the inspector’s header). It shows:
- whether the request matched, with the error and warning counts;
- the operation it matched: method, path template, summary and
operationId, with Show in Spec to read that operation’s definition; - notes, such as a body too large to check, or a
304that isn’t checked; - each problem: its kind, where it is (request or response; path, query, header, cookie, body, status or content type), the JSON path (
$.items[3].price) or parameter name, the message, the expected and actual values, and the schema location in the spec (#/components/schemas/Order/properties/total).
For a body problem, the JSON around it is shown with the offending line highlighted — or, for a missing required field, a ghost line where it should be.
Each problem with its JSON path, expected and actual values, where the rule is in the spec, and the body around it.
What’s checked
Section titled “What’s checked”Every kind of problem, what triggers it, and its default severity. Errors are drift that breaks a client; warnings are worth knowing but don’t fail a request. Change any of them in Options › Severity of Each Problem.
| Problem | Example | Default |
|---|---|---|
| Undocumented endpoint | POST /v2/cart/coupon — no path in the spec matches it (under a bound host and the base path) |
Error |
| Method not allowed | PUT /v2/users/me when the spec documents only GET and PATCH there |
Error |
| Missing parameter | A required X-Request-ID header, ?limit query or cookie wasn’t sent |
Error |
| Invalid parameter | ?limit=500 when the maximum is 100; /orders/abc for an integer orderId; a response header of the wrong type |
Error |
| Undocumented parameter | ?debug=1 isn’t listed for the operation |
Off |
| Missing body | A required request body wasn’t sent, or a documented response body didn’t come | Error |
| Undeclared content type | text/html where the spec declares only application/json; a body where the spec documents none (a warning) |
Error |
| Invalid JSON | A JSON body that doesn’t parse | Error |
| Undocumented status | 500 when the operation lists only 200 and 404 (and no default or 5XX) |
Error |
| Missing response header | A response header marked required: true wasn’t returned |
Error |
| Missing required field | $.data[0].createdAt is missing |
Error |
| Wrong type | $.total is the string "21.00", not a number |
Error |
| Unexpected null | $.price is null and the schema isn’t nullable |
Error |
| Value not allowed | $.status is "partially_shipped", not one of the enum values (also const, and an unknown discriminator value) |
Error |
| Out of range | Below minimum, above maximum, too short or long (minLength, maxItems…), not a multipleOf, outside the int32/int64 range |
Error |
| Pattern mismatch | $.currency is "usd" for pattern: '^[A-Z]{3}$' |
Error |
| Unexpected field | $.debugFlags where the schema says additionalProperties: false |
Error |
| No matching variant | A value that fits none of a oneOf/anyOf’s options; the message names the closest one and why |
Error |
| Invalid format | "2026-10-12 17:03:55" isn’t a date-time; also date, time, email, uuid, uri, hostname, ipv4, ipv6, byte |
Warning |
| Duplicate items | Repeated items in an array with uniqueItems: true |
Warning |
| Read-only field sent | A readOnly field (such as id) in a request body |
Warning |
| Write-only field returned | A writeOnly field (such as password) in a response |
Warning |
| Undocumented field | A field the schema doesn’t list, where extra fields are allowed (no additionalProperties: false) |
Off |
How schemas are read
Section titled “How schemas are read”type(a string, or in 3.1 an array such as[string, "null"]),properties,required,additionalProperties(true, false or a schema),patternProperties,itemsandprefixItems,enumandconst, the numeric, string, array and object bounds,patternandformat.nullable: true(3.0) and"null"in a type array (3.1) both allownull. AoneOfwith a{type: "null"}option does too.allOfcombines its parts, and treats them as one object foradditionalProperties:allOf: [Base, {additionalProperties: false, …}]doesn’t rejectBase’s fields.oneOfandanyOfpass when the value fits an option. With adiscriminator, the tag ("type": "card") picks the option, through itsmappingor the schema names, so problems inside it are reported with exact paths. Discriminators on a base schema extended withallOfwork too.- Whole numbers written as
2.0are integers, as JSON Schema says.readOnlyrequired fields aren’t required in requests, andwriteOnlyones aren’t required in responses. - Query, path and header values are read as the schema’s type:
?limit=10is the integer 10; arrays are repeated keys (?ids=1&ids=2, orids[]=) or delimited values perstyle/explode(collectionFormatin Swagger 2.0);deepObjectparameters and form bodies are checked field by field.
Options
Section titled “Options”| Option | Default | What it does |
|---|---|---|
| Check string formats | On (warnings) | Report strings that aren’t valid for their format |
| Flag fields the spec doesn’t list | Off | Report extra fields where additionalProperties isn’t false |
| Flag query parameters the spec doesn’t list | Off | Report query parameters the operation doesn’t document |
| Ignore 304 Not Modified | On | Don’t report 304 responses, which most specs don’t list |
| Largest body checked | 2 MB | Bigger bodies are matched but not validated against their schema (a note says so) |
| Severity of Each Problem | As in the table above | Make any kind an error, a warning, or off |
Changing an option checks the captured traffic again.
The report
Section titled “The report”The window’s three tabs summarise everything checked for the selected spec (or all specs):
- Endpoints — every endpoint the app called, problems first: its calls, how many failed, the status codes seen, and its most common problem. Requests the spec doesn’t document appear here too, marked Not in spec or Method not in spec. Select a row for its detail: the operation’s summary and
operationId, its problems by JSON path with counts, and its latest calls — click any of them to select those requests in the traffic list. Only problems hides the endpoints that pass. - Problems — every distinct problem, grouped by kind, with the endpoint, where it is, an example message, the schema location and how many requests it affects. Array indexes are generalised (
$.items[*].price), so the same drift in many responses is one row. Click a row to select those requests. - Coverage — how many of the spec’s operations the captured traffic exercised, which were never called, and what was called but not in the spec.
Problems grouped by kind. Click one to select the requests it affects.
Coverage: which documented operations your testing reached, and which it never touched.
Export
Section titled “Export”The share menu exports the report as:
- Markdown — the most common problems, the endpoints, what’s not in the spec, and coverage with the never-called operations. Good for a bug report or a pull request.
- JSON — the whole report as data.
- JUnit XML — a test suite per spec and a test case per operation: failing when a call broke the contract, skipped when it was never called, plus a failing case for each undocumented endpoint. CI systems show it as test results.
Create a starting spec from traffic
Section titled “Create a starting spec from traffic”No spec yet? Right-click a request or a host and choose Make Spec from … Traffic…, or Add Spec › Create a Starting Spec from Traffic…. Choose the host, and Cloak Proxy drafts an OpenAPI 3.0 spec from its captured requests:
- paths templated and named (
/orders/42→/orders/{orderId}), with a shared/v1-style prefix lifted intoservers; - path and query parameters, typed from the values seen (a query parameter is required only when every request sent it);
- request and response schemas merged across samples per status code: a field is required only when every sample had it,
nullmakes it nullable, and formats such asdate-time,uuidandemailare recognised; - a summary and
operationIdfor each operation (List orders,getOrder).
A draft spec from one host's traffic, ready to save, add to the API client, or check against.
Then Save As… to write it to a file, Add to API Client to edit it in Specs, or Check Traffic Against It to add it to API Contracts straight away. It describes what was captured, so review the types, required fields and responses before you rely on it — then let the checks tell you when the API changes. The same draft is available from the command line with cloakctl contract generate.
Empty states
Section titled “Empty states”With no spec, the window explains what contract checking does and offers Add Spec File…, From URL…, From the API Client… and Create a starting spec from captured traffic…. With a spec but no traffic to its hosts yet, it says which hosts are being watched and suggests captured hosts that look like the API.
Before the first spec.
From an AI assistant
Section titled “From an AI assistant”The MCP server has two tools:
check_contractchecks captured traffic against a spec you pass as text (spec) or a file (spec_path), or against the specs bound in API Contracts. Scope it withhost,idsorapp. It returns each failing request with its problems (kind, where, JSON path, message, expected, actual, schema location), or the report asmarkdown,jsonorjunit. Withsave: truethe spec is added to API Contracts and checks live traffic from then on.contract_reportreturns the report for the bound specs: totals, endpoints, the most common problems with example request ids, undocumented requests and coverage.
"Check the ShopApp traffic against ~/Projects/shopapp/openapi.yaml for api.shopapp.example and tell me which response fields drifted from the spec."See the MCP tools reference for every argument.
cloakctl contract check checks a saved session or a HAR file against a spec and exits non-zero when the traffic breaks the contract — so a build fails when the backend drifts from its spec.
cloakctl contract check --spec openapi.yaml --session e2e.cloaksession \ --host api.example.com --format junit --out contract-report.xml| Option | What it does |
|---|---|
--spec FILE |
The OpenAPI 3.x or Swagger 2.0 spec, YAML or JSON (required) |
--session FILE / --har FILE |
The captured traffic: a .cloaksession or .har (one is required) |
--host HOST |
Bind the spec to this host (repeatable). Default: the hosts in the spec’s servers. When the spec names none, only requests that match an operation are reported |
--format markdown|json|junit |
The report format (default markdown) |
--out FILE |
Write the report to a file instead of printing it |
--fail-on error|warning|never |
When to exit non-zero (default error) |
--strict |
Also report undocumented fields and query parameters |
| Exit code | Meaning |
|---|---|
0 |
No request had an error (or a warning, with --fail-on warning), or --fail-on never |
1 |
A usage error, such as an unreadable spec or session |
2 |
At least one request broke the contract |
A session to check can come from cloakctl run --record around your end-to-end tests. This GitHub Actions workflow runs on a self-hosted Mac runner with Cloak Proxy installed (GitHub’s hosted runners don’t have it):
name: API contracton: [pull_request]
jobs: contract: runs-on: [self-hosted, macOS] env: CLOAKCTL: /Applications/Cloak Proxy.app/Contents/Resources/cloakctl steps: - uses: actions/checkout@v4
- name: Record the end-to-end tests through Cloak Proxy run: | "$CLOAKCTL" run --ssl 'api.example.com' --record e2e.har --quiet -- npm run test:e2e
- name: Check the traffic against the spec run: | "$CLOAKCTL" contract check --spec openapi.yaml --har e2e.har \ --host api.example.com --format junit --out contract.xml
- name: Keep the report if: always() uses: actions/upload-artifact@v4 with: name: api-contract path: contract.xmlMost CI systems, and JUnit reporting actions, show contract.xml as test results: one test per operation.
cloakctl contract generate --session FILE --host HOST [--title TEXT] [--out FILE] prints a starting spec from the same traffic.
Checking happens off the main thread, on all cores, so it never slows the proxy or the traffic list. Compiled schemas are cached per spec, bodies over the size cap are skipped, and results reach the list a few times a second. On a recent Mac, 10,000 requests with JSON bodies are checked in well under two seconds, and a 30,000-request session re-checks in the background while the list keeps scrolling smoothly.
Limits
Section titled “Limits”- Only what was captured is checked. An endpoint the app never called shows as never called in Coverage, not as passing.
$refs to other files and URLs aren’t followed; values they describe aren’t checked (the spec’s row lists them).- XML, plain-text and binary bodies are checked for their content type only; multipart bodies aren’t checked field by field.
- Bodies cut short when captured, and bodies over Largest body checked, are matched to their operation but not validated.
- Overlapping
oneOfoptions (a value that fits more than one) aren’t reported: real specs often have them, and the value is still valid for a client. - Swagger 2.0 is converted on a best-effort basis.
- Specs, their host bindings and the options are saved on this Mac through a store that self-test and gallery runs redirect to a throwaway folder, so a test run never reads or changes your real specs.