Skip to main content
[AVAILABLE], published, parmana on PyPI. Real, public, installable with a plain pip install parmana. This page said v1.0.5 and described only an editable local install until 2026-09-14; both were stale, confirmed against the live PyPI registry while fixing this page. 79 passing tests, 0 placeholders, black/ruff/mypy --strict all clean across the whole package. Bearer-key authentication, the error taxonomy below, and PolicyApi.validate() are all closed and verified against a real running local server. test_quickstart_example.py proves the documented quickstart script itself runs. v1.1.2 added create_business_transaction() (see below) and the offline_verifier.py fix. Two quick follow-up releases, v1.1.3 and v1.1.4, corrected the published package metadata (repo name, docs domain, added a Website URL); v1.1.4, published 2026-09-14, is the current version, confirmed against the live PyPI registry. See Changelog and the fix history at the bottom of this page.

Install

Real registry install, confirmed live against PyPI while fixing this page’s own stale claim otherwise. To build from source inside this monorepo instead:
py.typed is shipped, this is a PEP 561 typed distribution, confirmed by installing into a clean venv and checking parmana.__file__’s directory for the marker.

Bearer-key authentication

ParmanaClient(api_key=...) is sent as Authorization: Bearer <api_key> on every request, set once on the underlying requests.Session():
api_key is optional, for the same reason it’s optional server-side: local development against a server started with PARMANA_AUTH_DISABLED=true needs no key. Every real deployment requires one — an omitted or wrong key gets a real 401, raised as AuthenticationError (see below), not a silent failure. See Authentication for how keys are minted.

Models are generated, not hand-maintained

Every model in python/parmana/models/*.py is generated directly from the TypeScript AST of packages/shared/src/domain/*.ts (and CryptoAlgorithms.ts) by python/scripts/generate_models.ts, not hand-aligned copies. A drift guard (npm run check:python-models, wired into CI) regenerates into memory and fails the build if the committed output would change. Spot-checked field-for-field against the real JSON schemas (Authority.display_name optional, Authorization.expires_at optional, BusinessTransactionMetadata correctly optional-except-business_transaction_id, ExecutionTrustRecord.settlement_confirmations optional): accurate, no drift found. Enums are real Python str, Enum classes (e.g. SignatureAlgorithm, VerificationStatus), not bare strings.

Errors, correctly mapped to real conditions

All inherit ParmanaHttpErrorApiError. Proven against the real HttpTransport (not a test double) using responses-mocked HTTP built from real, verified response shapes (python/tests/test_http_transport.py), and against an actual running server in python/tests/test_live_server_integration.py.
Two error-classification edge cases worth knowing about, both closed:
  1. 403 from the caller-principal-scoping check (isPrincipalAllowed, “Caller is not permitted to assert this authority.principalId.”) is unrelated to policy rejection and maps to a dedicated AuthorizationError, not ExecutionRejectedError.
  2. Policy rejection has its own dedicated 403 with code: "POLICY_DENIED" (see Error catalog), distinct from the caller-identity 403 above (no code). build_http_error checks for code == "POLICY_DENIED" ahead of the generic 403 branch, so the two never collide. A risk-rejected test:fixture-execute execution raises ExecutionRejectedError with status_code == 403.
The client also reuses a requests.Session() (connection pooling) and retries idempotent GETs with backoff on 502/503/504, POSTs are never retried.

Every endpoint the API exposes has a method

client.transactions.create() covers POST /transactions, a second independent entry point into the identical execution pipeline as execute(), differing only in its 201 status code. policy.validate takes (policy_id, policy_version), not a policy document, matching what the route actually reads.
POST /policies/validate never uses the shared {error, code?} envelope at 400/404. Every status it returns (200, 400, 404) is {valid, errors}, the caller’s answer, not an SDK-level failure, so policy.validate() returns {"valid": False, "errors": [...]} for an unknown policy rather than raising NotFoundError. 401 is not exempted: it’s generated by caller-auth middleware before this route’s own handler runs, using the shared envelope like every other route. See Error handling.

create_business_transaction(), no more hand-synced ids

Added 2026-09-14, ships in v1.1.2, published, available via a plain pip install parmana. A BusinessTransaction has three id pairs the server’s own BusinessTransactionValidator cross-checks before policy ever runs: metadata.business_transaction_id must equal business_transaction_id, authorization.authority_id must equal authority.authority_id, intent.authorization_id must equal authorization.authorization_id. Every “X must match Y” 400 documented in the end-to-end Paytm guide came from hand-building a request and getting one of those wrong. create_business_transaction() derives all three automatically:
business_transaction_id defaults to a fresh uuid4() if you don’t supply one. A value you do supply is your own responsibility to keep unique per attempt, since it also doubles as the server’s idempotency key. authority_type defaults to AuthorityType.SERVICE (never "AGENT", which doesn’t exist server-side). See examples/builder/run.py (the same flow as examples/quickstart/run.py, side by side, for direct comparison) and tests/test_builders.py (9 unit tests) / tests/test_builder_example.py (proves the generated ids round-trip correctly through a real running server, not just in isolation).

Test suite

79 tests, pytest, confirmed against a real run: unit tests for every error-mapping case including the two classification cases above, bearer-key header attachment tests, create_business_transaction() (9 cases, tests/test_builders.py), and three real-server integration suites. test_live_server_integration.py spawns the actual @parmana/api process (npx tsx packages/api/src/server.ts, the same entry point npm run dev runs) on a real OS-assigned TCP port and drives it with the real ParmanaClient over real HTTP: a real 401, a real 403, a real 400, a real policy rejection, a real 404, a real 409 duplicate, and the policy.validate() 404 case, each asserted against the exact typed result and exact message the server returned. test_quickstart_example.py does the same for run_quickstart(), the quickstart example script itself, additionally asserting its documented printed output still matches what it actually prints. test_builder_example.py does the same for run_builder_example() (see above), additionally asserting every builder-derived id pair round-trips correctly.
ruff/black/mypy --strict are clean across the whole package: python -m ruff check ., python -m black --check ., and python -m mypy all pass with zero findings.

Next

End-to-end: agent → Parmana → Paytm

The full, no-ambiguity runbook this SDK builds toward: real infrastructure, every error message, every fix.

TypeScript SDK

The other maintained SDK: same bearer-key model, same error taxonomy shape.

Error catalog

Every error the real API returns, independent of any SDK.