# Contract checking

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

Source: https://docs.subcloak.com/cloak-proxy/testing/contract-checking/

Added in Cloak Proxy 0.4.0.

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.

*[Screenshot: 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.. Each row is an endpoint the app called; red rows broke the contract. The detail pane lists the endpoint's problems by JSON path.]*

## Add a spec

1. Open **Tools › API Contracts…** (<kbd>⇧</kbd> <kbd>⌥</kbd> <kbd>⌘</kbd> <kbd>K</kbd>), 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.

> **On Linux:**
> API Contracts opens with <kbd>Ctrl</kbd> <kbd>Alt</kbd> <kbd>Shift</kbd> <kbd>K</kbd>, or from **Check Against Spec…** on a request's or a domain's right-click menu. The traffic list's **Contract** column starts hidden: right-click the column headers and choose **Contract** to show it. A spec's right-click menu has **Copy File Path** (or **Copy URL**) in place of **Open Spec File** and **Show in Finder**.

*[Screenshot: 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.. 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 `$ref`s 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.

> **No spec yet?:**
> Choose **Add Spec › Create a Starting Spec from Traffic…** — see [below](#create-a-starting-spec-from-traffic).

## 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

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.

## 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](https://docs.subcloak.com/cloak-proxy/inspect/filtering/#combine-several-conditions), where you can narrow it further, for example **Contract contains wrong type** or **Contract contains $.items**.

*[Screenshot: 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.. The Contract column, the Violations filter, and the request's Contract tab.]*

## 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 `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.

*[Screenshot: 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.. 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

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

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

## 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

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

*[Screenshot: 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.. Problems grouped by kind. Click one to select the requests it affects.]*

*[Screenshot: 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.. Coverage: which documented operations your testing reached, and which it never touched.]*

### 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

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`).

*[Screenshot: 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.. 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

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.

*[Screenshot: 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.. Before the first spec.]*

## From an AI assistant

The [MCP server](https://docs.subcloak.com/cloak-proxy/automation/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.

```text title="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](https://docs.subcloak.com/cloak-proxy/automation/mcp-tools-reference/#check_contract) for every argument.

## In CI

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

```bash title="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`](https://docs.subcloak.com/cloak-proxy/automation/headless-and-ci/) 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):

```yaml title=".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](#create-a-starting-spec-from-traffic) from the same traffic.

## Speed

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

- Only what was captured is checked. An endpoint the app never called shows as **never called** in Coverage, not as passing.
- `$ref`s 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.

## Related

  - [Specs in the API client](https://docs.subcloak.com/cloak-proxy/api-client/run/specs/): Import, edit and lint OpenAPI specs, and sync them with collections.
  - [Release diff](https://docs.subcloak.com/cloak-proxy/testing/release-diff/): Compare two builds' traffic: fields, statuses, speed and third parties.
  - [Authorization matrix](https://docs.subcloak.com/cloak-proxy/testing/authorization-matrix/): Replay every request as another user to find broken access controls.
  - [Filtering](https://docs.subcloak.com/cloak-proxy/inspect/filtering/): Quick filters, multiple conditions and saved presets.
