AgentID Public-Key Authentication
AgentID Public-Key Authentication
Register a scoped P-256 key and sign one AgentID approval without exposing the private key.
AgentID public-key credentials let an agent prove possession of a P-256 private key while approving an AgentID sign-in. Registration uses an existing AgentMail bearer API key. Approval uses only a compact signature and never sends that bearer key or the private key to AgentID.
Keep the private key below model context
Generate and use the private key in a keystore, HSM, KMS, or a small trusted signing process. Give the model an opaque signing capability, not the private JWK, PEM, environment variable, tool output, log entry, prompt, trace, or conversation history.
Protocol at a glance
- Generate a P-256 key pair in trusted code and persist the private key in your keystore.
- Export only
{kty: "EC", crv: "P-256", x, y}and register it atPOST /v0/api-keys/public-keyswith an existing AgentMail bearer API key. - Store the returned
api_key_idbeside the private-key handle. It is the JWSkid; do not compute or choose it yourself. - For one pending authorization transaction, sign exactly
{jti, inbox_id}with ES256 and protectedtyp: agentid-approval+jwt. - POST exactly
{assertion, inbox_id}tohttps://auth.agentid.com/authorize/approvewithout bearer authorization. Success is204 No Content.
Public-key credentials are only AgentID sign-in credentials. They cannot replace an AgentMail bearer API key for normal REST API calls.
Generate and register a key
The registration endpoint rejects private d, unknown JWK members, non-P-256
curves, malformed coordinates, and coordinates that are not on P-256. The
server computes the RFC 7638 SHA-256 fingerprint and returns it; compare or log
the public fingerprint when you need an audit handle, never the private key.
SDK release required
The generated SDK methods in this guide require an AgentMail Python and TypeScript SDK release that includes the public-key credential endpoints. If your installed client does not expose these methods yet, use the REST API or upgrade after that release is published.
After that SDK release, install the Python example dependencies with
pip install agentmail cryptography PyJWT httpx, or the TypeScript dependencies
with npm install agentmail jose.
The examples keep a process-local private key only to show the types. In a production helper, make the signer accept an opaque keystore handle and return a signature; do not make private key bytes an application-level return value.
Scope and expiry
Omitting scope inherits the registering bearer key’s exact live scope. An
explicit scope may be the caller’s scope or a live descendant, never an ancestor
or sibling.
For expires_at, omission inherits the registering bearer credential’s expiry.
If that bearer does not expire, the public-key credential does not expire. An
explicit expiry must be in the future and cannot be later than the creator’s
expiry. Scope, key material, AgentID eligibility, and expiry are immutable after
registration; only name can be patched.
Sign and submit one approval
The protected header and payload are intentionally smaller than a general JWT:
Do not add aud, iat, exp, nonce, scope, or any other claim. Do not add
jwk, jku, x5u, x5c, or crit to the protected header. The transaction’s
server-side expiry is authoritative. The assertion must be a three-segment
compact JWS no larger than 2 KiB; jti is 1–128 characters and inbox_id is
1–254 characters and must identify an email inbox.
The unsigned inbox_id in the JSON body is an ergonomic duplicate and must be
byte-for-byte equal to the signed claim. The server resolves kid only against a
stored public-key credential, verifies the signature, validates the transaction,
and rechecks the key, organization, scope, inbox, generation, and expiry before
committing one approval. Concurrent or repeated submissions have one winner.
List, rename, revoke, and rotate
The generated clients for this contract expose dedicated lifecycle methods.
Legacy api_keys.list, api_keys.create, and api_keys.delete remain
bearer-only and have no public-key request member.
Registration never updates in place. Even registering identical JWK coordinates
again returns a new api_key_id; store and use that new value as kid. Rotation
is therefore create new, deploy new, then delete old. Never reuse an old kid
for new key material.
For an emergency organization-wide fence, call
POST /v0/api-keys/public-keys/agentid-sign-in/revoke-all with an
organization-scoped bearer credential and a required UUID Idempotency-Key.
The caller normally needs api_key_delete. A verified self-serve agent
organization may instead use an unrestricted unmanaged bearer credential for
this emergency operation. The request has no body. Repeating the same UUID
returns the original {previous_generation, current_generation, revoked_at}
receipt and does not advance the generation twice. A new UUID advances it again.
Existing rows remain visible with revoked_at for audit; individually revoked
keys are deleted.
Intent and browser-session limitation
A valid signature does not prove who initiated the browser transaction
The approval assertion proves that the key holder approved the server-created
transaction identified by jti for one inbox. It does not prove that the key
holder initiated the transaction, controls the browser session, inspected the
relying party, or intended the relying party’s action.
An attacker can start a valid authorization transaction in the attacker’s own
browser, induce an agent to sign that transaction’s jti, and then continue in
the same attacker browser session. AgentID’s per-transaction cookie binding
prevents a different browser from continuing the flow, but it does not remove
this accepted transaction-intent/session-swap residual.
If your product requires intent assurance, bind the displayed relying party and transaction to an authenticated, trusted out-of-band instruction before calling the signing helper. Do not claim that signature validity alone verifies user intent.
