Python SDK

pip install allowly

Requires Python 3.10+.

Initialize

import allowly

client = allowly.Allowly(api_key=os.environ["ALLOWLY_KEY"])

Optional: derive a PII-safe user ID from email

Use your app's internal opaque user ID when you have one. If you need email-based lookup, hash the email locally before creating the authorization.

from allowly.identifiers import from_email

allowly_user_id = from_email(
    user.email,
    pepper=os.environ["ALLOWLY_PII_PEPPER"],
)

The helper trims and lowercases only, returns an email_hmac:v1:... identifier, and never sends the raw email or pepper to Allowly. Keep the pepper stable and backed up; changing it changes derived user IDs. See PII-safe identifiers.

Create an authorization

When your user authorizes your agent, create an authorization and store the returned authorization_id.

authorization = await client.authorizations.create(
    user_id=allowly_user_id,   # or your app's opaque internal ID
    agent_id="my-email-agent",
    scopes=[
        allowly.ScopeEntry(name="email.read"),
        allowly.ScopeEntry(name="email.send", constraints={"max_per_day": 5}),
        allowly.ScopeEntry(name="candidate.delete"),
    ],
    requires_confirm_for=["email.send"],
    requires_escalation_for=["candidate.delete"],
    escalation_targets={"candidate.delete": "manager"},
    expires_at="2026-12-31T00:00:00Z",
    metadata={"source": "onboarding_modal"},
)

await db.users.update(user.id, allowly_authorization_id=authorization.authorization_id)
# authorization.receipt is { status: "pending", receipt_id, url, ready_at_estimate }

Add a spend cap

On Pro and Enterprise workspaces, attach a budget cap to estimated-cost actions. The value is micro-USD.

authorization = await client.authorizations.create(
    user_id=allowly_user_id,
    agent_id="research-agent",
    scopes=["llm.enrich"],
    expires_at="2026-12-31T00:00:00Z",
    budget_limit_micros=50_000_000,  # $50.00
)

Check before every action

result = await client.check(
    authorization_id=await db.users.get_authorization_id(user.id),
    scopes=["email.send"],
    resource="gmail:thread:abc",
    session_id=current_session_id,
    context={"initiated_by": "user", "origin": "chat"},
)
scope_result = result.results["email.send"]

if scope_result.decision == "allow":
    await send_the_email()

elif scope_result.decision == "confirm":
    await prompt_user(scope_result.confirm_prompt_hint)
    await client.confirmations.approve(
        scope_result.confirm_nonce,
        approved=True,
        ttl_seconds=60,
    )
    # Re-call client.check(...) — now returns allow

elif scope_result.decision == "escalate":
    await route_to_approver(scope_result.escalation_to, scope_result.escalation_id)
    await client.escalations.approve(
        scope_result.escalation_id,
        resolved_by="manager:8821",
    )
    # Re-call client.check(...) — now returns allow once

else:
    # decision == "deny" — branch on reason for the best user experience
    match scope_result.reason:
        case "rate_limit_exceeded":
            raise Exception("Daily limit reached — try again tomorrow.")
        case "authorization_revoked" | "authorization_expired":
            raise Exception(f"Authorization no longer valid: {scope_result.reason}")
        case "scope_not_authorized":
            raise Exception("Agent is not authorized for this action.")
        case _:
            raise Exception(f"Allowly denied: {scope_result.reason}")

For a budgeted authorization, pass the estimated action cost:

result = await client.check(
    authorization_id=authorization_id,
    scopes=["llm.enrich"],
    estimated_cost_micros=24_000,
)

scope_result = result.results["llm.enrich"]
if scope_result.reason == "budget_exceeded":
    raise Exception("This action would exceed the authorization budget.")
if scope_result.budget:
    print(scope_result.budget.spent_after_micros)

Fetch and verify a signed receipt

# Fetch the signed receipt (polls until signed, raises on timeout)
signed_receipt = await client.receipts.fetch_signed(scope_result.receipt.receipt_id)

# Verify offline — no API call needed
from allowly.verify import fetch_keys_doc, verify_receipt, load_keys_from_json

keys_doc = fetch_keys_doc(
    workspace_id,
    # Optional out-of-band hash pin:
    # expected_sha256="<sha256 hex>",
)
keys = load_keys_from_json(keys_doc)

verify_receipt(signed_receipt, keys)  # raises VerificationError if invalid

fetch_keys_doc() enforces HTTPS, caches the keys document in-memory for 5 minutes, validates the document shape before use, and optionally enforces an out-of-band SHA-256 pin.

Revoke an authorization

await client.authorizations.revoke(authorization_id, revoked_by="user")

Types

from allowly import (
    Allowly,
    BudgetInfo,
    AuthorizationCreateResponse,
    ConfirmationApproveResponse,
    EscalationResolveResponse,
    ReceiptEnvelope,
    VerificationError,
)