Skip to content

API Version Negotiation — How Clients and Servers Agree on Versions

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you'll learn about Version Negotiation. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

API version negotiation is the Process by which client and server agree on which API version to use, balancing client capabilities with server support.

What You'll Learn

By the end of this lesson, you will implement client-driven and server-driven version negotiation, version discovery endpoints, capability detection, and graceful degradation when versions mismatch.

Why It Matters

Proper negotiation ensures clients always use a compatible version and fail gracefully when version mismatches occur, preventing integration failures.

Real-World Use

Durga Antivirus Pro clients negotiate API versions by requesting /api/version, receiving supported versions and capabilities, then selecting the latest compatible version.

Negotiation Strategies

flowchart TD
    Request-->Negotiation{Negotiation Type}
    Negotiation-->|Client-Driven|Client[Client Picks Version]
    Negotiation-->|Server-Driven|Server[Server Assigns Version]
    Negotiation-->|Content Negotiation|Content[Accept Header]
    Client-->Request2[Client includes version]
    Server-->Response[Server responds with version]
    Content-->MediaType[Media type includes version]

Version Discovery Endpoint

Provide a version discovery endpoint for clients.

from typing import Dict, List, Optional

class VersionDiscovery:
    def __init__(self):
        self.versions: Dict[str, Dict] = {}

    def add_version(self, version_id: str,
                    base_url: str,
                    status: str = "stable",
                    release_date: str = "",
                    sunset_date: str = "",
                    capabilities: Optional[List[str]] = None):
        self.versions[version_id] = {
            "version": version_id,
            "base_url": base_url,
            "status": status,
            "release_date": release_date,
            "sunset_date": sunset_date,
            "capabilities": capabilities or [],
        }

    def get_discovery_response(self,
                                client_version: Optional[str] = None
                                ) -> Dict:
        if client_version and client_version in self.versions:
            return self.versions[client_version]
        latest = self._get_latest_stable()
        return {
            "latest_version": latest["version"],
            "current_version": latest,
            "all_versions": list(self.versions.values()),
        }

    def _get_latest_stable(self) -> Optional[Dict]:
        stable = [
            v for v in self.versions.values()
            if v["status"] == "stable"
        ]
        if stable:
            return stable[-1]
        return None

    def get_supported_versions(self) -> List[str]:
        return sorted(self.versions.keys())

discovery = VersionDiscovery()
discovery.add_version("v1", "/api/v1", "deprecated",
                       sunset_date="2026-12-31")
discovery.add_version("v2", "/api/v2", "stable",
                       capabilities=["scan", "reports"])
discovery.add_version("v3", "/api/v3", "beta",
                       capabilities=["scan", "reports", "analytics"])
resp = discovery.get_discovery_response()
print(f"Latest stable: {resp['latest_version']}")
print(f"Supported: {discovery.get_supported_versions()}")

Client-Driven Negotiation

Clients explicitly specify their desired version.

from typing import Dict, Optional, List, Tuple

class ClientDrivenNegotiation:
    def __init__(self):
        self.supported = set()

    def add_supported(self, version: str):
        self.supported.add(version)

    def negotiate(self, client_headers: Dict
                  ) -> Tuple[str, Optional[str]]:
        requested = client_headers.get("X-API-Version", "")
        accept = client_headers.get("Accept", "")

        if requested in self.supported:
            return requested, None

        media_version = self._extract_media_version(accept)
        if media_version and media_version in self.supported:
            return media_version, None

        default = sorted(self.supported)[-1]
        return default, f"Requested version not available, using {default}"

    def _extract_media_version(self, accept: str) -> Optional[str]:
        import re
        match = re.search(r"vnd\.api\.v(\d+)", accept)
        if match:
            return f"v{match.group(1)}"
        return None

    def get_best_version(self, client_capabilities: List[str]
                         ) -> Optional[str]:
        for version in sorted(self.supported, reverse=True):
            vcaps = set()
            if all(c in vcaps for c in client_capabilities):
                return version
        return None

cneg = ClientDrivenNegotiation()
cneg.add_supported("v1")
cneg.add_supported("v2")
version, warning = cneg.negotiate({"X-API-Version": "v2"})
print(f"Negotiated: {version}, warning: {warning}")

Server-Driven Negotiation

Server selects the best version based on client profile.

from typing import Dict, Optional, List

class ServerDrivenNegotiation:
    def __init__(self):
        self.client_profiles: Dict[str, str] = {}

    def register_client(self, client_id: str,
                        preferred_version: str):
        self.client_profiles[client_id] = preferred_version

    def negotiate(self, client_id: str,
                  user_agent: str,
                  supported_versions: List[str]
                  ) -> str:
        preferred = self.client_profiles.get(client_id)
        if preferred and preferred in supported_versions:
            return preferred
        if "mobile" in user_agent.lower():
            return supported_versions[0]
        return supported_versions[-1]

    def upgrade_client(self, client_id: str,
                       new_version: str):
        self.client_profiles[client_id] = new_version

    def get_client_version(self, client_id: str
                           ) -> Optional[str]:
        return self.client_profiles.get(client_id)

sneg = ServerDrivenNegotiation()
sneg.register_client("partner-1", "v2")
version = sneg.negotiate(
    "partner-1",
    "DodaBrowser/2.0",
    ["v1", "v2", "v3"]
)
print(f"Server assigned version: {version}")

Common Mistakes

Mistake 1: No Version Discovery Endpoint

Clients should not guess which versions are available. Provide a /api/version or /.well-known endpoint.

Mistake 2: Silently Downgrading

If a client requests v3 but gets v1, it may break. Warn the client about version mismatches.

Mistake 3: No Negotiation Fallback

When negotiation fails, provide a clear error. Do not silently serve the wrong version.

Mistake 4: Ignoring Client Capabilities

Not all clients support all features. Negotiate based on client capabilities, not just version numbers.

Mistake 5: Hardcoded Version Selection

Version selection should be configurable per client, not hardcoded in the negotiation logic.

Practice Questions

  1. What is the difference between client-driven and server-driven negotiation?
  2. Why provide a version discovery endpoint?
  3. How does content negotiation work for versioning?
  4. What happens when version negotiation fails?
  5. How do you handle client capability detection?

Challenge

Build a version negotiation system that supports both client-driven (X-API-Version header) and server-driven (client profile-based) negotiation, provides a version discovery endpoint at /api/versions, and includes graceful fallback with clear warnings.

FAQ

What is API version negotiation?

Version negotiation is the process where client and server agree on which API version to use, ensuring compatibility and enabling gradual migration.

Who should drive version negotiation?

Both can work. Client-driven puts control with the consumer. Server-driven allows the provider to optimize based on client capabilities.

What is a version discovery endpoint?

An endpoint that returns the list of supported versions, their statuses (stable/beta/deprecated), base URLs, and sunset dates.

How does capability-based negotiation differ from version negotiation?

Capability negotiation negotiates specific features rather than entire versions, enabling finer-grained compatibility.

What headers are used for version negotiation?

X-API-Version for explicit version, Accept with custom media types for content negotiation, and User-Agent for server-driven selection.

Mini Project

Build a version negotiation system with a discovery endpoint (/api/versions), client-driven negotiation via X-API-Version header, server-driven fallback based on client profile, capability-based version selection, and clear error messages when negotiation fails.

What's Next

Learn about Accept Header Versioning for content-negotiation-based versioning, or explore Custom Header Versioning for header-based approaches.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro