Quickstarts

Give an AI agent an identity in 10 minutes

Create an agent identity, add a secret verifier, mint a five-minute OAuth token via client_credentials, and validate it in your API with jose.

Last updated 2026-06-12

Overview

By the end of this tutorial an AI agent will have its own identity in AuthPI — not a borrowed user account, not a shared API key. The flow: create the agent → give it a secret credential → exchange the credential for a short-lived token via the standard client_credentials grant → validate the token server-side.

Prerequisites

  • An AuthPI account and an Issuer (i_...) created in the console
  • A Core API key with write access to the issuer

Export everything once so the commands below are copy-paste:

export AUTHPI_KEY_ID="key_..."        # API key ID
export AUTHPI_KEY_SECRET="..."        # API key secret
export ACCOUNT_ID="acc_..."           # your account
export ISSUER_ID="i_..."              # your issuer

API keys authenticate with HTTP Basic auth — the key ID is the username, the secret is the password (curl’s -u builds the header for you).

Step 1 — 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": "invoice-reader", "scopes": ["invoices:read"] }'
{
  "data": {
    "id": "agt_9f2c1a7e4b3d48f6a0c5e8d2b7a91c3f",
    "name": "invoice-reader",
    "status": "active",
    "scopes": ["invoices:read"],
    "created_at": 1781222400000
  }
}

scopes is the agent’s full allowed set — tokens can request a subset of these, never more. Save the ID:

export AGENT_ID="agt_9f2c1a7e4b3d48f6a0c5e8d2b7a91c3f"

Step 2 — Add a secret verifier

The agent needs a credential to authenticate with. A secret verifier generates one server-side:

curl -X POST "https://api.authpi.com/v1/accounts/$ACCOUNT_ID/issuers/$ISSUER_ID/agents/$AGENT_ID/verifiers" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "type": "secret", "name": "pipeline-credential" }'
{
  "data": {
    "id": "v_5e8d2b7a91c3f49f2c1a7e4b3d48f6a0",
    "type": "secret",
    "status": "active",
    "name": "pipeline-credential",
    "secret": "mJ4kP9xQ2wL7nR5tY8vB3cD6fG1hS0aZ4eU7iO2pXq",
    "created_at": 1781222400000
  }
}

Important: the secret field is returned only once, at creation. AuthPI stores a SHA-256 hash and cannot show it again — capture it before moving on.

Step 3 — Mint a token via client_credentials

The agent exchanges its ID and secret at your issuer’s token endpoint — the standard OAuth 2.0 client_credentials grant (RFC 6749 §4.4), with the agt_ ID as the client_id:

export AGENT_SECRET="mJ4kP9xQ2wL7nR5tY8vB3cD6fG1hS0aZ4eU7iO2pXq"  # from step 2

curl -X POST "https://idp.authpi.com/$ISSUER_ID/token" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "client_id=$AGENT_ID" \
  --data-urlencode "client_secret=$AGENT_SECRET" \
  --data-urlencode "scope=invoices:read" \
  --data-urlencode "resource=https://api.example.com"
{
  "access_token": "eyJhbGciOiJFZERTQSIsImtpZCI6Ii4uLiJ9.eyJpc3MiOi4uLn0...",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "invoices:read"
}

Agent tokens live 5 minutes (expires_in: 300) by design — agents re-mint per work cycle, so scope changes and suspensions propagate within one TTL window. No refresh token or ID token is issued. resource sets the token’s aud claim (it defaults to the agent’s own ID if omitted); pin it to your API so the token can’t be replayed elsewhere.

Step 4 — Call a protected endpoint

The token is a standard Bearer JWT — the agent sends it to your API like any OAuth client. Inside, sub is the agt_ ID and a dat (data attestation) claim of {"type": "agent"} marks it as a non-human identity:

export AGENT_TOKEN="eyJhbGciOiJFZERTQSIs..."   # the access_token from step 3

curl https://api.example.com/invoices \
  -H "Authorization: Bearer $AGENT_TOKEN"

Step 5 — Validate the token in your API

Verify the signature against your issuer’s JWKS and check the claims. With jose:

import { createRemoteJWKSet, jwtVerify } from "jose";

const ISSUER = `https://idp.authpi.com/${process.env.ISSUER_ID}`; // e.g. .../i_8fK2mQp4xR7tWz
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/jwks.json`));

export async function verifyAgentToken(bearer: string) {
  // jwtVerify checks signature, iss, aud, and exp
  const { payload } = await jwtVerify(bearer, JWKS, {
    issuer: ISSUER,
    audience: "https://api.example.com", // the `resource` the token was minted for
  });
  // Agent guard: dat.type is the semantic check, the sub prefix is defense in depth
  const dat = payload.dat as { type?: string } | undefined;
  if (dat?.type !== "agent" || !String(payload.sub).startsWith("agt_")) {
    throw new Error("Not an agent token");
  }
  return { agentId: payload.sub as string, scopes: String(payload.scope ?? "").split(" ").filter(Boolean) };
}

A request without a token, with an expired token (remember: 5 minutes), or with a human user’s token (dat.type === "identity") all fail this check.

Step 6 — Clean up

Deleting the agent removes its verifiers, KV cache entries, and listing index entry in one call. The response is 204 No Content; tokens already minted stay valid until they expire — at most 5 minutes.

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

Next steps