HTTP API

The complete inbound surface of acdp-registry. Routes are assembled in build_router() (crates/acdp-registry-core/src/lib.rs); handlers live under crates/acdp-registry-core/src/handlers/.

Endpoint summary

MethodPathAuthBuilt when
GET/.well-known/acdp.jsonnonealways
GET/.well-known/jwks.jsonnonealways
GET/.well-known/did.jsonnonealways (404 unless [receipt] configured)
GET/healthznonealways
GET/metricsoptional scrape bearermetrics.enabled
POST/contextsproducer signaturealways
GET/contexts/{ctx_id}optional beareralways
GET/contexts/{ctx_id}/bodyoptional beareralways
GET/contexts/searchoptional beareralways
POST/contexts/{ctx_id}/retractproducer-signed eventalways (501 unless lifecycle.enabled)
POST/contexts/{ctx_id}/republishproducer-signed eventalways (501 unless lifecycle.enabled)
GET/lineages/{lineage_id}optional beareralways
GET/lineages/{lineage_id}/currentoptional beareralways
GET/log/checkpointnonealways (501 unless log.enabled)
GET/log/proofoptional beareralways (501 unless log.enabled)
GET/log/entriesoptional beareralways (501 unless log.enabled)
POST/auth/challengenoneauth.enabled
POST/auth/tokenchallenge signatureauth.enabled
POST/auth/token/revokebearerauth.enabled
GET/admin/statusadmin beareralways
GET/admin/lineages/{lineage_id}/auditadmin beareralways
POST/admin/contexts/{ctx_id}/retractadmin beareralways (501 unless lifecycle.enabled)
POST/admin/contexts/{ctx_id}/republishadmin beareralways (501 unless lifecycle.enabled)
GET/admin/contextsadmin bearerplayground feature
POST/admin/pinned-keys/reloadadmin bearerplayground feature

The /auth/* routes are mounted at runtime only when auth.enabled = true. The two /admin/{contexts,pinned-keys} routes are compiled in only with the playground Cargo feature; /admin/status always ships.

Media types and middleware

Every ACDP data and auth endpoint returns application/acdp+json — on both success bodies and error envelopes (RFC-ACDP-0007 §4). /.well-known/jwks.json returns application/jwk-set+json; /healthz and /admin/* return plain operational JSON.

All requests pass through, outermost first: request-id assignment (x-request-id, a UUIDv4 minted if absent and propagated downstream), TraceLayer, a 30 s TimeoutLayer, a RequestBodyLimitLayer capped at limits.max_payload_bytes (so even unauthenticated /auth/* calls can't push oversized JSON), and the CORS layer (off unless registry.cors.allowed_origins is populated). When metrics.enabled, a request-metrics layer near the top of the stack records count/latency/status by matched route.

The /auth/* subrouter additionally carries the FEAT-06 per-IP + process-global rate limiter ([rate_limit]): it admits or rejects a request with 429 + Retry-After before any DID resolution, keyed by the resolved client IP (TCP peer, or the trusted-proxy X-Forwarded-For policy).


Metadata

GET /.well-known/acdp.json

Capabilities document. Cache-Control: max-age=300.

{
  "acdp_version": "0.1.0",
  "registry_did": "did:web:registry.example.com",
  "supported_signature_algorithms": ["ed25519"],
  "supported_did_methods": ["did:web"],
  "profiles": ["acdp-registry-core", "acdp-registry-discovery"],
  "limits": {
    "max_payload_bytes": 1048576,
    "max_embedded_bytes": 65536,
    "idempotency_key_ttl_seconds": 86400
  }
}

supported_did_methods mirrors auth.did_methods; profiles mirrors registry.profiles; limits mirrors the [limits] config section.

With a [receipt] signing key configured (ACDP 0.2.0), acdp_version becomes "0.2.0" and profiles additionally carries "acdp-registry-receipts". supported_did_methods may include "did:key" when enabled via auth.did_methods.

GET /.well-known/jwks.json

JSON Web Key Set for verifying this registry's JWTs. Cache-Control: max-age=300, Content-Type: application/jwk-set+json.

  • EdDSA mode — one OKP/Ed25519 public key:
    { "keys": [ { "kty": "OKP", "crv": "Ed25519", "use": "sig",
                  "alg": "EdDSA", "kid": "<fingerprint-or-config>", "x": "<base64url>" } ] }
  • HS256 mode{ "keys": [] }. Symmetric secrets are never published.

See AUTHENTICATION.md.

GET /.well-known/did.json (ACDP 0.2.0)

The registry's own did:web DID document, generated from [receipt] — this is where consumers resolve the receipt verification key (did:web:<authority> resolves to exactly this URL). The active signing key appears in verificationMethod and assertionMethod; retired keys ([[receipt.retired_keys]]) appear in verificationMethod only, per the RFC-ACDP-0010 §9 retention rule. Cache-Control: max-age=300. 404 when no receipt key is configured. See RECEIPTS.md.

GET /healthz

Storage liveness. 200 with {"status":"ok","storage":true} when the backend responds, 503 with {"status":"degraded","storage":false} otherwise.

GET /metrics (FEAT-10)

Prometheus text exposition (Content-Type: text/plain; version=0.0.4). Mounted only when metrics.enabled = true (404 otherwise). Deliberately outside the ACDP auth pipeline and the /auth/* rate limiter so a scraper reaches it unimpeded; when metrics.bearer_token is set the endpoint requires Authorization: Bearer <token> and answers 401 otherwise. Exposed series:

MetricTypeLabelsMeaning
acdp_registry_request_totalcounterroute, method, status_classHTTP requests by matched route pattern (never the resolved ctx_id).
acdp_registry_request_duration_secondshistogramroute, methodRequest latency.
acdp_registry_publish_totalcounteroutcomePublishes: inserted, idempotent_replay (playground path), or a wire code (schema_violation, payload_too_large, …).
acdp_registry_receipts_minted_totalcounterRFC-ACDP-0010 receipts minted on accepted publishes.
acdp_registry_log_leaves_totalcounterRFC-ACDP-0012 transparency-log leaves appended.
acdp_registry_lifecycle_event_totalcounterevent, outcomeRetract / republish outcomes.
acdp_registry_witness_cosignatures_totalcounteroutcomeWitness cosignatures aggregated / rejected / store_error.
acdp_registry_rate_limit_rejections_totalcounterscope429s by scope (auth_per_ip, auth_global, publish_per_agent, challenge_per_agent, lifecycle_per_agent).

Adding a handler needs no metrics-middleware change: request metrics are automatic (from MatchedPath); domain counters are explicit metrics::counter! calls in the handlers.


Contexts

POST /contexts

Publish a context. Not bearer-authed — the producer's signature over the content_hash is the authentication. Runs the full RFC-ACDP-0003 §2.1 pipeline (see ARCHITECTURE.md).

Request headers:

HeaderRequiredNotes
Idempotency-Keyoptional1–256 ASCII chars; replays return the prior result within limits.idempotency_key_ttl_seconds.
X-Run-Idoptional≤256 chars; correlation id echoed into the context.published webhook.
X-Tenant-IdoptionalTenant fallback; see MULTI-TENANCY.md. For writes the producer's [[auth.tenant_agents]] binding is authoritative.

Body: an RFC-ACDP-0003 PublishRequest (JSON). Response: 200 with a PublishResponse (assigned ctx_id, lineage_id, version, status, and — on a receipts-advertising registry — the top-level registry_receipt, the signed RFC-ACDP-0010 attestation minted atomically with the row). A per-agent rate limit (limits.publish_rate_per_minute, default 60) is checked before the expensive verify — 429 + Retry-After when drained.

did:key producers (ACDP 0.2.0) are verified offline — no DID-document fetch — when "did:key" is in supported_did_methods; otherwise the publish is rejected with key_resolution_failed (400, permanent).

GET /contexts/{ctx_id}

Retrieve a full context. Optional Authorization: Bearer <jwt> identifies the caller for the visibility gate (RFC-ACDP-0008 §4.5). 404 when not found or not visible to the caller (no existence oracle). If ctx_id's authority differs from this registry's and registry.cross_registry_resolution = true, the request is resolved against the foreign registry anonymously — only remote public contexts are surfaced (see OPERATIONS.md).

On a receipts-advertising registry the response carries the top-level registry_receipt member (outside body and registry_state); contexts published before receipts were enabled omit it (no backfill — see RECEIPTS.md). Foreign retrievals pass the upstream's verified receipt through verbatim.

On a lifecycle-advertising registry (lifecycle.enabled, ACDP 0.3.0) a context that has been retracted/republished carries its append-only registry_state.lifecycle_events array (RFC-ACDP-0013 §4.1; omitted when empty) and its status reflects the §7.2 precedence (retracted > superseded > expired > active). Retraction is mark-not-delete: the body of a retracted context is served unchanged.

GET /contexts/{ctx_id}/body

As above, but returns only the context Body (no envelope metadata, and never registry_receipt — the immutable-cache story is unchanged).

GET /contexts/search

Keyword + filter search. Optional bearer scopes which contexts are disclosable.

Query parameters (all optional):

ParamMeaning
qFull-text query.
typeContext type filter.
domain, tags, agent_id, schema_uri, derived_fromExact-match facets.
statusStatus filter (default active). A retracted context matches only status=retracted — never the default, nor superseded/expired even where those facts also hold (RFC-ACDP-0013 §8.2).
visibilityNarrow to public / restricted / private.
created_after, created_beforeRFC 3339 bounds on creation time.
data_period_start_after, data_period_end_beforeBounds on the context data period.
expires_after, expires_beforeBounds on expiry.
limitPage size, default 20.
cursorOpaque pagination cursor from a prior next_cursor.

Response: a SearchResponse{ matches: [...], total_estimate, next_cursor }. Visibility (RFC-ACDP-0008 §4.5) is enforced in the SQL WHERE clause on both backends, and total_estimate is the count of §4.5-visible matches for the caller. Tenant narrowing is post-filtered with a bounded refill loop (up to 6 inner pages), so a page may return fewer than limit rows near the end of a result set even though next_cursor is set — keep paging until next_cursor is absent.

GET /lineages/{lineage_id}

Every version in a lineage as a FullContext array, visibility- and tenant-filtered. Optional bearer. Each version carries its own projected status and (under the lifecycle profile) its lifecycle_events — the lineage array is the record, and the record includes withdrawals.

GET /lineages/{lineage_id}/current

The newest version that is neither superseded nor retracted (RFC-ACDP-0004 §5.2 as amended by RFC-ACDP-0013 §8.3 — an expired head is still a valid head). 404 when the lineage is unknown, no version is visible, or every version is superseded-or-retracted; retracting a linear lineage's head therefore takes the lineage off /current entirely until the producer republishes it or supersedes it with a fresh version.

When receipt.head_receipts = true (ACDP 0.3.0 / RFC-ACDP-0011) the response additionally carries a top-level lineage_head_receipt: a registry-signed, per-response attestation that "as of as_of, the head of this lineage is head_ctx_id at head_version with head_status". It is minted after head selection (so it can never name a superseded or retracted head), signed with the RFC-ACDP-0010 receipt key, never persisted, and never attached to body-only responses. See RECEIPTS.md.

POST /contexts/{ctx_id}/retract, POST /contexts/{ctx_id}/republish (ACDP 0.3.0)

Lifecycle events & retraction (RFC-ACDP-0013 §6). Mounted always; a registry without lifecycle.enabled = true answers 501 not_implemented. The request body is a closed envelope with exactly one member:

{
  "event": {
    "event_id": "018f6d0a-7b2e-4c4d-9e1f-3a5b7c9d1e2f",
    "ctx_id": "acdp://registry.example.com/1234...",
    "event_type": "retracted",
    "occurred_at": "2026-07-04T09:15:42.000Z",
    "actor": "did:web:agents.example.com:producer",
    "reason": "underlying data source found to be fabricated",
    "signature": { "algorithm": "ed25519", "key_id": "…#key-2", "value": "…" }
  }
}

Processing follows §6 in order: visibility-first resolution (an invisible context 404s — no existence oracle), closed-shape validation (any body member or body-field-named member → 400 immutable_field; other unknown members → schema_violation; event.ctx_id must equal the path {ctx_id}; event_type must match the endpoint), actor authentication (actor must equal the context's body.agent_id; the event must be signed and the signature verifies through the same DID pipeline as a publish — did:web via resolution, did:key offline), then the strict alternation check (retracted only when not retracted, republished only when retracted; violation → 409 invalid_lifecycle_transition) and the atomic append. Per-agent rate limiting applies as to publish, keyed by the event actor.

Response: 200 with the post-transition full-retrieval envelope (body + registry_state, status re-derived, lifecycle_events including the new event). A retry with an already-appended event_id and byte-identical content is idempotent (200, nothing appended); the same event_id with different content is a 400 schema_violation.

Only the producer may use these endpoints (delegation and a registry-attested admin path are out of scope for now; registry-initiated events would be recorded directly by the operator against the store).

GET /log/checkpoint, GET /log/proof, GET /log/entries (ACDP 0.3.0)

Registry transparency log (RFC-ACDP-0012). Mounted always; a registry without log.enabled = true answers 501 not_implemented from every /log/* path. There is no log_unavailable anywhere (§7.1): with the profile advertised, every accepted publish appends its leaf in the same storage transaction as the context row and its receipt, so the proof for a context exists the moment its publish response does.

GET /log/checkpoint — the current signed tree head, bare:

{
  "checkpoint_version": "acdp-log/1",
  "log_id": "did:web:registry.example.com/log/1",
  "tree_size": 5,
  "root_hash": "sha256:…",
  "timestamp": "2026-07-04T12:00:00.000Z",
  "signature": { "algorithm": "ed25519", "key_id": "…#receipt-key-1", "value": "…" }
}

Signed with the RFC-ACDP-0010 receipt key (§6 — no new key role); timestamp is fresh per evaluation. Publicly readable wherever capabilities are.

Witness cosignatures (witness_signatures) (ACDP 0.4.0, RFC-ACDP-0015 §6.1). When the registry is configured with [[witnesses]] and has collected one or more verified witness cosignatures over the exact (log_id, tree_size, root_hash) it is serving, GET /log/checkpoint returns an envelope that wraps the bare checkpoint under log_checkpoint and adds a top-level witness_signatures array as a sibling:

{
  "log_checkpoint": {
    "checkpoint_version": "acdp-log/1",
    "log_id": "did:web:registry.example.com/log/1",
    "tree_size": 5,
    "root_hash": "sha256:…",
    "timestamp": "2026-07-04T12:00:00.000Z",
    "signature": { "algorithm": "ed25519", "key_id": "…#receipt-key-1", "value": "…" }
  },
  "witness_signatures": [
    {
      "cosignature_version": "acdp-cosig/1",
      "witness_id": "did:web:witness.example.org",
      "witnessed_checkpoint": { "log_id": "…/log/1", "tree_size": 5, "root_hash": "sha256:…", "timestamp": "…" },
      "witnessed_at": "2026-07-04T12:00:03.000Z",
      "signature": { "algorithm": "ed25519", "key_id": "did:web:witness.example.org#witness-key-1", "value": "…" }
    }
  ]
}

witness_signatures is OUTSIDE the signed checkpoint object — it is never inside it, never part of any content_hash, receipt, checkpoint, or leaf preimage (§6.1). The embedded log_checkpoint is the same closed, signed object as the bare form. When the registry has collected no cosignatures for the served tuple, the response is the bare checkpoint above (no envelope, no witness_signatures) — the array is never fabricated or served empty, and pre-0.4.0 consumers see exactly what they always did. The same top-level witness_signatures sibling is attached to the embedded checkpoint carried by GET /log/proof (inclusion and consistency modes) when cosignatures exist for that embedded checkpoint's tuple. A consumer verifies each cosignature under the witness DID's assertionMethod key and counts distinct trusted witnesses (the §8 N-witnessed verdict); the registry never holds a witness key, so it can neither forge a cosignature nor make aggregation a trust dependency — a consumer MAY always fetch direct from a witness (§6.2). See CONFIGURATION.md for [[witnesses]].

GET /log/proof — one path, two mutually exclusive parameter sets:

  • Inclusion mode: exactly one of ?ctx_id=<ctx_id> (the consumer surface — retrieval visibility applies exactly as for GET /contexts/{ctx_id}: an unauthorized or unlogged ctx_id is 404 not_found, indistinguishable from absence) or ?leaf_index=<n> (the auditor surface — positions are public, no visibility gate). Optional &tree_size=<n> requests the proof against a historical size (leaf_index < tree_size ≤ current); the registry signs a checkpoint at that size on demand (§8.2). The response is the log_inclusion object (log_id, leaf_index, tree_size, inclusion_path[], log_checkpoint), plus a convenience leaf echo only when the requester is authorized to retrieve the context — verifiers reconstruct the leaf from verified body + receipt material instead (§9.1 step 1).
  • Consistency mode: ?first=<m>&second=<n> with 0 < m ≤ n ≤ current size. Response: log_id, first_tree_size, second_tree_size, consistency_path[] (empty when m == n), and a log_checkpoint at the second size. The caller verifies against its own retained earlier root — that retained root is the whole point (§9.2). Hash-only; no visibility gate.

Mixing the parameter sets, omitting both, malformed integers, or out-of-range positions/sizes → 400 schema_violation.

GET /log/entries?start=<i>&end=<j> — leaves [start, end) (start < end ≤ current size). Every entry carries leaf_index and leaf_hash unconditionally — the ordered leaf hashes alone recompute every root, which is what makes third-party auditing possible (§8.3). The leaf body is present only for entries whose context the requester is authorized to retrieve (public contexts: always); otherwise it is absent, never null. The page is capped at 256 entries; continue from start + len(entries).

{
  "log_id": "did:web:registry.example.com/log/1",
  "start": 0,
  "entries": [
    { "leaf_index": 0, "leaf_hash": "sha256:…", "leaf": { "leaf_version": "acdp-log-leaf/1",  } },
    { "leaf_index": 1, "leaf_hash": "sha256:…" }
  ]
}

Visibility note (§15): leaf hashes, positions, and tree size are public by design — a registry with confidentiality requirements over publication volume/timing metadata must weigh that before enabling [log].


Auth

Mounted only when auth.enabled = true. Full flow and JWT details in AUTHENTICATION.md.

POST /auth/challenge

Body { "agent_id": "did:web:..." }. Returns an AuthChallenge:

{
  "nonce": "&lt;24 random bytes, url-safe base64>",
  "registry_authority": "registry.example.com",
  "expires_at": 1748000300,
  "signing_input": "acdp-registry-auth:v1:{nonce}:{agent_id}:{authority}:{expires_at}"
}

agent_id must be a did:web: DID (8–2048 bytes). Bounded by limits.challenge_rate_per_minute (default 60) per agent_id plus a process-global ceiling; 429 + Retry-After when drained.

POST /auth/token

Exchange a signed challenge for a JWT. Body:

{
  "agent_id": "did:web:agents.example.com:my-agent",
  "key_id":   "did:web:agents.example.com:my-agent#key-1",
  "nonce":    "<from the challenge>",
  "expires_at": 1748000300,
  "algorithm": "ed25519",
  "signature": "<base64 signature over signing_input>"
}

algorithm is ed25519 or ecdsa-p256; it must match the algorithm declared on the resolved verification method (downgrade defense). Response:

{ "token": "<jwt>", "token_type": "Bearer", "expires_at": 1748003600 }

POST /auth/token/revoke

Body { "jti": "<token id>" }. Requires Authorization: Bearer <jwt>; the caller's DID must own the jti. 204 on success. 503 if no revocation store is configured. See AUTHENTICATION.md.


Admin

Bearer-gated against auth.admin_tokens (constant-time compare; empty list disables every admin route). See OPERATIONS.md.

GET /admin/status

Operational snapshot. Always shipped.

{
  "storage":     { "healthy": true },
  "idempotency": { "records": 128 },
  "webhook":     { "enabled": true, "queue_in_flight": 0, "queue_capacity": 1024 },
  "revocation":  { "configured_feeds": 2 },
  "migrations":  { "backend": "Sqlite", "applied": true }
}

idempotency.records and the webhook queue fields are null when the backend doesn't track them.

GET /admin/lineages/{lineage_id}/audit (ACDP 0.2.0)

Full lineage walk as an on-demand integrity check (workstream D3): the publish path validates only against the immediate predecessor's persisted row (lineage anchoring); this endpoint re-walks the entire chain. Always shipped.

{
  "lineage_id": "lin:sha256:…",
  "versions": 4,
  "ok": true,
  "issues": [],
  "receiptless_contexts": 1
}

Checks: version contiguity from 1, supersedes links, the lineage_id derivation from v1's ctx_id, producer continuity, and the single-non-superseded-tip invariant. receiptless_contexts counts rows without a stored receipt (informational — pre-receipts history is legitimate; see RECEIPTS.md). 404 for an unknown lineage.

POST /admin/contexts/{ctx_id}/retract, POST /admin/contexts/{ctx_id}/republish (ACDP 0.3.0)

Registry-attested lifecycle events (RFC-ACDP-0013 §6, "registry-initiated events") — the policy/legal takedown path, distinct from the producer-signed POST /contexts/{ctx_id}/{retract,republish}. Use when a context must be formally withdrawn (or a withdrawal reversed) and the producer is unavailable: the registry mints the event itself and attributes it to its own DID (capabilities.registry_did = did:web:<authority>), not the producer.

Auth is the standard admin gate (auth.admin_tokens bearer). Requires [lifecycle] enabled — a registry not advertising acdp-registry-lifecycle answers 501 not_implemented (after the admin gate). Always mounted.

Request body — a closed object with one optional member (the operator supplies only the reason; the registry mints the event_id, occurred_at, actor, and signature):

{ "reason": "removed by policy: court order 2026-… (producer unavailable)" }

An empty body is accepted (reason is optional). Any other member → 400 schema_violation.

Signing (mirrors the SDK's record_registry_lifecycle_event contract exactly):

  • [receipt] key configured → the event MUST be signed under the receipt key (RFC-ACDP-0013 §5; the receipts-profile MUST). signature.key_id is did:web:<authority>#<receipt.key_id_fragment>, verifiable against the registry DID document at /.well-known/did.json.
  • No [receipt] key → the event is recorded unsigned but attributed: actor still names the registry DID, and signature is omitted. Consumers weight an unsigned registry event only as far as the response transport (§5). This is not a refusal — it is exactly what the SDK helper permits when no receipt signer is present.

Downstream semantics are identical to the producer path: atomic commit_lifecycle_event, the retracted > superseded > expired > active status projection, the strict-alternation transition check (409 invalid_lifecycle_transition on a double-retract or a republish of a non-retracted context), byte-identical-event_id idempotency, and the context.retracted / context.republished webhook (with actor = the registry DID). On success returns 200 with the full-retrieval envelope (body + registry_state), the minted event appended to registry_state.lifecycle_events so its registry attribution is visible to consumers.

Cross-actor alternation is allowed. RFC-ACDP-0013 §7.1 derives retraction state from event-type order alone (never the actor), and §6 authorizes the producer (actor == agent_id) and the registry (actor == registry_did) independently. So a producer may /republish a context the registry retracted, and vice versa; both events remain in the append-only history, attributed to their distinct actors. (The RFC is silent on actor symmetry for the reversal; allowing it is the natural reading of an actor-agnostic §7.1 and the intended "registry retracts by policy, producer later resolves and republishes" flow.)

GET /admin/contexts (playground feature)

Paginated dump of stored contexts for the requested tenant. Query: limit (default 50), cursor. Returns { items: [...], next_cursor }. Tenant filter applies at the SQL level.

POST /admin/pinned-keys/reload (playground feature)

Re-reads config from disk and hot-swaps only the [playground] section (pinned keys). No body. Returns { "ok": true, "count": <n> }. Other config sections require a restart.


Error envelope

Errors follow RFC-ACDP-0007 §5 and are emitted as application/acdp+json:

{
  "error": {
    "code": "schema_violation",
    "message": "human-readable detail",
    "details": { }
  }
}

details is present only for codes that carry structured context (e.g. superseded_target carries details.reason). internal_error responses never leak detail — the message is always "internal error", with the real cause in the server log only.

The code strings are the canonical RFC-ACDP-0007 §5 registry. Their definitive list, and how an acdp client maps each one back to a typed AcdpError (with retry guidance), is in acdp-rs · Errors & Retries — this page documents only the registry's HTTP-status projection of them.

Status / code table

HTTPWire codeRaised when
400schema_violationMalformed body, missing field, schema mismatch.
400hash_mismatchRecomputed content_hash ≠ declared.
400data_ref_hash_mismatchAn embedded/remote data_ref hash ≠ declared.
400key_resolution_failedDID document fetched but the key isn't usable.
400immutable_fieldA lifecycle request tried to supply/alter body content (RFC-ACDP-0013 §6 step 2).
400(signature)Bad signature / unsupported algorithm.
403not_authorizedBad/expired/revoked bearer, challenge failure, visibility denial, tenant-scope denial in strict mode.
404not_foundContext/lineage absent or not visible to the caller.
409duplicate_publish / superseded_targetIdempotency/lineage conflict (race).
409invalid_lifecycle_transitionDouble retract, or republish of a never-retracted context (RFC-ACDP-0013 §6 step 4).
413(payload)Body over max_payload_bytes, or embedded data over max_embedded_bytes.
429rate_limitedPublish/challenge bucket drained; carries Retry-After.
500internal_errorStorage/config/internal failure (detail logged, not returned).
501not_implementedUnimplemented protocol feature (incl. /log/* and lifecycle endpoints when their profiles are not enabled).
502key_resolution_unreachable / cross_registry_resolution_failedDID document or foreign registry unreachable (also covers SSRF-policy rejection).
502invalid_log_proofA transparency-log proof/checkpoint failed RFC-ACDP-0012 §9 verification. Emitted only when validating an upstream's proofs (federation); this registry's own /log/* handlers never raise it — their failures are schema_violation, not_found, or not_implemented, and there is no log_unavailable.

Note: auth failures surface as 403 not_authorized, not 401 — the registry does not emit a WWW-Authenticate challenge.