Guides

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.

Last updated 2026-06-12

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.

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:

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

Create the agent

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:

{
  "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

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:

{
  "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.

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):

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.

{
  "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:

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:

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 through the same members endpoint as users — pass the agt_ ID as member_id:

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:
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:

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:

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.

Next steps