Quickstart

Get from zero to your first signed receipt in under 10 minutes.

1. Get an API key

Create a workspace at allowly.ai and generate an API key from the API Keys page. It looks like:

allowly_l1_s001_3f9a...

2. Create an authorization

When your user authorizes your agent, call POST /v1/authorizations and store the returned authorization_id.

curl -X POST https://api.allowly.ai/v1/authorizations \
  -H "Authorization: Bearer allowly_l1_s001_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "usr_8821",
    "agent_id": "my_agent",
    "scopes": [
      { "name": "email.read" },
      { "name": "email.send", "constraints": { "max_per_day": 5 } }
    ],
    "requires_confirm_for": ["email.send"],
    "expires_at": "2026-12-31T00:00:00Z"
  }'
{
  "authorization_id": "auth_01HXZ2A...",
  "created_at": "2026-04-21T14:30:00.000Z",
  "expires_at": "2026-12-31T00:00:00.000Z",
  "receipt": {
    "status": "pending",
    "receipt_id": "rcp_01HXZAUTHORIZATION...",
    "ready_at_estimate": "2026-04-21T14:30:01.000Z",
    "url": "https://api.allowly.ai/v1/receipts/rcp_01HXZAUTHORIZATION..."
  }
}

Store authorization_id in your database, keyed to your user.

user_id is written into signed receipts. Prefer an opaque internal ID like usr_8821. If you need to derive it from email, use the SDK's local HMAC helper before calling Allowly; do not send raw email by default. See PII-safe identifiers.

Optional Pro+: add a spend cap

On Pro and Enterprise workspaces, if the agent action has an estimated cost, set budget_limit_micros when you create the authorization.

{
  "user_id": "usr_8821",
  "agent_id": "research_agent",
  "scopes": [{ "name": "llm.enrich" }],
  "expires_at": "2026-12-31T00:00:00Z",
  "budget_limit_micros": 50000000
}

50000000 is $50.00. Budgeted /check calls must include estimated_cost_micros.

3. Check before every action

Before your agent acts, call /v1/check with the stored authorization_id:

curl -X POST https://api.allowly.ai/v1/check \
  -H "Authorization: Bearer allowly_l1_s001_..." \
  -H "Content-Type: application/json" \
  -d '{
    "authorization_id": "auth_01HXZ2A...",
    "scopes": ["email.read"],
    "resource": "gmail:thread:abc123",
    "context": { "initiated_by": "user", "origin": "chat" }
  }'
{
  "authorization_id": "auth_01HXZ2A...",
  "user_id": "usr_8821",
  "agent_id": "my_agent",
  "authorization_expires_at": "2026-12-31T00:00:00Z",
  "policy_version": "2026-04-17.1",
  "results": {
    "email.read": {
      "decision": "allow",
      "reason": "authorization_granted_scope_active",
      "receipt": {
        "status": "pending",
        "receipt_id": "rcp_01HXY...",
        "ready_at_estimate": "2026-04-21T14:32:18.482Z",
        "url": "https://api.allowly.ai/v1/receipts/rcp_01HXY..."
      }
    }
  }
}

For budgeted authorizations, request one scope and include the estimated cost:

{
  "authorization_id": "auth_01HXZ2A...",
  "scopes": ["llm.enrich"],
  "estimated_cost_micros": 24000
}

Act on decision immediately. The signed receipt is delivered asynchronously — you don't need to wait for it.

4. Handle all four decisions

result = await client.check(
    authorization_id=authorization_id,
    scopes=["email.send"],
    resource="gmail:thread:abc",
)
scope_result = result.results["email.send"]

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

elif scope_result.decision == "confirm":
    # Prompt your user in-app, then:
    await client.confirmations.approve(
        scope_result.confirm_nonce,
        approved=True,
        ttl_seconds=60,
    )
    # Re-call /check — now returns allow

elif scope_result.decision == "escalate":
    # Route to the configured third-party approver, then:
    await client.escalations.approve(
        scope_result.escalation_id,
        resolved_by="manager:8821",
    )
    # Re-call /check — now returns allow once

elif scope_result.decision == "deny":
    raise PermissionError(scope_result.reason)

5. Fetch and verify the signed receipt

The receipt is signed asynchronously shortly after the decision. Fetch it when you need audit evidence:

curl https://api.allowly.ai/v1/receipts/rcp_01HXY... \
  -H "Authorization: Bearer allowly_l1_s001_..."
{
  "status": "signed",
  "receipt": {
    "version": "1.0",
    "receipt_id": "rcp_01HXY...",
    "workspace_id": "ws_01HXA1...",
    "issued_at": "2026-04-21T14:32:17.482Z",
    "decision": "allow",
    "reason": "authorization_granted_scope_active",
    "user_id": "usr_8821",
    "agent_id": "my_agent",
    "scope": "email.read",
    "resource": "gmail:thread:abc123",
    "context": { "initiated_by": "user", "origin": "chat" },
    "authorization_id": "auth_01HXZ2A...",
    "policy_version": "2026-04-17.1",
    "signature": {
      "alg": "Ed25519",
      "key_id": "workspace-signing-key-version",
      "value": "base64url..."
    }
  }
}

Verify it offline — no API call needed:

from allowly.verify import verify_receipt, load_keys_from_json
import httpx

keys_doc = httpx.get(
    f"https://api.allowly.ai/v1/workspaces/{workspace_id}/keys"
).json()

keys = load_keys_from_json(keys_doc)
verify_receipt(signed_receipt, keys)  # raises VerificationError if invalid

6. Revoke when the user opts out

curl -X DELETE https://api.allowly.ai/v1/authorizations/auth_01HXZ2A... \
  -H "Authorization: Bearer allowly_l1_s001_..." \
  -H "Content-Type: application/json" \
  -d '{ "revoked_by": "user" }'

Future /check calls against this authorization_id return deny with reason: "authorization_revoked", each producing its own signed scope receipt.