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,
)