Python SDK
bloonio_auth_relay_client is the Auth Relay’s backend SDK. It wraps HMAC #1
signing, the two-call sudo protocol, the pairing handshake, and callbacks — a
framework-agnostic core, plus thin FastAPI and Django adapters.
Installation
Section titled “Installation”pip install "bloonio-auth-relay-client[fastapi,redis]"# orpip install "bloonio-auth-relay-client[django,redis]"Available extras: fastapi, django, redis, mongo, all.
Concepts
Section titled “Concepts”RelayClient/AsyncRelayClient— an HMAC-signed HTTP wrapper around the relay. Synchronous version for Django, asynchronous for FastAPI; same surface.SudoInstructionStore— a short-lived store (~180 s) that tracks “this instruction passed sudo, here’s the re-call window withX-Sudo-Instruction-Key”. Default backend: Redis.PendingOpStore— a longer store (10–30 min) that keeps the original mutation for server-side replay (required for the group quorum v2 / cross-org v3).@sudo_required— the decorator; same arguments in FastAPI and Django.DataType+ValidationField— structured display blocks the authenticator app renders (currency, IBAN, date, entity reference, etc.).
Configuration via the environment
Section titled “Configuration via the environment”RelaySettings reads the BLOONIO_RELAY_ prefix. The essential keys:
| Variable | Role |
|---|---|
BLOONIO_RELAY_BASE_URL | The relay’s base URL, e.g. https://auth-relay.bloonio.com. |
BLOONIO_RELAY_TENANT_ID | The calling tenant’s tenant_id. |
BLOONIO_RELAY_TENANT_SECRET | The tenant_secret (HMAC #1 signing). |
BLOONIO_RELAY_STATE_BACKEND | redis (default) or memory. |
BLOONIO_RELAY_STATE_BACKEND_URL | Redis URL when state_backend=redis. |
BLOONIO_RELAY_CALLBACK_PATH | Path where the relay POSTs the ApprovalEvent (default /api/sudo-callback). |
BLOONIO_RELAY_BASE_URL=https://auth-relay.bloonio.comBLOONIO_RELAY_TENANT_ID=tnt_...BLOONIO_RELAY_TENANT_SECRET=sk_...BLOONIO_RELAY_STATE_BACKEND=redisBLOONIO_RELAY_STATE_BACKEND_URL=redis://localhost:6379/0FastAPI
Section titled “FastAPI”BloonioAuthAdapter.from_env(app) builds RelaySettings from the environment,
instantiates an AsyncRelayClient, and mounts the dispatch middleware, the
auto-response router (/_bloonio/submit-response), and the callback router.
For named routes where you know in advance which fields to display:
from fastapi import FastAPIfrom bloonio_auth_relay_client import DataType, SudoActionType, ValidationFieldfrom bloonio_auth_relay_client.adapters.fastapi import BloonioAuthAdapter
app = FastAPI()bloonio = BloonioAuthAdapter.from_env(app) # reads the BLOONIO_RELAY_*
@app.post( "/transfer/execute", dependencies=[bloonio.sudo_required( expected_action="transfer_funds", custom_type=SudoActionType.LOCAL_AUTH, user_socket_hash=lambda req: req.state.user.socket_hash, title="Confirm the transfer", fields=lambda req: [ ValidationField(key="amount", title="Amount", value=str(req.state.body["amount"]), data_type=DataType.CURRENCY_USD), ValidationField(key="to", title="Beneficiary", value=req.state.body["recipient"], data_type=DataType.PARTY_NAME), ], )],)async def transfer_execute(...): ...For generic routes where the need for sudo is determined at runtime, pass a
sudo_resolver that returns a SudoInfo (or None):
from bloonio_auth_relay_client import ( SudoInfo, ValidationMode, Validator,)from bloonio_auth_relay_client.adapters.fastapi import BloonioAuthAdapterfrom starlette.requests import Request
async def my_rbac_resolver(request: Request) -> SudoInfo | None: rbac = await fetch_rbac_for_path(request.url.path) if not rbac or not rbac.is_sudo_action: return None user = request.state.user return SudoInfo( required=True, mode=ValidationMode.SINGLE_ACTOR, custom_type=pick_random_confirmation_type(rbac), expected_action=rbac.expected_action, description=rbac.totp_app_description_str, actor=Validator( socket_hash=user.user_account_socket_hash, display_name=f"{user.first_name} {user.last_name}", ), display_title="Confirm the action", display_fields=build_fields_from_request(request), )
bloonio = BloonioAuthAdapter.from_env(app, sudo_resolver=my_rbac_resolver)When sudo is required, the SDK creates an instruction_id, writes the Redis
state, calls send_auth_challenge (FCM push), and returns 403 {error: "SUDO_INSTRUCTION_KEY_REQUIRED", instruction_id}. On approval, the state flips
to validated and the re-call with X-Sudo-Instruction-Key goes through. See
Sudo (step-up).
Django
Section titled “Django”INSTALLED_APPS = [..., "bloonio_auth_relay_client.adapters.django"]MIDDLEWARE = [..., "bloonio_auth_relay_client.adapters.django.middleware.SudoActionMiddleware"]
BLOONIO_AUTH_RELAY = { "BASE_URL": "https://auth-relay.bloonio.com", "TENANT_ID": os.environ["RELAY_TENANT_ID"], "TENANT_SECRET": os.environ["RELAY_TENANT_SECRET"], "STATE_BACKEND": "redis", "STATE_BACKEND_URL": os.environ["REDIS_URL"], "CALLBACK_PATH": "/api/sudo-callback/",}from bloonio_auth_relay_client.adapters.django import urls as relay_urlsurlpatterns = [..., path("", include(relay_urls))]from bloonio_auth_relay_client.adapters.django import sudo_requiredfrom bloonio_auth_relay_client import DataType, SudoActionType, ValidationField
@sudo_required( expected_action="transfer_funds", custom_type=SudoActionType.LOCAL_AUTH, user_socket_hash=lambda req: req.user.socket_hash, title="Confirm the transfer", fields=lambda req: [ ValidationField(key="amount", title="Amount", value=str(req.POST["amount"]), data_type=DataType.CURRENCY_USD), ],)def transfer_execute(request): ...Works on plain views, DRF’s @api_view and APIView/ViewSet, and async
views (Django 4.1+). Same decorator, same arguments — just req.user instead
of req.state.user and req.POST instead of req.state.body.
Pairing handshake
Section titled “Pairing handshake”Called once per device pairing, from your QR handler after the user is authenticated:
from bloonio_auth_relay_client import AsyncRelayClient, RelaySettings
relay = AsyncRelayClient(RelaySettings())
result = await relay.prepare_pairing( user_socket_hash=user.socket_hash, backend_user_id=str(user.id), user_email=user.email, user_phone=user.phone, first_name=user.first_name, last_name=user.last_name, display_name="Example", display_logo_url="https://example.com/logo.png",)pairing_proof = result["pairing_proof"] # to forward to the deviceSee Pairing & devices for the rest of the device-side flow.