Contract Compatibility Testing · OpenAPI Diff · Breaking-Change Detection ·
can-i-deployDeploy Gate · API Governance & Linting · Pact-File Ingest · Live-Traffic Capture · Spec-vs-Production Conformance · GitHub PR Checks
SpecShield™ is contract compatibility testing for APIs — catch breaking changes before they reach your consumers, and gate every deploy with can-i-deploy. (Contract compatibility testing is also known as bidirectional contract testing.)
npm install -g specshield
specshield compare base.yaml target.yaml --fail-on-breakingNo account, no config, no broker to run or host. Language-agnostic. Local mode never uploads your specs.
What the CLI does:
- Diff two OpenAPI specs and fail CI on breaking changes. (Free)
- Contract compatibility testing with
can-i-deploy— block a deploy that would break a consumer. (Team) govern— lint your spec against OWASP + design rules and gate the PR on violations. (Team)bdct capture from-har— turn recorded traffic into an accurate consumer contract (no Pact DSL). (Free)bdct verify-provider— prove the running provider actually matches its OpenAPI spec. (Free)
Get started
Why SpecShield
Commands — local & hosted compare
- Local Compare · Who does a change break? (usage-aware compare) · Safe removal: deprecate → drain → remove · Break → fix:
specshield fix· CI gate:specshield gate· Remote Compare · Comparison History · Share a Comparison · GitHub App PR Checks
Governance — lint & gate
Contract Compatibility Testing — the bdct commands
- Overview & full command reference
bdct capture from-har— HAR → consumer contractbdct verify-provider— spec-vs-production conformance
CI/CD
Reference
Project
This CLI is one surface of the SpecShield™ platform. The same contract-compatibility and governance engine powers every entry point — they share one project file (.specshield.yml), one API key, and one set of audit events. Adopt the one that fits your stack today and grow into the others:
| Surface | Best for |
|---|---|
| GitHub App — github.com/apps/specshield-deploy-gate | Zero-config PR checks. Install once; every PR gets a check run and a diff comment. |
GitHub Action — specshield-io/bdct-action@v1 |
Pinned, reproducible contract-compatibility + governance gate in any GitHub workflow. |
| CLI (this package) | Any CI — GitLab, Jenkins, CircleCI, Bitbucket, Drone — or your laptop. |
| IntelliJ plugin — JetBrains Marketplace | Inline spec findings, one-click auto-fixes, compare & explain — inside IntelliJ IDEA, WebStorm, PyCharm and more. |
MCP server — specshield-mcp-server |
Give Claude, Cursor, and other AI agents the same deploy gate. |
Plus a hosted dashboard at specshield.io — compatibility matrix, consumer registry, append-only audit log, and API governance scorecards (OWASP + design-rule linting, standard rule packs, waivers, SARIF/JUnit/HTML reports).
🤖 AI agents: the MCP server answers "is it safe to ship this API change to my consumers?" right inside your agent — analyze-only, it never touches your code. Same job, two entry points: this CLI for CI/CD, the MCP server for agents.
See how the platform stacks up against Pactflow, Pact, Optic, oasdiff, Bump.sh, Stoplight, and API gateways (Apigee, Azure API Management) at specshield.io/alternatives.
APIs break silently. Your users feel it first.
- A backend rename of
status→paymentStatus— the mobile app crashes for 10,000 users. - An endpoint gets removed — the frontend breaks with no error in your logs.
- A required field changes — partner integrations fail at 2am.
- You find out from an angry customer, not from CI.
It's rarely guarded against: in our survey of public repos that publish OpenAPI specs, 95% had no breaking-change check in CI at all.
Manual review doesn't catch it. Integration tests run too late. You need a contract between your services — and something that enforces it on every PR and every deploy.
Think of SpecShield as unit tests for your API contracts.
- Consumers declare (or record) what they expect from the provider.
- The provider proves it satisfies those expectations before deploying.
- The pipeline blocks the deploy if anything breaks.
Consumer publishes contract → Provider verifies it → CI passes or blocks
No runtime surprise. No production incident. No 3am page.
Catch a breaking spec change in CI:
$ specshield compare base.yaml target.yaml --fail-on-breaking
✖ BREAKING CHANGES DETECTED
1. DELETE /users endpoint removed
2. POST /payments — "amount" changed from optional to required
3. GET /orders/{id} — "status" type changed: string → object
Breaking changes : 3
Modifications : 1
Additions : 2
CI Result: FAILED · Exit code: 1Catch the running provider drifting from its own spec:
$ specshield bdct verify-provider --spec api/openapi.yaml --base-url https://staging.payments.acme.com
Provider conformance — spec vs https://staging.payments.acme.com
────────────────────────────────────────────────────────────
PASS GET /health (200)
FAIL GET /payments/{paymentId} (200)
/amount: must be number
PASS GET /orders (200)
────────────────────────────────────────────────────────────
2 pass · 1 fail · 0 error · 0 skip (3 probes)Turn recorded test traffic into an accurate consumer contract — no Pact DSL:
$ specshield bdct capture from-har --in checkout-tests.har --base-url https://api.acme.com --out consumer.yaml
✔ Wrote consumer.yaml (2 endpoints, 3 ops from 17/24 entries)That output just saved you a production incident.
No account, no login, no config file for compare. Just run it.
npm install -g specshield
specshield compare base.yaml target.yaml --fail-on-breakingWorks with OpenAPI 3.0 / 3.1 and Swagger 2.0, in YAML or JSON.
For the bdct commands, run the wizard once and never re-type the same flags again:
specshield initIt auto-detects your spec, service name (from package.json / pyproject.toml / pom.xml / go.mod), git branch, and environment; writes .specshield.yml; and (optionally) seeds a GitHub Actions workflow. Every subsequent specshield bdct … invocation reads this file, so your CI commands collapse to:
specshield bdct publish-provider --version $GITHUB_SHA
specshield bdct can-i-deploy --version $GITHUB_SHAQuiet install for CI / Docker: the post-install welcome banner auto-skips in CI (
CI,GITHUB_ACTIONS,BUILDKITE,CIRCLECI,GITLAB_CI,JENKINS_URL,TRAVIS,TF_BUILD). To silence it on a workstation too:export SPECSHIELD_NO_BANNER=1.
An illustrative scenario:
The
payment-serviceteam merged a change that renamedstatustopaymentStatus. Thecheckout-uiteam wasn't notified.Without SpecShield, this would have reached staging, broken checkout for every user, and triggered an incident at 2am.
With SpecShield, the provider's CI published the new spec via
specshield bdct publish-provider. The compatibility engine ran againstcheckout-ui's published contract and the mismatch was caught immediately:● MISSING_FIELD at $.status expected: "CREATED" → actual: null
can-i-deployreturned exit code1. The broken build never deployed. Zero users affected.
- Pull-request validation — catch breaking changes before merge with
specshield compare. - CI/CD gating — exit code
1stops the pipeline automatically. - Microservices contract safety — consumers publish what they expect, providers verify they deliver it, no cross-team surprises.
- API drift tracking — track how your specs change over time across the platform; know what changed, when, and by whom.
- API governance & linting — score every spec against OWASP + design rules, bring your own Spectral ruleset, and gate PRs on violations with
specshield govern. The same engine also powers the IntelliJ plugin, the GitHub Action, the MCP server, and the dashboard. - Provider conformance — make sure your running service actually matches its published OpenAPI spec, not just on paper.
- Pact-free consumer contracts — record real traffic with
capture from-harand get an OpenAPI consumer contract without writing a single line of Pact DSL.
No account needed.
# Basic compare
specshield compare base.yaml target.yaml
# Fail CI on breaking changes
specshield compare base.yaml target.yaml --fail-on-breaking
# JSON output
specshield compare base.yaml target.yaml --json
# Save to file
specshield compare base.yaml target.yaml --output result.json
# Ignore a specific change
specshield compare base.yaml target.yaml --ignore "DELETE /admin removed" --fail-on-breakingA spec diff tells you what changed. Your access logs know who still calls it.
specshield usage joins the two, so every breaking change shows the clients it would
break — and a removal that nobody calls stops failing the build.
✖ BREAKING CHANGES
● DELETE /orders/{orderId}/receipt method was removed
↳ impact: 1 client · 134 calls · last seen 72 min ago
MyShop-iOS/5.2 100% of calls 134 · last seen 72 min ago
● Parameter "expand" (query) was removed from GET /orders
↳ impact: 1 client · 200 calls sending "expand" · last seen 2 h ago
okhttp/4.12 33% of calls 200 · last seen 2 h ago
✔ BREAKING BUT UNUSED — safe to ship
0 calls between 2026-10-05 and 2026-10-25; these do not fail the build
○ GET /legacy-report was removed
1. Turn access logs into a usage summary (runs locally — raw logs never leave the machine):
specshield usage import /var/log/nginx/access.log* --spec api/openapi.yaml
# → usage.json: per endpoint × client, call counts, query-parameter names, first/last seen- Formats: nginx/Apache combined & common, AWS ALB, and JSON lines (nginx JSON
log_format, Kong file-log, API Gateway, Caddy, Traefik, Envoy, Google Cloud LB, ECS, and Kubernetes API-server audit logs). Auto-detected;.gzfiles and stdin (-) work. Force one with--format combined|alb|json. - Kubernetes: point it at the API-server audit log and the cluster's
swagger.json, and identify clients by their authenticated identity:specshield usage import audit.log --spec swagger.json --client field:user.username. Each request is counted once (final audit stage only). - Clients are identified by User-Agent by default (
MyShop-iOS/5.2.1→MyShop-iOS/5.2). Use a better signal if you log one:--client field:consumer.username(JSON logs),--client extra:1(first quoted field after the User-Agent in combined logs), and prefixhash:for API keys —--client hash:extra:1stores a one-way hash, never the key. - Privacy: only aggregates are stored — path templates, counts, timestamps, and query parameter names. No IDs from concrete paths, no query values, no IPs, no bodies.
--specmaps/v1/orders/123onto/orders/{orderId}exactly as your spec defines it (base paths fromservers/basePath, or--base-path). Without it, IDs are guessed.- Add more days later with
--append(a file imported twice is detected and skipped); limit the window with--since 30d. - One load balancer, several APIs? ALB and JSON logs record the host; pass
--host api.example.com(repeatable,*.example.comglobs) so another API's traffic doesn't count as usage of this one. The import warns when it sees mixed hosts. - Import logs from every server. If several instances each write their own log, import all of them — logs from one node make the others' clients look like non-users.
2. Compare with impact:
specshield compare base.yaml target.yaml --usage usage.json --fail-on-breaking- Endpoint and method removals, body/response field changes → the clients calling that
operation. Access logs don't show which response fields a client reads, so for field
changes this is "clients that may be affected", and the report says so. Request-body
changes (a field removed or made required) count only calls that send a body —
GETtraffic can't be affected by them. - Query parameters are judged exactly: a removed parameter affects only clients that send it; a parameter that became required affects only clients that omit it.
- Zero calls → "breaking but unused": listed separately and excluded from
--fail-on-breaking. SpecShield only does this when the evidence is good enough — at least--usage-min-days(default 7) of logs, the newest no older than--usage-max-age(default 30) days, calls that match the base spec, and at least--usage-min-coverage(default 25) percent of the spec's operations seen in the logs — fewer usually means the logs are filtered, sampled or from one server of several. Otherwise the change stays breaking and the report says why.--no-usage-downgradekeeps everything breaking. - Object-typed query parameters (e.g. a Spring
@ModelAttributeDTO) are sent as their properties, not under their own name, so they're judged by who calls the endpoint. - Leave health checks and bots out:
--usage-ignore-client 'kube-probe/*' --usage-ignore-client healthcheck. --jsonaddsimpactto each change, plusunusedBreakingChanges,summary.unusedBreakingand ausageblock. Without--usagethe JSON is unchanged.
3. Find dead and undocumented endpoints:
specshield usage show usage.json --spec api/openapi.yaml
# NEVER CALLED IN THIS WINDOW → candidates for deprecation
# CALLED BUT NOT IN THE SPEC → shadow endpoints your spec is missingDeprecation usually means a spreadsheet, a calendar reminder and hope. Mark it in the spec instead, and SpecShield tracks who still calls it until it is safe to delete.
1. Mark what is going away — an operation, a parameter, or a schema property:
paths:
/v1/pets:
get:
deprecated: true
x-sunset: 2026-12-31 # optional: the date you plan to remove it
/pets:
get:
parameters:
- { name: sort, in: query, deprecated: true, schema: { type: string } }
components:
schemas:
Pet:
properties:
tag: { type: string, deprecated: true }2. See who still uses it (same usage.json as above):
specshield deprecations api/openapi.yaml --usage usage.json --csv callers.csv SpecShield™ Deprecations — Pets API
usage.json · 2026-08-26 → 2026-10-24 (59 days) · safe after 30 quiet days
3 deprecated items · 1 still called · 1 quiet · 1 safe to remove
✖ STILL CALLED (1) — not safe to remove
GET /v1/pets · sunset 2026-09-30 PASSED 25 days ago
2 clients · 840 calls · last call 2 days ago weekly ▇█▇▅▃▁··▂
ShopApp-iOS/5.2 820 last 2 days ago → ios-team@shop.example
partner-x/1.0 20 last 41 days ago
○ QUIET (1) — no calls, keep watching
GET /pets query parameter "sort"
no calls for 11 of the 30 days needed
✔ SAFE TO REMOVE (1)
GET /pets/{id}/old-photo
last called 45 days before the newest log line
| Status | Meaning |
|---|---|
| still called | a client called it in the last 7 days of the logs |
| can't verify | a header or body field: logs don't show who sends or reads it, and the operation is still called |
| quiet | draining — no calls for a week or more, but not for --quiet-days (default 30) yet |
| safe to remove | no calls for at least --quiet-days |
| no verdict | the usage file can't support one (stale, filtered, no timestamps) — the report says why |
-
Query parameters are judged by who sends them; operations by who calls them.
-
A deprecated property of a shared schema (
$ref'd by many operations) is one row, judged by the traffic of every operation that uses it. -
Who to tell: map clients to owners in
.specshield.ymland the report (and--csv) names them:usage: file: usage.json quietDays: 30 contacts: ShopApp-iOS/*: ios-team@shop.example # * globs; otherwise the exact client id partner-x/1.0: "#partner-x"
-
--fail-on past-sunsetexits 1 when anything past itsx-sunsetdate is still called;--fail-on in-usewhen anything deprecated is still used.--jsonfor scripts.
3. Remove it — the compare gate knows it was deprecated:
specshield compare base.yaml target.yaml --usage usage.json --fail-on-breaking- Removing a deprecated item that is still called fails, listing the callers:
↳ deprecated (sunset 2026-09-30), but still called — tell the clients above before removing it. - A deprecated item with no calls for
--usage-quiet-days(default 30) has drained: its removal passes, like an unused change. - A removal that was never deprecated is flagged:
↳ removed without being deprecated first.
4. Track the drain week by week (Team plan): add --upload --org <orgKey> and every
run is stored on your dashboard (Deprecations page): the latest status per API, who
still calls what, weekly calls per item, and the history across uploads. When an item
becomes safe to remove, passes its sunset while still called, or is called again after
going quiet, SpecShield posts to Slack (subscribe a webhook to Deprecation update)
and emails whoever uploaded. Only the report is sent — counts per client, never logs.
# e.g. weekly in CI, after importing the week's logs
specshield deprecations api/openapi.yaml --usage usage.json --upload --org acme --api paymentsA blocked PR with no way forward is why teams switch gates off. specshield fix takes
the change that broke the gate and writes the non-breaking version of it — the
expand-and-contract edit you'd make by hand if you had the time.
specshield fix api/openapi.base.yaml api/openapi.yaml SpecShield™ Fix — Payments API 1.0.0 → 2.0.0
16 breaking changes · 14 fixed · 2 need a decision
Each fix was re-checked with the diff engine: it removes its breaking change and adds none.
✔ `status` was renamed to `paymentStatus`: return both, `status` deprecated
POST /payments, GET /payments/{id}
→ The server must return `status` with the same value as `paymentStatus`.
✔ Make `currency` optional (default "EUR")
→ Clients that don't send it get the default "EUR".
✔ Keep `amount` as integer; return the string value as `amountV2`
✔ Keep "ENABLED" as an allowed value of status (renamed to "ACTIVE")
✔ Keep GET /payments/{id}/receipt, marked deprecated
…
✖ NEEDS A DECISION (2)
● Type of "responses.200" changed from "array" to "object" in GET /payments
the list moved into `data` — one schema can't describe both shapes, so this needs a version layer (--spring)
✔ Wrote openapi.fixed.yaml (12 edits, comments and layout kept)
| The change | The fix |
|---|---|
| Field or parameter renamed | keep both names, the old one deprecated (a rename needs the same type and a shared word in the name) |
| Field, parameter, operation, response or schema removed | restore it, deprecated (--sunset 2027-03-31 adds x-sunset) — then specshield deprecations tracks it draining |
| Request field or parameter became required | optional again; the report says the server must default it |
| Field type changed | request: keep the old type · response: keep the old type, new type as <field>V2 |
Format only (integer → int64) |
keep the old format |
| Enum value removed | keep it (a value renamed while others stay is recognised) |
Body changed shape (list → { data: [...] }) |
no spec can say both → --spring |
- Nothing unverified is written. Every fix is applied and the diff engine re-run; a fix that leaves its breaking change or adds another is dropped and listed with the reason. Fixes that depend on each other (restoring a field brings back an enum value it needs) are applied in rounds, and the result may not contain any break the target didn't have.
- The fix is written to
<target>.fixed.yaml(or--out, or--writein place) as minimal edits: comments, ordering and quoting of every untouched line are kept, sogit diffshows only the fix.--dry-runwrites nothing;--jsonfor scripts. - Exit code
0when everything was fixed,1when something still needs a decision. - Each fix tells you what the server must do for the spec to be true — return the old field too, accept the old value, default a missing one.
Some changes can't be undone in the spec — a list wrapped in an envelope, a field that now
holds a string. --spring generates a Stripe-style version layer for a Spring Boot 3
service instead: the controllers speak only the new API; clients that don't send
X-API-Version: 2.0.0 get the old JSON, and their old requests are upgraded on the way in.
specshield fix base.yaml target.yaml --spring src/main/java --package com.acme.payments.compat- Generated:
ApiVersion_2_0_0.java(this change's rewrites, as data) plus a small fixed runtime — aResponseBodyAdviceand aRequestBodyAdviceon Jackson. No changes to your controllers. - Rewrites: rename a field, convert a scalar's type (
"1250"↔1250, only when the value converts exactly), map an enum value, default a missing request field, unwrap a list from its envelope — at any depth, inside arrays and maps. - Checked before it is generated: for every operation, a sample payload of the new shape is rewritten and validated against the old schema (and old requests against the new one). A rewrite that can't produce the old shape is not generated — e.g. a required field the new code no longer returns at all — and the report says why.
- Renames and enum mappings are inferred from the diff; they're marked
/* inferred rename — check */in the generated code. - Clients with no version header are treated as on the old version, so existing
integrations keep working without a change. Set the header name with
--version-headerorspecshield.compat.header; a laterfixrun adds anotherApiVersion_<v>class and the layer chains them. - Tested by compiling the generated code into a Spring Boot app and calling it as an old
and a new client (
test-harness/spring-compat/verify.sh), on Spring Boot 3.2 and 3.5.
compare diffs two files you choose. gate is the CI check: it decides which spec a change is judged against, which breaks the team has accepted, and whether failing is safe yet.
specshield gate openapi.yaml # in CI, or locally on your branchRight baseline, automatically. The spec is compared with the commit your branch started from (the merge base), never with the base branch's current tip and never with what's deployed. Against the tip, a branch that is behind sees every change merged since it forked as "removed", and a break that already landed fails every unrelated PR until someone notices — the fastest way for a gate to lose the team's trust. The baseline comes from the CI event:
| Where | Baseline |
|---|---|
GitHub pull_request |
merge base of the PR's base commit and HEAD |
GitHub merge_group (merge queue) |
the queue's base, so each PR is judged only on its own change |
GitHub push |
the commit before the push |
| GitLab merge request | CI_MERGE_REQUEST_DIFF_BASE_SHA |
| Bitbucket PR / anywhere else | merge base with the destination / origin's default branch |
--base <ref> overrides it. CI checkouts are usually shallow: the gate fetches the base commit and deepens history until the merge base is reachable (--no-fetch to forbid that, or use fetch-depth: 0). A PR that doesn't touch the spec passes without a diff.
Waivers with an owner, a reason and an expiry. Accepted breaks live in .specshield/waivers.yml, committed with the change they accept so the waiver is reviewed in the same PR:
specshield waivers add response-field-removed --operation "GET /pets/{id}" --field legacy_id \
--owner @alice --reason "Mobile 4.2+ stopped reading it; older builds unsupported" --days 30 --ticket PET-12
specshield waivers # list: owner, reason, days left; --check exits 1 on expired/invalid oneswaivers:
- change: response-field-removed # change type; "*" = any type on `operation`
operation: GET /pets/{id} # optional; "*" / "**" globs, "/pets/**" = any method
field: legacy_id # optional; name or dotted suffix
owner: "@alice"
reason: Mobile 4.2+ stopped reading it; older builds unsupported
created: "2026-10-27"
expires: "2026-11-26" # at most 90 days out (gate.maxWaiverDays)
ticket: PET-12Owner, reason (10+ characters) and expiry are required; a waiver missing one never applies. A waiver with an operation must cover every endpoint the change affects. After the expiry day the change blocks again and the output says whose waiver lapsed. Without operation/field a waiver covers every change of that type — a policy decision the expiry forces someone to revisit. The legacy ignore: list still works, but the gate reports what it skipped and how to move it to a waiver. The SpecShield GitHub App reads the same file with the same rules.
Shadow mode, then graduate. --shadow (or gate.mode: shadow) reports what the gate would have blocked and always exits 0. Add --upload (Team plan) and each run lands on Dashboard → Gate Readiness, which shows after two weeks: "Would have blocked 3 PRs — all 3 were real breaks. Safe to turn on blocking." A PR fixed after the warning, or accepted with a waiver, counts as a real break; anything merged with an unwaived break is yours to mark real or false alarm. Any false alarm, or any failure on a PR that didn't touch the spec, keeps it from graduating.
Merge queues. Add merge_group to the workflow triggers; the gate reports on the queue's commit with the queue's base as baseline.
# .github/workflows/api-gate.yml
on: [pull_request, merge_group]
jobs:
api-gate: # make this the required check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # shallow is fine; the gate fetches what it needs
- run: npx -y specshield@3 gate openapi.yaml --shadow --upload --org <org-key>
env: { SPECSHIELD_API_KEY: "${{ secrets.SPECSHIELD_API_KEY }}" }In GitHub Actions the gate also writes a job summary and annotations (--no-summary to skip). With --usage usage.json, breaking changes no client calls stop blocking (usage-aware compare). Exit codes: 1 when something blocks in blocking mode, 0 otherwise, 2 on errors. --json prints the full result: baseline, blocking and waived changes, and waiver-file health.
Sends your specs to SpecShield's hosted diff engine. Useful for keeping the
heavy comparison off your CI runner and for cross-validating against the
exact engine the dashboard uses. Requires an API key from a free account —
without one the CLI exits with code 2 (No API key found).
specshield compare base.yaml target.yaml --remote
specshield compare base.yaml target.yaml --remote --fail-on-breaking
specshield compare base.yaml target.yaml --remote --json --output result.jsonHistory note: set your key with
specshield login,--api-key, orSPECSHIELD_API_KEY. Each--remotecomparison is saved to your history, and the response includes ahistoryIdyou can hand tospecshield share.
Every comparison you run with --remote (and every one from the dashboard) is
saved to your account — useful for tracking API drift over time across team
members and machines. List them straight from the CLI:
specshield history # last 20 comparisons
specshield history --limit 50
specshield history --jsonThe dashboard at specshield.io/account/history shows the same data with per-comparison drill-down.
Example output:
Your recent comparisons
─────────────────────────────────────────────────────
482 3 breaking 2026-05-17 14:30 payment-v1.yaml → payment-v2.yaml
481 0 breaking 2026-05-17 11:02 user-api.yaml → user-api-updated.yaml
480 7 breaking 2026-05-16 18:55 billing-v3.yaml → billing-v4.yaml
Generate a public /r/<token> link for any comparison — paste it into Slack, a
GitHub PR comment, or a Jira ticket and anyone can view the diff without a
SpecShield account. You can also create links from the dashboard (open a
comparison at specshield.io/account/history
→ "Share").
Usage:
# Share an existing report by ID (from `specshield history`)
specshield share 482
# Compare two specs and share the result in one step
specshield share base.yaml target.yaml
# Time-limited link — expires in 30 days
specshield share 482 --expires 30 ✔ Share link ready
https://specshield.io/r/_Ru8OVubxY3r9zHOsylESaULphCqBYH5jTPYldSMU88
Expires: 2026-06-16T12:34:56Z
Links use a 256-bit random token (unguessable by enumeration). Revoke any time from the dashboard.
Automatic API contract checks on every pull request — no workflow YAML required.
Install once at Dashboard → GitHub Integration, choose your repos, done. Every PR that touches the OpenAPI spec gets:
- A GitHub check run visible on the PR, comparing the spec with the commit the branch started from (the merge base), not the base branch's moving tip
- A sticky PR comment with the breaking changes, any waived ones, and a ready-to-paste waiver
- Waivers read from
.specshield/waivers.ymlat the PR's head commit — the same file and rules asspecshield gate - Merge queue support: subscribe the App to Merge group events and the check runs on the queue's commit too
- Shadow mode for new repos: the check never fails, records what it would have blocked, and Dashboard → Gate Readiness tells you when it's safe to switch to blocking
Spec path, waiver file and shadow/blocking mode are set per repository in Dashboard → GitHub.
Static API governance: score your OpenAPI spec against OWASP + design rules and fail CI on violations. (Team plan and above — runs on SpecShield's hosted engine, so it needs an API key.)
# Lint against SpecShield's built-in governance rules
specshield govern api/openapi.yaml
# Lint against a Spectral-format ruleset (OWASP pack, your house style, …)
specshield govern api/openapi.yaml --ruleset .specshield/ruleset.yaml
# Gate CI: require a minimum score, and fail on warnings too
specshield govern api/openapi.yaml --min-score 80 --fail-on-warning
# Report only — print findings but never fail the build
specshield govern api/openapi.yaml --advisorySample output:
SpecShield™ Governance Report
─────────────────────────────────────────
error security-scheme-missing $.paths./payments.post
operation has no security scheme
fix: add a security requirement
info operation-operation-id $.paths./health.get
operation is missing an operationId
─────────────────────────────────────────
Score 74/100 · grade C · 1 error · 0 warning · 1 info
✖ FAIL — governance policy not met:
• 1 error-severity finding
Exit codes: 0 = passed the gate · 1 = failed the gate · 2 = error (invalid spec, missing token, or paid-plan required).
| Flag | Purpose | Default |
|---|---|---|
--ruleset <path> |
Lint against a Spectral-format ruleset file (uses the ruleset engine). | built-in rules |
--min-score <n> |
Minimum compliance score (0–100) required to pass. | 70 |
--fail-on-warning |
Fail the gate when any warning-severity finding is present. | off |
--no-fail-on-error |
Do not fail the gate on error-severity findings. | fails on error |
--org <key> |
Apply your org's active governance waivers to the verdict. | none |
--advisory |
Report findings but always exit 0 (never fail CI). |
off |
--json |
Machine-readable JSON output (the full gate response). | off |
--output <file> |
Also save the JSON result to a file. | — |
--api-token <token> |
API token (else SPECSHIELD_API_KEY / stored config). |
— |
--server <url> |
Override the SpecShield server URL (self-hosted / staging). | https://specshield.io |
Run it in CI right after your build:
- name: Govern the API spec
env: { SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }} }
run: specshield govern api/openapi.yaml --min-score 80The same governance engine powers the IntelliJ plugin, the GitHub Action, the MCP server, and the dashboard — with catalog scorecards, waivers, and SARIF/JUnit/HTML reports.
Compatibility without Pact's broker overhead or DSL.
Plan: the registry commands —
publish-provider,publish-consumer,verify,can-i-deploy,matrixand thelist*commands — need a Team plan — on Free the server rejects them with a paid-plan error (exit code2).capture from-harandverify-providerare free and run fully locally. See plans →
Both sides publish what they have — provider publishes its OpenAPI spec, consumer publishes either an OpenAPI subset or a Pact JSON file — and SpecShield's engine verifies that every endpoint/field the consumer relies on is satisfied by the provider. can-i-deploy then gates the deploy.
- Consumer publishes a contract (OpenAPI subset or Pact JSON).
- Provider publishes its full OpenAPI spec.
- SpecShield compares: endpoint presence, request schemas, response fields, status codes, types.
can-i-deployreturns0only when all consumers are compatible.
specshield bdct publish-provider \
--org acme-store \
--provider payment-service \
--version v2.1.0 \
--spec ./api/openapi.yaml \
--env production \
--branch main--branch is optional — stamped on the published spec so you can correlate a spec version with the git branch it came from in bdct list-providers.
The consumer contract is an OpenAPI subset describing only the endpoints the consumer uses:
# consumer-contract.yaml — only the subset checkout-ui uses
openapi: "3.0.0"
info: { title: checkout-ui → payment-service contract, version: "1.0.0" }
paths:
/payments:
post:
requestBody:
content:
application/json:
schema:
type: object
required: [orderId, amount, currency]
properties:
orderId: { type: string }
amount: { type: number }
currency: { type: string }
responses:
"201":
content:
application/json:
schema:
type: object
properties:
paymentId: { type: string }
status: { type: string }specshield bdct publish-consumer \
--org acme-store \
--consumer checkout-ui \
--provider payment-service \
--version 2.0.0 \
--contract ./contracts/checkout-ui-payment.yaml \
--format OPENAPIPact JSON contracts are also supported — pass --format PACT and point --contract at a Pact file. The same compatibility engine runs against your provider's OpenAPI spec:
specshield bdct publish-consumer \
--org acme-store \
--consumer checkout-ui \
--provider payment-service \
--version 2.0.0 \
--contract ./pacts/checkout-ui-payment-service.json \
--format PACT--format defaults to OPENAPI if omitted.
specshield bdct verify \
--org acme-store \
--consumer checkout-ui --consumer-version 2.0.0 \
--provider payment-service --provider-version v2.1.0 \
--env production ✖ INCOMPATIBLE
Endpoints checked: 2
Compatible : 1
Incompatible : 1
Issues
● POST /payments [ERROR] RESPONSE_FIELD_MISSING
field: $.status
Consumer expects it — provider spec does not return it
● GET /payments/{id} [WARNING] TYPE_MISMATCH
field: $.amount
consumer: integer → provider: string
specshield bdct can-i-deploy \
--org acme-store --service payment-service \
--version v2.1.0 --env production ✔ PASS: payment-service v2.1.0 is deployable in production
─────────────────────────────────────────────────────
Consumer Version Status Verified At
─────────── ─────── ────────── ───────────────
checkout-ui 2.0.0 COMPATIBLE 2026-05-13 14:32
mobile-app 1.5.0 COMPATIBLE 2026-05-13 14:32
partner-sdk 3.2.1 COMPATIBLE 2026-05-13 14:32
Exit codes: 0 = deployable · 1 = blocked · 2 = error.
specshield bdct matrix --org acme-store --env production Consumer \ Provider payment-service order-service
─────────────────── ─────────────── ─────────────
checkout-ui COMPATIBLE COMPATIBLE
mobile-app INCOMPATIBLE COMPATIBLE
partner-sdk COMPATIBLE UNKNOWN
specshield bdct list-providers --org acme-store
specshield bdct list-providers --org acme-store --provider payment-service
specshield bdct list-consumers --org acme-store
specshield bdct list-consumers --org acme-store --consumer checkout-ui
specshield bdct list --org acme-store --provider payment-service --env production# 1. Provider publishes spec on every release
specshield bdct publish-provider \
--org acme-store --provider payment-service \
--version v2.1.0 --spec ./api/openapi.yaml
# 2. Each consumer publishes their contract (update on contract change)
specshield bdct publish-consumer \
--org acme-store --consumer checkout-ui \
--provider payment-service --version 2.0.0 \
--contract ./contracts/checkout-ui.yaml
# 3. Gate the provider deployment
specshield bdct can-i-deploy \
--org acme-store --service payment-service \
--version v2.1.0 --env productionAll bdct commands support --json for CI parsing:
specshield bdct can-i-deploy --org acme-store --service payment-service --version v2.1.0 --json{
"deployable": false,
"service": "payment-service",
"version": "v2.1.0",
"environment": "production",
"reason": "payment-service v2.1.0 is INCOMPATIBLE with: checkout-ui@2.0.0",
"verifications": [
{ "consumerName": "checkout-ui", "consumerVersion": "2.0.0", "status": "INCOMPATIBLE",
"compatibleCount": 1, "incompatibleCount": 1 }
]
}This is the feature that makes contract testing actually work for normal teams. Pact requires every consumer team to learn a DSL, instrument their tests, and run a broker. SpecShield asks for something they already have: a recording of an HTTP test run, in the universal HAR format that every browser, every test framework, and every recording proxy already produces.
A hand-written consumer contract drifts the moment you forget a field. A HAR-derived contract reflects what your code actually called, every time you re-record. It's the difference between "we documented the integration" and "we proved the integration."
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. RECORD 2. CAPTURE 3. GATE │
│ │
│ Your existing test run → specshield bdct → publish-consumer │
│ → traffic.har capture from-har verify │
│ (browser / Playwright / → consumer-contract.yaml can-i-deploy │
│ Cypress / mitmproxy / │
│ Postman / k6 / …) ← language-agnostic ← every provider │
│ CLI does the OpenAPI PR re-checks │
│ inference every consumer │
└─────────────────────────────────────────────────────────────────────────┘
Step 1 already happens in your team — you're just saving the file. Step 2 is one CLI command. Step 3 is the contract registry you publish to.
Pick the recorder that matches how the consumer is exercised in your project. You almost never have to write recorder code yourself.
| Recorder | Use when the consumer is… | Effort |
|---|---|---|
| Browser DevTools (Chrome / Firefox / Edge / Safari) | A frontend SPA, a Swagger UI, or any browser-driven app | Zero code |
Playwright with recordHar |
Exercised by Playwright UI/integration tests | One config field |
Cypress + cypress-har-generator |
Exercised by Cypress UI tests | One plugin |
| mitmproxy | A backend service-to-service caller in a test env | One brew install, no consumer changes |
| Postman / Insomnia / Bruno | Manually exercised during exploratory testing | Right-click → export |
| k6 load tests | Already running k6 against the provider | One --out har=… flag |
Step-by-step recipes for each recorder (DevTools, Playwright, Cypress,
mitmproxy, Postman, k6) are in
docs/har-recipes.md.
The Step 2 command is identical whichever recorder you use.
This is the one CLI command. The same command works regardless of which recorder produced the HAR.
specshield bdct capture from-har \
--in traffic.har \
--base-url https://api.acme.com \
--out consumer-contract.yamlExpected output:
✔ Wrote consumer-contract.yaml (4 endpoints, 6 ops from 23/41 entries)
The 23/41 summary means 23 entries survived the filters (right host,
JSON body) out of 41 total the recorder captured. Open the YAML to see
exactly what your code talks to — endpoint by endpoint, field by field.
| Flag | Purpose | Default |
|---|---|---|
--in <path> |
Required. Input HAR file (HAR 1.2). | — |
--out <path> |
Output file. If omitted, writes to stdout. | stdout |
--base-url <url> |
Keep only entries matching this URL prefix. Critical when the consumer talks to multiple backends — see "multi-host" below. | (no filter — keeps every host) |
--method <verbs> |
Comma-separated methods to include, e.g. GET,POST. |
all methods |
--title <title> |
OpenAPI info.title. |
Captured consumer contract |
--version <ver> |
OpenAPI info.version — usually your git SHA in CI. |
0.1.0 |
--format <fmt> |
yaml or json. |
yaml |
--include-non-json |
Keep entries with non-JSON bodies (HTML, images, binary). Schemas can't be inferred but the endpoints will appear. | off |
For every HAR entry that passes the filters:
- Path templating.
/users/123/orders/abc-2026becomes/users/{userId}/orders/{orderId}. Parameter names come from the preceding noun in the URL — not a generic{id}— so the resulting OpenAPI matches the parameter names your provider likely already uses ({userId},{orderId},{accountId}, …). - Per-status schema merging. If three
GET /users/{userId}samples return slightly different shapes, the emitted schema is the merger: fields seen in every sample stayrequired; fields seen in some samples become optional;integer+numberwidens tonumber; a type conflict falls back tostring. - Format detection. UUIDs, RFC 3339 date-times, and email addresses
get
format: uuid/format: date-time/format: email. - Status-code coverage. A 404 sample produces its own response schema alongside the 200 — so the contract documents the error shapes your code handles, not just the happy path.
- JSON-only by default. Entries with non-JSON bodies are dropped; the HTML page load from a Playwright test never ends up in the contract.
# 1. Publish the captured contract (run on every consumer PR)
specshield bdct publish-consumer \
--org acme-store \
--consumer checkout-ui \
--provider payment-service \
--version "$GIT_SHA" \
--format OPENAPI \
--contract consumer-contract.yaml
# 2. Gate the deploy (run before promoting the consumer)
specshield bdct can-i-deploy \
--org acme-store \
--service checkout-ui --version "$GIT_SHA" --env staging
# exit 0 = safe; exit 1 = a verification says NO and the deploy is blockedFull command reference is in the Contract Compatibility Testing section above.
A raw HAR can contain Authorization headers, session cookies, and real
PII in request/response bodies. Scrub before publishing. Two patterns:
Quick scrub with jq (zero deps if you already have jq):
jq 'del(.. | .headers? | .[]? | select(.name | ascii_downcase | IN("authorization","cookie","set-cookie","x-api-key")))' \
traffic.har > scrubbed.harHeavier scrub via har-sanitizer (npm tool — supports body redaction
patterns, allowlists, etc.):
npx har-sanitizer --input traffic.har --output scrubbed.har --scrub-words 'email,ssn,creditCard'The contract that bdct capture from-har emits only carries field names
and types — not values — so once the HAR is scrubbed of secrets, the
published contract is safe to share with the provider team.
--base-url is a prefix match, so it doubles as a host filter:
# Keep only calls to the payment service
specshield bdct capture from-har \
--in traffic.har --base-url https://payment.acme.com \
--out contracts/checkout-ui-payment.yaml
# Same HAR, different provider — keep only calls to inventory
specshield bdct capture from-har \
--in traffic.har --base-url https://inventory.acme.com \
--out contracts/checkout-ui-inventory.yamlOne test run → one HAR → N consumer contracts, one per provider.
The whole pattern is "record from your existing tests, publish from CI." A
typical .github/workflows/contract-test.yml:
- name: Run integration tests (produces traffic.har)
run: npx playwright test # or ./mvnw verify, or whatever you use
- name: HAR → consumer contract
run: |
specshield bdct capture from-har \
--in traffic.har \
--base-url ${{ vars.PROVIDER_BASE_URL }} \
--out consumer-contract.yaml
- name: Publish contract
env:
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
run: |
specshield bdct publish-consumer \
--org ${{ vars.SPECSHIELD_ORG }} \
--consumer ${{ vars.SERVICE_NAME }} \
--provider ${{ vars.PROVIDER_NAME }} \
--version ${{ github.sha }} \
--format OPENAPI \
--contract consumer-contract.yaml
- name: Gate the deploy
env:
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
run: |
specshield bdct can-i-deploy \
--org ${{ vars.SPECSHIELD_ORG }} \
--service ${{ vars.SERVICE_NAME }} \
--version ${{ github.sha }} \
--env stagingIdentical pattern on GitLab, CircleCI, Jenkins — only the secret-injection syntax changes.
| Symptom | Cause | Fix |
|---|---|---|
0 endpoints, 0 ops from 0/N entries |
--base-url doesn't match any host in the HAR |
Drop --base-url to see what hosts ARE in the HAR; re-run with the right prefix. |
| Contract is missing your POST | Recorder captured a 4xx for that POST | Fix the test so the POST succeeds, OR keep the 4xx (it'll document the error path). |
| Path got templated when you didn't want it to | A literal path segment looked like a numeric/UUID id | Path segments are templated when multiple entries share a prefix but differ on that segment. A single entry stays literal. |
| Playwright HAR file is empty / not written | context.close() didn't run |
Create the context explicitly with chromium.launch() → browser.newContext({ recordHar }) and await context.close() at the end of the test. |
| Backend rejects POST when mitmproxy is in the path | TLS cert not trusted by the consumer | See the mitmproxy CA-cert link above; one env var or one JKS import. |
Active spec-vs-production conformance — Dredd-style, in CI.
Most teams trust the provider's OpenAPI as the source of truth and never check whether the running service actually matches it. SpecShield's compatibility engine is only as accurate as that trust — so verify-provider closes the loop by firing probes derived from the spec at a running endpoint (staging, usually) and validating every response body against the spec's schema for its status code.
Catches things like: the spec says status ∈ {paid, pending, refunded} but the live API sometimes returns partially_refunded. Or a field documented as required is sometimes missing. Or a format: uuid field actually contains a numeric ID.
By default verify-provider only probes safe methods (GET, HEAD, OPTIONS) — it must never side-effect your staging data. Mutating methods are opt-in with --include-mutating.
specshield bdct verify-provider \
--spec api/openapi.yaml \
--base-url https://staging.payments.acme.com Provider conformance — spec vs https://staging.payments.acme.com
────────────────────────────────────────────────────────────
PASS GET /health (200)
FAIL GET /payments/{paymentId} (200)
/amount: must be number
PASS GET /orders (200)
SKIP GET /unconfigured/{thingId}
unresolved path params (no --path-params or spec example): thingId
────────────────────────────────────────────────────────────
2 pass · 1 fail · 0 error · 1 skip (4 probes)
Exit codes: 0 = all pass · 1 = at least one fail/error · 2 = config/spec error.
| Flag | Purpose |
|---|---|
--spec <path> |
Required. Provider OpenAPI spec (YAML or JSON; $refs are resolved). |
--base-url <url> |
Required. Base URL of the running provider (e.g. https://staging.payments.acme.com). |
--include-mutating |
Also probe POST/PUT/PATCH/DELETE. Off by default — never side-effects staging. |
--path-params <kvList> |
Resolve path params: paymentId=pay-123,userId=u-7. Overrides spec examples. Repeatable. |
--header <header> |
Extra request header to send, e.g. --header "Authorization: Bearer $TOKEN". Repeatable. |
--timeout-ms <ms> |
Per-request timeout (default: 8000). |
--json |
Machine-readable JSON output (for CI parsing). |
For path templates like /payments/{paymentId}, the resolution priority is:
--path-params paymentId=pay-123on the CLI (always wins)- The spec's
parameters[].examplefor that param - SKIPPED with a reason if neither resolves
So you can declare examples in the spec once and never repeat them on the CLI:
paths:
/payments/{paymentId}:
get:
parameters:
- name: paymentId
in: path
required: true
schema: { type: string }
example: pay-123 # ← verify-provider uses this automatically- Type / format —
string/integer/number/boolean/array/object, plusformat: uuid|date-time|email|...(viaajv-formats). - Required fields — flags any
requiredfield that's missing. - Enums — flags values outside the documented enum set.
nullable: true— properly allowsnullvalues (OAS-3.0-isms handled).- Status code coverage — actual status must match an exact code, a wildcard (
4XX), or thedefaultresponse. An undocumented status is a FAIL.
- name: Verify production matches spec
env:
STAGING_TOKEN: ${{ secrets.STAGING_TOKEN }}
run: |
specshield bdct verify-provider \
--spec api/openapi.yaml \
--base-url https://staging.payments.acme.com \
--header "Authorization: Bearer $STAGING_TOKEN" \
--json > conformance.jsonname: API Contract Check
on:
pull_request:
branches: [main]
jobs:
check-api-contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm install -g specshield
- run: git show origin/main:api/openapi.yaml > /tmp/base.yaml
- run: specshield compare /tmp/base.yaml api/openapi.yaml --fail-on-breakingname: Publish Provider & Deploy Gate
on:
push:
branches: [main]
paths: ['api/openapi.yaml']
jobs:
publish-bdct:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm install -g specshield
- name: Publish provider spec
env: { SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }} }
run: |
specshield bdct publish-provider \
--org ${{ vars.SPECSHIELD_ORG }} \
--provider payment-service \
--version ${{ github.sha }} \
--spec ./api/openapi.yaml \
--env production
- name: Gate the deploy
env: { SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }} }
run: |
specshield bdct can-i-deploy \
--org ${{ vars.SPECSHIELD_ORG }} \
--service payment-service \
--version ${{ github.sha }} \
--env productionname: Provider Conformance
on:
schedule: [{ cron: '0 7 * * *' }] # daily 07:00 UTC
workflow_dispatch:
jobs:
conformance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm install -g specshield
- name: Verify production matches its spec
env: { STAGING_TOKEN: ${{ secrets.STAGING_TOKEN }} }
run: |
specshield bdct verify-provider \
--spec api/openapi.yaml \
--base-url https://staging.payments.acme.com \
--header "Authorization: Bearer $STAGING_TOKEN"| SpecShield | Pact / PactFlow (SmartBear) | oasdiff | Specmatic / Microcks (OSS) | |
|---|---|---|---|---|
| OpenAPI-native | ✅ | partial (Pact DSL) | ✅ | ✅ |
| No broker to run or host | ✅ | ❌ (Pact Broker or paid PactFlow) | ✅ | ✅ |
| Bi-directional contract testing | ✅ | ✅ (Pactflow paid) | ❌ | partial |
| Breaking-change detection | ✅ | ❌ | ✅ | ❌ |
can-i-deploy gating |
✅ | ✅ (broker) | ❌ | ❌ |
| Pact JSON ingest | ✅ | native | ❌ | ❌ |
| HAR → consumer contract capture | ✅ | ❌ (requires Pact DSL in tests) | ❌ | ❌ |
| Spec-vs-production conformance | ✅ | partial (ReadyAPI / Dredd) | ❌ | ✅ |
| GitHub App PR checks | ✅ | ❌ | ❌ | ❌ |
| Hosted dashboard | ✅ | ✅ (paid) | partial | ❌ |
| CLI-first | ✅ | ❌ | ✅ | partial |
| Free tier | ✅ | ❌ | ✅ (OSS) | ✅ (OSS) |
Note: Optic — a popular OpenAPI-diff tool — was archived in January 2026 and is no longer maintained; SpecShield is a drop-in replacement for its diff and CI checks. For the full, up-to-date comparison (including Bump.sh, Stoplight, and API gateways like Apigee and Azure API Management), see specshield.io/alternatives.
| Plan | Price | What's included |
|---|---|---|
| Free | $0 forever | Local compare · bdct capture from-har · bdct verify-provider · public web diff · 7-day compare history · 1 GitHub App PR check |
| Team | $89/mo, or $900/yr ($75/mo) · up to 10 users | Everything in Free + consumer registry · enforced can-i-deploy gate · compatibility matrix · govern governance gate · audit trail · Slack alerts · team workspace + RBAC · priority email support |
| Enterprise | Custom · unlimited users | Everything in Team + SSO and on-prem / private-cloud (on request) · advanced RBAC + audit-log export · dedicated onboarding + SLA |
Prices exclude VAT/GST, added at checkout where applicable. No credit card for the free plan.
The local-only commands are first-class.
compare,bdct capture from-har, andbdct verify-providerrun entirely in your CI on the Free plan — your spec text never leaves your infrastructure. Get started free → · For Enterprise: sales@specshield.io.
Run once at the root of your project. The wizard:
- Detects your OpenAPI spec (looks under
api/,spec/,docs/, repo root). - Detects your service name (
package.json,pyproject.toml,pom.xml,go.mod,Cargo.toml, or directory name). - Detects your git branch and suggests
productionformain/master,stagingotherwise. - Asks whether this project is a provider, a consumer, or both.
- Asks for your org key — copy it from the dashboard at specshield.io/account/team.
- Validates / stores your API key.
- Writes
.specshield.ymland (optionally) a starter.github/workflows/specshield-bdct.ymlusingspecshield-io/bdct-action@v1.
specshield initschemaVersion: 1
failOnBreaking: true
severity: error
bdct:
org: acme-pay
environment: staging
provider:
name: payment-service
spec: api/openapi.yaml
# consumer (optional — present when --kind=consumer or --kind=both):
# consumer:
# name: checkout-ui
# provider: payment-service
# contract: contracts/payment-service.yaml
# format: OPENAPI
github:
specPath: api/openapi.yaml
failOnBreaking: true
commentOnPr: trueCLI flags always override this file. Paths in the file are resolved relative to the file's own directory, so you can run specshield bdct … from any subdirectory.
# Provider-only project
specshield init --no-interactive \
--kind provider --org acme-pay \
--provider payment-service --spec api/openapi.yaml \
--env staging --write-workflow
# Consumer-only project
specshield init --no-interactive \
--kind consumer --org acme-pay \
--consumer checkout-ui --consumer-provider payment-service \
--contract contracts/payment-service.yaml --format OPENAPI \
--env staging| Flag | Purpose |
|---|---|
--no-interactive |
Run without prompts; required fields must come from flags or auto-detection. |
--print |
Detect everything, print the proposed YAML to stdout, write nothing. |
--force |
Skip the overwrite-confirmation if .specshield.yml already exists. |
--server <url> |
Use a non-default SpecShield endpoint (self-hosted / staging). |
--kind <kind> |
provider, consumer, both, or skip. |
--org <key> |
Organization key. |
--provider <name> |
Provider service name. |
--spec <path> |
Path to the provider OpenAPI spec. |
--consumer <name> |
Consumer service name. |
--consumer-provider <name> |
The provider this consumer talks to. |
--contract <path> |
Path to the consumer contract (OpenAPI or Pact JSON). |
--format <fmt> |
Consumer contract format: OPENAPI (default) or PACT. |
--env <environment> |
Default environment for bdct operations. |
--write-workflow |
Also write a starter GitHub Actions workflow. |
What is not written into
.specshield.yml: your API key. It's stored in~/.specshield/config.json(set byspecshield login) or read fromSPECSHIELD_API_KEYin CI. The project file is committed; never commit a secret into it.
Step 1 — Create your account
Go to https://specshield.io and sign in with GitHub or Google (30 seconds).
Step 2 — Generate an API key
Dashboard → API Keys → Generate Key → copy the ss_ token.
Step 3 — Authenticate the CLI
specshield login --api-key ss_your_token_here✔ Logged in successfully.
Customer : Your Name
Plan : FREE
The token is stored in ~/.specshield/config.json — no need to pass it on every command.
Or use an environment variable (recommended for CI):
export SPECSHIELD_API_KEY=ss_your_token_hereThis one env var works for every command — compare, login, and every bdct subcommand all read it.
Token resolution order:
compare/login:--api-keyflag →SPECSHIELD_API_KEYenv var → stored config →.specshield.ymlbdct …subcommands:--api-tokenflag →SPECSHIELD_API_KEYenv var → stored config
Why two flag names?
compareandloginwere built first and used--api-key. The newerbdctcommands use--api-tokento keep "key" reserved for the stored-config concept. The env var unifies both.
bdct capture from-harandbdct verify-providerdon't require auth — they run entirely locally against files / your own staging URL, and nothing is uploaded.
Run specshield init to generate .specshield.yml, or hand-write it. Every specshield … invocation reads it from the project root (or any parent directory) and uses its values as defaults — CLI flags always win.
schemaVersion: 1
# Local-compare defaults.
failOnBreaking: true
severity: error # info | warning | error
ignore:
- "DELETE /admin removed" # match by substring; repeatable
# CI gate (specshield gate; flags win).
# gate:
# spec: openapi.yaml
# mode: shadow # shadow | blocking (default blocking)
# waivers: .specshield/waivers.yml
# maxWaiverDays: 90
# upload: true # record runs for the readiness report (Team)
# org: <org-key>
# Usage-aware compare (same as --usage …; see "Who does a change break?").
# usage:
# file: usage.json
# ignoreClients: ["kube-probe/*"]
# minDays: 7
# maxAgeDays: 30
# minCoverage: 25
# downgrade: true
# quietDays: 30 # deprecated + no calls this long = safe to remove
# contacts: # who to tell, per client id (* globs)
# ShopApp-iOS/*: ios-team@shop.example
# Hosted compare (set --remote on the CLI to override).
remote:
enabled: false
url: https://specshield.io/compare
timeout: 10000
# `bdct` defaults (used by every `specshield bdct …` subcommand).
bdct:
org: acme-pay
environment: staging
# server: https://specshield.io # only for self-hosted / staging
provider:
name: payment-service
spec: api/openapi.yaml # paths are relative to this file
# branch: main # informational tag on each publish
# consumer:
# name: checkout-ui
# provider: payment-service
# contract: contracts/payment-service.yaml
# format: OPENAPI # OPENAPI | PACT
# GitHub App + bdct-action defaults.
github:
specPath: api/openapi.yaml
failOnBreaking: true
commentOnPr: trueWith this file, bdct commands collapse to --version:
specshield bdct publish-provider --version $GITHUB_SHA
specshield bdct can-i-deploy --version $GITHUB_SHAspecshield compare <base> <target> [options]| Option | Description |
|---|---|
--remote |
Use the SpecShield hosted API |
--api-key <key> |
API token |
--fail-on-breaking |
Exit code 1 on breaking changes |
--allow-breaking |
Override fail-on-breaking |
--json |
Machine-readable JSON output |
--output <file> |
Save result to file |
--ignore <change> |
Ignore a specific change (repeatable) |
--severity <level> |
Minimum severity shown: info / warning / error (default info) |
--config <path> |
Path to .specshield.yml |
--remote-url <url> |
Override the hosted API base URL |
--usage <file> |
usage.json from specshield usage import — show which clients each breaking change affects |
--usage-ignore-client <pattern> |
Leave a client out of the impact (repeatable; * globs, otherwise substring) |
--usage-min-days <n> |
Days of usage needed before an unused breaking change stops failing the build (default 7) |
--usage-max-age <days> |
The newest log line may be at most this old for unused changes to pass (default 30) |
--usage-min-coverage <pct> |
Percent of spec operations that must appear in the logs for unused changes to pass (default 25) |
--no-usage-downgrade |
Annotate impact only; keep unused breaking changes breaking |
--usage-quiet-days <n> |
A deprecated item nobody called for this many days may be removed (default 30) — see Safe removal |
--timeout <ms> |
Request timeout for remote mode (default 10000) |
specshield history [options]| Option | Description |
|---|---|
--limit <n> |
Number of comparisons to list (default 20) |
--json |
Machine-readable JSON output |
--api-key <key> |
Override stored API key |
specshield share <reportId | base.yaml target.yaml> [options]| Option | Description |
|---|---|
--expires <days> |
Make the link expire after N days (default: never) |
--api-key <key> |
Override stored API key |
specshield whoami [options] # show the signed-in account
specshield logout # remove the stored API keyOption (whoami) |
Description |
|---|---|
--api-token <key> |
Override stored / env API token for this call |
--server <url> |
Override SpecShield server URL |
--json |
Machine-readable JSON output |
specshield govern <spec> [options]| Option | Description |
|---|---|
--ruleset <path> |
Lint against a Spectral-format ruleset file |
--min-score <n> |
Minimum compliance score (0–100) to pass (default 70) |
--fail-on-warning |
Fail the gate on any warning-severity finding |
--no-fail-on-error |
Do not fail the gate on error-severity findings |
--org <key> |
Apply the org's active governance waivers |
--advisory |
Report findings but always exit 0 |
--json |
Machine-readable JSON output |
--output <file> |
Save the JSON result to a file |
--api-token <token> |
API token (overrides env / stored config) |
--server <url> |
Override SpecShield server URL |
specshield bdct <subcommand> [options]| Subcommand | Description |
|---|---|
publish-provider |
Publish a provider OpenAPI spec |
publish-consumer |
Publish a consumer contract (OpenAPI subset or Pact JSON) |
verify |
Manually trigger verification for a consumer/provider pair |
can-i-deploy |
Check if a service version is safe to deploy |
matrix |
Compatibility matrix across all consumer/provider pairs |
list-providers |
List published provider specs |
list-consumers |
List published consumer contracts |
list |
List verification history |
capture from-har |
Turn a recorded HAR file into an OpenAPI consumer contract |
verify-provider |
Check that a running provider service matches its OpenAPI spec |
Every bdct subcommand accepts these flags in common:
| Flag | Purpose |
|---|---|
--org <key> |
Organization key (or read from .specshield.yml's bdct.org) |
--json |
Machine-readable JSON output |
--server <url> |
Override SpecShield server URL (self-hosted / staging) |
--api-token <token> |
API token (overrides env / stored config) |
Per-subcommand flags:
| Subcommand | Flags |
|---|---|
publish-provider |
--spec <path>, --provider <name>, --version <ver>, --env <env>, --branch <branch> |
publish-consumer |
--contract <path>, --consumer <name>, --provider <name>, --version <ver>, --format OPENAPI|PACT |
verify |
--consumer <name>, --provider <name>, --consumer-version <ver>, --provider-version <ver>, --env <env> |
can-i-deploy |
--service <name>, --version <ver>, --env <env> |
matrix |
--env <env> |
list-providers |
--provider <name> (filter) |
list-consumers |
--consumer <name>, --provider <name> (filters) |
list |
--consumer <name>, --provider <name>, --env <env>, --page <n>, --size <n> |
capture from-har |
--in <path> required, --out <path>, --base-url <url>, --method <verbs>, --title <s>, --version <s>, --format yaml|json, --include-non-json |
verify-provider |
--spec <path> required, --base-url <url> required, --include-mutating, --path-params <kv> (repeatable), --header <h> (repeatable), --timeout-ms <n>, --json |
| Code | Meaning |
|---|---|
0 |
Clean — no breaking changes / deployable / all probes pass |
1 |
Breaking changes found / not deployable / one or more probes failed |
2 |
Config error, missing token, or runtime error |
MIT © SpecShield Software Private Limited
The SpecShield™ platform: Dashboard · GitHub App · GitHub Action · IntelliJ Plugin · MCP Server
⭐ Star on GitHub · 📦 View on npm · 🚀 Create free account · ⚖️ Compare
Stop finding out about API breakage from your users.
