Companion repository for the talk "Contract-First APIs in the Age of Agents" by Chad Green (chadgreen.com).
Your API now has two kinds of consumers. A human developer reads between the lines. An AI agent executes literally, with no exceptions. Ambiguity a human navigates becomes an outage an agent creates.
This repo contains the three patterns from the talk as OpenAPI 3.1 contracts, each in a before state (the contract that failed Brittany on Monday morning) and an after state (the agent-ready contract), with a governance pipeline that proves the difference. Take them, use them, adapt them.
| # | Guide | Principle | Before → After |
|---|---|---|---|
| 01 | The Status Enum Pattern | Make the happy path boring | before · after |
| 02 | The Structured Error Schema | Your error is an agent's next step | before · after |
| 03 | The Idempotency-Key Header | Agents will retry. Make it safe. | before · after |
Each guide stands on its own: the story, the principle, a walkthrough of the before-and-after contracts, the governance that enforces it, and a checklist for applying it to your own API. Read them in any order.
What you will find in each guide
- 01 — Status enums. Why
status: "ok"double-booked Brittany; closed enums with a documented meaning per value; explicit nullability (always present, sometimesnull); the async202+ poll pattern; one pagination vocabulary; why a new status value is a breaking change and how CI catches it. - 02 — Error schema. Why "Something went wrong. Please try again." caused a six-retry loop; the
error_code/message/retryable/retry_after/remediationschema; whyretryabledescribes the occurrence and not the code; extensible error codes vs. closed status enums; mapping to RFC 9457 Problem Details. - 03 — Idempotency-Key. Why a correct retry created a second booking and a second charge; stating the guarantee in the parameter, the description, and a machine-readable extension; replay, in-progress, and key-reuse semantics; an optional-then-required rollout that avoids breaking existing clients.
Every failure in the story traces to a gap in the contract, and every gap has a named fix in this repo.
| What went wrong | Contract-first fix | Where |
|---|---|---|
False success — status: "ok"; the agent could not tell confirmed from pending |
Enums and deterministic schemas: PENDING | CONFIRMED | FAILED |
01 |
| Duplicate charge — no idempotency key; the retry created a second booking | Idempotency-Key header documented in the contract; retries reuse the key |
03 |
| Retry loop ×6 — unstructured "Something went wrong" | Actionable error schema with retryable and remediation |
02 |
| No audit trail — no trace IDs across the multi-call chain | X-Correlation-ID, X-Request-ID, and W3C traceparent on every response |
All after files; lint rule agent-trace-headers-on-every-response |
| Breaking change reached production undetected | Lint + semantic diff + consumer-driven contract tests in CI | Governance and section 5 of every guide |
.
├── README.md ← you are here
├── docs/
│ ├── 01-status-enum-pattern.md
│ ├── 02-structured-error-schema.md
│ └── 03-idempotency-key-header.md
├── schemas/
│ ├── status-enum/
│ │ ├── before.openapi.yaml
│ │ └── after.openapi.yaml
│ ├── error-schema/
│ │ ├── before.openapi.yaml
│ │ └── after.openapi.yaml
│ └── idempotency-key/
│ ├── before.openapi.yaml
│ └── after.openapi.yaml
├── governance/
│ ├── agent-ready.spectral.yaml ← the style guide, as lint rules
│ └── examples/
│ └── status-enum.breaking-change.openapi.yaml ← a change CI must block
├── scripts/
│ ├── lint.ps1 ← after must pass, before must fail
│ └── breaking-check.ps1 ← semantic diff + semver gate
├── .github/workflows/contract-governance.yml
└── .spectral.yaml ← lets a bare `spectral lint` pick up the ruleset
- Every pair is a controlled experiment. A before file and its after file differ only in the principle being taught. Everything else is already agent-ready in both.
diff schemas/error-schema/before.openapi.yaml schemas/error-schema/after.openapi.yamlshows the error design and nothing else. - Every after file is a complete, self-contained contract. Copy one file into your project and it lints, validates, and renders on its own. The shared pieces (the
Errorschema, trace headers, theIdempotency-Keyparameter) are repeated in each file on purpose so you do not have to chase$refs across files. - Every example validates. Spectral's built-in
oas3-valid-media-examplerule checks each example payload against its schema. - Versioning is real. Before files are
1.4.0on/v1; after files are2.0.0on/v2, because the migrations are breaking changes andoasdiffsays so. Each guide explains how to ship the change, including non-breaking intermediate steps. - Apex Airways is fictional. Hosts use the reserved
.exampledomain.
Contracts without governance are just documents. Contracts have tests. Tests have pipelines. Pipelines have opinions.
The pipeline from the talk:
flowchart LR
openAPI[<strong>OpenAPI spec</strong><br />source of truth]
spectral[<strong>Spectral lint</strong><br />naming & schema rules]
contractTests[<strong>Contract tests</strong><br />Pact, consumer-driven]
semanticDiff[<strong>Semantic diff</strong><br />breaking change detection]
passFail[<strong>PASS:</strong> Deploy<br /><strong>FAIL:</strong> Block + notify]
openAPI --> spectral --> contractTests --> semanticDiff --> passFail
What this repo implements:
| Stage | Implementation | What it catches |
|---|---|---|
| Spectral lint | governance/agent-ready.spectral.yaml, run by scripts/lint.sh |
Freeform status strings, off-standard pagination, text/plain errors, missing retryable/remediation, missing Retry-After, writes without Idempotency-Key, undocumented idempotency, missing trace headers, one-line descriptions |
| Rules are tested too | lint.sh requires every after file to pass and every before file to fail |
A rule that is loosened or broken so it no longer catches its anti-pattern |
| Semantic diff | scripts/breaking-check.sh using oasdiff |
Removed or renamed fields, removed responses, new required parameters, new values in closed status enums; unless the major version is bumped |
| Contract tests | Illustrative Pact consumer tests in section 5 of each guide; a commented provider-verification job in the workflow | A provider implementation that no longer does what a specific consumer relies on |
| CI | .github/workflows/contract-governance.yml |
Runs lint on every PR and push; runs the semantic diff of every after contract against the base branch on PRs |
Requirements: Spectral CLI 6.x and oasdiff 1.x on your PATH (or set SPECTRAL= / OASDIFF= to their paths).
# Lint everything: after files must pass, before files must fail
scripts/lint.sh
# Lint a single contract with full output
spectral lint -r governance/agent-ready.spectral.yaml schemas/status-enum/before.openapi.yaml
# See why each before → after migration is a major version
oasdiff breaking schemas/idempotency-key/before.openapi.yaml schemas/idempotency-key/after.openapi.yaml
# Watch the gate block a "Friday afternoon" change (renamed field + new status value, minor bump)
scripts/breaking-check.sh schemas/status-enum/after.openapi.yaml \
governance/examples/status-enum.breaking-change.openapi.yamlExpected result of scripts/lint.sh:
== AFTER contracts (must pass) ==
PASS schemas/error-schema/after.openapi.yaml
PASS schemas/idempotency-key/after.openapi.yaml
PASS schemas/status-enum/after.openapi.yaml
== BEFORE contracts (must fail) ==
PASS schemas/error-schema/before.openapi.yaml (rejected as expected)
PASS schemas/idempotency-key/before.openapi.yaml (rejected as expected)
PASS schemas/status-enum/before.openapi.yaml (rejected as expected)
- Semantic versioning for the API; the major version is in the server URL (
/v1,/v2). - A breaking change is any
oasdifffinding at WARN or above. That is stricter than oasdiff's default on purpose: it treats a new value in a closed status enum as breaking, because for an agent with an exhaustiveswitchit is. - Breaking changes ship only with a major version bump. The previous major keeps running with
DeprecationandSunsetresponse headers, and the sunset date is documented in its spec. - Error codes are an
x-extensible-enum, so new codes can ship in a minor version; consumers fall back toretryable. - The contract is a shared artifact. Consumer teams review changes and publish pacts; both sides are accountable to the pipeline.
These are not the focus of a guide, but every after contract applies them:
- Trace IDs are not optional (Principle 4). Every response declares
X-Correlation-ID(shared across one agent workflow),X-Request-ID(unique per call), andtraceparent(W3C Trace Context). Error bodies repeat the two IDs. Requests may sendX-Correlation-IDandtraceparentso the agent's workflow ID propagates. - Descriptions written for an intelligent but literal colleague. Every operation description says what it does, when to call it versus a related operation, and what to do with the response. The lint rule
agent-operation-description-lengthsets a floor. - Async job pattern.
POST /bookingsreturns202 Accepted, aPENDINGstatus, aLocationto poll, andpoll_after_seconds. - Per-agent identity. The
agentOAuthsecurity scheme is OAuth 2.1 client credentials with scoped tokens, so each agent has its own auditable, revocable identity separate from the traveler. Idempotency keys are scoped to it. - MCP-ready. A well-structured OpenAPI contract makes deriving a Model Context Protocol tool surface largely mechanical. The operation IDs, descriptions, enums, and error schema here are exactly what a tool manifest is built from.
Five steps from the end of the talk. Any one of them ships value. Start with the one that scares you most.
- Audit one API for agent-readiness. Enums on every status field? Correlation ID on every response? Idempotency on side-effecting POSTs? Run
governance/agent-ready.spectral.yamlagainst its spec for a quick answer. - Write OpenAPI first for your next endpoint. Before a single line of code. Get a teammate to review the spec.
- Add idempotency keys to your highest-risk endpoint. The one that charges money or sends notifications. See 03.
- Set up one consumer-driven contract test in CI. One Pact test. It has teeth.
- Rewrite your description fields for your three most-called endpoints, as if an intelligent but literal system will read them. Because one is.
- OpenAPI Specification — contract tooling; start here
- Spectral — OpenAPI linting and style guides as code
- oasdiff — OpenAPI diff and breaking-change detection
- Pact — consumer-driven contract testing
- W3C Trace Context — the
traceparentheader - RFC 9457: Problem Details for HTTP APIs
- IETF draft: The Idempotency-Key HTTP Header Field
- Model Context Protocol
MIT. Use these contracts as a starting point for your own.
Brittany is not a fictional character. Brittany is your users. Somewhere, someone is building an AI agent to call your API today. The question is whether your contract is ready for it.