Development

Contract Tests Between Your Smoke Suite and Its Vendors

Antonio Evans9 min read

A split scene: on the left, a billing probe script (a small terminal labeled 'smoke: billing') sends a request to a vendor box.
Key points

Key points

  1. 1A contract test records the vendor response shape (status codes, required fields, field types, error envelopes) and asserts it on a schedule, so a vendor change surfaces as a shape mismatch rather than a failed billing probe.
  2. 2Record the baseline from the vendor's documented sandbox, not from the live billing endpoint, so the test is cheap, repeatable and never charges a real card.
  3. 3Keep shape assertions in their own test file and job, separate from the business logic your smoke probe checks, so a failure points at exactly one cause.
  4. 4Alert on mismatch with the field path that changed, and run the check both in CI and on a schedule, because most breaking changes arrive without any commit of yours.

Why a billing probe fails for the wrong reason

A production smoke suite answers one question: is this build healthy enough to keep moving through the pipeline? QA Wolf's smoke testing guide frames the smoke gate as a fast go/no-go check that stops broken builds before deeper testing starts, and a well-built suite finishes in seconds to a minute. TestMu AI's checklist puts a typical suite at 5 to 15 minutes with 20 to 50 cases.

The trouble starts when one of those cases calls a payment provider. Your probe does something like: create a test customer, attach a test card, create an intent, assert the intent comes back with a status you recognize and an amount in minor units. That single assertion now depends on three things at once:

  1. Your own code and configuration.
  2. Your credentials and network path to the vendor.
  3. The vendor's response shape, which you do not control and cannot see changing.

When the job goes red at 3 a.m., the on-call engineer has to work out which of the three broke. Often the answer is the vendor renamed a field or moved a status value, and your probe was the messenger. That is exactly the failure mode contract testing exists to catch: a change that passes every functional test and still breaks something, because nobody wrote down what the two sides had actually agreed to, as ACCELQ's contract testing guide puts it.

Redocly's introduction to contract testing defines the contract as endpoints and methods, request and response payload structures, expected status codes, and required headers and authentication. For a billing probe, that list is your shopping list.

What a vendor-shape contract covers

A contract test for a third-party payment API is narrower than a consumer-driven pact between two teams you both employ. You cannot ask the vendor to verify a pact file. What you can do is pin the parts of the response your probe depends on, and assert them against a recorded baseline.

ElementExample assertionWhy the probe cares
Transport statusIntent creation returns 200 or 201A 402 or 500 means auth or account issue, not a shape change
Required top-level fieldsid, object, status, amount, currency all presentYour probe reads these directly
Field typesamount is an integer, currency is a three-letter stringA string amount breaks arithmetic silently
Enum valuesstatus is one of a known setA renamed status makes your probe assert on a value that never fires
Error envelopeDeclined card returns a error object with code and messageYour probe's negative path depends on the same shape
Nested objectspayment_method and its type discriminatorCard versus wallet branching lives here

The method matters less than the discipline. Pactflow's walkthrough of schema-based contract testing shows the shape of the workflow: author the contract, generate a schema from the consumer's types, then diff the provider's actual output against that schema. You can do the same with the vendor as the "provider," using a JSON schema you keep in your repo.

Two choices are worth calling out because they decide whether the test is useful or noisy:

  • Assert on presence and type, not exact values. Production traffic and sandbox differ in values constantly. You want to know that amount is an integer, not that the fee is 30 cents.
  • Allow additive change by default, block on removal and retype. A vendor adding a field is usually harmless. A vendor removing status or turning amount into a decimal string is not. If your schema validator rejects unknown properties, every additive vendor release becomes a false alarm.

DigitalAPI's contract testing writeup uses the example of a weather API that promised a number and started returning text: the mismatch is caught by shape, not by any functional expectation. Your amount field is the same story with money attached.

Recording the baseline from the sandbox, not production

The single most important design decision is where the contract comes from. Record it from the vendor's documented sandbox with test credentials, and never from the live billing endpoint. QASkills' production smoke suite skill is blunt about this: production smoke tests must never create permanent data, modify accounts, trigger billing events, or send real notifications, and every test should be read-only or use dedicated accounts with sandboxed permissions. A smoke test that accidentally charges a customer is worse than no test.

A workable recording procedure:

  1. Pick three or four representative interactions, not the whole API. A successful intent creation, a declined card, an invalid request (400), and an auth failure (401) cover most of what a billing probe touches.
  2. Capture only the shape. Strip values that change on every call (ids, timestamps, idempotency keys) and keep the type and presence. A small script that maps a response to a shape, or a library that infers a schema, both work.
  3. Store the result in version control next to the contract test, so a change to the expected shape is a reviewable diff.
  4. Record against the vendor's documentation as well. If the sandbox and the docs disagree, that is itself a useful finding, and Pactflow notes that comparing against a published schema helps stop documentation drifting from implementation.
  5. Re-record deliberately, not automatically. If your test rewrites the baseline every time it runs, it can never fail. Recording must be a separate command a human runs and commits.

The schema below is the smallest useful version. It encodes required fields and types, tolerates unknown extra fields, and pins the status enum:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["id", "object", "status", "amount", "currency"],
  "properties": {
    "id": { "type": "string" },
    "object": { "const": "payment_intent" },
    "status": { "enum": ["requires_payment_method", "requires_confirmation", "succeeded", "canceled"] },
    "amount": { "type": "integer" },
    "currency": { "type": "string", "pattern": "^[a-z]{3}$" },
    "payment_method": { "type": ["string", "null"] }
  },
  "additionalProperties": true
}

additionalProperties: true is the decision that keeps the test quiet through additive vendor releases. The const on object and the enum on status are the decisions that make it loud when the vendor renames a discriminator or a state.

Wiring it into CI and a schedule

GitHub Actions is the convenient default. Workflows are YAML files stored in .github/workflows and can be triggered by events or by a defined schedule, per GitHub's workflow syntax reference, and the Actions documentation covers the runners and concepts if you are new to it. freeCodeCamp's step-by-step guide is useful for the vocabulary of events, jobs and runners.

The contract job has two triggers, and the split matters:

  • On pull request: run the contract test against the recorded baseline using a mock or replayed response. This catches the case where you change your parsing code but not the schema, and it runs in seconds with no vendor call.
  • On a schedule: call the vendor sandbox, validate the live response against the baseline, and alert on mismatch. This catches the case where the vendor changed overnight and no commit of yours was involved.
name: billing-vendor-contract
on:
  pull_request:
    paths: ["src/billing/**", "contracts/**"]
  schedule:
    - cron: "17 */4 * * *"
jobs:
  contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - name: Validate recorded baseline against fixtures
        if: github.event_name == 'pull_request'
        run: npm run contract:fixtures
      - name: Validate live sandbox shape
        if: github.event_name == 'schedule'
        env:
          PAYMENTS_SANDBOX_KEY: ${{ secrets.PAYMENTS_SANDBOX_KEY }}
        run: npm run contract:sandbox

Two practical notes from the CI side. First, preserve the exit code. Momentic's CI/CD testing guide warns that shell wrappers and constructs like || true can turn a failed run into a passing job, so capture the status and return it after any artifact upload. Second, give the scheduled job a distinctive name, because branch protection and alert routing both key on it, and renaming it can silently detach the rule.

If your pipeline already gates on test results via exit codes, the contract check fits the same pattern. TestCollab's quality gate documentation lays out the exit code convention clearly: 0 meets every criterion, 1 evaluated and failed, 2 could not evaluate. Treat every nonzero status the same way and block.

Keeping the probe honest about what it asserts

The second half of the job is cleaning up the smoke probe itself. A contract test upstream is worthless if the probe then asserts on vendor internals anyway. The rule is simple: the probe should assert on your outcome, the contract test should assert on the vendor's shape.

Probe assertionBelongs inReason
"Charge of 500 minor units returns status succeeded"Smoke probeBusiness outcome your code decides on
"Response contains amount as an integer"Contract testVendor shape
"Webhook fires within 30 seconds"Smoke probe, with wide timeoutYour integration's latency, not the vendor's schema
"Error 402 body has a code field"Contract testVendor error envelope

MakerKit's SaaS smoke testing guide puts payment integration in the smoke suite as a plan selection plus a test card and a visible success state, which is the right level: does the flow complete, not what every field looks like. Momentic's overview lists "can critical integrations be reached" as a smoke concern, and that is precisely where the boundary sits. Reachability and outcome belong to the probe. Shape belongs to the contract.

When a contract test fails, the alert should name the path. A message like "amount expected integer, got string" tells the on-call engineer in one glance that the vendor changed something and that the probe failure, if any, is downstream. A message like "contract test failed" tells them nothing and costs another twenty minutes, which is the same cost the test was supposed to remove. Momentic's CI guide makes the point for Slack alerts generally: send the failed check and run details, not a bare pass or fail. For contract checks the diff is the detail that matters.

Store the last passing schema and the current failure side by side in the alert or artifact. That turns the alert into a reviewable change, and the person who investigates can decide in seconds whether the vendor's change is additive (update the baseline, note it in the changelog) or breaking (open a support ticket and patch your parsing). Redocly notes that contract tests give early alerts on discrepancies so teams can address problems before they reach production; the alert content is what makes "early" real.

One more discipline: version the schema and treat a baseline change as a code change. Pactflow's workflow publishes contracts and runs a can-i-deploy check before promoting, so a schema change is reviewed and deliberate. You do not need the full broker for a single vendor, but the review habit is free: require one approval on diffs under contracts/.

A worked decision rule for what to assert

You will not get this right on the first pass, because the first pass usually encodes too much. Use this test to decide whether a field earns a schema assertion:

  • Does your probe or app code read it? If no, do not assert it. It is the vendor's business.
  • Would a type change break arithmetic or a branch? Yes means assert the type (integers stay integers, currency stays a string).
  • Would a rename break a branch? Yes means assert presence and, where it is a discriminator, the value.
  • Is it a free-text message or a timestamp? Assert type only, never format, because vendors reformat those without warning.

Run the first pass, then wait. Every time your contract test fails, note whether it caught a real breaking change. If two consecutive failures were additive changes you had to update, loosen the assertion or accept the maintenance as the cost of the warning. The shape of a mature suite is a handful of assertions that almost never fire, each of which means a genuine integration problem.

Where the contract test gives way to better tooling

Contract testing is a discipline first. Where the test touches a vendor with a complex auth flow or several response shapes, the recording and the scheduled runs are repetitive to build and maintain. Teams that do a lot of this often centralize the plumbing, and the same is true of the probe: if a smoke job calls models or third-party APIs in several steps, routing them through one gateway keeps the credentials and the shape checks in one place. Quanta Inference is worth a look for that pattern, since it puts text, image and embedding models behind one OpenAI-compatible key and pricing in dollars, which reduces the credential sprawl a billing probe has to manage.

The general discipline holds regardless. Record the shape, assert it away from the live endpoint, schedule the check, and alert on the field that moved. When the vendor changes something at 2 a.m., the contract test should tell you what changed before your billing probe tells you something broke.

FAQ

FAQ

What is a contract test, briefly?

It is a test that checks a provider's response against an agreed shape: endpoints, status codes, required fields, field types and error envelopes, as Redocly outlines. It does not check business behavior, only the interface two sides depend on. For a billing probe, that means your probe can fail for its own reasons, and a vendor shape change fails the contract test instead.

Should the contract test call the live vendor endpoint?

No. The recorded baseline and the routine assertions should run against the vendor's sandbox with test credentials, and the scheduled run should only hit the sandbox, not production billing. QASkills' guidance is explicit that smoke tests must never trigger billing events or use real accounts. Keeping the contract test on the sandbox also makes failures reproducible.

How is this different from my existing integration test?

An integration test asks "does the flow work end to end." A contract test asks "does the shape still match the agreement." ACCELQ's writeup draws the line the same way: functional tests check whether an API works, contract tests check whether it still honors the agreement other services depend on. In practice, the contract test is faster, cheaper and pinpoints the field.

How often should the scheduled contract run?

Often enough to beat your probe's alert path. If your smoke job runs every fifteen minutes and pages on failure, a contract check a few times a day still gives you hours of warning, and it costs nothing when nothing changes. Align the schedule to how much notice you need to react, not to how often the vendor releases.

Won't the test break every time the vendor adds a field?

Only if you configure it to. Allow additional properties, assert types rather than values, and avoid exact-match on free-text fields. Pactflow's schema approach treats additive changes and breaking changes differently for exactly this reason. The test should go red when something is removed or retyped, not when the payload grows.

Sources

Sources

  1. Smoke Testing: What It Is, Examples & Checklist
  2. Smoke Testing: Definition, Examples & Checklist 2026
  3. API Contract Testing: Techniques, Tools, and Best Practices
  4. API Contract Testing 101: Benefits, challenges, and when ...
  5. Using Pactflow for Schema-Based Contract Testing
  6. API Contract Testing: Definition, Benefits, and Best Practices
  7. Production Smoke Suite - QASkills.sh
  8. Workflow syntax for GitHub Actions
  9. GitHub Actions documentation
  10. Learn to Use GitHub Actions: a Step-by-Step Guide - freeCodeCamp
  11. CI/CD Pipeline Testing: Run, Gate and Alert on E2E Tests
  12. Quality Gates in CI/CD: Fail a Pipeline on Test Results with ...
  13. Smoke Testing Your SaaS: A Practical Guide for Founders - MakerKit
  14. Smoke Testing: What It Is, When to Run It, Best Practices - Momentic AI
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles