> Markdown version of https://authpi.com/docs/guides/agent-identities/ — fetch the complete AuthPI docs index at https://authpi.com/llms.txt to discover all available pages.

# Give an AI Agent Its Own Credentials

Create an AI agent identity in AuthPI, issue it a secret, authenticate with client_credentials, validate agent tokens, and manage its lifecycle.

This guide gives an AI agent its own identity: create the agent, issue it a secret, authenticate with the `client_credentials` grant, validate agent tokens in your API, and manage the agent's lifecycle. For the conceptual background, see [Agent identities](/docs/concepts/agents/).

Management calls go to the Core API and authenticate with an API key over HTTP Basic auth — the key ID is the username, the secret is the password (curl's `-u` flag builds the header). Set up your environment:

```bash
export AUTHPI_KEY_ID="key_..."
export AUTHPI_KEY_SECRET="..."
export ACCOUNT_ID="acc_..."
export ISSUER_ID="i_x7Kp2mQv9LbRwS"
```

## Create the agent

```bash
curl -X POST "https://api.authpi.com/v1/accounts/$ACCOUNT_ID/issuers/$ISSUER_ID/agents" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support Triage Agent",
    "description": "Triages inbound support tickets and drafts replies",
    "model": "claude-sonnet-4-5",
    "provider": "anthropic",
    "scopes": ["tickets:read", "tickets:triage"]
  }'
```

Only `name` is required. `model`, `provider`, and `version` are descriptive fields that surface in listings and audit events. `scopes` are opaque strings your application defines (up to 256) — they become the ceiling for what this agent's tokens can request. The response wraps the new agent in `data`:

```json
{
  "data": {
    "id": "agt_7c1de2f3a4b5c6d7e8f9a0b1c2d3e4f5",
    "issuer_id": "i_x7Kp2mQv9LbRwS",
    "name": "Support Triage Agent",
    "description": "Triages inbound support tickets and drafts replies",
    "model": "claude-sonnet-4-5",
    "provider": "anthropic",
    "status": "active",
    "scopes": ["tickets:read", "tickets:triage"],
    "created_at": 1781222400000,
    "updated_at": 1781222400000
  }
}
```

The agent exists but can't authenticate yet — it has no credentials.

## Add a secret verifier

```bash
curl -X POST "https://api.authpi.com/v1/accounts/$ACCOUNT_ID/issuers/$ISSUER_ID/agents/agt_7c1de2f3a4b5c6d7e8f9a0b1c2d3e4f5/verifiers" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "type": "secret", "name": "primary" }'
```

AuthPI generates a 42-character random secret and returns it in the response:

```json
{
  "data": {
    "id": "v_9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b",
    "agent_id": "agt_7c1de2f3a4b5c6d7e8f9a0b1c2d3e4f5",
    "type": "secret",
    "status": "active",
    "name": "primary",
    "credential": { "algorithm": "sha256" },
    "usage_count": 0,
    "created_at": 1781222460000,
    "secret": "kT9mWqL2xZ8vN4cP7rA1bD5fG3hJ6sU0eYiOoQwErT"
  }
}
```

**The `secret` field is returned exactly once.** AuthPI stores only its SHA-256 hash — the plaintext cannot be retrieved later, so store it in your agent's secret manager now.

An agent can hold up to 20 verifiers. Besides secrets, agents support `wallet` verifiers (a CAIP-2 blockchain address for x402-style payment flows) — see [x402 agent auth](/docs/guides/x402-agent-auth/).

## Authenticate with client_credentials

The agent exchanges its ID and secret for an access token at the issuer's token endpoint, using the standard OAuth 2.0 `client_credentials` grant (RFC 6749 §4.4):

```bash
curl -X POST "https://idp.authpi.com/$ISSUER_ID/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=agt_7c1de2f3a4b5c6d7e8f9a0b1c2d3e4f5" \
  -d "client_secret=$AGENT_SECRET" \
  -d "resource=https://api.example.com/tickets" \
  --data-urlencode "scope=tickets:read tickets:triage"
```

You can also send the credentials as HTTP Basic auth (`-u "agt_...:$AGENT_SECRET"`) instead of body parameters. `scope` must be a subset of the agent's scopes (anything else fails with `invalid_scope`); omit it to receive the full set. `resource` (RFC 8707) sets the token's `aud` claim — without it, the audience defaults to the agent's own ID.

```json
{
  "access_token": "eyJhbGciOiJFZERTQSIsImtpZCI6Ii4uLiJ9...",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "tickets:read tickets:triage"
}
```

Note what's there and what isn't: the token expires in **300 seconds (5 minutes), by design and not configurable**, and there is no refresh token or ID token. Agents should mint a fresh token at the start of each work cycle rather than caching one long-term — this is how scope changes and suspensions propagate within minutes.

## Call your API and validate the token

The agent presents the access token as a Bearer token to *your* API:

```bash
curl "https://api.example.com/tickets?status=open" \
  -H "Authorization: Bearer $AGENT_ACCESS_TOKEN"
```

Your resource server validates it like any JWT — signature against the issuer's JWKS, plus `iss`, `aud`, and `exp` — then applies the agent-specific check: `dat.type === "agent"` **and** a `sub` starting with `agt_`. Both must agree; the `dat` claim is the semantic guard that distinguishes agent tokens from human user tokens, and the prefix is the structural backstop. Using [jose](https://github.com/panva/jose):

```javascript
import { createRemoteJWKSet, jwtVerify } from "jose";

const ISSUER = "https://idp.authpi.com/i_x7Kp2mQv9LbRwS";
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/jwks.json`));

export async function verifyAgentToken(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: ISSUER,
    audience: "https://api.example.com/tickets",
  });

  if (payload.dat?.type !== "agent" || !payload.sub?.startsWith("agt_")) {
    throw new Error("Not an agent token");
  }

  const scopes = (payload.scope ?? "").split(" ").filter(Boolean);
  return { agentId: payload.sub, scopes };
}
```

Then enforce scopes per route — for example, require `tickets:triage` before letting the agent update a ticket.

## Add the agent to an organization

Agents join [Organizations](/docs/concepts/organizations/) through the same members endpoint as users — pass the `agt_` ID as `member_id`:

```bash
curl -X POST "https://api.authpi.com/v1/accounts/$ACCOUNT_ID/issuers/$ISSUER_ID/organizations/org_5h8k2m9q4w7r3t6y1u0p5a8s3/members" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "member_id": "agt_7c1de2f3a4b5c6d7e8f9a0b1c2d3e4f5",
    "scopes": ["tickets:triage"],
    "user_title": "Triage bot"
  }'
```

The membership comes back with `member_type: "agent"`, and the agent appears in the org's member list alongside humans. Note that agent access tokens do **not** embed organization claims — the grant mints tokens from the agent's own scopes only. To enforce org-level permissions for an agent, query its membership via the members endpoints.

## Rotate the secret

There is no in-place rotation endpoint; rotation is add-then-remove, and it's zero-downtime because the token endpoint accepts **any** active secret verifier:

1. Add a second secret verifier (same call as above, e.g. `"name": "rotation-2026-06"`) and capture the new secret.
2. Deploy the new secret to the agent. During the overlap, both secrets authenticate.
3. Remove the old verifier:

```bash
curl -X DELETE "https://api.authpi.com/v1/accounts/$ACCOUNT_ID/issuers/$ISSUER_ID/agents/agt_7c1de2f3a4b5c6d7e8f9a0b1c2d3e4f5/verifiers/v_9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET"
```

Returns `204 No Content`. Check the old verifier's `usage_count` and `last_used_at` (`GET .../agents/{agent_id}/verifiers`) before removing it to confirm it's no longer in use. Removing an agent's last secret verifier disables `client_credentials` for it entirely.

## Suspend or delete the agent

To stop an agent without destroying it, suspend it — a `status_reason` is required:

```bash
curl -X PATCH "https://api.authpi.com/v1/accounts/$ACCOUNT_ID/issuers/$ISSUER_ID/agents/agt_7c1de2f3a4b5c6d7e8f9a0b1c2d3e4f5" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "status": "suspended", "status_reason": "Anomalous ticket volume; investigating" }'
```

Suspension blocks new tokens immediately (secret validation fails for non-active agents); outstanding tokens simply expire within 5 minutes. Reactivate by patching `status` back to `"active"`. Deletion is permanent — it removes the agent, all of its verifiers, and its wallet reverse lookups:

```bash
curl -X DELETE "https://api.authpi.com/v1/accounts/$ACCOUNT_ID/issuers/$ISSUER_ID/agents/agt_7c1de2f3a4b5c6d7e8f9a0b1c2d3e4f5" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET"
```

Returns `204 No Content`. Every step in this guide emitted an audit event (`agent.created`, `agent.verifier.added`, `organization.membership.created`, `agent.verifier.removed`, `agent.updated`, `agent.deleted`) that you can consume via [webhooks](/docs/guides/webhooks).

## Next steps

- Understand the model behind these steps in [Agent identities](/docs/concepts/agents/)
- Add a wallet verifier and pay-per-call auth with [x402 agent auth](/docs/guides/x402-agent-auth/)
- Look up every claim in agent tokens in the [token claims reference](/docs/reference/token-claims/)
- Browse all agent operations in the [Agents API reference](/docs/reference/core-api/agents/)