Skip to content

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.

  1. A spec, its hosts and an on/off switch
  2. Endpoints, Problems and Coverage
  3. Requests the spec doesn't document
  4. The selected endpoint's problems
The API Contracts window. On the left, the ShopApp API 2.3.0 spec (OpenAPI 3.0, 17 operations, bound to api.shopapp.example, 30 checked, 13 with errors) and a paused Payments Gateway spec. On the right, the Endpoints tab: GET /v2/orders/{orderId} with 4 calls and 4 failing, GET /v2/products, GET /v2/users/me, POST /v2/cart/items, POST /v2/payments, then requests marked Not in spec (POST /v2/cart/coupon, GET /v2/recommendations) or Method not in spec (PUT /v2/users/me), then passing endpoints with green checks. The selected order endpoint's detail pane shows its problems: Wrong type at $.total, Value not allowed at $.status and Invalid format at $.createdAt, and its latest calls.
  1. Open Tools › API Contracts… (⇧ ⌥ ⌘ K), or right-click a request or a host and choose Check Against Spec…

  2. Click Add Spec and choose where the spec comes from:

    Source How it’s kept up to date
    File A .yaml, .yml or .json file. Cloak Proxy watches it and reloads it when you save it in your editor.
    URL Fetched with a plain GET when you add it and when you choose Reload. It’s sent directly, not through the proxy, without cookies; add one header (for example Authorization: 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.

  3. 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’s servers name, then hosts whose requests match its paths. Use *.example.com for every subdomain, or localhost:8080 for one port.

  4. 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.

The Add a spec sheet: File, URL and API Client tabs with File selected, the path ~/Projects/shopapp/openapi.yaml with a Choose button, a green check with ShopApp API 2.3.0, OpenAPI 3.0.3, 17 operations, https://api.shopapp.example/v2, then Name and Hosts fields with api.shopapp.example, and Cancel and Add Spec buttons.

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.

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.

A request is checked only when its host is bound to a spec. Then:

  • The spec’s servers give the base path. With https://api.example.com/v2, a request to /v2/orders/42 is matched against /orders/{orderId}. Server variables (https://{region}.example.com/{version}) are expanded from their enum and default. 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/summary beats /orders/{orderId}, left to right. A trailing slash is ignored, and HEAD is checked against GET.
  • 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 OPTIONS requests.

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.

  1. The Contract column
  2. Show only requests with problems
  3. The request's Contract tab
The main window filtered to api.shopapp.example. The Contract column, next to Status, shows red ✕ 1 markers on several rows, checkmarks on passing rows, and ✕ 2 ! 1 on the selected GET /v2/orders/4977. The filter bar has a Violations 13 tab before the search field. The inspector header shows the same ✕ 2 ! 1 badge, and the request's Contract tab lists 2 errors and 1 warning against GET /v2/orders/{orderId}, starting with Value not allowed at $.status.

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 304 that 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.

The Contract tab for GET /v2/orders/4977: 2 errors, 1 warning, checked against ShopApp API, the matched operation GET /v2/orders/{orderId} Get an order with a Show in Spec button, then a Value not allowed card at $.status with expected values pending, paid, shipped, delivered, cancelled, actual partially_shipped, spec location #/components/schemas/Order/properties/status and the response object with the status line highlighted, then a Wrong type card at $.total.

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
  • type (a string, or in 3.1 an array such as [string, "null"]), properties, required, additionalProperties (true, false or a schema), patternProperties, items and prefixItems, enum and const, the numeric, string, array and object bounds, pattern and format.
  • nullable: true (3.0) and "null" in a type array (3.1) both allow null. A oneOf with a {type: "null"} option does too.
  • allOf combines its parts, and treats them as one object for additionalProperties: allOf: [Base, {additionalProperties: false, …}] doesn’t reject Base’s fields.
  • oneOf and anyOf pass when the value fits an option. With a discriminator, the tag ("type": "card") picks the option, through its mapping or the schema names, so problems inside it are reported with exact paths. Discriminators on a base schema extended with allOf work too.
  • Whole numbers written as 2.0 are integers, as JSON Schema says. readOnly required fields aren’t required in requests, and writeOnly ones aren’t required in responses.
  • Query, path and header values are read as the schema’s type: ?limit=10 is the integer 10; arrays are repeated keys (?ids=1&ids=2, or ids[]=) or delimited values per style/explode (collectionFormat in Swagger 2.0); deepObject parameters and form bodies are checked field by field.
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 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.

The Problems tab grouped by kind: Wrong type (4 requests, 1 place) for GET /v2/orders/{orderId} at response body $.total, Unexpected null at $.items[*].price for GET /v2/products, Value not allowed at $.status and at the request body's $.wallet for POST /v2/payments, Undocumented endpoint for GET /v2/recommendations and POST /v2/cart/coupon, Missing required field at the request body's $.quantity for POST /v2/cart/items, each with its message, schema location and request count.

Coverage: which documented operations your testing reached, and which it never touched.

The Coverage tab: ShopApp API, 13 of 17 operations exercised, 76%, a progress bar, an Exercised list of 13 operations with call counts and a Never called list with PATCH /v2/orders/{orderId}, PATCH /v2/users/me, POST /v2/addresses and GET /v2/gift-cards/{code}, then Called but not in the spec.

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.

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 into servers;
  • 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, null makes it nullable, and formats such as date-time, uuid and email are recognised;
  • a summary and operationId for each operation (List orders, getOrder).

A draft spec from one host's traffic, ready to save, add to the API client, or check against.

The Create a starting spec from traffic sheet: an explanation, the Host picker set to api.shopapp.example (30), a Title field, 18 endpoints from 30 requests, a YAML preview starting openapi: 3.0.3 with the info, servers and paths, a note to review it, and Save As, Add to API Client, Cancel and Check Traffic Against It buttons.

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.

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.

The API Contracts window with no specs: Check your traffic against an API spec, an explanation, Add Spec File, From URL and From the API Client buttons, and a Create a starting spec from captured traffic link.

The MCP server has two tools:

  • check_contract checks 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 with host, ids or app. It returns each failing request with its problems (kind, where, JSON path, message, expected, actual, schema location), or the report as markdown, json or junit. With save: true the spec is added to API Contracts and checks live traffic from then on.
  • contract_report returns the report for the bound specs: totals, endpoints, the most common problems with example request ids, undocumented requests and coverage.
Ask your assistant
"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.

Terminal
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):

.github/workflows/contract.yml
name: API contract
on: [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.xml

Most 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.

  • 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 oneOf options (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.