How to check API versioning

Find out which of three places the server reads the version from, the URL path, a request header or a query parameter, and what it does when the version is unknown. curl -s -D - -H 'X-GitHub-Api-Version: 1999-01-01' https://api.github.com/ answers 200 and names the version it actually used, not the one asked for.

Why check this

A client pins a version so a server change cannot break it. That promise holds only if the server reads the pin and refuses what it cannot serve. Run this check when integrating a new API, and again whenever the provider announces a new version. The failure it prevents is silent: the client asks for a version that no longer exists, the server serves the current one, and a field that changed type reaches production untested.

Prerequisites

const http = require('node:http');
const SUPPORTED = ['v1', 'v2'];

http.createServer((req, res) => {
  const url = new URL(req.url, 'http://localhost');
  const fromPath = /^\/(v\d+)\//.exec(url.pathname);
  const version = fromPath?.[1] ?? req.headers['api-version'] ?? url.searchParams.get('version') ?? 'v1';
  const send = (code, body) => {
    res.writeHead(code, { 'content-type': 'application/json', 'api-version': version });
    res.end(JSON.stringify(body));
  };
  if (!SUPPORTED.includes(version)) {
    return send(400, { title: 'Unsupported API version', status: 400, requested: version, supported: SUPPORTED });
  }
  if (version === 'v1') return send(200, { version, price: 10 });
  send(200, { version, price: { amount: 10, currency: 'EUR' } });
}).listen(8790, () => console.log('versioned-api on 8790'));

Stop it with Ctrl+C when the run is over.

Steps

  1. Step 1.

    Call a path-versioned endpoint and confirm the version in the URL is served.

    curl -s -w '\nHTTP %{http_code} %{content_type}\n' 'https://developer.mozilla.org/api/v1/whoami'
    
    {"geo":{"country":"Ukraine","country_iso":"UA"},"username":null, …}
    HTTP 200 application/json
  2. Step 2.

    Ask the same service for a version number that does not exist.

    curl -s -o /dev/null -w 'HTTP %{http_code} %{content_type}\n' 'https://developer.mozilla.org/api/v2/whoami'
    
    HTTP 404 text/html

    Path versioning answers a missing version through the router, so the reply is a 404 and its body is an HTML page. A JSON client fails at the parser rather than on the status.

  3. Step 3.

    Now a header-versioned API. Send a version that was never published and read the response headers.

    curl -s -D - -o /dev/null -H 'X-GitHub-Api-Version: 1999-01-01' 'https://api.github.com/' | grep -i -E '^HTTP|^x-github-api-version-selected|^x-github-media-type'
    
    HTTP/2 200
    x-github-api-version-selected: 2022-11-28
    x-github-media-type: github.v3; format=json

    The request asked for 1999-01-01 and the answer came from 2022-11-28. The version that was served is in the response, and it is the only reliable place to read it.

  4. Step 4.

    Against the local target, call two path versions of one resource and compare the bodies.

    for v in v1 v2; do curl -s -w '\n' "http://localhost:8790/$v/products/1"; done
    
    {"version":"v1","price":10}
    {"version":"v2","price":{"amount":10,"currency":"EUR"}}

    price changed from a number to an object. This is the change that a pinned version is supposed to hide from old clients.

  5. Step 5.

    Select the version with a header instead, on a path that carries no version, and check the response says which one it used.

    curl -s -D - -H 'Api-Version: v2' 'http://localhost:8790/products/1' | grep -i -E '^HTTP|^api-version'
    
    HTTP/1.1 200 OK
    api-version: v2
  6. Step 6.

    Select it with a query parameter and confirm the same body comes back.

    curl -s -w '\n' 'http://localhost:8790/products/1?version=v2'
    
    {"version":"v2","price":{"amount":10,"currency":"EUR"}}

    Three selection styles on one service is two too many. Find out which one your API treats as authoritative when they disagree.

  7. Step 7.

    Ask for a version the target does not support.

    curl -s -w ' HTTP %{http_code}\n' -H 'Api-Version: v9' 'http://localhost:8790/products/1'
    
    {"title":"Unsupported API version","status":400,"requested":"v9","supported":["v1","v2"]} HTTP 400

    A 400 that names what was requested and what exists is the answer a client can act on. Compare it with step 3.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | A response header naming the served version | The server tells you what answered | Assert that header in every contract test. | | 200 for a version that does not exist | The pin is decorative | Treat the client as unpinned. A default change will reach it. | | 400 listing the supported versions | The server refuses clearly | Assert the list. It is the deprecation notice you get earliest. | | 404 with an HTML body | A path version was routed, not validated | Confirm the client handles a non-JSON error on this route. | | No version anywhere in the response | You cannot prove which code answered | Ask for a version header before writing contract tests. | | Path and header disagree and one wins silently | Precedence is undefined in the docs | Test both orders and write down the winner. | | Sunset or Deprecation headers present | The version has an end date | Read the date, then plan the client change. |

Common mistakes

Sign: An unsupported version returns 200 and every test passes.Cause: The server ignores a version it does not know and serves the current one. A request to api.github.com with X-GitHub-Api-Version set to 1999-01-01 answered 200 on 2026-09-11, with the served version 2022-11-28 in the response header.
Sign: The version header works from a laptop and is ignored from the deployed client.Cause: A proxy or CDN in front of the API drops headers that are not on its allow list. The request arrives without the version and the default answers, so the behaviour depends on the network path, not on the code.
Sign: The client pinned a version and still broke after a release.Cause: A version pin covers the shape the provider agreed to freeze. Added fields, changed error bodies and new required parameters often ship inside one version. Read what the provider promises the version covers.
Sign: Tests assert the version in the request and never in the response.Cause: The request states an intention, the response states a fact. Only the served version tells you which code ran, which is why step 3 reads the response and not the command.

What to check next

FAQ

How do I check which API version a service is running?

Read the response, not the docs. Look for a header such as x-github-api-version-selected or a version field in the body. If neither exists, request two versions and compare the bodies, since a difference proves the selection worked.

Which versioning style should I expect?

Path versioning, as in /api/v1/, a request header, or a media type such as application/vnd.acme.v2+json. A query parameter is the fourth. All four appear in production APIs, so check the documentation first and then confirm with a request.

What should happen when a client asks for a version that does not exist?

A 4xx that names the supported versions, as in step 7. Serving the default instead leaves the client believing it is pinned while it is not.

Does a version in the URL mean the response body cannot change?

No. Providers add fields inside a version and call it compatible, which it is for clients that ignore unknown fields. Assert the fields you read rather than the whole body, and pin the version as well.

Verified

Verified by Maks Vernycurl 8.21.0node 22.23.2

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

basic8 minpublished updated Maks Verny