> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://docs.postnuvia.com/llms.txt.

# AgentID Public-Key Authentication

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

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

To sign an inbox in to an app, start with [AgentID Sign-In](https://docs.postnuvia.com/agentid-sign-in). [Connect App](https://docs.postnuvia.com/api-reference/apps/connect) and [Authorize Inbox](https://docs.postnuvia.com/api-reference/inboxes/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 postnuvia@latest`, or the CLI with `npm install -g postnuvia-cli@latest`. Set `POSTNUVIA_API_KEY` in your environment.

> **Note**
>
> The examples use TypeScript SDK 0.5.25 and CLI 1.4.0, apart from the `app_connect` permission: it needs the first TypeScript SDK and CLI releases after the permission rename. Earlier releases know it only as `provider_connect` (TypeScript: `providerConnect`), which the API rejects. 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`.

> **Warning**
>
> 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.postnuvia.com/v0/inboxes/agent%40example.com/api-keys",
    headers={"Authorization": f"Bearer {os.environ['POSTNUVIA_API_KEY']}"},
    json={"public_key": public_key, "client_id": "production-signer-v1",
          "name": "production signer", "permissions": {"app_connect": True}},
    timeout=30,
)
response.raise_for_status()
print(response.json()["api_key_id"])
```

**`TypeScript`**

```typescript title="TypeScript"
import { generateKeyPairSync } from "node:crypto";
import { PostNuviaClient } from "postnuvia";

const { privateKey, publicKey } = generateKeyPairSync("ec", {
  namedCurve: "prime256v1",
});
const jwk = publicKey.export({ format: "jwk" });
const client = new PostNuviaClient({ apiKey: process.env.POSTNUVIA_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: { appConnect: 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"
postnuvia inboxes api-keys create \
  --inbox-id agent@example.com \
  --name "production signer" \
  --public-key "$(cat public-jwk.json)" \
  --permissions '{"app_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)`                  | `postnuvia api-keys create`                               |
| Pod            | `POST /v0/pods/{pod_id}/api-keys`      | `client.pods.apiKeys.create(podId, request)`      | `postnuvia pods api-keys create --pod-id <pod_id>`        |
| Inbox          | `POST /v0/inboxes/{inbox_id}/api-keys` | `client.inboxes.apiKeys.create(inboxId, request)` | `postnuvia 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 may be later than the creating key's own expiry, since the registered key is independent of its creator. 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
postnuvia api-keys list --type public_key

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

# revoke the key when it is no longer needed
postnuvia 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.

> **Note**
>
> See the [API Keys reference](https://docs.postnuvia.com/api-reference/api-keys/create) for complete request and response fields.