Quickstart¶
From zero to a validated contract that detects breaking changes: about five minutes. Everything runs locally with no account, no registry, nothing to sign up for.
1. Install Pacto¶
Or via Go:
See Installation for other methods and for what to do if the script exits with Failed to fetch latest version — that is the anonymous GitHub API rate limit, not a broken release.
2. Create a contract¶
Run this from an empty directory and stay there through step 5; step 6 moves inside my-service.
This writes three files: pacto.yaml (the contract), interfaces/openapi.yaml (a placeholder API spec) and configuration/schema.json (a placeholder config schema). The argument is the service name, not a path. It becomes the directory and service.name, so pacto init fleet/checkout creates a bundle that fails validation.
Edit my-service/pacto.yaml and set service.version: 1.0.0. Then replace my-service/interfaces/openapi.yaml with an API that has required response fields:
openapi: "3.0.0"
info:
title: my-service
version: 1.0.0
paths:
/orders/{id}:
get:
summary: Get an order
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
required: [id, total]
properties:
id:
type: string
total:
type: number
3. Validate¶
Validation checks three things: the file's shape, that fields agree with each other (a capability must bind to a declared interface, for example) and your policies. See the Contract Reference for the full rules.
4. Add consumers¶
Two services depend on my-service, each with a different compatibility range. Copy the bundle twice and declare the dependencies:
In checkout/pacto.yaml, set service.name: checkout and replace the commented-out dependencies: block with:
In reporting/pacto.yaml, set service.name: reporting and use a wider range:
checkout accepts ^1.0.0 (1.x only), reporting accepts >=1.0.0 (1.x and above). This difference is what makes the next step interesting.
5. Introduce a breaking change¶
Copy my-service to v2 and delete the total field from the response schema:
In v2/pacto.yaml, set service.version: 2.0.0. In v2/interfaces/openapi.yaml, remove total from required and from properties so the schema becomes:
6. Diff the two versions¶
From inside my-service, compare the original contract against v2:
$ pacto diff . ../v2
Classification: BREAKING
Changes (3):
[NON_BREAKING] service.version (modified): service.version modified [1.0.0 -> 2.0.0]
[BREAKING] openapi.paths[/orders/{id}].methods[GET].responses[200].content.application/json.schema.properties.total (removed): openapi.paths[/orders/{id}].methods[GET].responses[200].content.application/json.schema.properties.total removed [- map[type:number]]
[BREAKING] openapi.paths[/orders/{id}].methods[GET].responses[200].content.application/json.schema.required[total] (removed): openapi.paths[/orders/{id}].methods[GET].responses[200].content.application/json.schema.required total removed [- total]
breaking changes detected
$ echo $?
1
The classification is BREAKING and the exit code is 1. That is the CI gate: pacto diff exits non-zero when the change breaks consumers, so a branch-protection rule that requires passing checks blocks the merge.
7. Find the affected consumers¶
pacto impact takes that diff, finds every service that depends on my-service and tells you which ones break:
$ pacto impact . ../v2 --local ..
Impact: my-service 1.0.0 -> 2.0.0
Classification: BREAKING
Breaking changes: 2
Potentially breaking changes: 0
Affected consumers (2):
checkout direct confidence=contractual compat=incompatible owner=my-team
reporting direct confidence=contractual compat=compatible owner=my-team
checkout and reporting declare the same edge to the same service and get opposite verdicts. checkout pins ^1.0.0 (1.x only) and breaks. reporting accepts >=1.0.0 (any major) and survives. A catalog that records only "checkout depends on my-service" cannot tell those two apart; Pacto can, because the contract carries the compatibility range.
confidence=contractual means the dependency is declared with a usable range and nothing observed it at runtime. direct means each consumer declares my-service itself rather than reaching it through another service.
Optional: publish to a registry¶
The steps above need no registry. To see the full round trip, start a local registry and push the contract:
docker run -d --rm -p 127.0.0.1:5001:5000 --name pacto-registry registry:3
pacto push oci://localhost:5001/demo/my-service-pacto -p .
Port 5001 because macOS binds 5000 for AirPlay. 127.0.0.1: because the registry accepts anonymous writes.
Read it back without pulling:
Or pull the bundle:
Clean up:
For a real registry like GHCR, you need pacto login with a personal access token. See Installation for the supply-chain guarantees and CI integration for what to commit to your repository.
What to do next¶
| Goal | Guide |
|---|---|
| Put it in your repository | CI integration |
| Understand every contract field | Contract Reference |
| See contracts for real services | Examples |
| Explore contracts visually | Run pacto dashboard or pacto tui |
| Check what a running service does on the network | Runtime conformance |
| Check a service in CI or on your laptop | pacto run |
| Check every service on a Docker host or server | pacto agent |
| Runtime compliance in Kubernetes | Kubernetes Operator |