> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://docs.agentmail.to/llms.txt. For full content including API reference and SDK examples, see https://docs.agentmail.to/llms-full.txt.

# AgentID Public-Key Authentication

> Register a public P-256 key, choose its scope and permissions, and manage it through the current AgentMail API Keys endpoints.

Register a public P-256 key when your application manages its own signing key. AgentMail stores the public key and returns an `api_key_id`; the private key stays in your application or keystore.

For provider sign-in, start with [AgentID Sign-In](https://docs.agentmail.to/agentid-sign-in). Connect and authorize create sign-in keys for you. This guide covers registering and managing keys you generate yourself.

## Before you start

Use a bearer API key with `api_key_create` to register keys. Reading, updating, and deleting keys require `api_key_read`, `api_key_update`, and `api_key_delete`, respectively.

Install the TypeScript SDK with `npm install agentmail@latest`, or the CLI with `npm install -g agentmail-cli@latest`. Set `AGENTMAIL_API_KEY` in your environment.

The examples use TypeScript SDK 0.5.25 and CLI 1.4.0. Python SDK 0.5.10 still exposes the previous public-key endpoints. The Python example uses the current HTTP route with `httpx`; install its dependencies with `pip install httpx cryptography`.

Generate and store private keys in trusted code or a keystore. Keep private key material out of model context, tool output, and logs. Register only the four public JWK fields: `kty`, `crv`, `x`, and `y`.

## Generate and register a key

The following examples generate a P-256 key and register it for one inbox. They keep the private key in memory to demonstrate key generation; persist it securely before registration in production.

**`Python`**

```python title="Python"
import base64
import os
import httpx
from cryptography.hazmat.primitives.asymmetric import ec

private_key = ec.generate_private_key(ec.SECP256R1())
numbers = private_key.public_key().public_numbers()

def coordinate(value: int) -> str:
    return base64.urlsafe_b64encode(value.to_bytes(32, "big")).rstrip(b"=").decode()

public_key = {
    "kty": "EC", "crv": "P-256",
    "x": coordinate(numbers.x), "y": coordinate(numbers.y),
}
response = httpx.post(
    "https://api.agentmail.to/v0/inboxes/agent%40example.com/api-keys",
    headers={"Authorization": f"Bearer {os.environ['AGENTMAIL_API_KEY']}"},
    json={"public_key": public_key, "client_id": "production-signer-v1",
          "name": "production signer", "permissions": {"provider_connect": True}},
    timeout=30,
)
response.raise_for_status()
print(response.json()["api_key_id"])
```

**`TypeScript`**

```typescript title="TypeScript"
import { generateKeyPairSync } from "node:crypto";
import { AgentMailClient } from "agentmail";

const { privateKey, publicKey } = generateKeyPairSync("ec", {
  namedCurve: "prime256v1",
});
const jwk = publicKey.export({ format: "jwk" });
const client = new AgentMailClient({ apiKey: process.env.AGENTMAIL_API_KEY! });
const credential = await client.inboxes.apiKeys.create("agent@example.com", {
  publicKey: { kty: "EC", crv: "P-256", x: jwk.x!, y: jwk.y! },
  clientId: "production-signer-v1",
  name: "production signer",
  permissions: { providerConnect: true },
});
console.log(credential.apiKeyId);
```

To register an existing public JWK with the CLI, put only those four public fields in `public-jwk.json`:

**`CLI`**

```bash title="CLI"
agentmail inboxes api-keys create \
  --inbox-id agent@example.com \
  --name "production signer" \
  --public-key "$(cat public-jwk.json)" \
  --permissions '{"provider_connect":true}'
```

The response has `type: "public_key"`, an `api_key_id`, and the public key's server-computed fingerprint. It does not contain a bearer secret. Registered keys have no `status`; sign-in keys created by connect or authorize do.

The API rejects private JWK fields such as `d`, extra JWK members, other curves, and invalid P-256 coordinates.

## Choose the scope

The route determines the key's scope:

| Scope          | Registration path                      | TypeScript method                                 | CLI command                                               |
| -------------- | -------------------------------------- | ------------------------------------------------- | --------------------------------------------------------- |
| Caller's scope | `POST /v0/api-keys`                    | `client.apiKeys.create(request)`                  | `agentmail api-keys create`                               |
| Pod            | `POST /v0/pods/{pod_id}/api-keys`      | `client.pods.apiKeys.create(podId, request)`      | `agentmail pods api-keys create --pod-id <pod_id>`        |
| Inbox          | `POST /v0/inboxes/{inbox_id}/api-keys` | `client.inboxes.apiKeys.create(inboxId, request)` | `agentmail inboxes api-keys create --inbox-id <inbox_id>` |

Include `public_key` in the request (TypeScript: `publicKey`; CLI: `--public-key`) to register a public key. A request without it creates a bearer key. The target scope must be accessible to the creating key; `pod_id` and `inbox_id` in the response identify the resulting scope.

## Permissions, expiry, and client IDs

* `permissions` defaults to the creating key's permissions. Explicit grants cannot exceed what that key holds.
* `expires_at` defaults to the creating key's expiry. An explicit expiry must be in the future and cannot exceed the creator's expiry. If the creator has no expiry and none is supplied, the registered key does not expire.
* Registered public keys are independent of their creator afterward. Changing or deleting the bearer key does not change or revoke them.
* Update `name` and `permissions` with `PATCH /v0/api-keys/{api_key_id}`. Key material, type, scope, and expiry are immutable.
* An optional `client_id` is a caller-chosen alias, unique within the organization. It must be URL-safe and contain no slash or `@`. You can use it instead of `api_key_id` to get, update, or delete a public key.
* Registration with the same `client_id` and matching key returns the existing credential; a conflicting registration returns `409`. Persist and reuse the key pair when retrying registration, rather than generating a new key for the same alias.

## List, update, and revoke

Use the same API Keys endpoints for bearer keys, registered public keys, and sign-in keys:

**`CLI`**

```bash title="CLI"
# list public keys
agentmail api-keys list --type public_key

# inspect and rename a key
agentmail api-keys get --api-key-id 4d795cc3-ae87-4f84-85e3-4ad4ca656f44
agentmail api-keys update \
  --api-key-id 4d795cc3-ae87-4f84-85e3-4ad4ca656f44 \
  --name "production signer v2"

# revoke the key when it is no longer needed
agentmail api-keys delete --api-key-id 4d795cc3-ae87-4f84-85e3-4ad4ca656f44
```

| Operation        | HTTP request                       | TypeScript method                             |
| ---------------- | ---------------------------------- | --------------------------------------------- |
| List public keys | `GET /v0/api-keys?type=public_key` | `client.apiKeys.list({ type: "public_key" })` |
| Get a key        | `GET /v0/api-keys/{api_key_id}`    | `client.apiKeys.get(apiKeyId)`                |
| Update a key     | `PATCH /v0/api-keys/{api_key_id}`  | `client.apiKeys.update(apiKeyId, request)`    |
| Delete a key     | `DELETE /v0/api-keys/{api_key_id}` | `client.apiKeys.delete(apiKeyId)`             |

Follow `next_page_token` until it is absent, including when a page is empty. Pass it as `page_token` in HTTP, `pageToken` in TypeScript, or `--page-token` in the CLI.

To rotate a key, generate and persist a replacement, register it with a new `client_id`, deploy and verify it, then delete the old key. To revoke multiple keys, list the intended credentials and delete each one individually.

See the [API Keys reference](https://docs.agentmail.to/api-reference/api-keys/create) for complete request and response fields.