Skip to main content
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.
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.

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.
StageWhat it meansWhat your code should do
ActiveFully supported. The default state of everything in v1.Nothing.
DeprecatedStill works exactly as before, but a replacement exists and a removal date may be set. Marked in the 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.
SunsetA removal date is now committed and published in a Sunset header. The surface still responds until that date.Migrate before the Sunset date.
RemovedGone. The endpoint returns 404; the field is absent; the SDK export no longer exists in that major line.Already migrated.
“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.

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 classExampleMinimum notice before removal
Additive (non-breaking)New endpoint, new optional request field, new response field or enum valueNone required — it cannot break a lenient client. See Forward compatibility.
Deprecated endpoint or field inside v1A 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 contractA 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 breakA change we must make to close a vulnerabilityAs 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.
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.

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:
  • DeprecationRFC 9745. A Structured-Field Date (@ + Unix seconds) marking when the resource became (or will become) deprecated. It may be a future date.
  • SunsetRFC 8594. 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:
Deprecated response (format)
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.
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.

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:
Verify — no live sunset headers today
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.
DeprecatedReplacement
RoutePOST /api/v1/advisors/registerPOST /api/v1/advisors/self-register
StatusStill serving — forwards to the replacement, adds no new accessCanonical route
WhyThe 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:
The deprecated route still forwards (live)
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:
curl -s -X POST "$BASE/api/v1/advisors/register" \
  -H "Content-Type: application/json" \
  -d '{"did":"...","public_key_b64url":"...","roles":["editor"]}'
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 and self-serve registration.

How a deprecation reaches you

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

The changelog

Every deprecation lands as a dated entry, tagged with the replacement and the removal date. This is the canonical, human record.

Response headers

Deprecation + Sunset + Link on the wire, so automated monitoring catches it without reading docs.

Your account email

Owners of orgs with recent traffic on a deprecated surface get a direct notice at announcement and before the sunset date.

The SDK

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.

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):
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"

What we will never do

Break v1 in place

A breaking change gets a new path (/api/v2), never a mutation of v1.

Remove without a Sunset

Nothing in v1 is removed until a Sunset date has been published and the notice window has elapsed.

Change a field's meaning

We add a new field beside the old one; we do not silently repurpose an existing field or enum value.

Substitute frozen artifacts

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.

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.

Next steps

Versioning

The four version layers — API path, OCSS spec version, Annex B editions, SDK semver — and how to pin each.

Changelog

The canonical dated record of every change, deprecation, and SDK release.

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.

Errors

The closed set of error classes a deprecated or removed surface returns.