Sudo (step-up)
Sudo (step-up / elevation) asks one or more human validators to approve a
sensitive action from their Bloonio Authenticator app before the backend
executes it. The relay exposes a dispatch primitive (HMAC #1 signed); the
two-call protocol on the tenant side — 403 then re-call — is provided by
the Python SDK.
The two-call protocol
Section titled “The two-call protocol”- The client calls your protected route (e.g.
POST /transfer/execute) without theX-Sudo-Instruction-Keyheader. → the SDK creates aninstruction_id, triggers the dispatch + push, and returns403 { "error": "SUDO_INSTRUCTION_KEY_REQUIRED", "instruction_id": "abc..." }. - The device receives the push → the user approves → the relay POSTs the
ApprovalEventto your callback (HMAC-verified) → the store marks theinstruction_idasvalidated. - The client re-issues the same request with the
X-Sudo-Instruction-Key: abc...header. → the request goes through.
Dispatch an event (relay primitive)
Section titled “Dispatch an event (relay primitive)”POST /relay/sudo/dispatch creates a validation event and broadcasts the
challenge pushes to the participants. The relay forwards it to auth_api and
returns event_id, status, and expires_at.
Body validation rules:
event_type∈{ sudo_action, sudo_group_action, sudo_delegated_action }.action_type∈{ creation, deletion, update, upsert }.- At least one of
relay_user_linked_id_list(individual targets) orrelay_group_linked_id_list(group targets) must be non-empty. data_itemsis required whendata_access_type=static;data_fetch_urlwhendata_access_type=dynamic.tenant_idis not in the body — the relay derives it from the authenticated context.
curl $BASE_URL/api/v1/relay/sudo/dispatch \ -X POST \ -H "X-Bloonio-Tenant-Id: tnt_..." \ -H "X-Bloonio-Timestamp: 1718966400000" \ -H "X-Bloonio-Signature: <hex_hmac_sha256>" \ -H "Content-Type: application/json" \ -d '{ "event_type": "sudo_action", "action_type": "update", "idempotency_key": "idem_abc123", "relay_user_linked_id_list": ["652f1f77bcf86cd799439011"], "title": "Confirm the transfer", "description": "Approve a transfer of 1,000 USD to ACME Corp.", "data_access_type": "static", "data_items": [ { "display_title": "Amount", "display_value": "1000 USD", "data_type": "CURRENCY_USD" }, { "display_title": "Beneficiary", "display_value": "ACME Corp", "data_type": "PARTY_NAME" } ], "on_validate_callback_url": "https://api.example.com/relay-callbacks/sudo-validated", "on_reject_callback_url": "https://api.example.com/relay-callbacks/sudo-rejected" }'For an M-of-N quorum, target a group and use data_access_type: "dynamic". The
requested_ttl_seconds can then exceed 24 h (up to 5 months).
curl $BASE_URL/api/v1/relay/sudo/dispatch \ -X POST \ -H "X-Bloonio-Tenant-Id: tnt_..." \ -H "X-Bloonio-Timestamp: 1718966400000" \ -H "X-Bloonio-Signature: <hex_hmac_sha256>" \ -H "Content-Type: application/json" \ -d '{ "event_type": "sudo_group_action", "action_type": "deletion", "idempotency_key": "idem_def456", "relay_group_linked_id_list": ["652f1f77bcf86cd799439099"], "title": "Approve the account closure", "description": "M of N validators must approve the closure.", "data_access_type": "dynamic", "data_fetch_url": "https://api.example.com/sudo/data/idem_def456", "on_validate_callback_url": "https://api.example.com/relay-callbacks/sudo-validated", "on_reject_callback_url": "https://api.example.com/relay-callbacks/sudo-rejected", "requested_ttl_seconds": 604800 }'{ "success": true, "status_code": 201, "message": "Dispatched.", "data": { "event_id": "...", "status": "pending", "expires_at": "2026-06-21T..." }}Individual targets vs groups
Section titled “Individual targets vs groups”relay_user_linked_id_list— opaque relay-side identifiers of individual validators (relay_user_id).relay_group_linked_id_list— group identifiers;auth_apiexpands the membership and broadcasts the pushes to the members. The relay cannot expand groups itself (membership lives onauth_api).
The relay_user_id pivot is the universal validator: see
Tenancy & relay_user_id.
Resolve identifiers (directory lookups)
Section titled “Resolve identifiers (directory lookups)”To pass relay_user_linked_id / relay_group_linked_id to dispatch, resolve
them first from the public identifiers. These GETs are pure pass-throughs (HMAC
#1 inbound); auth_api enforces the tenant scope.
| Method | Path | Role |
|---|---|---|
| GET | /relay/sudo/groups | List the validation groups visible to the tenant. |
| GET | /relay/sudo/groups/by-public-id | Resolve a bgr-… to a relay_group_linked_id (?public_id_str=). |
| GET | /relay/sudo/paired-users | List paired users (potential validators). |
| GET | /relay/sudo/paired-users/by-public-id | Resolve a bth-… to a relay_user_linked_id (?public_id_str=). |
| GET | /relay/sudo/pending-validations | Pending validations for a user (?relay_user_linked_id=). |
| GET | /relay/sudo/paired-users/signature | A user’s signature image (?relay_user_linked_id=). |
curl "$BASE_URL/api/v1/relay/sudo/groups/by-public-id?public_id_str=bgr-abc123" \ -H "X-Bloonio-Tenant-Id: tnt_..." \ -H "X-Bloonio-Timestamp: 1718966400000" \ -H "X-Bloonio-Signature: <hex_hmac_sha256>"Verify a TOTP
Section titled “Verify a TOTP”POST /relay/sudo/verify-totp validates the TOTP issued by Bloonio
Authenticator for the pairing identified by relay_user_linked_id, without the
tenant holding the per-user secret. Returns 200 on a match, 401 on a wrong
/ unknown code.
curl $BASE_URL/api/v1/relay/sudo/verify-totp \ -X POST \ -H "X-Bloonio-Tenant-Id: tnt_..." \ -H "X-Bloonio-Timestamp: 1718966400000" \ -H "X-Bloonio-Signature: <hex_hmac_sha256>" \ -H "Content-Type: application/json" \ -d '{ "relay_user_linked_id": "652f1f77bcf86cd799439011", "totp": "123456" }'With the SDK: @sudo_required
Section titled “With the SDK: @sudo_required”In FastAPI, the decorator mounts the entire two-call protocol on the route:
@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), ], )],)async def transfer_execute(...): ...See the Python SDK for installation, configuration, and the Django equivalent.