# How to contract test an API you do not own

> Write down the fields your integration reads, check a recorded response against them, and run the same check against the live API on a schedule.

Source: https://voxgig.com/howto/contract-test-an-api-you-do-not-own

- Audience: api-consumer
- Level: intermediate
- Languages: typescript, javascript
- Verified: 2026-09-06
- Published: 2026-09-06

## Short answer

List the paths your code reads and the type each one must have, and check that list against a recorded response in your test suite. Run the same check against the live API in a scheduled job rather than in the build. Upstream changes to fields you never read pass, and a change to one you depend on names the field that moved.

---
## You will need

Node 22 or later, and one recorded response from the API. A sandbox account is better than
production, and the recording matters more than where it came from: it is the thing your contract is
first written against.

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [A field-level contract](https://martinfowler.com/bliki/ContractTest.html) | Any upstream, and you can name the fields you read | You maintain the list, and a field added to your code without the contract goes unchecked | The provider publishes a schema you can validate against directly |
| [Consumer-driven contracts](https://docs.pact.io/) | The provider is another team who will run your contract in their build | A broker to operate, and a provider willing to take part | The upstream is a vendor who will never run your tests |
| [Recorded fixtures alone](https://github.com/nock/nock) | Fast, offline unit tests of your own logic | The recording ages, and nothing tells you when the live API stopped matching it | You need to know that the upstream still behaves this way |
| [Schema validation](https://json-schema.org/) | The provider publishes an OpenAPI or JSON Schema document you trust | Their document can disagree with their API, so you test the document rather than the service | No published schema, or one that is out of date |

The distinction that matters is between checking your assumptions and checking their whole API.
Pact needs the provider's cooperation, which a vendor will not give you. Schema validation needs a
document that matches reality. A field-level contract needs neither, and its weakness is that it
only covers what you thought to write down.

## Write down what you read

The contract is a list of paths, not a copy of the response.

```ts title="contract.mjs"
/** What this integration reads from the upstream invoice endpoint, and nothing else. */
export const INVOICE_CONTRACT = {
  id: 'string',
  'amount.value': 'number',
  'amount.currency': 'string',
  'customer.email': 'string',
  lines: 'array',
}
```

Keeping the list this short is the point of writing one at all. The recorded response has a dozen more fields, and every one you
add to the contract is a field that can fail your build without breaking your code. A vendor adding `auto_advance` to every invoice should cost you nothing at all, and with a short
contract it does.

Put the contract next to the code that reads those paths, and treat adding a read as a change to
both. That is the discipline the approach depends on, and it is the one that decays: a new feature
reads `customer.name`, nobody adds it, and the contract silently stops covering the integration.

## Run it in two places

The same function runs against a fixture and against the live API, and the two runs answer different
questions. Against the fixture it asks whether your parsing still matches the recording, and it runs
in your build in milliseconds. Against the live API it asks whether the recording is still true, and
it needs credentials, network and a tolerance for the upstream being down.

Keep the second one out of the build. A scheduled job that runs nightly and opens an issue tells you
about drift within a day. A build that calls a vendor fails whenever that vendor has a bad
afternoon.

Where the vendor publishes a sandbox, run the scheduled check there rather than against production.
This usually takes one of two shapes. [Stripe's test mode](https://docs.stripe.com/test-mode) is a
parallel environment returning the same response shapes.
[GitHub's REST API](https://docs.github.com/en/rest) is a documented service you can call read-only
with a low-privilege token. Either gives you a live answer without a live side effect.

## Check it worked

The check has to pass on the recording and fail on a drifted one, naming what moved.

```bash
node demo.mjs
```

```text output
fixture.json: contract holds
drifted.json: 2 failures
  amount.value: expected number, found string
  customer.email: missing
```

Those two lines are the failure from the opening paragraph, caught by name. The drifted fixture also
adds a field, and nothing reports it, which is the behavior a contract for one consumer should have.

```bash
node --test contract.test.mjs
```

```text output
1..3
# tests 3
# suites 0
# pass 3
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 113.137809
```

## When it goes wrong

The contract passes and production still breaks. Types are the cheap half of a contract, and meaning
is the expensive half. An amount that stays a number while changing from cents to units passes every
check on this page, and a currency code that starts arriving lowercase does too. Where a value has
semantics, assert on the semantics: a range, a set of allowed values, or a known identifier.

The second failure is a fixture that nobody refreshes. A recording made once and never renewed drifts
from the live API, and the scheduled run is what catches that. Re-record against the sandbox on the
same schedule, and treat a difference between the new recording and the old one as the thing to read
rather than as noise to overwrite.

## When not to do this

Do not write a contract covering every field in the response. That is schema validation with extra
steps, and it fails your build every time the vendor ships anything.

Do not run the live check on every pull request. A vendor's rate limit, sandbox outage, or slow
afternoon becomes your build failure, and the signal you wanted arrives just as noisily a few hours
later from a scheduled run.

Do not put credentials for the live check in the same place as the build's. The scheduled job needs
a read-only sandbox credential, and giving the build a token that can reach production is a much
larger change than adding a test.

Do not treat a passing contract as permission to skip error handling. It tells you the shape was
right on the last run, and says nothing about the upstream returning a 500 at the moment you call it.

## Related how-tos

- [Choose consumer-driven or spec-driven contract tests](/howto/choose-consumer-driven-or-spec-driven-contract-tests)

- [Stub outbound HTTP calls in Node tests with nock](/howto/stub-outbound-node-http-calls-with-nock)

## Last verified

Verified 2026-09-06 against Node 22.22.2. Both output blocks are what the preceding command printed,
against two checked-in fixtures rather than a live vendor API.