Skip to content
API Reference

Protect Any 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.

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
go get github.com/keycardai/credentials-go/mcp

The package name says mcp but the middleware is standard net/http. No MCP transport dependency. Requires Go 1.22+.

package main
import (
"encoding/json"
"log"
"net/http"
"os"
"github.com/keycardai/credentials-go/mcp"
)
func main() {
zoneURL := os.Getenv("KEYCARD_ZONE_URL")
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mx := http.NewServeMux()
// Tell Keycard how to issue tokens for this API.
mx.Handle("/.well-known/", mcp.AuthMetadataHandler(
mcp.WithIssuer(zoneURL),
mcp.WithScopesSupported([]string{"read", "write", "admin"}),
mcp.WithResourceName("My API"),
))
// Validate tokens. Reject anything without "read" scope.
handler := mcp.RequireBearerAuth(
mcp.WithRequiredScopes("read"),
)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := mcp.AuthInfoFromRequest(r)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"data": "hello from protected API",
"meta": map[string]any{
"agent": auth.ClientID,
"scopes": auth.Scopes,
},
})
}))
mx.Handle("GET /api/data", handler)
log.Printf("Protected API listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, mx))
}

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
go run ./main.go

Check that discovery responds:

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

Check that unauthenticated requests are rejected:

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

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

{
"iss": "https://my-zone-id.keycard.cloud",
"sub": "emnagera4jhr4fsqbsje5p7bq6",
"aud": ["http://localhost:9090"],
"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:9090). 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 path.

Add the resource to your keycard.toml with the scopes you need. At startup, keycard run exchanges a scoped token for each entry and injects the result as an environment variable.

[[credentials.default]]
env_var = "MY_API_TOKEN"
resource = "http://localhost:9090"

Each entry is a request, not a grant. When keycard run exchanges each credential, the STS checks whether the user has authorized access to that resource and whether access policy permits it. If the user hasn’t consented, keycard run opens the browser. If policy denies access, the exchange fails.

Start the agent:

Terminal window
keycard run -- claude

The agent finds $MY_API_TOKEN in its environment, scoped to read:

Terminal window
$ curl -s -H "Authorization: Bearer $MY_API_TOKEN" \
http://localhost:9090/api/data | jq
{
"data": "hello from protected API",
"meta": {
"agent": "cli/metadata.json",
"scopes": ["read"]
}
}

The agent’s code has no Keycard imports, no exchange calls, no OAuth handling. If the user hasn’t authorized this resource yet, Keycard returns interaction_required and keycard run opens the browser for OAuth consent before launching. The server never participates in consent.

For credentials not in the template, keycard credential read does an on-demand exchange:

Terminal window
TOKEN=$(keycard credential read http://localhost:9090 --scope "read")

Note that the ambient path exchanges tokens once at session start. For per-action exchange (a fresh token per tool call), use the SDK path.

The SDK path: agent frameworks and production

Section titled “The SDK path: agent frameworks and production”

For agent frameworks and backend services, use @keycardai/oauth directly. These examples run token exchange from a trusted backend using client credentials, not from a browser.

import { TokenExchangeClient } from "@keycardai/oauth/tokenExchange";
const exchange = new TokenExchangeClient(process.env.KEYCARD_ZONE_URL!, {
clientId: process.env.KEYCARD_CLIENT_ID!,
clientSecret: process.env.KEYCARD_CLIENT_SECRET!,
});
const result = await exchange.exchangeToken({
subjectToken: userAccessToken,
resource: "http://localhost:9090",
scope: "read",
});
const data = await fetch("http://localhost:9090/api/data", {
headers: { Authorization: `Bearer ${result.accessToken}` },
});

The subjectToken is the user’s Keycard access token. In a web app, you get it via OAuth 2.0 authorization code + PKCE against your zone’s authorize endpoint (https://<zone-id>.keycard.cloud/oauth/2/authorize). With keycard run, the CLI handles this automatically. In production on EKS, use EKSWorkloadIdentity. See Secure Agentic Coding for the full auth lifecycle.