An API is not defined solely by its PHP controller or a published specification. It is also defined by the expectations that other systems have already incorporated: a route, a field name, an HTTP status code, an error format, or the order required to paginate through results. Contract testing for PHP APIs turns those expectations into automated checks before merging code or deploying it.
The goal is not to prevent all evolution. It is to detect whether a change alters an agreement observable by a consumer and make a conscious decision: maintain compatibility, introduce a transition, or version the interface. This is especially important for internal APIs with multiple teams, B2B integrations, and asynchronous flows where failure can appear hours after release.
What contract testing solves and what it does not replace

A contract test checks that provider and consumer agree on an interaction: given a valid request, the provider produces a response with agreed structure, types, and rules. Conversely, a consumer can declare which requests it needs, and the provider verifies that it can handle them.
This approach detects incompatibilities that unit tests often miss. A unit test may confirm that a serializer returns customer_id; it does not prove that the consumer still understands that field if it previously expected customerId. A local integration test may cover the endpoint, but it does not necessarily capture the real assumptions of each integration.
They do not replace other controls:
- Unit tests, for domain rules, validation, and transformations.
- Integration tests, for databases, queues, cache, authentication, or connected services.
- End-to-end tests, for complete critical journeys in controlled environments.
- Security and performance tests, for authorization, abuse, data exposure, latency, and capacity.
- Production observability, to detect consumers that still use behaviors being retired.
A contract does not certify that a response is correct for the business either; it certifies that it preserves the declared shape and semantics. It should therefore be accompanied by examples that express relevant rules, not only empty schemas.
What is part of an API contract
The contract is any behavior that a consumer can observe and depend on. Limiting it to the JSON of a successful response excludes the most frequent breakages. For each operation, it is advisable to agree on at least the following elements.
- Request: method, route, query parameters, headers, body, required fields, formats, and limits.
- Response: HTTP status code, relevant headers, structure, types, optional fields, fields that allow null values, and date, currency, or identifier formats.
- Errors: status codes, error body, stable functional code, and the conditions that generate them. Text for people can change; a code such as
validation_failedis better suited to automation. - Pagination and filtering: meaning of
limit, cursor or page, ordering stability, representation of the next cursor, and handling of empty sets. - Authentication and authorization: supported mechanism, required headers, scopes or permissions, and the distinction between invalid credentials, missing credentials, and denied access.
- Events and webhooks: event name, payload version or schema, signature, retries, event identifier, non-guaranteed ordering, and idempotency expectations.
Whether a property is required and whether it allows null are independent rules. A field can be required and allow null, optional and not allow it when present, or optional and allow it if present. Likewise, an omitted property, a property present with null, and a property with an empty string are different states. If the consumer interprets them differently, the contract must express and test them.
Small changes that can break consumers
A modification may seem harmless from the provider's perspective and be incompatible with a generated client, a strict validator, or business logic. Changing an integer to a string, for example 42 to "42", breaks comparisons and schemas. Making a field optional does not by itself determine whether it accepts null: the first rule defines whether the property must be present, while the second defines the valid values when it is included.
Other risky changes include returning 200 where 201 was previously returned, replacing an empty list with null, changing the precision of a decimal, renaming an error code, or changing pagination order without notice. Adding a field is usually compatible for tolerant readers, but it is not if a consumer validates a closed schema or calculates signatures over the full body.
Compatibility depends on the actual agreement, not on an isolated rule. It is advisable to classify each change according to known consumers, declared tolerance, and flow criticality. If that information is unknown, it must be treated as a risk rather than a favorable assumption.
Choosing between a specification, consumer contracts, or both
An interface specification, such as an OpenAPI description, works well as a common source for routes, operations, parameters, schemas, and responses. It can be validated in the pipeline to detect incompatible changes against a reference version. It is useful when there are many consumers or when clients and documentation are generated from the same definition.
However, a schema does not always capture what matters to each consumer: combinations of filters, a specific error under a business condition, or a dependency on an example value. Consumer-driven contracts declare specific interactions that each consumer needs. The provider verifies them against its implementation.
Using both levels is often reasonable: the specification governs the overall surface, and consumer contracts cover high-value flows or semantics that are difficult to reduce to a schema. There must be clear ownership for each artifact. If a specification is not updated when the code changes, it stops being a source of truth and becomes fictional documentation.
Representative examples and edge cases
A contract example should contain realistic structural data, not production data. For an order resource, include a case with items, an empty one if valid, identifiers in the agreed format, and complete dates with a time zone when that is the convention. Add cases for denied authorization, failed validation, a nonexistent resource, and final pagination.
Avoid pinning irrelevant details that legitimately change, such as a random identifier, the current time, or JSON property order. Use precise assertions for stable elements and explicit tolerance for variable ones. Each example must answer a known need; a huge collection of invented responses increases maintenance without increasing confidence.
Incremental implementation in an existing PHP API
You do not need to model the entire API before gaining value. Start with an inventory of consumers: internal applications, B2B clients, batch processes, mobile applications, automations, and webhook receivers. Record the owner, contact channel, operation used, criticality, and ability to update.
Then prioritize endpoints that create or modify resources, authenticate users, feed financial processes, or trigger automations. Establish a baseline of their current behavior through a reviewed specification and tests against a reproducible API instance. In PHP, the test must exercise the application's real HTTP layer, not invoke a service class directly: the contract includes routing, middleware, serialization, and exception handling.
POST /api/orders
Authorization: Bearer token
Content-Type: application/json
{"items":[{"sku":"ABC-1","quantity":2}]}
201 Created
{"id":"ord_123","status":"pending","items":[...]}The previous example is useful only if it is accompanied by rules: which fields are required, whether id is always a string, which errors an invalid SKU returns, and whether the initial status is guaranteed. Those rules are what must be turned into assertions.
Validation in continuous integration and before deployment
The pipeline must fail before merging if the implementation violates approved contracts. A practical flow includes running unit tests, bringing up controlled dependencies, starting the PHP API with test configuration, and validating the specification, provider contracts, and representative consumer contracts. Tests must use isolated and deterministic data so that a failure is reproducible.
In a change request, also compare the proposed specification with the published version to flag route removals, stricter presence requirements, changes to null acceptance, type changes, and removed responses. Diagnostics must identify the operation, interaction, and violated rule; a simple schema error requires too much investigation.
Before deployment, run the same suite against the artifact that will be released, not against a different build. After deployment, monitor error codes, client-reported deserialization failures, version usage, and traffic to deprecated routes. Pre-deployment validation reduces risk; it does not replace confirming behavior under real traffic.
Compatibility, deprecation, and safe retirement

When a change is not compatible, prefer an explicit transition. You can add a new field while retaining the previous one, introduce a new operation or version, and communicate a retirement date backed by usage signals. Deprecation is an operational period with owners, communication, and measurement; it is not just a note in the documentation.
Do not retire a behavior because a deadline has passed if you cannot identify pending consumers or if the flow is critical. Where possible, expose controlled warnings and metrics to locate old usage without changing the response. Gradually rolling out a new version makes it possible to observe errors and correct contracts before extending the change.
The most common mistakes are testing only successful responses, modeling human messages instead of error codes, assuming that all clients ignore new fields, and failing to involve real or representative consumers. Contract tests provide value when they reflect agreements maintained by both parties and run as a normal delivery condition.



