API Contract Testing: What It Is, How It Works and Tools
Quick answer
API contract testing checks that an API and its consumers still agree on a shared, machine-readable contract: an OpenAPI spec or a Pact file covering endpoints, fields, types and status codes. Tests run in CI on every change, so a renamed field or removed response fails the pull request instead of breaking a client in production.
Reviewed by Rishi Gaurav
API contract testing verifies, on every change, that an API provider and the clients that call it still agree on a shared, machine-readable interface: which endpoints exist, what the request and response bodies look like, which fields are required, which status codes and headers are valid, and how errors are represented. The contract is usually an OpenAPI document (schema-first) or a set of Pact files that consumers publish (consumer-driven). The tests run inside the pull request, so a breaking change fails a build instead of a customer's app.
Every distributed system eventually hits the same incident. A backend engineer renames user_name to username in a response. The OpenAPI file is not updated. Unit tests pass, the integration build is green, the change ships, and forty minutes later a mobile client starts throwing parse errors. Nothing was watching the boundary between the two services. Contract testing is the thing that watches it.
This guide is the hub for our OpenAPI, contract and schema content. It covers what a contract is, a beginner-friendly example, how contract testing differs from unit, integration and API testing, the three approaches (schema-first, consumer-driven and snapshot), a step-by-step CI workflow with a breaking-change table, and how to pick tools. The deeper dives live in the spokes linked from each section, starting with contract testing tools compared and API schema validation.
In this guide
- What is API contract testing?
- Contract testing vs unit, integration and API testing
- Three approaches: schema-first, consumer-driven and snapshot
- How to do API contract testing, step by step
- API contract testing tools
- Common mistakes and best practices
- API contract testing FAQ
What is API contract testing?
An API contract is the published agreement between a producer and its consumers. It states what endpoints exist, what inputs they take, what outputs they return and under what conditions. A contract only counts for testing when it is explicit and machine-readable. In practice it lives in one of these artifacts:
| Interface | Contract artifact |
|---|---|
| REST / HTTP | OpenAPI 3.x (or Swagger 2.0) document |
| GraphQL | SDL schema |
| SOAP | WSDL plus XSD |
| gRPC | .proto files |
| Events (Kafka, AMQP, MQTT) | AsyncAPI document, or schemas in a schema registry |
| Consumer-driven | Pact files, one per consumer-provider pair |
API contract testing is the automated practice of proving, on every change, that both sides still honour that artifact. The provider must return responses that match it. Consumers must send requests that conform to it. When either side diverges, a test fails in CI and the merge is blocked.
Contract testing is deliberately narrow. It does not check that creating an order deducts inventory, that an endpoint is fast, or that authorization is correct. It answers one question: does the interface still match what was promised? That narrowness is why it is cheap. Contract checks touch only the interface layer, so they are fast, deterministic and easy to run in parallel, and because they run on every commit they catch interface regressions before the code is merged.
A beginner-friendly API contract testing example
Take one endpoint, GET /orders/{id}, and one consumer, a mobile app that shows an order's status and total. Here is the relevant part of the OpenAPI contract:
paths:
/orders/{id}:
get:
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
'200':
description: Order found
content:
application/json:
schema: { $ref: '#/components/schemas/Order' }
'404':
description: No order with this id
components:
schemas:
Order:
type: object
required: [id, status, total]
properties:
id: { type: string }
status: { type: string, enum: [pending, paid, shipped, cancelled] }
total: { type: number, minimum: 0 }
A contract test does three things: call the endpoint, check the status code is one the contract documents, and validate the body against the Order schema.
// contract.test.js — schema-first check against the OpenAPI contract
import Ajv from 'ajv';
import yaml from 'js-yaml';
import { readFileSync } from 'node:fs';
const spec = yaml.load(readFileSync('openapi.yaml', 'utf8'));
const ajv = new Ajv({ allErrors: true, strict: false });
const validateOrder = ajv.compile(spec.components.schemas.Order);
test('GET /orders/{id} honours the Order contract', async () => {
const res = await fetch(`${process.env.API_URL}/orders/123`);
expect([200, 404]).toContain(res.status); // documented codes only
if (res.status === 200) {
const body = await res.json();
if (!validateOrder(body)) throw new Error(ajv.errorsText(validateOrder.errors));
}
});
Now a developer renames total to amount. The handler still works and its unit tests still pass. The contract test fails with must have required property 'total', and the pull request goes red before the mobile app ever sees the change. If the developer adds a new status refunded without updating the enum, the same test fails on the enum rule. That is the whole idea: the contract is the source of truth, and the implementation is not allowed to drift from it silently.
For how the schema keywords in that example behave (required, enum, $ref, formats and drafts), see the dedicated JSON Schema validation guide; for the request and response validation patterns built on them, see API schema validation.
Contract testing vs unit, integration and API testing
These terms get used interchangeably, but they answer different questions and catch different bugs.
| Unit testing | Contract testing | Integration testing | API testing (general) | |
|---|---|---|---|---|
| Question answered | Does this function work? | Does the interface still match the promise? | Do these components work together? | Does this API behave correctly? |
| Scope | One function or class, collaborators mocked | The wire format between two services | Real interactions between real components | Any endpoint: behaviour, rules, errors, security, speed |
| Dependencies | None | None (mock or recorded contract) | Real or containerized services | Varies |
| Speed | Milliseconds | Milliseconds to seconds | Seconds to minutes | Varies |
| Typical catch | Logic bugs | Renamed fields, type changes, removed responses | Wiring bugs, wrong queries, broken auth flow | Functional bugs, validation, business logic |
Contract testing vs unit testing. A unit test lives inside one service and knows nothing about the network interface. A service can have every unit test passing and still break all its consumers by renaming a JSON field, because the serializer is outside what the unit test asserts. Contract tests sit exactly on that seam.
Contract testing vs API testing. "API testing" is the umbrella. Contract testing is the slice of it that checks structure: fields, types, required properties, status codes, headers. A functional API test asks "did POST /orders create an order with the right total?"; a contract test asks "did it return a 201 with a body shaped like Order?". You need both, and contract tests are the cheaper half to automate because the contract already states what correct looks like.
Contract testing vs integration testing. Integration tests prove that services actually work together, but they need a shared environment and get slower and flakier as the service count grows. Contract tests verify each side on its own against the agreement, so each service can deploy independently. See unit vs integration vs system testing for where each sits in the test pyramid.
Three approaches: schema-first, consumer-driven and snapshot
Schema-first (provider-side)
The provider owns a specification, usually OpenAPI, and the running implementation is validated against it. A test runner sends real requests and checks each response against the schema for that operation and status code.
┌──────────┐ real requests ┌────────┐
│ Test │ ────────────────> │ Server │
│ runner │ <──── JSON ────── │ │
└──────────┘ └────────┘
│ validate against openapi.yaml
▼
pass / fail
Schema-first is the easiest place to start: if you already maintain an OpenAPI file, you already have the contract. It also covers consumers you do not know about, such as public API users. Its blind spot is that it proves the provider matches its spec, not that the spec covers what each consumer actually depends on. Tools: Schemathesis, Dredd, Prism in proxy mode, Specmatic, and spec-driven platforms.
Score your OpenAPI spec (free)
Free, runs in your browser, no sign-up needed.
Consumer-driven (Pact-style)
Each consumer publishes the expectations it has of the provider, and the provider must satisfy all of them before it deploys. The consumer only describes the fields it reads, so the contract is minimal.
// Consumer side (Pact): record what this consumer needs
await provider
.given('an order with ID 123 exists')
.uponReceiving('a request for order 123')
.withRequest({ method: 'GET', path: '/orders/123' })
.willRespondWith({
status: 200,
body: { id: '123', status: like('shipped'), total: like(49.99) },
});
Running the consumer tests writes a Pact file, which is published to a Pact Broker. The provider's CI fetches every pact for its version and replays each interaction against the real service; can-i-deploy then answers whether a given version is safe to release to an environment. Consumer-driven contracts give sharper protection for a known set of internal consumers, at the cost of coordination: every consumer team needs the Pact library, provider states and a broker. Tools: Pact with the open-source Pact Broker or PactFlow, and Spring Cloud Contract on the JVM.
Bi-directional contract testing sits between the two: the provider publishes its OpenAPI document, consumers publish their pacts, and the broker checks one against the other without replaying interactions against a running provider. PactFlow offers this. Our Pact vs Postman for contract testing post covers what a schema assertion can and cannot prove compared with a real consumer contract.
Snapshot-based
Record live responses once, commit them, and compare every later run against the snapshot. Any difference fails.
real server → capture JSON → commit to repo → compare on the next run
Snapshots are quick to set up and useful for large, loosely specified responses. They are brittle: any change, including a harmless new field, fails the test, and teams quickly learn to run --update whenever a snapshot breaks, at which point the snapshot documents the latest change rather than the intent. Use them sparingly, next to a real contract rather than instead of one.
Most mature teams combine the approaches: schema tests on every build, Pact verification for the few inter-service relationships where a break is expensive, and snapshots only for responses the schema cannot describe well.
How to do API contract testing, step by step
1. Make the contract explicit and version it
The contract lives in the service repository, next to the code, and changes in the same pull request as the code. If the spec is written after the code, it describes the bug rather than the intent. Choose which side is authoritative: generating the spec from code on every build means every code change silently becomes a contract change, so if you generate, review the generated diff like any other contract change. For the spec-first loop end to end, see schema-first API development and testing; which OpenAPI version you are on matters too, because OpenAPI 3.1 and 3.0 treat nullable, exclusiveMinimum and examples differently.
2. Lint the spec
Run a linter such as Spectral as a required check. A useful ruleset requires documented error responses (400, 401, 403, 404, 409, 422, 500 where they apply), example values, descriptions, and required arrays on response schemas. Contract tests against a spec that only documents the 200 path are green and meaningless.
3. Decide what the schema tests cover
For every endpoint in the spec, the schema-first suite should check that:
- every documented status code is reachable with an appropriate request;
- the response body matches the schema for that status code;
- required fields are always present;
- nullable fields are either
nullor the declared type; - enum fields only contain declared values;
- nested objects match their referenced schemas;
- the response
Content-Typematches the spec; - documented headers are present, such as
Locationon a201.
Tools can derive all of that from an OpenAPI file. What they cannot decide for you is which business-rule combinations matter, which specific status code a given test should trigger, or how to obtain realistic credentials. Contract tests plus hand-written business tests is what full coverage looks like.
4. Classify changes as breaking or non-breaking
Every contract change falls into one of three buckets. This table is the policy most teams converge on:
| Change | Breaking? | What to do |
|---|---|---|
| Add a new endpoint | No | Safe to ship |
| Add an optional field to a response | No (unless consumers validate strictly) | Safe for tolerant readers |
| Make a required request field optional | No | Safe |
| Remove a field from a response | Yes | Deprecate first, remove in the next major version |
| Rename a field | Yes | Add the new field, deprecate the old, remove later |
| Make an optional request field required | Yes | Existing clients will start getting 400s |
| Change a field's type | Yes | Major version bump |
| Remove an enum value | Yes | Breaking |
| Add an enum value | Maybe | Breaking for consumers that switch exhaustively |
| Remove a documented success response | Yes | Breaking |
| Change a default value | Yes | Silent behaviour change |
Diff tools such as oasdiff and openapi-diff apply rules like these automatically to two versions of a spec; graphql-inspector does the same for GraphQL schemas, and the Pact Broker's can-i-deploy does it at the level of verified consumer contracts.
5. Wire it into the pull request
For an API with an OpenAPI contract, the CI shape looks like this:
- A pull request opens.
- CI runs the schema-test suite against the branch's build.
- CI diffs the branch's OpenAPI file against
main's. - The diff classifies each change as safe or breaking.
- Breaking changes require an explicit, reviewed override (for example a
breaking-change-approvedlabel and a version bump). - On merge, the spec is published to wherever consumers read it (a registry, a portal, or the repo).
- Consumer teams are notified of the new version.
git show origin/main:openapi.yaml > /tmp/base.yaml
oasdiff breaking /tmp/base.yaml openapi.yaml --fail-on ERR
This turns contract drift from a gradual decay into a discrete, reviewable event. For pipeline recipes see API test automation with CI/CD.
API contract testing with Postman
Postman can do schema-first checks inside a collection. A test script validates the response body against a JSON Schema:
// Postman test tab
const orderSchema = {
type: 'object',
required: ['id', 'status', 'total'],
properties: {
id: { type: 'string' },
status: { type: 'string', enum: ['pending', 'paid', 'shipped', 'cancelled'] },
total: { type: 'number', minimum: 0 },
},
};
pm.test('response honours the Order contract', () => {
pm.response.to.have.status(200);
pm.response.to.have.jsonSchema(orderSchema);
});
Two limits are worth knowing. The schema is usually copied into the test, so it can drift from the OpenAPI file unless you load it from the spec. And Postman checks the provider against a schema; it does not capture consumer expectations and verify them against the provider, which is what makes a contract consumer-driven. Teams that need that add Pact. The Pact vs Postman post goes deeper.
Contract tests for event-driven APIs
REST and GraphQL are not the only contracts. Event-driven systems (Kafka, RabbitMQ, SNS) need contract testing too:
- A schema registry (Confluent Schema Registry, Apicurio) stores the schema of each event type.
- Producers register a schema version when publishing; consumers read with the version they understand.
- Compatibility modes (backward, forward, full) decide which schema changes the registry accepts.
- AsyncAPI documents the channels and messages; Pact's message pacts cover consumer-driven checks for events.
API contract testing tools
The tool landscape splits into three groups, and most teams use one from each:
- Consumer-driven: Pact (with the open-source Pact Broker or PactFlow) and Spring Cloud Contract.
- Schema-first and spec-driven: Specmatic, Schemathesis, Dredd, Prism, Microcks, and spec-driven test platforms.
- Spec quality and diffing: Spectral for linting;
oasdiff,openapi-diffand Optic for breaking-change detection.
Free PDF + code examples
OpenAPI to Test Generation Template Pack
Go from OpenAPI spec to full test coverage. Includes sample specs, example generated tests, edge case patterns, and CI/CD integration guides.
Download FreeThe full side-by-side comparison, with selection criteria, Java options and open-source picks, is in contract testing tools compared, so it is not repeated here.
Where generation fits. Hand-writing schema checks for a few hundred endpoints is the part teams never finish. Spec-driven generation reads the OpenAPI document and produces positive, negative and boundary tests per operation, so the schema-first baseline exists from day one; how AI generates API tests from OpenAPI explains the mechanics. Shift-Left API is one example of this approach: it imports an OpenAPI, Swagger, WSDL or GraphQL contract, plans tests deterministically from the spec without needing an AI key, and adds field and type checks derived from each response schema. When a spec is re-imported it diffs the new version against the current endpoints, classifies each change as breaking or non-breaking, and holds it for an Approve & Import or Reject decision; approving creates new endpoint versions and does not edit existing tests. Passing runs are also checked for responses that no longer match the documented contract (a missing required field, a changed primitive type, an undeclared enum value). It is a schema-first tool, not a Pact broker. See API contract testing with Shift-Left API.
Measure what you cover. Track the share of endpoints, methods and documented status codes that have contract checks, and do not let new endpoints ship without them. How to measure API test coverage defines the dimensions, and the free OpenAPI coverage tool scores a spec in your browser.
Common mistakes and best practices
Mistakes that make contract testing fail
- "We'll write the OpenAPI later." Contract tests need a contract. A spec written after the code documents whatever the code happens to do.
- Trusting the happy path. A suite that only covers
200responses misses half the contract. Every documented error response needs a test, and an undocumented one is a spec bug. - No diff-based gating in CI. If a breaking change can reach
mainwithout a reviewed override, the rest of the apparatus does not matter. - Pact without a broker. Pact's value comes from centralizing consumer expectations and
can-i-deploy. Without a broker you are running extra tests that nobody consults before a release. - Snapshots that update on every PR. If
--updateis the reflex response to a red snapshot, the snapshot is no longer an assertion. - Contracts that mirror the whole response. A consumer contract should list only the fields that consumer reads. A pact that copies the provider's full schema fails every time the provider adds a field, and teams start ignoring it.
- Flaky data. Contract tests that depend on specific database rows or shared environments fail for reasons unrelated to the interface. Use provider states, ephemeral data and mocks, and assert shape rather than specific values.
Best practices
- Treat the contract as code. Review contract changes in pull requests, version them with the implementation, and require an API owner's approval on spec changes.
- Use schema-first as the baseline and consumer-driven where it pays. Start with the spec you have. Add Pact for the relationships where a break is expensive: payments, authentication, partner-facing APIs.
- Gate merges, not just deployments. A contract failure found at deploy time has already cost a review cycle.
- Automate what the contract implies; hand-write what it cannot. Generate schema validation, per-field negative tests, status-code reachability and the breaking-change diff. Write business scenarios that cross endpoints, multi-step flows and domain edge cases by hand.
- Version and deprecate explicitly. Use
deprecated: truein OpenAPI, give consumers a migration window, and run old and new contract versions side by side during the transition. - Mark only what you guarantee as
required. Every required field is a promise to every consumer. Think before addingadditionalProperties: falseto response schemas: it catches accidental data leaks, but turns every new field into a breaking change for strict validators. - Keep PR feedback fast. Shard contract tests in parallel and keep the pull-request run in single-digit minutes, or developers will route around it.
- Remember what contract tests do not cover. They catch interface drift. Business-logic bugs, slow endpoints and authorization flaws need functional, performance and security tests alongside them.
A minimal adoption path: commit and lint a spec for one pilot service, generate or write its schema-first suite, add the breaking-change diff to its pull requests, gate merges on both, then expand service by service. Add a Pact Broker when a second consumer team's expectations start diverging from the published spec.
More from the OpenAPI, Contract & Schema Testing hub
Every guide in this hub, grouped by format. The OpenAPI, Contract & Schema Testing hub page has the full list.
Guides and concepts
- API Versioning Testing: Backward Compatibility Checks in CI
- Swagger to OpenAPI Migration Guide for Testing Teams (2026)
How-to tutorials
Tools and comparisons
- Postman vs OpenAPI: Collections vs Spec-Driven Testing
- 8 Best OpenAPI Testing Tools in 2026 (Compared)
API contract testing FAQ
What is an example of API contract testing?
A mobile app reads the fields id, status and total from GET /orders/{id}. The OpenAPI contract marks those three fields as required and gives their types. A contract test calls the endpoint and validates the response against that schema. If a developer renames total to amount, the response no longer matches, the test fails in the pull request, and the app never sees the broken payload.
What is the difference between contract testing and unit testing?
A unit test checks one function or class inside a service, with its collaborators mocked, and knows nothing about the network interface. A contract test checks the boundary between two services: that the requests and responses on the wire still match the agreed contract. A service can have 100% passing unit tests and still break every consumer by renaming a JSON field; only a contract test notices.
What is the difference between contract testing and API testing?
API testing is the broad category: functional behaviour, business rules, error handling, security and performance of an API. Contract testing is a narrow slice of it that only asks whether the interface still matches the published agreement: fields, types, required properties, status codes and headers. It does not check that an order was actually created correctly, which is why teams run both.
How do you do API contract testing?
Pick the contract artifact (an OpenAPI spec for schema-first, or Pact files for consumer-driven), store it in version control, and lint it. Generate or write tests that validate every documented response against the schema, run them in CI on every pull request, diff the spec against main to classify breaking changes, and block merges that break the contract without an explicit, reviewed version change.
Can you do API contract testing with Postman?
Partly. A Postman test can validate a response body against a JSON Schema with pm.response.to.have.jsonSchema(), which gives you provider-side schema checks per request. Postman does not record consumer expectations or verify them against a provider the way Pact does, so it covers schema-first checks but not consumer-driven contracts. Teams often pair Postman or a spec-driven tool with Pact.
Do I still need contract testing if I have integration and end-to-end tests?
Yes. Integration and end-to-end tests run late, need shared environments and get slower as the number of services grows. Contract tests verify each service's interface on its own, in seconds, inside the pull request. In most pipelines contract tests take over the interface checks, and end-to-end tests stay for a few critical user journeys.
Sources and further reading
- OpenAPI Specification: the normative spec for describing HTTP APIs.
- JSON Schema specification: the schema language OpenAPI 3.1 response schemas use.
- Pact documentation: the consumer-driven contract testing toolchain.
- Pact Broker: can-i-deploy: the deployment gate built on verification results.
- AsyncAPI specification: contracts for event-driven APIs.
- Spectral: the OpenAPI linter used as a pull-request check.
- oasdiff: breaking-change detection between two OpenAPI documents.
- Ajv: the JSON Schema validator used in the code examples.
Key takeaways
- API contract testing checks one thing, whether the interface still matches the published agreement, and checks it on every pull request.
- Schema-first (OpenAPI) is the cheapest baseline and covers unknown consumers; consumer-driven (Pact) adds sharper protection for the relationships where a break is expensive.
- A useful suite validates every documented response, including error responses, and pairs it with a breaking-change diff that blocks unreviewed contract changes.
- Contract tests complement unit, functional, performance and security tests; they do not replace them.
Want to see a schema-first contract suite generated from your own OpenAPI spec, with breaking-change review on every re-import? Start a free trial and import a spec.
Continue learning
Go deeper in the Learning Center
Hands-on lessons with runnable code against our live sandbox.
Most API bugs live in input validation. Here's how to test it systematically.
2xx means success, 4xx means you messed up, 5xx means they messed up — but the details matter. Here's the list every tester should know by heart.
Ready to shift left with your API testing?
Import your OpenAPI spec and get a runnable test suite, in our cloud or on your own infrastructure.