# Third-Party API Deprecation and Sunset Header Watch (`kingii98/third-party-api-deprecation-and-sunset-header-watch`) Actor

Watch the third-party API endpoints that you call in production for RFC 9745 Deprecation and RFC 8594 Sunset response headers. The Actor compares each endpoint against a stored baseline, reports new deprecations and moved sunset dates, and fails a gate wh

- **URL**: https://apify.com/kingii98/third-party-api-deprecation-and-sunset-header-watch.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 run starteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Third-Party API Deprecation and Sunset Header Watch

Watch the third-party API endpoints that you call in production. Learn about a
planned shutdown before the endpoint stops, and learn the same day when a
version you call is gone.

The Actor sends one request to each endpoint, reads every deprecation signal the
vendor sends, and compares the signal set against the baseline of the previous
run. It reports each new deprecation, each gone endpoint and each moved sunset
date. A gone endpoint, or a sunset date inside the warning window, sets the gate
to fail.

### What the Actor watches

A probe of 201 vendor endpoints on 2026-09-11 (`docs/signal-probe.md`) found that
only one vendor sends the RFC headers, and that a retired API version answers
**HTTP 410 Gone with a notice in the error body** far more often. The Actor
therefore reads all of these:

| Signal source | Example |
| --- | --- |
| `rfc-deprecation-header` | RFC 9745 `Deprecation: @1756339200` (Bitbucket) |
| `rfc-sunset-header` | RFC 8594 `Sunset: Tue, 14 Apr 2026 00:00:00 GMT` (Bitbucket) |
| `link-relation` | `Link: <...>; rel="deprecation"` or `rel="sunset"` |
| `vendor-header` | `x-ad-api-version-warning: ... v2.0 has been deprecated` (Facebook Graph), `X-Shopify-API-Deprecated-Reason`, `Warning: 299 - "... deprecated"` |
| `http-410-gone` | HTTP 410 on a retired version (Twitter v1, GitLab v3, Mailchimp 2.0, Etsy v2, Yelp v2, Docker Hub v1, Bintray) |
| `error-body-notice` | `"API V3 is no longer supported. Use API V4 instead."` in the body of the error answer (GitLab, Twitter, Mailchimp, Bitbucket) |

An HTTP 404 alone is not a signal: a wrong path looks the same. It becomes the
change class `endpoint-gone` when the baseline shows that the endpoint answered
before. A `successor-version` link alone only says that a newer version exists.

The Actor uses HTTP only. It uses no browser and no proxy. It reads the response
headers. On an **error answer** (HTTP 4xx) it also reads at most 64 KB of the
body for a vendor notice; the body of a success answer is never read, so a data
record with the word "sunset" in it can never trip it.

### What you get

- One row for each endpoint: the HTTP status and the previous HTTP status, the
  list of signal sources, the raw Deprecation header and its parsed date, the raw
  Sunset header and its parsed date, every Link target with rel `deprecation`,
  `sunset` or `successor-version`, every API version response header, the vendor
  deprecation headers, the notice found in the error body, the days until the
  sunset, whether the endpoint is gone, and the change class against the baseline.
- One summary row: the endpoint count, the deprecated count, the gone count, the
  nearest sunset date, and the boolean `gate_pass`.
- Optional: one JSON POST to your webhook for each endpoint whose deprecation
  state changed.

### The baseline

The Actor keeps the last parsed header set of each endpoint in the **named**
key-value store `api-sunset-header-baseline`, under the record key that
`baseline_name` gives. The named store stays after the run ends. The key of one
endpoint is its method and its URL, so one URL with two methods is two endpoints.

- **First run for an endpoint**: the Actor writes the baseline. It reports every
  present signal as `new-deprecation`. The row has `baseline_status: "created"`.
- **Later run**: the Actor compares the current headers against the baseline. The
  row has `baseline_status: "compared"`.
- **An endpoint that did not give usable headers** (timeout, DNS failure, refused
  target, HTTP 5xx or 429 after the retry): the Actor keeps the stored entry. The
  row has `baseline_status: "unavailable"` and no change class. An error page from
  a gateway must not look like a removed signal.

The Actor writes the record one time, at the end of the run. An aborted run does
not change the previous baseline. Endpoints that this run did not request keep
their entry. The record holds at most 500 endpoints.

### Change classes

A **signal** is any source in the table above. A `successor-version` link alone
is not a signal, and a 404 alone is not a signal.

| Change class | Meaning |
| --- | --- |
| `endpoint-gone` | The endpoint answers HTTP 410, or HTTP 404 where the baseline shows it answered before. This is the most urgent class. |
| `new-deprecation` | A signal source appeared where the baseline had none, or a new source appeared next to an existing one: a Deprecation header next to an existing Sunset header, or a body notice next to an existing 410. |
| `sunset-earlier` | The Sunset date moved earlier, or a Sunset date appeared where there was none. |
| `sunset-later` | The Sunset date moved later, or the Sunset date disappeared while the endpoint stays deprecated. |
| `signal-removed` | The baseline had a signal, and the endpoint now sends none. |
| `unchanged` | No change. A reformatted header with the same date is unchanged. |

When two changes occur at the same time, the more urgent class wins:
`endpoint-gone`, then `sunset-earlier`, then `new-deprecation`, then `sunset-later`.

### The gate

`gate_pass` is `false` when at least one checked endpoint is gone (HTTP 410, or
404 where the baseline answered), or has a Sunset date inside `sunset_warning_days`
from now. A Sunset date in the past is also inside the window. A Deprecation
header, a vendor header or a body notice without a Sunset date does not set the
gate to fail: the endpoint still answers.

A failed gate is a **result**, not a fault. The run writes it to the dataset and to
the status message, and the run **succeeds**. The same applies to a changed
signal, an unreachable endpoint and an unusable input entry.

### Input

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `endpoints` | array of 1 to 100 records | four public demo endpoints | Each record has `url`, `method` (`GET`, `HEAD` or `OPTIONS`, default `GET`), optional static `headers`, and optional `use_auth_header`. A plain URL string is a GET request. |
| `auth_header` | secret string | none | One header that you own, in the form `Name: secret`. |
| `sunset_warning_days` | integer 0 to 3650 | `90` | A Sunset date inside this window sets the gate to fail. A gone endpoint sets it to fail too. |
| `alert_webhook_url` | HTTPS URL | empty | One JSON POST for each deprecation state change. |
| `baseline_name` | string | `DEFAULT` | The record key of the baseline. Use one name for each endpoint list. |
| `request_timeout_seconds` | integer 2 to 60 | `15` | Timeout for each request and each webhook POST. |
| `concurrency` | integer 1 to 20 | `5` | Endpoints requested at the same time. |
| `max_redirects` | integer 0 to 10 | `3` | Redirect hops for each request. |
| `max_run_seconds` | integer 30 to 3600 | `240` | Deadline for the endpoint list. |

Example endpoint list:

```json
[
  {"url": "https://api.vendor.example/v1/orders?limit=1", "method": "GET",
   "headers": {"Api-Version": "2024-01-01"}, "use_auth_header": true},
  {"url": "https://api.other.example/v2/status", "method": "HEAD"}
]
```

Every field except `auth_header` has a schema default. A run with the empty input
`{}` watches the four demo endpoints and succeeds. One of them, GitLab API v3, is
retired and answers 410 with a notice, so the demo run shows a gone endpoint and
a failed gate.

#### The secret auth header

- The Actor sends `auth_header` only to the endpoints with `"use_auth_header": true`.
  Other vendors never receive your key.
- The Actor sends it only to the origin (scheme, host and port) of the endpoint URL.
  A redirect to a different origin does not get it.
- The Actor never writes the secret to the log, the dataset, the baseline or the
  webhook payload. The row shows only `auth_header_sent: true` and the names of the
  static request headers.

#### Safety limits

- Only `http` and `https` URLs. No credential in a URL.
- `localhost`, and any hostname that resolves to a private or reserved address, is
  refused. The Actor resolves and checks each redirect hop again before it follows
  the hop. A redirect chain that repeats a URL stops.
- An input entry that is not usable (a malformed URL, a foreign scheme, a
  `localhost` target, a method other than GET, HEAD or OPTIONS, a bad header, a
  duplicate) becomes **one row** with the reason code `INVALID_ENDPOINT` or
  `BLOCKED_TARGET`. It does not stop the run, and it is not charged.
- At most 100 endpoints, 20 static headers for each endpoint, 20 Link targets and
  20 API version headers for each row.
- One endpoint gets one request and one retry. The retry occurs after a timeout, a
  connection error, or HTTP 429, 500, 502, 503 or 504. One endpoint takes at most
  `2 x request_timeout_seconds + 5` seconds. The worst-case wall clock of the run is
  `min(ceil(endpoints / concurrency) x that budget, max_run_seconds)`. With the
  default fixture that is 35 seconds.
- The webhook gets no redirect. After 3 failed POSTs in a row, the run stops calling
  it and marks the other alerts as skipped.

### Output

The dataset holds one summary row and one row for each endpoint. The `record_type`
field separates them.

**Endpoint row** (`record_type: "endpoint"`): `endpoint_url`, `method`, `final_url`,
`reason_code`, `error`, `http_status`, `previous_http_status`, `signal_present`,
`deprecated`, `signal_sources`, `endpoint_gone`, `vendor_headers`, `body_notice`,
`deprecation_header`, `deprecation_date`, `sunset_header`, `sunset_date`,
`days_until_sunset`, `sunset_within_window`, `previous_sunset_date`,
`link_targets` (each with `rel` and `url`), `deprecation_links`, `sunset_links`,
`successor_version_links`, `api_version_headers`, `change_class`, `changed`,
`baseline_status`, `first_seen`, `last_changed`, `alert_status`, `attempts`,
`redirects`, `auth_header_sent`, `request_header_names`, `response_time_ms` and
`observed_at`.

**Summary row** (`record_type: "summary"`): `endpoint_count`, `endpoints_checked`,
`endpoints_unavailable`, `deprecated_count`, `gone_count`, `sunset_within_window_count`,
`nearest_sunset_date`, `nearest_sunset_endpoint`, `days_until_nearest_sunset`,
`sunset_warning_days`, `gate_pass`, `changes_total`, `changes_by_class`,
`baselines_created`, `alerts_sent`, `alerts_failed`, `baseline_store`,
`baseline_name` and `observed_at`.

**Webhook payload**: `event` (`deprecation-change-flagged`), `endpoint_url`,
`method`, `change_class`, `http_status`, `previous_http_status`, `signal_sources`,
`endpoint_gone`, `vendor_headers`, `body_notice`, `deprecation_header`, `deprecation_date`,
`sunset_header`, `sunset_date`, `previous_sunset_date`, `days_until_sunset`,
`sunset_within_window`, `link_targets` and `observed_at`.

#### Reason codes

`OK`, `SERVER_ERROR`, `TIMEOUT`, `DNS_FAIL`, `CONNECT_FAIL`, `BLOCKED_TARGET`,
`REDIRECT_LOOP`, `RUN_DEADLINE`, `CHARGE_LIMIT` and `INVALID_ENDPOINT`. Only `OK`
means that the Actor parsed the headers. An HTTP 4xx answer, for example
410 Gone, is `OK`: the 410 itself is the signal, and vendors often send the
Sunset header or a body notice with it.

### Pricing: pay per event

| Event | Unit | Counted as |
| --- | --- | --- |
| `run-started` | one Actor run | Charged one time, before the Actor requests an endpoint. |
| `endpoint-checked` | one endpoint requested and its deprecation signals parsed in one run | Charged one time for each endpoint row with `reason_code: "OK"`. An endpoint that did not give usable headers, and an input entry that is not usable, are not charged. |
| `deprecation-change-flagged` | one endpoint whose deprecation or sunset state changed since the baseline | Charged one time for each endpoint row with a change class other than `unchanged`. On the first run, each endpoint with a present signal is a `new-deprecation`, so it is charged. |

Before the Actor requests an endpoint, it limits the endpoint list to the number of
`endpoint-checked` events that the maximum total charge of the run allows. The
endpoints outside that limit get the reason code `CHARGE_LIMIT`, are not requested,
and keep their baseline for the next run.

### Repeat use

A vendor can add a deprecation signal, or retire a version, at any time. Put the Actor on a
daily or weekly Apify schedule with one saved endpoint list and one
`baseline_name`. The first run creates the baseline. Each later run reports only
the endpoints whose state changed, and the gate tells you when a shutdown comes
near.

### Local development

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `endpoints` (type: `array`):

1 to 100 endpoints. Each record has a "url", a "method" (GET, HEAD or OPTIONS, default GET), optional static request "headers" (for example an API version header), and an optional "use\_auth\_header" (true sends the secret auth header to this endpoint only). A plain URL string is a GET request. The Actor reads the response headers, and on an error answer (HTTP 4xx) at most 64 KB of the body for a vendor deprecation notice. If you leave this field empty, the Actor watches the four public demo endpoints shown below.

## `auth_header` (type: `string`):

Optional. One header that you own, in the form "Name: secret", for example "X-Api-Key: abc123". The Actor sends it only to the endpoints with "use\_auth\_header": true, and only to the origin of that endpoint URL: a redirect to a different origin does not get it. The Actor never writes the secret to the log, the dataset or the baseline.

## `sunset_warning_days` (type: `integer`):

A Sunset date inside this many days from now, or a date in the past, sets gate\_pass to false. A gone endpoint (HTTP 410, or 404 where the baseline answered) sets it to false too.

## `alert_webhook_url` (type: `string`):

Optional. One public HTTPS URL. The Actor sends one JSON POST for each endpoint whose deprecation state changed since the baseline. Private and reserved addresses are refused, and a redirect is not followed. Leave it empty to read the changes in the dataset only.

## `baseline_name` (type: `string`):

The record key of the stored baseline inside the named key-value store "api-sunset-header-baseline". Use one name for each endpoint list. The first run writes the baseline and reports every present signal as new-deprecation.

## `request_timeout_seconds` (type: `integer`):

Timeout for each request and for each webhook POST. One endpoint gets one request and one retry, so it takes at most 2 x this value + 5 seconds.

## `concurrency` (type: `integer`):

Maximum number of endpoints requested at the same time.

## `max_redirects` (type: `integer`):

Maximum redirect hops for each request. The Actor checks each hop again against the private-address rules.

## `max_run_seconds` (type: `integer`):

Wall-clock deadline for the endpoint list. An endpoint that the run does not reach before this deadline gets the RUN\_DEADLINE reason code and keeps its baseline. The run still succeeds.

## Actor input object example

```json
{
  "endpoints": [
    {
      "url": "https://api.bitbucket.org/2.0/repositories",
      "method": "GET"
    },
    {
      "url": "https://postman-echo.com/response-headers?Deprecation=%401735689600&Sunset=Wed%2C%2031%20Dec%202031%2023%3A59%3A59%20GMT&Link=%3Chttps%3A%2F%2Fpostman-echo.com%2Fget%3E%3B%20rel%3D%22successor-version%22",
      "method": "GET"
    },
    {
      "url": "https://api.github.com/",
      "method": "GET",
      "headers": {
        "X-GitHub-Api-Version": "2022-11-28"
      }
    },
    {
      "url": "https://gitlab.com/api/v3/projects",
      "method": "GET"
    }
  ],
  "sunset_warning_days": 90,
  "alert_webhook_url": "",
  "baseline_name": "DEFAULT",
  "request_timeout_seconds": 15,
  "concurrency": 5,
  "max_redirects": 3,
  "max_run_seconds": 240
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "endpoints": [
        {
            "url": "https://api.bitbucket.org/2.0/repositories",
            "method": "GET"
        },
        {
            "url": "https://postman-echo.com/response-headers?Deprecation=%401735689600&Sunset=Wed%2C%2031%20Dec%202031%2023%3A59%3A59%20GMT&Link=%3Chttps%3A%2F%2Fpostman-echo.com%2Fget%3E%3B%20rel%3D%22successor-version%22",
            "method": "GET"
        },
        {
            "url": "https://api.github.com/",
            "method": "GET",
            "headers": {
                "X-GitHub-Api-Version": "2022-11-28"
            }
        },
        {
            "url": "https://gitlab.com/api/v3/projects",
            "method": "GET"
        }
    ],
    "sunset_warning_days": 90,
    "alert_webhook_url": "",
    "baseline_name": "DEFAULT",
    "request_timeout_seconds": 15,
    "concurrency": 5,
    "max_redirects": 3,
    "max_run_seconds": 240
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/third-party-api-deprecation-and-sunset-header-watch").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "endpoints": [
        {
            "url": "https://api.bitbucket.org/2.0/repositories",
            "method": "GET",
        },
        {
            "url": "https://postman-echo.com/response-headers?Deprecation=%401735689600&Sunset=Wed%2C%2031%20Dec%202031%2023%3A59%3A59%20GMT&Link=%3Chttps%3A%2F%2Fpostman-echo.com%2Fget%3E%3B%20rel%3D%22successor-version%22",
            "method": "GET",
        },
        {
            "url": "https://api.github.com/",
            "method": "GET",
            "headers": { "X-GitHub-Api-Version": "2022-11-28" },
        },
        {
            "url": "https://gitlab.com/api/v3/projects",
            "method": "GET",
        },
    ],
    "sunset_warning_days": 90,
    "alert_webhook_url": "",
    "baseline_name": "DEFAULT",
    "request_timeout_seconds": 15,
    "concurrency": 5,
    "max_redirects": 3,
    "max_run_seconds": 240,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/third-party-api-deprecation-and-sunset-header-watch").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "endpoints": [
    {
      "url": "https://api.bitbucket.org/2.0/repositories",
      "method": "GET"
    },
    {
      "url": "https://postman-echo.com/response-headers?Deprecation=%401735689600&Sunset=Wed%2C%2031%20Dec%202031%2023%3A59%3A59%20GMT&Link=%3Chttps%3A%2F%2Fpostman-echo.com%2Fget%3E%3B%20rel%3D%22successor-version%22",
      "method": "GET"
    },
    {
      "url": "https://api.github.com/",
      "method": "GET",
      "headers": {
        "X-GitHub-Api-Version": "2022-11-28"
      }
    },
    {
      "url": "https://gitlab.com/api/v3/projects",
      "method": "GET"
    }
  ],
  "sunset_warning_days": 90,
  "alert_webhook_url": "",
  "baseline_name": "DEFAULT",
  "request_timeout_seconds": 15,
  "concurrency": 5,
  "max_redirects": 3,
  "max_run_seconds": 240
}' |
apify call kingii98/third-party-api-deprecation-and-sunset-header-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/third-party-api-deprecation-and-sunset-header-watch"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/XsVjcQcryZBZoaqmm/builds/xTWYP2uiaBwfIVlvL/openapi.json
