Every agent in the registry signs with keys it holds. The registry keeps the
history of those keys: which are proven, which were replaced and until when
they still count, and which were reported compromised. The normative text is
spec/agent-keys.md.
All routes on this page take the developer API key, and only work on the
developer’s own agents: another developer’s agent answers 404.
Key identity
A key is identified by its JWK Thumbprint (RFC 7638, SHA-256, base64url). Only
the required members count (crv, kty, x, y for EC; e, kty, n for
RSA; crv, kty, x for Ed25519), so adding kid, alg or use to a JWK
never changes its thumbprint. Send public keys only: a JWK with a private
member such as d is refused.
One key belongs to one agent. A key already in another agent’s history is
refused with 409 AGENT_KEY_CONFLICT by POST /v1/agents/{agentId}/keys,
and by POST and PATCH /v1/agents when AGENT_KEY_HISTORY_MIRROR_ENABLED
is true.
Adding a key
The key starts pending. It cannot be used until its possession is proven.
An agent may hold at most 10 keys that are pending, active or within a
rotation overlap.
A key registered as publicJwk on POST or PATCH /v1/agents enters the
history too when the operator has set AGENT_KEY_HISTORY_MIRROR_ENABLED=true
(off by default). With it off, add the registered key through
POST /v1/agents/{agentId}/keys to bring it into the history. A key in the
history becomes proven when the agent presents a DPoP proof with it at the
OAuth endpoints, or through the challenge below.
Payments rails need P-256
Declare the rails the agent uses:
The rails are ap2, verifiable_intent, acp and ucp. An agent that
declares a payments rail (ap2 or verifiable_intent) may hold only ES256
keys on P-256: any other key is refused with KEY_ALGORITHM_NOT_ALLOWED, and
the rail cannot be declared while the agent still holds another key type,
either in its history (pending, active or within a rotation overlap) or as its
registered publicJwk. PATCH /v1/agents refuses a non-P-256 publicJwk
for such an agent only when AGENT_KEY_HISTORY_MIRROR_ENABLED is true.
Without a payments rail, Ed25519 keys are accepted as well.
Proving possession
Ask for a challenge for the pending key:
Sign it with the key, inside the agent, where the private key lives:
and send the result:
The key becomes active. A challenge proves possession once: a replayed or
expired proof, a proof signed by another key, or a proof for another
registry is refused with key_unproven (or audience_mismatch). Asking for a
new challenge cancels the previous one.
Rotating a key
Add the new key and prove it first, then rotate the old one:
In the key history the old key stays usable for the overlap (7 days by
default, set by AGENT_KEY_ROTATION_OVERLAP_SECONDS, at most 30 days per
request) and is not usable from validTo on. A replacement that is still
pending is refused with key_unproven.
The registered key is not changed by a rotation
The auth service’s own token endpoints (the OAuth PAR, code exchange,
refresh and token exchange, POST /v1/authorize and POST /v1/token,
delegation) and the agent’s DID document do not read the key history yet.
They bind grants to the agent’s registered key, publicJwk, and to nothing
else (FINDINGS G-85). A rotation does not change publicJwk, so until you
change it:
- the rotated key keeps working at those endpoints, after its
validTo too,
for as long as it stays the registered key;
- the replacement is not accepted there.
When the agent has switched to signing with the replacement, make it the
registered key:
The history is unchanged by this: the replacement stays active and the
rotated key keeps its validTo. At the token endpoints the change is
immediate, with no overlap: from then on they accept only the replacement,
and keyPossessionVerified is false until the agent presents a DPoP proof
with it at the OAuth endpoints, as after any change of publicJwk.
Reporting a compromised key
The key ends immediately and can never be registered again, by any agent,
including after the agent that held it is deleted: compromised keys are kept
in a record that outlives the agent. Registering one through
POST /v1/agents/{agentId}/keys is refused with 409 key_not_active, and
so is registering it through POST or PATCH /v1/agents when
AGENT_KEY_HISTORY_MIRROR_ENABLED=true. A delegation to an agent whose
registered key was reported compromised is refused with 409 key_not_active,
including one that was already in flight.
Every grant bound to it (cnf.jkt) is revoked, with every grant delegated
from those, and each revocation is written to the audit chain. The response
says how many grants were revoked. If the key was the agent’s registered
publicJwk, the newest proven replacement takes its place (agentKey: "promoted"); with no replacement the registered key is cleared and the agent
is suspended (agentSuspended: true) until a new key is registered. Calling
it again is safe.
Reading the history
Each key carries its status (pending, active, rotated,
compromised), validFrom, validTo, possessionProvedAt, rotatedFrom
and whether it is usable now; a key that is not usable says why in denial
(key_unproven or key_not_active). Last modified on September 28, 2026