Documentation

Everything you need to issue, delegate, verify, and revoke credentials for your AI agents.

Overview

Attest gives every AI agent in your pipeline a cryptographically signed, scoped, auditable credential -- so you always know who authorized what action and when.

The Missing Protocol for AI Agent Authorization

A short companion piece on the product thesis behind Attest. It explains why delegated authority, intent binding, and provenance need to move together as agents fan out across tools and sub-agents. Read the article.

Each credential is an RS256-signed JWT carrying:

The credential lifecycle:

Signup
->
Issue root credential
->
Delegate to sub-agents
->
Verify (offline RS256)
->
Revoke (cascades)

Quickstart

The fastest path to value is not raw credential issuance. It is protecting one real MCP tool call end to end.

Fastest activation path: run the demo, add withAttest() to one MCP server, issue a credential with the exact discovered scopes, then confirm an allowed call succeeds and an out-of-scope call is blocked.

Want the shortest real path? Use the dedicated MCP quickstart for the copy-paste integration path, then come back here for the broader API reference.

1. Install the SDK and protect one tool

npm install @attest-dev/sdk @modelcontextprotocol/sdk
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { withAttest } from "@attest-dev/sdk/mcp";

const server = new McpServer({ name: "my-tools", version: "1.0.0" });
const protectedServer = withAttest(server, {
  issuerUri: "https://api.attestdev.com",
});

protectedServer.tool("send_email", schema, handler);

2. Expose scopes, then issue only what the tool needs

import { getAttestScopes } from "@attest-dev/sdk/mcp";

app.get("/.well-known/attest-scopes", (_req, res) => {
  res.json({ tools: getAttestScopes(protectedServer) });
});
# Create an organisation and save the API key (shown once)
curl -X POST https://api.attestdev.com/v1/orgs \
  -H "Content-Type: application/json" \
  -d '{"name": "acme-corp"}'

# Issue only the discovered scope for send_email
curl -X POST https://api.attestdev.com/v1/credentials \
  -H "Authorization: Bearer att_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "orchestrator-v1",
    "user_id": "usr_alice",
    "scope": ["email:send"],
    "instruction": "Send the weekly digest"
  }'

3. Confirm the protected-tool moment

# Expected behavior after wiring Attest into the MCP server
# - a tool call with email:send succeeds
# - a tool call requiring crm:write is blocked
# - the task tree can be revoked with DELETE /v1/credentials/{jti}
# - the evidence trail is queryable with GET /v1/tasks/{tid}/audit

API Reference

Base URL: https://api.attestdev.com

Organisations

Method Path Auth Description
POST /v1/orgs public Create an org. Returns api_key (shown once), key_id, org.
GET /v1/org Bearer Return the authenticated org's details.

Credentials

MethodPathAuthDescription
POST /v1/credentials Bearer Issue a root credential. Body: agent_id, user_id, scope, instruction, ttl_seconds.
POST /v1/credentials/delegate Bearer Delegate to a child agent. Body: parent_token, child_agent, child_scope, ttl_seconds.
DELETE /v1/credentials/{jti} Bearer Revoke a credential and all its descendants. Body: revoked_by.
GET /v1/revoked/{jti} public Check if a JTI is revoked. Returns {"revoked": true|false}.

Audit

MethodPathAuthDescription
GET /v1/tasks/{tid}/audit Bearer Retrieve the append-only, cryptographically chained audit log for a task tree.

JWKS

MethodPathAuthDescription
GET /orgs/{orgID}/jwks.json public Return the org's active RSA-2048 public key as a JSON Web Key Set.

Key Management

Manage API keys and rotate your org's RSA signing key.

MethodPathAuthDescription
GET /v1/org/keys Bearer List all API keys (id, name, created_at, revoked_at).
POST /v1/org/keys Bearer Create a new API key. Body: {"name": "ci-key"}. Returns api_key (shown once) and key_id.
DELETE /v1/org/keys/{keyID} Bearer Revoke an API key. Cannot revoke the key used to authenticate the request.
POST /v1/org/keys/rotate Bearer Rotate the org's RSA signing key. Returns {"kid": "new-key-id"}. Fetch updated JWKS before old tokens expire.

Key rotation: after calling POST /v1/org/keys/rotate, re-fetch /orgs/{orgID}/jwks.json to get the new public key. Existing tokens signed with the old key remain valid until they expire -- they will just fail to verify against the new JWKS if you clear your cache.

TypeScript SDK

Install

npm install @attest-dev/sdk

Issue and verify

import { AttestClient, AttestVerifier } from "@attest-dev/sdk";

const client = new AttestClient({ apiKey: "att_live_..." });

// Issue a root credential
const { token, claims } = await client.issue({
  agentId:     "summary-agent",
  userId:      "user-123",
  scope:       ["files:read", "db:query"],
  instruction: "Summarise the quarterly report",
  ttlSeconds:  3600,
});

// Delegate to a sub-agent with narrower scope
const { token: childToken } = await client.delegate({
  parentToken: token,
  childAgent:  "db-agent",
  childScope:  ["db:query"],
  ttlSeconds:  900,
});

// Verify (offline -- no API call needed after JWKS is cached)
const verifier = new AttestVerifier({ orgId: claims.att_tid });
const result = await verifier.verify(childToken);
console.log(result.valid, result.claims?.att_scope);

Revoke and audit

// Revoke (cascades to all descendants)
await client.revoke(claims.jti, "user-requested");

// Fetch audit chain
const events = await client.audit(claims.att_tid);
events.forEach(e => console.log(e.event_type, e.agent_id));

Python SDK

Install

pip install attest-sdk

Issue and delegate

from attest import AttestClient, IssueParams, DelegateParams

client = AttestClient(api_key="att_live_...")

# Issue a root credential
token = client.issue(IssueParams(
    agent_id="summary-agent",
    user_id="user-123",
    scope=["files:read", "db:query"],
    instruction="Summarise the quarterly report",
    ttl_seconds=3600,
))
print(token.claims.att_tid)   # task-tree UUID
print(token.claims.att_scope)  # ["files:read", "db:query"]

# Delegate to a sub-agent
child = client.delegate(DelegateParams(
    parent_token=token.token,
    child_agent="db-agent",
    child_scope=["db:query"],
    ttl_seconds=900,
))

Verify with AttestVerifier

from attest import AttestVerifier

# No API key needed -- fetches JWKS once and caches it
verifier = AttestVerifier(org_id="<orgID>")
result = verifier.verify(child.token)
print(result.valid)          # True
print(result.claims.att_depth)  # 1

Revoke and audit

# Revoke (cascades to descendants)
client.revoke(token.claims.jti, revoked_by="user-requested")

# Fetch audit chain
events = client.audit(token.claims.att_tid)
for e in events:
    print(e.event_type, e.agent_id)

Scope format

Scopes are strings in resource:action format. Wildcards are supported: files:* grants all actions on files.

ScopeGrants
files:readRead files only
files:*All actions on files
db:queryRun read-only database queries
*:readRead any resource

A delegated credential's scope must be a subset of its parent's scope. Attempting to expand scope during delegation returns a 422 error.

Delegation depth

Each credential carries an att_depth field. Root credentials have depth 0. Each delegation increments depth by 1. There is no hard cap on depth, but practical pipelines rarely exceed 3-4 levels.

Chain integrity

The att_chain field is an ordered list of ancestor JTIs, starting from the root and ending with the current credential's own JTI. Verifiers should assert:

Revoking any credential in the chain cascades to all descendants -- any credential whose att_chain contains the revoked JTI is also considered revoked.

Key rotation

Call POST /v1/org/keys/rotate to generate a new RSA-2048 signing key. The old key is retired but not deleted -- existing tokens signed with it remain verifiable until they expire (as long as you retain the old JWKS).

After rotation, re-fetch /orgs/{orgID}/jwks.json. The JWKS endpoint always returns the current active key, so verifiers that cache JWKS should call clear_jwks_cache() (Python) or equivalent after rotation.