Skip to content
API Reference
Guides
Use Keycard SDKs

Protect an API

Build an API that knows which agent is calling and why, then give agents scoped credentials without touching their code.

When you give an agent your GitHub token, every agent on your machine shares that token. You can’t tell which agent did what, can’t revoke one without breaking the others, can’t constrain scope. One misconfigured prompt and the agent pushes to main with your credentials.

Keycard issues a separate credential per agent action: short-lived, scoped to one API, tied to a real user’s authorization.

Your APIAny backend service
Bearer AuthRequests verified by Keycard
Per-Request TokensShort-lived, never stored
Agent IdentityKnow which agent called what
Shared API keysKeycard
IdentityThe userUser + agent + session
ScopeWhatever the OAuth app grantsConstrained by access policy
LifetimeDays or weeks, on diskMinutes, in-memory
RevocationRevoke the whole tokenPer-agent or per-session
Audit”A token was used""This agent, on behalf of this user, accessed this resource with these scopes”

Your server needs discovery endpoints so Keycard can issue tokens for it, and bearer auth middleware to validate those tokens.

Terminal window
pip install keycardai-starlette fastapi uvicorn

keycardai-starlette provides an AuthProvider that installs discovery endpoints and AuthenticationMiddleware in a single call. Works with FastAPI and any Starlette-compatible app.

import os
from fastapi import FastAPI, Request
from keycardai.starlette import AuthProvider, requires
ZONE_URL = os.environ["KEYCARD_ZONE_URL"]
auth = AuthProvider(zone_url=ZONE_URL)
app = FastAPI()
auth.install(app) # mounts /.well-known/* and auth middleware
@app.get("/health")
async def health():
return {"ok": True}
@app.get("/api/data")
@requires("authenticated")
async def get_data(request: Request):
user = request.user
return {
"data": "hello from protected API",
"meta": {"agent": user.client_id, "scopes": list(request.auth.scopes)},
}

auth.install(app) mounts the RFC 9728 / RFC 8414 /.well-known/* discovery endpoints and registers AuthenticationMiddleware so all routes have access to request.user. @requires("authenticated") rejects unauthenticated requests with an RFC 6750 WWW-Authenticate challenge.

Set KEYCARD_ZONE_URL to your zone URL from Keycard Console (zone settings) and start the server:

Terminal window
export KEYCARD_ZONE_URL=https://<your-zone-id>.keycard.cloud
uvicorn main:app --port 8080

Check that discovery responds:

Terminal window
$ curl -s http://localhost:8080/.well-known/oauth-protected-resource | jq
{
"resource": "http://localhost:8080",
"scopes_supported": ["read", "write", "admin"],
"resource_name": "My API"
}

Check that unauthenticated requests are rejected:

Terminal window
$ curl -D - http://localhost:8080/api/data
HTTP/1.1 401 Unauthorized
Www-Authenticate: Bearer resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource"

When a valid token arrives, your middleware unpacks these claims:

{
"iss": "https://<your-zone-id>.keycard.cloud",
"sub": "emnagera4jhr4fsqbsje5p7bq6",
"aud": ["http://localhost:8080"],
"scope": "read",
"client_id": "app_agent-framework-456",
"sid": "nr3hb6dx0a228kscyasis6uuwp",
"exp": 1774137902,
"iat": 1774137302,
"jti": "019d12d2-ddec-7b0c-b293-52af0ca5a2f0"
}

Your API can use the delegation context for decisions that a standard OAuth resource server can’t make. You can enforce these in your code, in Keycard’s access policy, or both.

app.get("/api/data", auth, (req, res) => {
const { clientId, scopes } = (req as AuthenticatedRequest).auth;
// Rate limit per agent, not per user.
if (rateLimiter.exceeded(clientId)) {
return res.status(429).json({ error: "rate limit exceeded for this agent" });
}
// Scope-aware responses.
const data = scopes.includes("admin") ? getFullData() : getReadOnlyData();
// Agent-attributed logs.
logger.info({ agent: clientId, scopes, path: req.path });
res.json(data);
});

In Keycard Console:

  1. Go to Resources, Create Resource. Set the identifier to your server’s URL (http://localhost:8080). Add the scopes your API supports: read, write, admin. Select Zone Provider as the credential provider. No redirect URL needed; redirect URLs are for upstream OAuth providers like GitHub.
  2. Go to Applications, Create Application. Set your API as the provided resource. Generate a Client ID and Client Secret. Agents use these when exchanging tokens via the SDK. For local testing, also add http://localhost:8765/callback as a redirect URI.
  3. In Zone Settings, turn off “Require invitation to sign in to this zone” so users can self-register when they authenticate for the first time.

Use the SDK to exchange tokens from a trusted backend using client credentials.

The exchange takes a subjectToken: the user’s Keycard access token. In production this comes from your app’s OAuth flow. For local testing, both SDKs have a built-in PKCE helper that opens the browser and returns the token:

import asyncio, httpx
from keycardai.oauth.pkce import authenticate
async def get_subject_token():
async with httpx.AsyncClient() as http:
r = await http.get("http://localhost:8080/api/data")
www_auth = r.headers["www-authenticate"]
token = await authenticate(
client_id="<client-id>",
client_secret="<client-secret>",
resource_url="http://localhost:8080",
www_authenticate_header=www_auth,
scopes=["read"],
)
return token.access_token

This opens the browser for the Keycard zone login. The zone login is a separate account from your Keycard Console login. If it’s your first time, click Sign up to create one.

import httpx
from keycardai.oauth import AsyncClient, BasicAuth
from keycardai.oauth.types.models import TokenExchangeRequest
async with AsyncClient(zone_url, auth=BasicAuth(client_id, client_secret)) as client:
response = await client.exchange_token(TokenExchangeRequest(
subject_token=user_access_token,
resource="http://localhost:8080",
scope="read",
))
async with httpx.AsyncClient() as http:
data = await http.get(
"http://localhost:8080/api/data",
headers={"Authorization": f"Bearer {response.access_token}"},
)

The subjectToken is the user’s Keycard access token, obtained via OAuth 2.0 authorization code + PKCE against your zone’s authorize endpoint (https://<zone-id>.keycard.cloud/oauth/2/authorize). In production on EKS, use EKSWorkloadIdentity instead of ClientSecret.