Skip to main content

AgentAdmit SDK for Python

User-mediated AI agent authorization for Python apps. Supports FastAPI, Flask, and Django.

Get started: Sign up at agentadmit.com and get your test keys. Install the SDK and build. Test Mode keys are available immediately after signup. Live keys become available when you subscribe an app.

Quick Start

Requires Python 3.10+ (the install fails on 3.9 and older with a confusing resolver error — check python3 --version first).

pip install agentadmit
agentadmit init

Edit agentadmit.yaml to define your scopes, then add to your FastAPI app:

from fastapi import FastAPI, Depends
from agentadmit import AgentAdmitMiddleware, require_scope_if_agent, get_current_user_or_agent

app = FastAPI()

# One-line setup
app.add_middleware(
    AgentAdmitMiddleware,
    config_path="agentadmit.yaml",
    get_current_user=your_auth_dependency,
    verify_user_token=your_token_verifier,
    users_collection="users",
)

# Add scope enforcement to any route
@app.get("/api/orders")
async def get_orders(
    auth_ctx=Depends(get_current_user_or_agent),
    _scope=Depends(require_scope_if_agent("read:orders")),
):
    user = auth_ctx["user"]
    # Your existing logic - unchanged
    return {"orders": get_orders_for_user(user["user_id"])}

Your app now supports AI agent connections with:

  • Scoped access control (you define the scopes)
  • User-controlled connection duration
  • Token generation and exchange
  • Revocation
  • Audit logging

How It Works

  1. User clicks "AgentAdmit" in your app
  2. Selects scopes and connection duration
  3. Gets a token to give to their AI agent
  4. Agent exchanges the token for scoped API access
  5. User revokes anytime

The token goes to the human, not the agent. No automated delivery = no prompt injection surface.

CLI

agentadmit init      # Generate agentadmit.yaml config file
agentadmit check     # Validate configuration and show scope summary
agentadmit sync      # Sync scopes from agentadmit.yaml to the hosted service
agentadmit keys      # Prints a deprecation notice (hosted service holds keys)

Flask Integration

from flask import Flask
from agentadmit.integrations.flask_integration import AgentAdmitFlask

app = Flask(__name__)
aa = AgentAdmitFlask(app, config_path="agentadmit.yaml")

@app.route('/api/orders')
@aa.require_scope_if_agent('read:orders')
def get_orders():
    return get_user_orders()

Django Integration

# settings.py
AGENTADMIT_CONFIG = {
    'APP_ID': 'app_yourappid',
    'API_KEY': 'aa_test_yourkey',
    'VERIFY_URL': 'https://api.agentadmit.com/api/v1/verify',
}

# views.py
from agentadmit.integrations.django_integration import require_scope_if_agent

@require_scope_if_agent('read:orders')
def get_orders(request):
    return get_user_orders(request)

MCP Server Integration

Building an MCP server in Python? AgentAdmit is the auth layer. MCP servers are app owners. Same SDK, same pricing.

For STDIO transport (most MCP servers), the agent includes the token in tool arguments:

import requests
import os

AGENTADMIT_VERIFY_URL = "https://api.agentadmit.com/api/v1/verify"
AGENTADMIT_API_KEY = os.environ["AGENTADMIT_API_KEY"]

def handle_tool_call(name: str, arguments: dict) -> dict:
    # 1. Extract token from tool arguments
    token = arguments.pop("agentadmit_token", None)
    if not token:
        raise PermissionError("agentadmit_token required")
    
    # 2. Validate via AgentAdmit hosted service
    resp = requests.post(
        AGENTADMIT_VERIFY_URL,
        headers={
            "Authorization": f"Bearer {AGENTADMIT_API_KEY}",
            "Content-Type": "application/json",
        },
        json={"token": token},
        timeout=5,
    )
    if resp.status_code != 200:
        raise RuntimeError(f"AgentAdmit verification failed: {resp.status_code}")
    ctx = resp.json()
    # Invalid / expired / revoked tokens return HTTP 200 with active: false
    if not ctx.get("active"):
        raise PermissionError("Invalid or expired token")
    
    # 3. Check scope for this tool
    required_scope = SCOPE_MAP.get(name)
    if required_scope and required_scope not in ctx.get("scopes", []):
        raise PermissionError(f"Missing scope '{required_scope}'")
    
    # 4. Run the tool
    return TOOL_HANDLERS[name](arguments, ctx)

For HTTP transport (FastAPI-based MCP servers), use the full SDK middleware. The agent sends the token via Authorization: Bearer header, same as any HTTP API.

Full MCP integration guide with complete before/after examples: agentadmit.com/docs/mcp-guide

MCP operators: You also get admin scopes for your own AI agent to monitor your server and full audit trail for billing. See the Admin Panel section in the dashboard for details.

Important

Mandatory introspection. All token validation goes through api.agentadmit.com. There is no self-hosted mode. No local JWT validation. No bypass. This is required for security, audit logging, and scope enforcement.

User revocation. Users can revoke any of their own agent connections via DELETE /agentadmit/connections/{connection_id} (requires the user to be authenticated via your app's session). The SDK router registers this endpoint when you call create_agentadmit_router().

In-app AI scopes. If your app has built-in AI features (analysis, plan generation, photo recognition), do not expose those as agent scopes. The user's AI agent can read the raw data and do the analysis itself. Exposing in-app AI endpoints to agents creates double cost.

Consent Ledger (Caller-Identity Consent)

AgentAdmit can host per-user consent switches for three independent caller classes: human_session, in_app_ai, and external_agent. No class's setting implies another's.

External agents: the verify response already carries the verdict; it appears on the agent context as auth_ctx["consent"] when present. The hosted service deliberately omits the verdict when its consent store is unreadable (degraded mode), so treat an absent verdict as unresolved, never as a grant — resolve it with check_consent:

consent = auth_ctx.get("consent")
if not (isinstance(consent, dict) and isinstance(consent.get("granted"), bool)):
    consent = check_consent(auth_ctx["user"]["user_id"], "external_agent")  # fail closed on error
if consent["granted"] is not True:
    raise HTTPException(403, "The data owner has not enabled external agent access.")

Human sessions and in-app AI never hold AgentAdmit tokens, so ask directly:

from agentadmit import check_consent

verdict = check_consent(app_user_id="user_8842", caller_class="in_app_ai")
if not verdict["granted"]:
    ...  # do not run AI over this user's data

Consent is orthogonal to revocation: a denied verdict means your app returns its own 403; the connection and token stay valid so the user can flip consent back on without re-connecting. Write switches through PUT /api/v1/consent/settings from your backend; export the audit trail with GET /api/v1/consent/export (every plan).

One-dependency drop-in. Instead of wiring the three paths by hand, caller_consent() classifies the caller from the credential and evaluates the right independent path:

from fastapi import Depends
from agentadmit import caller_consent

@app.get("/api/records/{owner_id}")
async def get_record(
    ctx=Depends(caller_consent(
        # derive the class from your own credential structure, never caller input
        classify_non_agent=lambda r: "in_app_ai" if r.headers.get("x-internal-ai") == INTERNAL_SECRET else "human_session",
        resolve_data_owner_id=lambda r: r.path_params["owner_id"],
        required_scope="read:records",
    )),
):
    ...  # ctx["caller_class"] is external_agent | in_app_ai | human_session

External agents are checked via hosted introspection — the consent verdict is evaluated before the scope check (a caller whose class the owner denied learns nothing about scope state or step-up), and an absent verdict is resolved through the Consent Ledger, fail-closed. In-app AI goes through the Consent Ledger (fail closed); the human path defers to your own permission model unless you pass gate_human=True. It is a consent gate, not an authenticator, so run it after your own authentication.

Presence Verification (WebAuthn Step-Up)

The verify response can carry a human-presence fact for the connection: whether the person who authorized it completed a WebAuthn ceremony on the consent page. It appears on the agent context as auth_ctx["presence"] when the platform returns it. Older servers omit it, and verified is false for connections minted without a ceremony (direct-API tokens, presence-off sessions, pre-presence connections).

from agentadmit import presence_verified

if not presence_verified(auth_ctx):
    ...  # not presence-verified; absent or malformed data never counts as verified

Guard sensitive endpoints with the fail-closed dependency; a connection without a completed ceremony gets a 403 presence_required:

from agentadmit import require_presence

@app.post("/api/transfers")
async def create_transfer(agent_ctx=Depends(require_presence())):
    ...

Flask and Django ship the same guard: @aa.require_presence() on the Flask integration, and require_presence() from agentadmit.integrations.django_integration.

Presence gate for embedded token minting

If you mount the SDK's user-authenticated /agentadmit/connections/generate-token route inside your own app, gate that route with your app's own human-presence ceremony. A browser-driving agent can ride a logged-in user session, so scoped token minting should require a fresh passkey, WebAuthn, or equivalent out-of-band confirmation before the hosted token call is made.

FastAPI:

from fastapi import HTTPException

def require_token_mint_presence(*, request, current_user, body):
    attestation_id = body.presence_attestation_id
    if not verify_and_consume_passkey_attestation(
        user_id=current_user["user_id"],
        attestation_id=attestation_id,
        purpose="token_mint",
    ):
        raise HTTPException(
            status_code=403,
            detail={"error": "presence_attestation_required"},
        )

wellknown_router, agentadmit_router = create_agentadmit_router(
    get_current_user=get_current_user,
    require_token_mint_presence=require_token_mint_presence,
)

Flask accepts the same callback name on AgentAdmitFlask(...); Django reads it from AGENTADMIT_CONFIG["require_token_mint_presence"]. The hook runs after user authentication and local request validation, and before AgentAdmit's hosted token mint or any local connection record is written. If no hook is configured, existing apps keep the previous behavior.

Deny by raising. The hook must raise (an HTTPException in FastAPI, abort() / an exception in Flask/Django) to deny, and must verify and consume the attestation single-use (checking alone lets it be replayed). Returning None allows the mint; returning an AppAttestedPresence allows the mint and forwards the ceremony fact (below); any other return value fails closed with a 500 so a malformed hook can never emit a misleading success response.

App-Attested Presence (forward the ceremony fact)

Your ceremony is origin-bound, so AgentAdmit never witnesses it: by default the hosted service reports presence.verified: false for connections your hook gated, even though a real passkey ceremony happened. To close that gap, return an AppAttestedPresence from the hook after verifying and consuming your attestation:

from agentadmit import AppAttestedPresence

def require_token_mint_presence(*, request, current_user, body):
    attestation = verify_and_consume_passkey_attestation(
        user_id=current_user["user_id"],
        attestation_id=body.presence_attestation_id,
        purpose="token_mint",
    )
    if attestation is None:
        raise HTTPException(
            status_code=403,
            detail={"error": "presence_attestation_required"},
        )
    return AppAttestedPresence(
        method="my_webauthn",              # lowercase alphanumeric/underscore
        verified_at=attestation.created_at,  # MUST be timezone-aware
    )

The SDK forwards it to the hosted mint as presence: {verified: true, uv: true, method, verified_at}. The hosted service validates freshness (10-minute window, 60 s future clock-skew slack) and stores the method provenance-marked app:<method> so app-attested facts stay distinct from ceremonies AgentAdmit witnessed itself. Introspection, the grant-event ledger, and the evidence API then carry presence.verified: true for the connection, and the local record behind GET /connections carries the same presence object.

Honesty ceiling: this is your app's attestation, recorded and provenance-marked. It is not witnessed by AgentAdmit and not independently verifiable. Only construct one for a ceremony that verified the user with UV (biometric or PIN user verification); verified/uv are literal true; a ceremony without UV carries no presence fact, so return None instead. verified_at must be timezone-aware (datetime.now(timezone.utc), not datetime.utcnow()); naive timestamps are rejected at construction because they serialize without an offset and the hosted mint rejects them.

Declared Purpose

Declared purpose: the user-facing reason recorded on the grant at the consent moment. Review-time record only, never an enforcement input; authorization decisions ride scopes, connection status, and consent.

Pass an optional purpose (1–300 characters) when generating a connection token. The SDK forwards it to the hosted mint, where it is shown to the human on the consent page ("Declared purpose: …"), stamped into every audit log row, and returned by /verify introspection:

POST /agentadmit/connections/generate-token
{
  "scopes": ["read:workouts"],
  "purpose": "Book my Tuesday workout sessions"
}

The field works the same on the FastAPI router, the Flask blueprint, and the Django view. Omit it to mint without one — existing callers keep working. A purpose over 300 characters is rejected locally with 400 invalid_request before any hosted call, and the field has no interaction with the token-mint presence hook.

On verification, the purpose appears on the agent context as auth_ctx["purpose"] when the platform returns it (older servers omit it, as do grants recorded without one). Integrators calling /verify directly — the STDIO MCP pattern above — can parse the response with the typed VerifyResponse model; purpose is Optional[str] and defaults to None when absent:

from agentadmit import VerifyResponse

result = VerifyResponse.model_validate(resp.json())
if result.purpose:
    show_in_review_ui(result.purpose)  # display/audit only — never an access decision

User-Declared Intent

User-declared intent: the user's own words, typed at the consent moment. Where purpose is the app's words (what the application says the grant is for), user_intent is what the user themselves typed when approving it. Review-time record only, never an enforcement input; authorization decisions ride scopes, connection status, and consent.

Pass an optional user_intent (1–300 characters) when generating a connection token. It flows identically to purpose: forwarded to the hosted mint, recorded on the grant, stamped into audit log rows and ledger events, and returned by /verify introspection:

POST /agentadmit/connections/generate-token
{
  "scopes": ["read:workouts"],
  "purpose": "Book my Tuesday workout sessions",
  "user_intent": "I want my assistant to handle my gym schedule for me"
}

The field works the same on the FastAPI router, the Flask blueprint, and the Django view. Omit it to mint without one — existing callers keep working. A user_intent over 300 characters is rejected locally with 400 invalid_request before any hosted call, and the field has no interaction with the token-mint presence hook.

When the hosted presence ceremony runs, user_intent is included in the verifiable-consent-evidence commitment — the user's authenticator signs their own words alongside the rest of the grant record.

On verification, it appears on the agent context as auth_ctx["user_intent"] when the platform returns it (older servers omit it, as do grants recorded without one). On the typed VerifyResponse model, user_intent is Optional[str] and defaults to None when absent:

from agentadmit import VerifyResponse

result = VerifyResponse.model_validate(resp.json())
if result.user_intent:
    show_in_review_ui(result.user_intent)  # display/audit only — never an access decision

Rate Limiting

The AgentAdmit introspection endpoint enforces rate limits. The Python SDK handles HTTP 429 responses automatically with exponential backoff and jitter - no changes needed in your app code.

Retry behavior

Parameter Default Description
Initial delay 1 second First retry wait
Backoff multiplier 2x Doubles each retry
Cap 30 seconds Maximum wait per retry
Jitter 0-500 ms Random addition to each delay
Max retries 3 Configurable

The SDK also respects the Retry-After response header - if present, it overrides the computed backoff delay.

Configuring max retries

In agentadmit.yaml:

max_retries: 5  # default: 3. Set to 0 to disable retries.

Handling exhausted retries

When all retries are exhausted, the SDK raises RateLimitError:

from agentadmit.exceptions import RateLimitError

try:
    # Any endpoint protected with require_scope / get_agentadmit_user
    ...
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after}s")
    print(f"Limit: {e.limit}, Remaining: {e.remaining}, Reset: {e.reset}")
    # Return 429 to the caller or queue for retry

RateLimitError attributes:

  • retry_after - seconds from Retry-After header (or None)
  • limit - X-RateLimit-Limit header value (or None)
  • remaining - X-RateLimit-Remaining header value (or None)
  • reset - X-RateLimit-Reset Unix timestamp (or None)

Route Registration Order (FastAPI)

When using create_agentadmit_router(), the SDK registers default endpoints for /agentadmit/scopes, /agentadmit/connections/generate-token, etc.

If you need to override any SDK endpoint with your own (e.g., a user-aware /scopes endpoint), register your route before calling app.include_router(). FastAPI resolves routes in registration order - the first matching route wins.

# Correct: custom /scopes registered before SDK router
@app.get("/agentadmit/scopes")
async def my_scopes(current_user: dict = Depends(get_current_user)):
    # your user-aware logic
    ...

wellknown_router, agentadmit_router = create_agentadmit_router(...)
app.include_router(wellknown_router)
app.include_router(agentadmit_router, prefix="/agentadmit")

# Wrong: custom route registered AFTER SDK router (shadowed, never reached)
wellknown_router, agentadmit_router = create_agentadmit_router(...)
app.include_router(agentadmit_router, prefix="/agentadmit")  # SDK route wins
@app.get("/agentadmit/scopes")  # never reached
async def my_scopes(): ...

Tip: Use the filter_scopes_for_user callback parameter on create_agentadmit_router() as a cleaner alternative to overriding /scopes entirely - the SDK handles the endpoint and calls your function to filter results.

Documentation

Full integration guide: https://agentadmit.com/docs/app-owner-guide

Data Collection & Privacy

The AgentAdmit Python SDK runs server-side and does not interact with app stores or end-user devices directly.

What the SDK does

  • Validates AgentAdmit tokens by calling AgentAdmit's hosted introspection endpoint (https://api.agentadmit.com/api/v1/verify) on every agent request - this is mandatory introspection; there is no local or offline validation mode
  • Enforces scope-based access control on your API routes
  • Manages connection lifecycle (create, revoke, audit) using your configured storage backend (MongoDB or in-memory)

What the SDK does NOT do

  • Does not transmit raw end-user PII (such as name, email, or device identifiers) - each introspection request sends the opaque access token and your API key
  • Does not perform passive background telemetry or analytics - network calls occur only during active token validation

What the AgentAdmit hosted service records

On every token validation, AgentAdmit's /api/v1/verify endpoint receives the access token and API key, resolves the token to its user_id, connection_id, granted scopes, and agent_label, and records per-call metadata (including the endpoint and timestamp) for billing, audit logging, the security alerts engine, and usage metering. This is integral to how AgentAdmit works and applies to both Test Mode and live keys. See the "Mandatory introspection" notes above and the compliance guide for the full data-handling description.

Privacy impact

Since this SDK runs on your server, it has no direct App Store or Play Store compliance surface. Your client-side integration (e.g., the AgentAdmit React SDK) handles privacy manifest and data safety requirements.

For complete compliance guidance, see our compliance guide.

License

All rights reserved. Patent pending.

Security Alerts

Monitor suspicious agent activity with the AgentAdmit alerts API. Six alert types are supported:

  • volume_spike - unusual request volume
  • failed_scope_attempts - repeated scope access failures
  • burst_pattern - rapid burst of requests
  • stale_reactivation - dormant connection suddenly active
  • new_scope_usage - agent using a scope for the first time
  • revoked_connection_attempt - revoked connection trying to authenticate

Configure Alert Thresholds

from agentadmit import configure_alerts

result = configure_alerts(
    app_id="app_abc123",
    alert_type="volume_spike",
    enabled=True,
    threshold_value=100,
    threshold_window_minutes=5,
    kill_switch_enabled=True,
    kill_switch_threshold_value=500,
    kill_switch_threshold_window_minutes=10,
)
# {"ok": True, "config": {...}}

List Alert Events

from agentadmit import list_alerts

events = list_alerts(app_id="app_abc123", alert_type="volume_spike", limit=50)
# {"events": [...], "total": 12, "limit": 50, "offset": 0}

Get Current Config

from agentadmit import get_alert_config

config = get_alert_config(app_id="app_abc123")
conn_config = get_alert_config(app_id="app_abc123", connection_id="conn_xyz")

Notifying Your Users

AgentAdmit detects anomalies, fires alerts, and (with kill switch) auto-revokes connections. How you notify your own users is up to you. AgentAdmit provides the data - you deliver it through your own system (in-app notifications, email, push, etc.).

  • Poll alerts - Use the SDK methods above from your backend to check for new events, then notify users through your existing system.

  • Webhook delivery - Configure a webhook URL in your AgentAdmit dashboard. When an alert fires, AgentAdmit POSTs the payload to your server, signed with your whsec_... secret. The payload carries alert_id, alert_type, severity, the connection's agent_label, and the grant's declared purpose; the full shape is documented in the Webhook Delivery section of the MCP guide at https://agentadmit.com/docs/mcp-guide. Always verify the signature before trusting the payload:

    from agentadmit import verify_webhook_signature, WebhookSignatureError
    
    @app.post("/agentadmit/alerts")
    async def alerts(request: Request):
        payload = await request.body()
        try:
            verify_webhook_signature(
                payload,
                request.headers.get("X-AgentAdmit-Signature", ""),
                secret=os.environ["AGENTADMIT_WEBHOOK_SECRET"],  # whsec_...
            )
        except WebhookSignatureError:
            return JSONResponse({"error": "invalid_signature"}, status_code=400)
        event = json.loads(payload)
        ...
    

    The header format is t=<unix_ts>,v1=<hex> - an HMAC-SHA256 of {t}.{raw_body} keyed with your signing secret. The helper compares in constant time and rejects timestamps more than 5 minutes off (replay protection).

  • React SDK - Embed the <AlertsPanel> component so users can view their own alert history and tighten thresholds.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agentadmit-1.9.0.tar.gz (86.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agentadmit-1.9.0-py3-none-any.whl (65.1 kB view details)

Uploaded Python 3

File details

Details for the file agentadmit-1.9.0.tar.gz.

File metadata

  • Download URL: agentadmit-1.9.0.tar.gz
  • Upload date:
  • Size: 86.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for agentadmit-1.9.0.tar.gz
Algorithm Hash digest
SHA256 098d4fe906137d5210baf658146fe4f873f6d8bda257b90d673a772e3fa0d242
MD5 a9b156b082b5e03b00fe8895d00c01b3
BLAKE2b-256 485408c6cc3b8cd9f0ad8d3b9bc13debdde20c198e93f2330a1db2385130352d

See more details on using hashes here.

Provenance

The following attestation bundles were made for agentadmit-1.9.0.tar.gz:

Publisher: publish.yml on PhoenixCo-Founder/agentadmit-sdk-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agentadmit-1.9.0-py3-none-any.whl.

File metadata

  • Download URL: agentadmit-1.9.0-py3-none-any.whl
  • Upload date:
  • Size: 65.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for agentadmit-1.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 66ac6d534dcb04838a7167452e2e8259640cd249e864a61962c4902e97da8cd0
MD5 273cfe86635cfbf651fa6df516548bf8
BLAKE2b-256 1f9973710dfd800b81117db724dd0ed1fb321c4b660e4cdb977dbd7731b01abd

See more details on using hashes here.

Provenance

The following attestation bundles were made for agentadmit-1.9.0-py3-none-any.whl:

Publisher: publish.yml on PhoenixCo-Founder/agentadmit-sdk-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.11.0

2 files

1.10.1

2 files

1.10.0

2 files

This release

1.9.0 This release

2 files

1.8.0

2 files

1.7.1

2 files

1.7.0

2 files

1.6.0

2 files

1.5.1

2 files

1.5.0

2 files

1.4.0

2 files

1.3.0

2 files

1.2.0

2 files

1.1.2

2 files

1.1.1

2 files

1.1.0

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page