> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phosra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deprecation & Sunset Policy

> The lifecycle every v1 endpoint, field, and SDK export follows before it can change — the advance-notice window, the Deprecation / Sunset response headers (RFC 9745 + RFC 8594), the breaking-change timeline, and how each deprecation reaches you.

Phosra ships changes **additively**. A stable `v1` surface — an endpoint, a response
field, an SDK export — never disappears or changes shape under you without a named
deprecation, an advance-notice window, and a machine-readable signal on the wire. This
page is that contract: what "deprecated" means, how long you get, what you will see,
and how to migrate.

<Info>
  **Verified live.** The endpoint behavior and header presence on this page were
  captured from `https://phosra-api-sandbox-production.up.railway.app` on **2026-07-06**.
  For how the four version layers pin together, start with [Versioning](/versioning).
</Info>

## The one guarantee

> We do not make **silent, breaking, immediate** changes to a stable `v1` surface.
> Every breaking change is announced first, carries a machine-readable deprecation
> signal, and honors a minimum notice window before the old behavior can be removed.

Everything below is the mechanism behind that sentence.

## Lifecycle stages

Every stable surface moves through four stages, in order. It can sit in a stage
indefinitely, but it can never skip one.

| Stage          | What it means                                                                                                                                                                                                                            | What your code should do                                           |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| **Active**     | Fully supported. The default state of everything in `v1`.                                                                                                                                                                                | Nothing.                                                           |
| **Deprecated** | Still works exactly as before, but a replacement exists and a removal date may be set. Marked in the [changelog](/changelog), in the SDK (`@deprecated`), and — once it enters a sunset window — on the wire via a `Deprecation` header. | Plan your migration. No rush, but stop building *new* usage on it. |
| **Sunset**     | A removal date is now committed and published in a `Sunset` header. The surface still responds until that date.                                                                                                                          | Migrate before the `Sunset` date.                                  |
| **Removed**    | Gone. The endpoint returns `404`; the field is absent; the SDK export no longer exists in that major line.                                                                                                                               | Already migrated.                                                  |

<Note>
  **"Deprecated" is not "broken."** A deprecated endpoint returns the same status
  codes and the same body it always did. The only thing that changes is that we have
  told you a better path exists and, eventually, when the old one stops. You are never
  forced to move on the day of the announcement.
</Note>

## Advance-notice windows

The minimum notice you are guaranteed depends on the blast radius of the change. These
are floors, not targets — most changes get more.

| Change class                                 | Example                                                                                            | Minimum notice before removal                                                                                                                                                   |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Additive** (non-breaking)                  | New endpoint, new optional request field, new response field or enum value                         | **None required** — it cannot break a lenient client. See [Forward compatibility](/integration/forward-compatibility).                                                          |
| **Deprecated endpoint or field** inside `v1` | A route superseded by a better-named one; a response field renamed with both served during overlap | **≥ 90 days** from the `Sunset` header / changelog entry to removal                                                                                                             |
| **New major API version** (`/api/v2`)        | A genuinely incompatible contract                                                                  | A **new path**. `v1` keeps serving; **≥ 12 months** overlap before a `v1` sunset can even be *proposed*, and that proposal is its own separately-announced deprecation          |
| **Security-forced break**                    | A change we must make to close a vulnerability                                                     | As much notice as is safe to give, clearly labeled `security`. This is the only lane that can be shorter than 90 days, and only when a longer window would leave users exposed. |

<Warning>
  **Pre-1.0 SDKs are the exception you opt into.** The published packages are `0.x`
  (`@phosra/sdk@0.1.0`, `@phosra/link@0.1.2`, `@openchildsafety/ocss@0.1.3`, …). Under
  semver, a `0.x` **minor** bump may carry a breaking change with no separate notice
  window — that is what `0.x` *means*. Pin exact and read the changelog before you
  bump. The wire API (`v1`) gives you the windows above; a floating SDK caret range
  does not. See [SDK versioning](/versioning#sdk-versioning-pin-exact).
</Warning>

## On the wire: `Deprecation` and `Sunset` headers

Phosra follows the two IETF standards for signalling this in HTTP, so a generic client
or gateway can detect a deprecation without reading these docs:

* **`Deprecation`** — [RFC 9745](https://www.rfc-editor.org/rfc/rfc9745.html). A
  Structured-Field **Date** (`@` + Unix seconds) marking when the resource became (or
  will become) deprecated. It may be a future date.
* **`Sunset`** — [RFC 8594](https://www.rfc-editor.org/rfc/rfc8594.html). An HTTP-date
  marking the moment the resource stops responding.
* **`Link`** — a pointer to the human migration guide, related by
  `rel="deprecation"` and/or `rel="sunset"`.

A response from an endpoint that has entered a sunset window looks like this:

```http Deprecated response (format) theme={null}
HTTP/2 200
content-type: application/json
deprecation: @1788220800
sunset: Tue, 01 Dec 2026 00:00:00 GMT
link: <https://docs.phosra.com/deprecation>; rel="deprecation",
      <https://docs.phosra.com/changelog>; rel="sunset"

{ ...unchanged response body... }
```

Read it as: *"this became deprecated on 2026-09-01 (`@1788220800`), it stops responding
after 2026-12-01, and the migration guide is at the linked URL."* The `200` and the body
are exactly what you got before — the headers are the only difference.

<Note>
  `Deprecation` carries a **Structured-Field Date**, so the value is `@` followed by
  Unix seconds — not an HTTP-date. `Sunset` carries a classic **HTTP-date**. They are
  intentionally different formats because they come from two different RFCs; parse each
  accordingly.
</Note>

### What you see today

No `v1` endpoint has entered a sunset window yet, so **you will not see a `Sunset`
header on a live response today** — and we would rather you verify that than take our
word for it:

```bash Verify — no live sunset headers today theme={null}
BASE=https://phosra-api-sandbox-production.up.railway.app
curl -s -D - -o /dev/null "$BASE/api/v1/platforms" \
  | tr -d '\r' | grep -iE '^(deprecation|sunset|link):' \
  || echo "no deprecation / sunset / link headers — nothing in a sunset window"
# → no deprecation / sunset / link headers — nothing in a sunset window
```

The block above is the exact shape you will parse **when** a surface does enter its
window. Wiring the check now means your integration lights up the day one appears,
instead of the day it breaks.

## The one deprecation live today

There is exactly one deprecated route on the current census: the legacy advisor
self-registration shim.

|            | Deprecated                                                                                                             | Replacement                           |
| ---------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| **Route**  | `POST /api/v1/advisors/register`                                                                                       | `POST /api/v1/advisors/self-register` |
| **Status** | Still serving — forwards to the replacement, adds no new access                                                        | Canonical route                       |
| **Why**    | The old route multiplexed two different call shapes on one path; the split gives each its own single-contract endpoint | —                                     |

It still works — it forwards by the `did` discriminator and returns the same result the
replacement would:

```bash The deprecated route still forwards (live) theme={null}
BASE=https://phosra-api-sandbox-production.up.railway.app
# A 32-byte-key validation error proves the request reached the handler and forwarded —
# the route is live, not 404. (Send a real key to get a 200 provisional registration.)
curl -s -X POST "$BASE/api/v1/advisors/register" \
  -H "Content-Type: application/json" \
  -d '{"did":"did:ocss:example","public_key_b64url":"AAAA","roles":["editor"]}'
# → {"error":"Bad Request","message":"invalid_public_key_b64url: must decode to 32 bytes, got 3","code":400}
```

**Migration is a one-line URL change** — the request and response shapes are identical:

<CodeGroup>
  ```bash Before (deprecated) theme={null}
  curl -s -X POST "$BASE/api/v1/advisors/register" \
    -H "Content-Type: application/json" \
    -d '{"did":"...","public_key_b64url":"...","roles":["editor"]}'
  ```

  ```bash After (canonical) theme={null}
  curl -s -X POST "$BASE/api/v1/advisors/self-register" \
    -H "Content-Type: application/json" \
    -d '{"did":"...","public_key_b64url":"...","roles":["editor"]}'
  ```
</CodeGroup>

The published `@phosra/cli` already calls `self-register`, so `phosra register` needs no
change — only hand-rolled callers of the old path do. See the
[CLI reference](/integration/cli) and [self-serve registration](/integration/platform-registration).

## How a deprecation reaches you

We announce on every channel a developer actually watches — no single point of failure:

<CardGroup cols={2}>
  <Card title="The changelog" icon="clock-rotate-left" href="/changelog">
    Every deprecation lands as a dated entry, tagged with the replacement and the
    removal date. This is the canonical, human record.
  </Card>

  <Card title="Response headers" icon="server">
    `Deprecation` + `Sunset` + `Link` on the wire, so automated monitoring catches it
    without reading docs.
  </Card>

  <Card title="Your account email" icon="envelope">
    Owners of orgs with recent traffic on a deprecated surface get a direct notice at
    announcement and before the sunset date.
  </Card>

  <Card title="The SDK" icon="box">
    Deprecated exports carry a `@deprecated` JSDoc tag (your editor and `tsc` flag it)
    and a `0.x` → next-minor removal follows semver. Read the changelog before bumping.
  </Card>
</CardGroup>

## Detect deprecations programmatically

Fail loud in CI or a health check the moment any endpoint you depend on starts
advertising a sunset. This runs today (and prints nothing, correctly):

<CodeGroup>
  ```bash curl theme={null}
  BASE=https://phosra-api-sandbox-production.up.railway.app
  for path in /api/v1/platforms /.well-known/ocss/trust-list; do
    hdr=$(curl -s -D - -o /dev/null "$BASE$path" \
      | tr -d '\r' | grep -iE '^(deprecation|sunset):')
    if [ -n "$hdr" ]; then
      echo "DEPRECATION NOTICE on $path:"; echo "$hdr"
    fi
  done
  echo "checked — no sunset windows open"
  ```

  ```ts TypeScript theme={null}
  const BASE = "https://phosra-api-sandbox-production.up.railway.app";

  async function checkSunset(path: string): Promise<void> {
    const res = await fetch(`${BASE}${path}`);
    const deprecation = res.headers.get("deprecation"); // @<unix-seconds> or null
    const sunset = res.headers.get("sunset");           // HTTP-date or null
    if (sunset) {
      const stopsAt = new Date(sunset);
      const daysLeft = Math.round((+stopsAt - Date.now()) / 86_400_000);
      throw new Error(`${path} is sunset on ${stopsAt.toISOString()} (${daysLeft}d left) — migrate`);
    }
    if (deprecation) console.warn(`${path} is deprecated (since ${deprecation}) — plan migration`);
  }

  for (const p of ["/api/v1/platforms", "/.well-known/ocss/trust-list"]) {
    await checkSunset(p); // resolves quietly today; throws the day a sunset lands
  }
  ```

  ```python Python theme={null}
  import requests
  from datetime import datetime, timezone
  from email.utils import parsedate_to_datetime

  BASE = "https://phosra-api-sandbox-production.up.railway.app"

  def check_sunset(path: str) -> None:
      r = requests.get(f"{BASE}{path}")
      if sunset := r.headers.get("Sunset"):          # HTTP-date
          stops_at = parsedate_to_datetime(sunset)
          days_left = (stops_at - datetime.now(timezone.utc)).days
          raise SystemExit(f"{path} sunset {stops_at.isoformat()} ({days_left}d left) — migrate")
      if dep := r.headers.get("Deprecation"):        # @<unix-seconds>
          print(f"{path} deprecated (since {dep}) — plan migration")

  for p in ("/api/v1/platforms", "/.well-known/ocss/trust-list"):
      check_sunset(p)  # silent today; raises the day a sunset lands
  ```
</CodeGroup>

## What we will never do

<CardGroup cols={2}>
  <Card title="Break v1 in place" icon="shield-check">
    A breaking change gets a new path (`/api/v2`), never a mutation of `v1`.
  </Card>

  <Card title="Remove without a Sunset" icon="calendar-xmark">
    Nothing in `v1` is removed until a `Sunset` date has been published and the notice
    window has elapsed.
  </Card>

  <Card title="Change a field's meaning" icon="arrows-rotate">
    We add a new field beside the old one; we do not silently repurpose an existing
    field or enum value.
  </Card>

  <Card title="Substitute frozen artifacts" icon="lock">
    A pinned Annex B edition is content-addressed — its bytes never change, and a
    missing edition is a clean `404`, never a nearby substitution. See
    [Versioning](/versioning#dated-artifacts-annex-b-editions).
  </Card>
</CardGroup>

## Migration expectations

When you receive a deprecation notice, the contract you can count on:

1. **A named replacement exists before the notice ships.** We never deprecate a path
   without a place to go.
2. **The old and new surfaces overlap** for at least the notice window — you can run
   both, cut over, and verify without a flag-day.
3. **The migration is documented** at the `Link: rel="deprecation"` URL, with a
   before/after example like the one above.
4. **Idempotent, replay-safe cutover.** Re-issuing the same write against the new path
   is safe — see [Idempotency](/idempotency).

## Next steps

<CardGroup cols={2}>
  <Card title="Versioning" icon="code-branch" href="/versioning">
    The four version layers — API path, OCSS spec version, Annex B editions, SDK
    semver — and how to pin each.
  </Card>

  <Card title="Changelog" icon="clock-rotate-left" href="/changelog">
    The canonical dated record of every change, deprecation, and SDK release.
  </Card>

  <Card title="Forward compatibility" icon="arrows-rotate" href="/integration/forward-compatibility">
    Why most evolution reaches you as runtime data with no reinstall — and how to write
    a client that never breaks on an additive change.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/errors">
    The closed set of error classes a deprecated or removed surface returns.
  </Card>
</CardGroup>
