Skip to content

Reports

check and diff accept --format:

Format Use
text (default) Terminals and CI logs
markdown Pull request comments and $GITHUB_STEP_SUMMARY
json Scripts. Has ok, summary counts, every change, and expired/stale allowlist entries

--output FILE writes the report to a file instead of stdout. Changes are sorted worst first. The report never changes the exit code: see Exit codes.

Text

The default. Changes are listed by severity. Each change shows its endpoint, what changed, an at: line when the code behind it is known (see attribution), and its id:. Safe changes have no id, since nothing needs acknowledging.

$ api-contract check
API contract: 4 changes (2 breaking)

BREAKING
  POST /api/orders/  new required request field "currency"
      id: POST /api/orders/:request:currency:required-request-field-added
  GET /api/orders/{id}/  response field "total" type changed: number -> string
      id: GET /api/orders/{}/:response:200:total:type-changed

POTENTIALLY BREAKING
  GET /api/orders/{id}/  response field "status" enum value added: "on_hold"
      id: GET /api/orders/{}/:response:200:status:response-enum-value-added:"on_hold"

SAFE
  POST /api/coupons/  new endpoint

Fix the change, or acknowledge it:
  api-contract allow 'POST /api/orders/:request:currency:required-request-field-added' --reason \"...\" --expires 2026-11-24

When changes are allowed, the id is replaced by the reason and date:

$ api-contract check
API contract: 4 changes (2 breaking, 2 allowed)

BREAKING
  POST /api/orders/  new required request field "currency"
      allowed until 2026-11-15: Clients send currency since 4.0
  GET /api/orders/{id}/  response field "total" type changed: number -> string
      allowed until 2026-12-01: Mobile 4.2+ reads total_display instead

POTENTIALLY BREAKING
  GET /api/orders/{id}/  response field "status" enum value added: "on_hold"
      id: GET /api/orders/{}/:response:200:status:response-enum-value-added:"on_hold"

SAFE
  POST /api/coupons/  new endpoint

Expired and stale allowlist entries get their own sections; see Allowlist. The closing hint suggests the allow command for the first failing change.

Markdown

For pull request comments and $GITHUB_STEP_SUMMARY. The heading ends with passed or failed, and safe changes are folded into a <details> block.

$ api-contract check --format markdown
### API contract: 4 changes (2 breaking, 2 allowed) — passed

#### Breaking (2)

- **POST** `/api/orders/` — new required request field "currency"
  - allowed until 2026-11-15: Clients send currency since 4.0
- **GET** `/api/orders/{id}/` — response field "total" type changed: number -> string
  - allowed until 2026-12-01: Mobile 4.2+ reads total_display instead

#### Potentially breaking (1)

- **GET** `/api/orders/{id}/` — response field "status" enum value added: "on_hold"
  - id: `GET /api/orders/{}/:response:200:status:response-enum-value-added:"on_hold"`

<details><summary>Safe (1)</summary>

- **POST** `/api/coupons/` — new endpoint

</details>

JSON

For scripts. One object per change, ids unchanged, never grouped:

{
  "version": 1,
  "ok": true,
  "fail_on": "breaking",
  "summary": {
    "total": 4,
    "breaking": 2,
    "potentially-breaking": 1,
    "safe": 1,
    "allowed": 2,
    "failing": 0
  },
  "changes": [
    ...
  ],
  "allowlist": {
    "expired": [],
    "stale": []
  }
}

The full report has these top-level keys:

Key Meaning
version The report schema version, currently 1
ok true when check would exit 0
fail_on The threshold in effect
summary Counts: total, breaking, potentially-breaking, safe, allowed, failing
changes One object per change
allowlist expired and stale entries, each with id, reason and expires

Each element of changes looks like this (an allowed breaking change; allowed is null otherwise, and attribution is null for file sources):

{
  "id": "GET /api/orders/{}/:response:200:total:type-changed",
  "kind": "type-changed",
  "severity": "breaking",
  "direction": "response",
  "method": "GET",
  "path": "/api/orders/{}/",
  "raw_path": "/api/orders/{id}/",
  "location": "response:200:application/json",
  "pointer": "total",
  "detail": null,
  "old": [
    "number"
  ],
  "new": [
    "string"
  ],
  "message": "response field \"total\" type changed: number -> string",
  "component": "Order",
  "attribution": null,
  "allowed": {
    "id": "GET /api/orders/{}/:response:200:total:type-changed",
    "reason": "Mobile 4.2+ reads total_display instead",
    "expires": "2026-12-01"
  }
}

old and new hold the schema fragments (or types) before and after.

Grouping

One change to a shared serializer can show up once per endpoint and once per content type (JSON, form, multipart). The text and markdown reports fold changes that share a kind, location and pointer, and differ only in endpoint or media type, into one entry that lists the affected endpoints and media types. Here the FastAPI model Order lost a field that three endpoints return:

API contract: 3 changes (3 breaking)

BREAKING
  POST /orders  response field "total" removed
      at: app.main.Order (app/main.py:7)
      id: POST /orders:response:201:total:response-field-removed
  response field "total" removed  (2 endpoints)
      GET /orders/{order_id}
      PUT /orders/{order_id}
      media types: application/json
      at: app.main.Order (app/main.py:7)
      id: GET /orders/{}:response:200:total:response-field-removed
      (+1 similar changes; --no-group or --format json lists every id)

Fix the change, or acknowledge it:
  api-contract allow 'POST /orders:response:201:total:response-field-removed' --reason \"...\" --expires 2026-11-24

POST /orders returns it with status 201, so it is a different location and stays on its own; the two 200 responses are folded.

Grouping only affects how the text and markdown reports are laid out. The counts in the summary line, the exit code and the json report are the same. Pass --no-group to list every change on its own line, for example to copy every id into the allowlist:

API contract: 3 changes (3 breaking)

BREAKING
  POST /orders  response field "total" removed
      at: app.main.Order (app/main.py:7)
      id: POST /orders:response:201:total:response-field-removed
  GET /orders/{order_id}  response field "total" removed
      at: app.main.Order (app/main.py:7)
      id: GET /orders/{}:response:200:total:response-field-removed
  PUT /orders/{order_id}  response field "total" removed
      at: app.main.Order (app/main.py:7)
      id: PUT /orders/{}:response:200:total:response-field-removed

Fix the change, or acknowledge it:
  api-contract allow 'POST /orders:response:201:total:response-field-removed' --reason \"...\" --expires 2026-11-24

Changelog

api-contract changelog writes a consumer-facing summary, without ids, severities or attribution, that you can paste into release notes. With no arguments it compares the snapshot with the current source; changelog OLD NEW compares two documents. --title sets the heading (default API changes) and -o FILE writes to a file.

$ api-contract changelog --title v2.1
## v2.1

### Breaking changes

- `POST /api/orders/`: new required request field "currency"
- `GET /api/orders/{id}/`: response field "total" type changed: number -> string

### Changes that may affect you

- `GET /api/orders/{id}/`: response field "status" enum value added: "on_hold"

### New and improved

- New endpoint `POST /api/coupons/`

Changes are grouped by how much they should worry a client: breaking changes, "changes that may affect you", and "new and improved". Deprecations get their own section. With no differences it writes No changes to the API contract.