Allowly Python SDK
Async Python client for the Allowly runtime API.
MCP middleware ships inside this SDK: pip install 'allowly[fastmcp]', then from allowly.mcp import AllowlyMCPMiddleware. In TypeScript, it lives in the separate @allowly/mcp package.
Subject authorization pattern
Do not send raw user/customer PII to Allowly receipts unless you intentionally want it in your audit trail. Create one authorization per subject, store the returned authorization ID in your own app database, and use that ID for later checks.
import asyncio
import os
from allowly import Allowly
async def main() -> None:
async with Allowly(
api_key=os.environ["ALLOWLY_API_KEY"],
base_url=os.getenv("ALLOWLY_API_URL", "https://api.allowly.ai"),
) as allowly:
# Your app creates a stable internal subject ID.
subject_id = "subject_abc123"
# Store this in your app table, for example:
# allowly_authorizations(subject_id, policy_id, allowly_authorization_id, status)
authorization = await allowly.authorizations.create(
user_id=f"subject:{subject_id}",
policy_id="research_agent",
metadata={"source": "import"},
)
# Before the agent acts, check whether this action is allowed.
decision = await allowly.check(
authorization_id=authorization.authorization_id,
actions=["web.search"],
resource=f"subject:{subject_id}",
context={"stage": "research"},
)
if decision.results["web.search"].decision != "allow":
raise RuntimeError("Action is not authorized")
asyncio.run(main())
Local development against the documented Caddy endpoint requires the edge token that Cloudflare injects for public traffic. Pass it explicitly:
Allowly(
api_key=os.environ["ALLOWLY_API_KEY"],
base_url="http://localhost:8443",
dangerously_allow_insecure_base_url=True,
edge_token=os.environ["ALLOWLY_EDGE_TOKEN"],
)
The token is only sent when provided; never set it for the public API.
Send JSON through a private SEAL webhook
Copy the private URL from the dashboard's SEAL page. The URL is the only credential this client sends; it does not use an ordinary API key.
import asyncio
import os
from allowly import SealWebhookClient
async def seal_event(raw_json: str, event_id: str):
async with SealWebhookClient(os.environ["ALLOWLY_SEAL_WEBHOOK_URL"]) as webhook:
delivery = await webhook.send(
raw_json,
idempotency_key=event_id,
type="invoice",
reference="INV-1042",
statement="Approved for payment",
)
while delivery.status in {"received", "signing"}:
await asyncio.sleep(1)
delivery = await webhook.get_delivery(delivery.attempt_id)
if delivery.status != "sealed":
raise RuntimeError(delivery.error_code or "SEAL delivery failed")
return delivery.receipt, await webhook.get_keys()
The webhook processes your JSON to create a fingerprint; Allowly stores the
fingerprint and signed receipt. Keep the original record in your workflow.
Receipt details are sent in the three explicit Allowly-Seal-* headers. Their
values must use printable ASCII, may contain interior spaces, and must not have
leading or trailing whitespace. The client rejects invalid values instead of
changing them. Direct API metadata still supports its existing Unicode values.
When a signed receipt is present, the client returns its signed metadata and
rejects a conflicting top-level delivery projection.
Treat the full URL like a password and keep it out of logs, tickets, and source
control. Regenerating or disabling it stops the old URL. Delivery associations
and status remain available for 7 days; preserve signed receipts and keys under
your own retention policy. With no idempotency_key, retrying after a lost
response can create another seal.
Seal a JSON record with local hashing
seal hashes strict raw JSON in your process, sends only its digest to Allowly,
and waits for the full signed receipt. Generate and persist request_id in
your workflow so a retry recovers the same seal:
import uuid
request_id = str(uuid.uuid4())
sealed = await allowly.seal(
raw_json,
request_id=request_id,
metadata={"source": "invoice-workflow"},
)
save_beside_record(sealed.receipt)
Use seal_value(parsed_json, ...) only when the original JSON text is no
longer available. A parsed value cannot reveal duplicate object names or the
original number spelling, so seal is the safer input boundary.
To verify later, preserve the authenticated workspace_id response and a key
document fetched from Allowly through an authenticated or previously trusted
source. Keep the signature and record checks separate:
from allowly.verify import load_keys_from_json, verify_seal_json
result = verify_seal_json(
raw_json,
sealed.receipt,
load_keys_from_json(keys_doc),
expected_workspace_id=sealed.workspace_id,
trusted_key_fingerprints=configured_key_fingerprints,
)
assert result.signature_verified
assert result.record_matches
Inline authorization creation requires agent_id, actions, and expires_at.
Policy-based creation uses policy_id instead and rejects inline action or
decision-override fields.
Unavailable checks fail closed unless an action is explicitly mapped to
"fail_open" with fallback_by_action. Unmapped actions always fail closed.
For actions that need third-party approval, define the escalation rule on the
agent policy, create the authorization from that policy_id, and then resolve
returned escalation results with
await allowly.escalations.approve(escalation_id, resolved_by="manager:123")
or reject(...), then re-check before running the action.
If you need lookup by email later, import from_email from
allowly.identifiers and store from_email(email, pepper=APP_PII_PEPPER).
The helper trims and lowercases only, prefixes the result with email_hmac:v1,
and never sends the raw email or pepper to Allowly. Keep the pepper stable and
backed up; changing it changes derived user IDs. Keep raw names, emails,
documents, and profile URLs out of Allowly receipts unless those fields are
intentionally part of your audit record.
Do not add raw HTTP fallbacks in application code for APIs the SDK is missing. Patch this SDK first, then use the typed client from the app. That keeps the integration examples honest and makes SDK gaps visible early.
Offline receipt verification
Install allowly[verifier] to hash SEAL records and verify signed receipts
locally. The extra uses allowly-receipt-format>=4.1.0,<5.0.0, which verifies
receipt wire format 4 (the package major equals the wire format). alg and
key_id are signed top-level fields, and signature is the base64url string.
Wire format 4 also supports daily receipt.checkpoint commitments. For an
untrusted receipt bundle, pass caller-trusted key fingerprints to
verify_receipt; a fingerprint copied from that same bundle is not a trust
anchor.
Key-document fetching requires HTTPS by default. For the documented local
Caddy endpoint only, pass dangerously_allow_insecure_base_url=True and its
edge_token to fetch_keys_doc, matching the client options above.
Metadata
Release files for allowly 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| allowly-0.5.0.tar.gz | 119.7 kB | With provenance | Details for allowly-0.5.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| allowly-0.5.0-py3-none-any.whl | Python 3 | none | any | Details for allowly-0.5.0-py3-none-any.whl |
Total release size: 142.6 kB
Release files / allowly-0.5.0.tar.gz
| Download URL | allowly-0.5.0.tar.gz |
|---|---|
| Size | 119.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8d9b7fb6231b4098019cc08ad37542d7ff5a793cb06c671043c3a445cfd37bf1
|
|
BLAKE2b-256 checksum How to use checksums |
795faa8a4c980f3ec0d7a76090985f05a3a143cdad6576d906f42d76826fbecd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency logRelease files / allowly-0.5.0-py3-none-any.whl
| Download URL | allowly-0.5.0-py3-none-any.whl |
|---|---|
| Size | 22.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1e6ea2a26389e9b2a06e84a07b0df931b71d2a0829a74cf023c42e879be8b998
|
|
BLAKE2b-256 checksum How to use checksums |
c07540625df9e397880c0e15a39b7835f846fce40a964625eefaed3ad6b00408
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency logSecurity for allowly 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for allowly-0.5.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.