OpenAPI Diff · Breaking-Change Detection · Bi-Directional Contract Testing ·
can-i-deployGate · Pact-File Ingest · Live-Traffic Capture · Spec-vs-Production Conformance · GitHub PR Checks
SpecShield is the one CLI that does four things to keep your API safe:
- Diff two OpenAPI specs and fail CI on breaking changes.
- Bi-directional contract testing with
can-i-deploy— block a deploy that would break a consumer. bdct capture from-har— turn recorded traffic into an accurate consumer contract (no Pact DSL).bdct verify-provider— prove the running provider actually matches its OpenAPI spec.
OpenAPI diff + BDCT + HAR → consumer contract + spec-vs-production conformance — in one CLI.
No broker. No Pact DSL. Language-agnostic. Works in 30 seconds. Local mode never uploads your specs.
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.
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 any OpenAPI 3.x YAML or JSON.
For BDCT, 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.
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 governance — track API drift over time across your platform; know what changed, when, by whom.
- Provider conformance (new) — make sure your running service actually matches its published OpenAPI spec, not just on paper.
- Pact-free consumer contracts (new) — record real traffic with
capture from-harand get an OpenAPI consumer contract without writing a single line of Pact DSL.
| Feature | Local (Free) | Team | Enterprise |
|---|---|---|---|
| Compare two spec files | ✅ | ✅ | ✅ |
| Breaking change detection | ✅ | ✅ | ✅ |
| JSON / human output | ✅ | ✅ | ✅ |
| Fail CI on breaking change | ✅ | ✅ | ✅ |
bdct capture from-har |
✅ | ✅ | ✅ |
bdct verify-provider |
✅ | ✅ | ✅ |
specshield history — compare timeline |
❌ | ✅ | ✅ |
specshield share — public report URLs |
❌ | ✅ | ✅ |
| Hosted dashboard | ❌ | ✅ | ✅ |
| GitHub App PR checks | ❌ | ✅ | ✅ |
| BDCT consumer registry + can-i-deploy gate | ❌ | ✅ | ✅ |
| BDCT compatibility matrix | ❌ | ✅ | ✅ |
| Team workspace + audit log + RBAC | ❌ | ✅ | ✅ |
| Slack notifications | ❌ | ✅ | ✅ |
| SAML SSO, SCIM, on-prem | ❌ | ❌ | ✅ |
The local-only commands are first-class.
compare,bdct capture from-har, andbdct verify-providerall run entirely in your CI — your spec text never leaves your infrastructure.
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-breakingSends your specs to SpecShield and stores results in your dashboard. Requires a free account.
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.jsonEvery specshield compare --remote is saved to your account. List recent comparisons from any machine — useful for tracking API drift over time across CI pipelines + local runs.
specshield history # last 20 comparisons
specshield history --limit 50
specshield history --json 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, tokenized URL for any comparison report. Great for Slack threads, PR comments, Jira tickets.
# 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 (pass/fail) visible on the PR
- A sticky PR comment with the full diff (breaking changes highlighted)
- Configurable
failOnBreakingper repository
Configure per-repo via .specshield.yml:
github:
specPath: api/openapi.yaml
failOnBreaking: true
commentOnPr: trueCompatibility without Pact's broker overhead or DSL.
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 BDCT 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 BDCT 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 |
Recipes follow — keep reading for the exact commands per tool, then jump to Step 2 (the capture command, which is identical regardless of recorder).
- Open the page that calls your API in the browser.
- Open DevTools (
F12/Cmd+Opt+I) → Network tab. - Click the 🚫 button to clear, then reload the page and click through the flows you want to record.
- Right-click anywhere in the request list → Save all as HAR with
content → save as
traffic.har.
That's the entire recording. The file is byte-compatible with everything SpecShield does.
Safari note: Safari's "Export HAR" is under the Develop menu → Export… in the Network tab; it produces the same format.
Add one option to your playwright.config.js:
// playwright.config.js
const path = require('path');
module.exports = {
testDir: './tests',
use: {
baseURL: 'https://staging.acme.com',
recordHar: { path: path.resolve(__dirname, 'traffic.har'), mode: 'full', content: 'embed' },
},
};Run your tests as usual:
npx playwright test
# → writes traffic.har on context closeThe HAR will contain every HTTP call the browser made during the test —
HTML, JS, images, and the API JSON. The --onlyJson default in Step 2
filters out the noise automatically.
Need the HAR file flushed per test? Create the context explicitly in your test and call
await context.close()at the end. A runnable copy-paste starter lives inexamples/playwright-har/of this repo — clone,npm install,npx playwright install chromium,npm testproducestraffic.haragainst the public JSONPlaceholder sandbox so you can verify the toolchain works before pointing it at your own provider.
npm i -D @neuralegion/cypress-har-generator// cypress.config.js
const { install } = require('@neuralegion/cypress-har-generator');
module.exports = { e2e: { setupNodeEvents(on, config) { install(on); return config; } } };
// cypress/support/e2e.js
beforeEach(() => cy.recordHar());
afterEach(() => cy.saveHar({ outDir: './har-out' }));Run npx cypress run and the HAR for each spec lands in har-out/.
When the consumer is a backend service (no browser), point it through mitmproxy in your integration-test env. No consumer code change.
# Install once
brew install mitmproxy # macOS — also available via pip and apt
# Start the proxy and tell it to dump HAR
mitmdump --set hardump=traffic.har --listen-port 8080
# In another terminal: run your tests with HTTPS_PROXY pointing at it
HTTPS_PROXY=http://localhost:8080 HTTP_PROXY=http://localhost:8080 \
./run-integration-tests.sh
# Ctrl-C mitmdump when done — traffic.har is on diskThe hardump add-on is built into mitmproxy ≥ 9.0. For TLS hosts you'll
need to trust mitmproxy's CA in your test JVM/runtime (see the
mitmproxy CA docs);
in many test environments it's a single env var
(NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, or a
JVM cacerts import).
Run the request (or a whole collection runner). Right-click the response
→ Save Response as HAR. Repeat for each request you want included,
then either keep them as separate .har files (pass --in once per file
via a loop in CI) or merge them with any HAR-merge tool.
k6 run --out har=traffic.har script.jsk6 emits a HAR alongside its load metrics. Same file works with
bdct capture from-har.
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 BDCT command reference is in the Bi-Directional Contract 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. |
Every other contract-testing product on the market asks the consumer team to change their code — adopt a DSL, instrument their tests, run a broker. That tax is why most teams that "should" be doing contract testing aren't.
bdct capture from-har removes the tax. Your team's existing test run is
already the contract; the CLI just converts the format. The HAR-capture →
publish → can-i-deploy loop is what turns "we have OpenAPI specs in a
folder" into "no provider PR merges if it would break a deployed consumer"
— with measurable enforcement, in one CI step, and no per-language SDK.
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 for BDCT 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: BDCT Publish & 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 required | ✅ | ❌ (Pact Broker) | ✅ | ✅ |
| 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) |
| 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 ($75 annual) | Everything in Free + consumer registry · enforced can-i-deploy gate · audit trail · BDCT compatibility matrix · Slack alerts · team workspace + RBAC · priority email support |
| Enterprise | Talk to us | Everything in Team + SAML SSO · SCIM · on-prem / private-cloud · advanced RBAC + audit-log export · dedicated onboarding + SLA |
No credit card for the free plan. 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 (autocompletes from your account if signed in).
- Validates / stores your API key.
- Writes
.specshield.ymland (optionally) a starter.github/workflows/specshield-bdct.ymlusingspecshield26/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
# 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> |
info / warning / error |
--config <path> |
Path to .specshield.yml |
--timeout <ms> |
Request timeout for remote mode |
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 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 © Deepak Satyam
⭐ Star on GitHub · 📦 View on npm · 🚀 Create free account
Stop finding out about API breakage from your users.