Skip to main content

If your credential was minted before /apps

This page is for platforms and providers that were provisioned the old way: a keypair minted by hand or by a seeding script, a Trust List entry published separately, and a credential issued against it. That arrangement works right up until the two halves stop agreeing — and when they do, the symptom is uninformative and the cause is invisible from outside.

The failure class, precisely

A credential embeds a signing key. The census separately publishes signing keys for that DID. On every request the SDK compares them:
When those were minted by two separate operations, nothing structurally guaranteed they would match. Any of these breaks the equality, silently and at a distance:
  • A re-seed that redefines an existing kid. A seeder whose upsert overwrites the published key material for a kid that already exists changes what the census publishes without changing what any deployed credential holds. Nothing rotates, nothing is versioned, and every credential naming that kid stops verifying at once.
  • A rotation that drops a kid. A new kid is published and the old one removed, while a deployment still holds a credential naming the old one.
  • A credential re-issued against a different key than the one on the entry.
The result is the same in every case: every /api/phosra/* route returns 503 PHOSRA_NOT_READY, with a message identical to the one you would get from a missing credential, a lapsed standing, or an unmigrated database.
An SDK upgrade cannot fix a divergence. Both sides of the failing comparison are data — one lives in your credential, the other on the census. Upgrading changes neither. The only fix is to make them the same key again.

Detecting it: one call

Upgrade to @phosra/gatekeeper ≥ 0.8.68 and read the readiness endpoint. No code changes are needed — the route is mounted by the same handler that already mounts your protocol routes.
If your package.json declares ^0.6.0, npm will not resolve 0.8.68 — a caret on a 0.x version pins the minor. Change the range to ^0.8.68. If you vendored the package, delete the vendored copy first.
A divergence prints both public halves side by side:
Both values are public key material — safe to read, safe to paste into a ticket. The sibling code PLATFORM_SIGNING_KEY_UNPUBLISHED means the census publishes no key under that kid at all (the rotation-dropped-it case), which needs the same fix.

Every precondition and what to do about it

The full readiness contract — thirteen codes, each with its cause and its fix.

Checking from outside, without deploying anything

The trust list is world-readable, so you can inspect what the census publishes for any DID right now:
Real output for did:ocss:custo — note the dropped link-1 discussed below:
document is served as a JSON string, not a nested object — it is the exact byte sequence the root signature covers, which is why fromjson is needed.
This tells you the census’s half. It cannot tell you your credential’s half — only the readiness endpoint, running inside your deployment, can do that. That asymmetry is exactly why the endpoint exists.

Fixing it

1

Confirm it is a divergence, not something else

PLATFORM_SIGNING_KEY_MISMATCH or PLATFORM_SIGNING_KEY_UNPUBLISHED. If you see PLATFORM_STANDING_NOT_ACCREDITED instead, your keys are fine — an attestation probably lapsed, which is a different and much smaller fix.
2

Re-provision so both halves come from one key

POST /api/v1/developers/orgs/{orgId}/apps generates the keypair, registers and claims the DID, publishes the Trust List entry, and mints the credential from that same key. They cannot disagree afterwards, because there is only one key.
3

Set the returned credential and redeploy

Two environment variables. Capture writer_seed in the same step — it is shown once and Phosra does not store it.
4

Verify before you close the ticket

curl -s .../api/phosra/status | jq .preconditionPLATFORM_READY. Do not infer success from a route that stopped 503ing under load.
Re-provisioning an existing DID is not currently possible through /apps. The endpoint is create-only by design, and both walls are deliberate:
  • It derives identity from your org and app name and binds it in the org↔DID table. A DID that was hand-seeded has no such binding, so /apps would mint a different DID rather than re-key the existing one.
  • Claiming an already-registered DID requires proving possession of that DID’s real keypair. A caller who has lost key agreement with the census by definition cannot do that, so the claim fails closed — which is correct (it is the same check that stops one org claiming another org’s DID) but means the case that most needs re-provisioning is the case /apps refuses.
So today, migrating an existing diverged DID is an operator action, not a self-serve one. Detect it with the readiness endpoint, then raise it with Phosra with the signingKey block attached. New apps get the structural guarantee from day one.

Who is exposed today

Verified against the production census (prodapi.phosra.com, trust list issue 144, root root-prod-bootstrap-2026-07) and by probing each deployment directly, 2026-07-27.
One standing issue worth knowing, unrelated to key divergence. did:ocss:propagate is served tier: accredited with a conformance attestation 16.5 days old against a 7-day TTL. Every other accredited entry is 1.2 days old or newer. If your integration depends on propagate’s standing, be aware it is presently accredited on an attestation that has passed its stated lifetime.

What the new path guarantees

For anything created through /apps, this failure class does not exist: one keypair is generated once and used for both the credential and the published Trust List entry, inside a single request, and the census refuses to release the credential envelope unless the freshly recompiled, root-verified Trust List already resolves that key id to those exact bytes. Credential and census cannot diverge, because there is only ever one key.