Skip to content

Sunset Headers for API Versioning

DodaTech Updated 2026-06-28 5 min read

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

Sunset headers are HTTP response headers that inform clients when an API version or feature will be removed, following the proposed HTTP Sunset Header standard.

What You'll Learn

By the end of this lesson, you will implement Sunset headers, combine them with deprecation warnings, and build client-side tooling to react to scheduled removals.

Why It Matters

Sunset headers give clients an automated way to know when a version will stop working. Clients can parse these headers and schedule migrations without manual monitoring.

Real-World Use

GitHub returns Sunset headers on deprecated APIs with the date the endpoint will stop working. API management platforms like Kong support adding these headers automatically.

Sunset Header Flow

sequenceDiagram
    Client->>API: GET /v1/users
    API->>Client: 200 + Sunset: Sat, 31 Dec 2026
    Client->>Client: Parse Sunset header
    Client->>Client: Schedule Migration before date
    Client->>API: GET /v2/users
    API->>Client: 200 (no sunset header)

Sunset Header Implementation

# sunset_headers.py
from datetime import datetime, timezone, timedelta
from typing import Dict, Optional

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

    def set_sunset(self, version: str, sunset_date: str,
                   successor: str, reason: str = "deprecated"):
        self.policies[version] = {
            "sunset": sunset_date,
            "successor": successor,
            "reason": reason,
        }

    def build_headers(self, version: str) -> Dict[str, str]:
        policy = self.policies.get(version)
        if not policy:
            return {}

        headers = {
            "Sunset": policy["sunset"],
            "Link": f'<{policy["successor"]}>; rel="successor-version"',
        }

        sunset_dt = self._parse_date(policy["sunset"])
        if sunset_dt:
            days = (sunset_dt - datetime.now(timezone.utc)).days
            if days <= 90:
                headers["Warning"] = f'299 sunset "{days} days until removal"'

        return headers

    def _parse_date(self, date_str: str) -> Optional[datetime]:
        formats = [
            "%a, %d %b %Y",
            "%Y-%m-%d",
        ]
        for fmt in formats:
            try:
                return datetime.strptime(date_str, fmt).replace(tzinfo=timezone.utc)
            except ValueError:
                continue
        return None

    def days_until_sunset(self, version: str) -> Optional[int]:
        policy = self.policies.get(version)
        if not policy:
            return None
        sunset_dt = self._parse_date(policy["sunset"])
        if not sunset_dt:
            return None
        return (sunset_dt - datetime.now(timezone.utc)).days

service = SunsetHeaderService()
service.set_sunset("v1", "2026-12-31", "/docs/v2-migration")

headers = service.build_headers("v1")
for key, value in headers.items():
    print(f"{key}: {value}")

print(f"\nDays to sunset: {service.days_until_sunset('v1')}")

Expected output:

Sunset: 2026-12-31
Link: </docs/v2-migration>; rel="successor-version"
Warning: 299 sunset "<n> days until removal"

Days to sunset: <n>

Client-Side Sunset Parsing

# sunset_client.py
from datetime import datetime, timezone
from typing import Dict, Optional

class SunsetClient:
    def __init__(self):
        self.known_sunsets: Dict[str, datetime] = {}
        self.warnings: list = []

    def parse_response(self, url: str, headers: Dict):
        sunset = headers.get("Sunset")
        if not sunset:
            return

        try:
            sunset_dt = datetime.strptime(sunset, "%Y-%m-%d").replace(tzinfo=timezone.utc)
        except ValueError:
            try:
                sunset_dt = datetime.strptime(sunset, "%a, %d %b %Y").replace(tzinfo=timezone.utc)
            except ValueError:
                return

        self.known_sunsets[url] = sunset_dt
        days = (sunset_dt - datetime.now(timezone.utc)).days

        if days < 0:
            self.warnings.append(f"PAST: {url} sunset was {abs(days)} days ago")
        elif days < 30:
            self.warnings.append(f"URGENT: {url} sunsets in {days} days")
        elif days < 90:
            self.warnings.append(f"WARN: {url} sunsets in {days} days")
        else:
            self.warnings.append(f"INFO: {url} sunsets in {days} days")

client = SunsetClient()
client.parse_response("/api/v1/users", {"Sunset": "2026-12-31"})
client.parse_response("/api/v1/posts", {"Sunset": "2030-01-01"})

for w in client.warnings:
    print(w)

print(f"\nTracked URLs: {len(client.known_sunsets)}")

Expected output:

INFO: /api/v1/users sunsets in <n> days
INFO: /api/v1/posts sunsets in <n> days

Tracked URLs: 2

Sunset Policy Enforcement

# sunset_enforcer.py
from datetime import datetime, timezone
from typing import Dict, Optional, Tuple

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

    def schedule(self, version: str, date_str: str):
        self.sunsets[version] = date_str

    def enforce(self, version: str) -> Tuple[int, Dict]:
        policy = self.sunsets.get(version)
        if not policy:
            return 200, {"status": "active"}

        try:
            sunset_dt = datetime.strptime(policy, "%Y-%m-%d").replace(tzinfo=timezone.utc)
        except ValueError:
            return 200, {"status": "active"}

        now = datetime.now(timezone.utc)

        if now > sunset_dt:
            return 410, {
                "error": "Gone",
                "message": f"Version {version} was removed on {policy}",
                "successor": "/api/v2",
            }

        days_left = (sunset_dt - now).days
        return 200, {
            "status": "active",
            "version": version,
            "sunset": policy,
            "days_left": days_left,
        }

enforcer = SunsetEnforcer()
enforcer.schedule("v1", "2026-12-31")
enforcer.schedule("v0", "2020-01-01")

for ver in ["v1", "v0", "v2"]:
    status, body = enforcer.enforce(ver)
    print(f"{ver}: {status} - {body.get('status', body.get('error'))}")

Expected output:

v1: 200 - active
v0: 410 - Gone
v2: 200 - active

Common Mistakes

1. Using Non-Standard Date Formats

Always use RFC 1123 format (Sat, 31 Dec 2026 23:59:59 GMT) or ISO 8601 (2026-12-31) for Sunset headers.

2. No Successor Link

Clients need to know what to migrate to. Always include a Link header with rel="successor-version".

3. Not Updating Sunset Dates

If you extend a sunset deadline, update the header. Clients parse the header and may migrate unnecessarily.

4. Removing the Version on Sunset Day

Give a grace period after the sunset date. Some clients may have clock skew or delayed migration. Return 410 after the grace period.

5. Not Logging Sunset Header Delivery

Track which clients received Sunset headers to know who has been warned and who has migrated.

Practice Questions

1. What does the Sunset HTTP header indicate?

The date when an API version or feature will be removed from service.

2. What format should the Sunset header value use?

RFC 1123 format (e.g., Sat, 31 Dec 2026 23:59:59 GMT) or ISO 8601 date.

3. How do clients know the replacement for a sunset endpoint?

The Link header with rel="successor-version" provides the URL to the replacement.

4. What HTTP status code should sunset endpoints return after removal?

410 Gone, indicating the resource is permanently gone and will not return.

Challenge

Build a sunset header monitoring system that tracks all active Sunset headers across your API, sends alerts to client contacts, and validates migration completion before the sunset date.

FAQ

Is Sunset a standard HTTP header?

It is a proposed standard (draft-ietf-httpapi-sunset-header). It is widely adopted by major API providers.

Should I include Sunset headers on every response?

Only on responses for deprecated versions or endpoints. Active, current version endpoints do not need Sunset headers.

How do I test if my Sunset header works?

Use curl -I to inspect response headers. Write automated tests that assert the Sunset header format and date.

Can Sunset headers be used for non-version reasons?

Yes. You can sunset a specific endpoint (e.g., /legacy/reports) even if the overall API version is active.

What if multiple sunset dates apply?

Return the earliest sunset date. Clients care about when access will be removed, not the full deprecation history.

Mini Project: Sunset Dashboard

# sunset_dashboard.py
from datetime import datetime, timezone, timedelta

class SunsetDashboard:
    def __init__(self):
        self.endpoints = {}

    def register(self, endpoint: str, sunset_str: str, successor: str):
        dt = datetime.strptime(sunset_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
        self.endpoints[endpoint] = {"sunset": dt, "successor": successor}

    def summary(self):
        now = datetime.now(timezone.utc)
        for ep, info in sorted(self.endpoints.items(), key=lambda x: x[1]["sunset"]):
            days = (info["sunset"] - now).days
            status = "PAST" if days < 0 else f"{days}d left"
            print(f"{ep:30s} sunset={info['sunset'].strftime('%Y-%m-%d')} {status:10s} -> {info['successor']}")

dash = SunsetDashboard()
dash.register("/api/v1/users", "2026-12-31", "/api/v2/users")
dash.register("/api/v1/posts", "2027-06-30", "/api/v2/posts")

dash.summary()

Expected output:

/api/v1/posts                    sunset=2027-06-30 <n>d left  -> /api/v2/posts
/api/v1/users                    sunset=2026-12-31 <n>d left  -> /api/v2/users

What's Next

You understand sunset headers. Next, learn about versioning database schemas, then explore versioning microservices.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro