# AgentID public-key authentication (/advanced/agentid)

<!-- agent-signals: reading_time_min: 4 · est_tokens: 1437 · updated: 2026-09-06 -->
Related: [Webhooks](/advanced/webhooks.md), [WebSockets](/advanced/websockets.md), [Agent safety](/advanced/safety.md), [Custom domains](/advanced/custom-domains.md), [Deliverability and warmup](/advanced/deliverability.md), [Build a multi-tenant platform](/advanced/multi-tenant.md)



# Approve AgentID sign-in with a registered P-256 public key

Register the public half of a P-256 key pair to get an `api_key_id`, then approve individual sign-in transactions by submitting a strict ES256 assertion. The private key stays in a keystore, HSM, KMS, or a small trusted signing process, never in model context, tool output, logs, prompts, or conversation history.

## Do this

1. In trusted code, generate a P-256 key pair, persist the private key in secure storage, and export only the public JWK components `kty`, `crv`, `x`, `y`.

2. Register the public key with an existing bearer key. Save the returned `api_key_id`; it is your JWS `kid`:

```bash
curl -X POST "https://api.agentmail.to/v0/api-keys/public-keys" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "public_key": { "kty": "EC", "crv": "P-256", "x": "<base64url>", "y": "<base64url>" } }'
```

3. Persist `{keystore_handle, kid}` in trusted application state before continuing.

4. To approve one sign-in transaction, sign exactly `{ "jti": "<challenge>", "inbox_id": "<inbox>" }` with ES256 under the protected header `{ "alg": "ES256", "typ": "agentid-approval+jwt", "kid": "<api_key_id>" }`, then submit without bearer auth or cookies:

```bash
curl -X POST "https://auth.agentid.com/authorize/approve" \
  -H "Content-Type: application/json" \
  -d '{ "assertion": "<compact.jws>", "inbox_id": "<inbox>" }'
```

Success is `204 No Content`.

## SDK

* List: TypeScript `client.apiKeys.listPublicKeys({ limit: 20 })`, Python `client.api_keys.list_public_keys(limit=20)`. Results never include bearer credentials.
* Rename: TypeScript `client.apiKeys.updatePublicKeyName(apiKeyId, { name })`, Python `client.api_keys.update_public_key_name(api_key_id, name=...)`.
* Revoke one: TypeScript `client.apiKeys.revokePublicKey(apiKeyId)`, Python `client.api_keys.revoke_public_key(api_key_id)`.
* Revoke all sign-in keys: TypeScript `client.apiKeys.revokeAllAgentIdSignInKeys({ idempotencyKey })`, Python `client.api_keys.revoke_all_agent_id_sign_in_keys(idempotency_key=...)`.
* Dependencies for signing helpers: `agentmail` + `jose` (TypeScript), `agentmail` + `cryptography` + `PyJWT` + `httpx` (Python).

## Facts

* Credential lifecycle endpoints: `GET /v0/api-keys/public-keys` lists (never mixing in bearer credentials), `PATCH /v0/api-keys/public-keys/{api_key_id}` renames, `DELETE /v0/api-keys/public-keys/{api_key_id}` revokes one.
* `POST /v0/api-keys/public-keys` registers a public P-256 JWK and returns the server-owned `api_key_id` used as the JWS `kid`. The server rejects a private `d` member, unknown JWK members, and non-P-256 curves, verifies the point lies on the curve, and computes the RFC 7638 SHA-256 fingerprint.
* Registering identical JWK coordinates again returns a new `api_key_id`. Rotation is create new, deploy new `kid`, then delete old. Never reuse an old `kid` for new key material.
* Omitted `scope` inherits the registering bearer's scope. An explicit scope must be the caller's scope or a live descendant. Formats: `{"type": "organization"}`, `{"type": "pod", "id": "<pod_id>"}`, `{"type": "inbox", "id": "<inbox_id>"}`.
* Omitted `expires_at` inherits the bearer's expiry; a never-expiring bearer yields a never-expiring credential. An explicit expiry must be future and cannot exceed the creator's expiry.
* Scope, key material, AgentID eligibility, and expiry are immutable. Only `name` is patchable.
* The approval assertion signs exactly `jti` (1-128 chars) and `inbox_id` (1-254 chars), header `{"alg": "ES256", "typ": "agentid-approval+jwt", "kid": ...}`, as a three-segment compact JWS of at most 2 KiB.
* `POST https://auth.agentid.com/authorize/approve` is served by the AgentID issuer, not the AgentMail REST API or the generated SDKs. Body: `{"assertion", "inbox_id"}`. No bearer authorization, no cookies. The unsigned `inbox_id` must be byte-for-byte identical to the signed claim. Success is `204`.
* The issuer resolves `kid`, verifies the signature, validates the transaction, and rechecks key, organization, scope, inbox, generation, and expiry. Concurrent or repeated submissions have one winner.
* `POST /v0/api-keys/public-keys/agentid-sign-in/revoke-all` needs an organization-scoped bearer and a UUID `Idempotency-Key` header, takes no body, and returns `previous_generation`, `current_generation`, `revoked_at`. The same UUID returns the original receipt; a different UUID advances the generation. Rows revoked by revoke-all stay visible with `revoked_at` for audit; individually revoked keys are deleted.

## Not supported

* Extra JWT claims (`aud`, `iat`, `exp`, `nonce`, `scope`) or header members (`jwk`, `jku`, `x5u`, `x5c`, `crit`) on the approval assertion.
* Widening a credential: explicit scope can never be an ancestor or sibling of the caller's scope, and expiry can never exceed the creator's.
* Editing anything but `name` after registration.
* Proving user intent by signature validity alone. The assertion proves the key holder approved the transaction `jti` for one inbox, not that the key holder initiated it, controls the browser session, inspected the relying party, or intended the relying party's action. Bind intent to an authenticated out-of-band instruction if your product requires it.

## Verify

A successful registration returns an `api_key_id`, and `GET /v0/api-keys/public-keys` lists the credential. A successful approval submission returns `204 No Content`.

## Related

* `/advanced/multi-tenant` - pods, scoped API keys, and the scope hierarchy credentials inherit.
* `/extras/security-compliance` - platform-wide authentication and key handling.
* `/resources/changelog` - the release entry introducing these endpoints.
