Describe an API Change as a Contract the Agent Can Check
Drafted and reviewed using AI assistance.
On this page
An API-change brief should show what existing consumers receive, what new consumers can request, how errors behave and which compatibility claims still need checking. “Add a summary field” is not enough: an absent field, a field containing null and a required string are three different contracts.
This guide designs an entirely fictional, read-only card catalog at https://api.example.invalid. The host is deliberately non-live; no requests were sent and no service was implemented. The contract, messages and data are original teaching examples.
Start with the consumer and the unchanged response
The existing consumer requests:
GET /v1/cards?limit=2 HTTP/1.1
Host: api.example.invalid
Accept: application/json
Its illustrative response is:
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [
{"id": "c1", "title": "Review draft"},
{"id": "c2", "title": "Plan export"}
],
"next_cursor": null
}
The proposed feature lets a new consumer opt into a summary property using include_summary=true. Omission or explicit false keeps the existing shape. This is a chosen migration strategy, not a universal rule that optional fields are always safe.
| Contract point | Before | After |
|---|---|---|
| Method and path | GET /v1/cards |
Unchanged |
limit |
Optional integer, default 20, range 1–50 | Unchanged |
| No summary option | Each item has id and title |
Same shape; summary is absent |
include_summary=false |
Not an existing option | Same shape as omission |
include_summary=true |
Not an existing option | Every returned item also has summary, string or null |
| Result order and selected IDs | Existing selection behavior | Unchanged for the same data and limit |
| Access model | Fictional public catalog | Unchanged; no authorization changes |
This public-data example does not prescribe removing authentication from another service. Use that service's actual access and ownership rules when adapting the brief.
Show the new request and its response
GET /v1/cards?limit=2&include_summary=true HTTP/1.1
Host: api.example.invalid
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [
{"id": "c1", "title": "Review draft", "summary": "Review the proposed wording."},
{"id": "c2", "title": "Plan export", "summary": null}
],
"next_cursor": null
}
Here null means a card has no summary available. It does not mean “the client did not request summaries.” Omission of summary communicates that other case. A new client must handle both a string and an explicit null after opting in.
For an empty result, return items: [] and next_cursor: null. The “every returned item” rule then applies to zero items; it does not require inserting a placeholder card.
Define errors at the boundary
For this example, include_summary accepts only the lowercase query literals true and false. The limit value is a canonical decimal integer from 1 to 50: 2 is valid, while 02, 2.0 and an empty value are not. Repeating either recognized parameter is an error rather than an instruction to use the first or last value.
| Request condition | Chosen response |
|---|---|
| Valid limit and omitted/false summary option | 200, existing shape |
Valid limit and summary option true |
200, each item contains nullable summary |
Limit 0, 51, 02, two or empty |
400, INVALID_QUERY |
Summary option yes, 1, False or empty |
400, INVALID_QUERY |
| Repeated recognized parameter | 400, INVALID_QUERY |
An illustrative error body is:
{
"code": "INVALID_QUERY",
"message": "include_summary must be true or false."
}
The message should explain the particular invalid input; code is the stable machine-readable field. These query rules do not redefine unrelated service errors as 400. Existing routing, unknown-parameter and server-failure behavior remain outside this small change and must be preserved from the actual service contract.
Use OpenAPI to describe structure, then check conditional behavior
The following teaching document uses OpenAPI 3.1.1, a fixed version, not a claim about the latest release. The OpenAPI 3.1.1 specification defines the document, parameter and Schema Object structures used here. In this version, a type array can express a string-or-null field.
{
"openapi": "3.1.1",
"info": {
"title": "Fictional Card Catalog",
"version": "1.1.0-example"
},
"servers": [{"url": "https://api.example.invalid"}],
"paths": {
"/v1/cards": {
"get": {
"operationId": "listCards",
"summary": "List fictional cards, optionally including summaries",
"parameters": [
{
"name": "limit",
"in": "query",
"required": false,
"description": "Canonical decimal 1-50 only; repeated values are invalid.",
"schema": {"type": "integer", "minimum": 1, "maximum": 50, "default": 20}
},
{
"name": "include_summary",
"in": "query",
"required": false,
"description": "Lowercase true/false only; repeated values are invalid. True requires summary on every returned item; otherwise summary is absent.",
"schema": {"type": "boolean", "default": false}
}
],
"responses": {
"200": {
"description": "Cards; item shape also follows the include_summary behavior contract.",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/CardList"}
}
}
},
"400": {
"description": "Invalid recognized query parameter",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/QueryError"}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Card": {
"type": "object",
"required": ["id", "title"],
"additionalProperties": false,
"properties": {
"id": {"type": "string"},
"title": {"type": "string"},
"summary": {"type": ["string", "null"]}
}
},
"CardList": {
"type": "object",
"required": ["items", "next_cursor"],
"additionalProperties": false,
"properties": {
"items": {"type": "array", "items": {"$ref": "#/components/schemas/Card"}},
"next_cursor": {"type": ["string", "null"]}
}
},
"QueryError": {
"type": "object",
"required": ["code", "message"],
"additionalProperties": false,
"properties": {
"code": {"type": "string", "const": "INVALID_QUERY"},
"message": {"type": "string"}
}
}
}
}
}
This schema allows summary to be present or absent. It does not enforce presence conditional on a query parameter. Likewise, its integer parameter schema does not alone establish the server's handling of lexical forms or duplicate query values. Those requirements need behavioral checks at the HTTP boundary.
The companion document was parsed as JSON. A full OpenAPI validator, generated client and running service were not exercised. Treat those as separate implementation checks rather than inferring them from valid JSON syntax.
Check compatibility by consumer behavior
| Consumer | Effect under this proposed contract | Check before migration |
|---|---|---|
| Existing client that omits the option | Receives the old shape | Compare old and new response fields for fixed data |
| Existing strict client that rejects unknown fields | Still receives the old shape by default | Verify that its requests do not begin opting in accidentally |
| New client requesting summaries | Receives string or null on each item | Check both values and an empty list |
| Unknown consumer | Compatibility is unknown | Identify its requests and decoding assumptions |
Deploying server support before a client starts sending the new parameter is one possible ordering. First check how the existing server handles unknown parameters; an older server might reject the new request. Likewise, reversing a deployment can leave new clients calling an unsupported option. An opt-in design needs a migration plan as well as a schema.
A complete implementation request
Implement the fictional GET /v1/cards opt-in summary contract.
Use the attached OpenAPI 3.1.1 document and both request/response examples.
The example.invalid host is illustrative; do not contact it.
Preserve the existing method, path, data selection, ordering, limit default
20, limit range 1-50, response envelope and access behavior.
New behavior:
- include_summary omitted or false: summary is absent from every item.
- include_summary=true: summary is present on every item, string or null.
- Null means unavailable; do not substitute an empty string.
- Empty results remain items: [] with next_cursor: null.
- Accept only lowercase true/false for include_summary.
- Accept canonical decimal limit 1-50; reject duplicate recognized params.
- Invalid recognized input returns 400 with code INVALID_QUERY and a
useful message. Preserve unrelated error behavior.
Check the HTTP boundary for omitted, false and true options; a null summary;
an empty list; limit boundaries; invalid lexical values; and duplicates.
Compare IDs, titles, order and envelope against fixed pre-change fixtures.
Check presence rules separately: the response schema cannot enforce them.
Deliver implementation, actual check results, consumer-impact notes and a
server/client deployment-order proposal. Do not change authentication,
write endpoints, live deployments or unknown consumer contracts.
Do not describe JSON parsing alone as OpenAPI or compatibility validation.
Give the reviewer observable before-and-after behavior. That makes it possible to assess whether the agent delivered the requested API change without treating an apparently small field addition as a blanket compatibility guarantee.