Aira

Human Approval

Hold high-stakes actions for human review at authorize() time, before the agent acts. The receipt is only minted after approval and execution.

Overview

When a policy decides an action needs human review, Aira holds the action server-side at authorize() time. The agent's authorize() call returns status=pending_approval. An email goes to your configured approvers. The agent must not execute until a human approves. After approval, the agent executes the real action and then calls notarize() to mint the receipt.

This is a hard gate. Held actions never run until a human clears them. That is exactly what regulators expect:

  • FINRA 2026 requires documented human sign-off on AI-generated trade recommendations above defined thresholds.
  • EU AI Act Art. 14 mandates human oversight capability for high-risk AI systems, including the ability to intervene before an action takes effect.

The Status Flow

authorize()  -->  pending_approval  -->  approved  -->  (agent executes)  -->  notarize()  -->  notarized
                         |
                         +--> denied_by_human  (agent must not execute, no receipt)

Approval alone does not mint a receipt. The action moves to status="approved", the agent executes the real work, and only then does notarize() mint the receipt (transitioning the action to notarized or failed).

Each transition is recorded in the audit trail. The final receipt commits to the original intent, the human approver, and the actual outcome.

How a policy triggers approval

Human approval is not a request-time flag. It is a policy decision. Configure a policy with decision=require_approval in the dashboard (any of the three modes: rules, AI, or consensus). When that policy matches an action, authorize() returns pending_approval automatically.

For example, a rule like "require approval for any wire_transfer above 50,000 EUR" will hold every matching action without any change to agent code.

authorize() accepts an optional require_approval=True hint, but it does not bypass policies. If any policy still denies the action, the request fails with POLICY_DENIED. Hard gates belong in policies so the decision lives on the server.

Agent-side code

The agent does not need to know whether a particular action will be held. It just handles all three branches of authorize().

Python

from aira import Aira, AiraError

aira = Aira(api_key="aira_live_...")

try:
    auth = aira.authorize(
        action_type="wire_transfer",
        details="Send 75,000 EUR to vendor X",
        agent_id="payments-agent",
    )
except AiraError as e:
    if e.code == "POLICY_DENIED":
        log.error(f"Blocked: {e.message}")
        return
    raise

if auth.status == "authorized":
    # Safe to execute right now
    result = send_wire(75000, to="vendor")
    aira.notarize(
        action_uuid=auth.action_uuid,
        outcome="completed",
        outcome_details=f"Sent. ref={result.id}",
    )

elif auth.status == "pending_approval":
    # Held for human review. Agent must wait.
    # Record the action_uuid somewhere and resume later.
    queue.enqueue(auth.action_uuid)

TypeScript

import { Aira, AiraError } from "aira-sdk";

const aira = new Aira({ apiKey: "aira_live_..." });

try {
  const auth = await aira.authorize({
    actionType: "wire_transfer",
    details: "Send 75,000 EUR to vendor X",
    agentId: "payments-agent",
  });

  if (auth.status === "authorized") {
    const result = await sendWire(75000, "vendor");
    await aira.notarize({
      actionId: auth.action_uuid,
      outcome: "completed",
      outcomeDetails: `Sent. ref=${result.id}`,
    });
  } else if (auth.status === "pending_approval") {
    await queue.enqueue(auth.action_uuid);
  }
} catch (e) {
  if (e instanceof AiraError && e.code === "POLICY_DENIED") {
    console.error(`Blocked: ${e.message}`);
    return;
  }
  throw e;
}

What the authorize response looks like

pending_approval response
{
  "action_uuid": "act_01J9B...",
  "status": "pending_approval",
  "created_at": "2026-04-07T14:30:00.000Z",
  "request_id": "req_01JAB...",
  "warnings": [
    "Action held for approval by policy 'High-value wire gate'."
  ]
}

That is the entire Authorization response. There is no receipt_uuid, no signature, no payload_hash. Nothing has been signed yet, because nothing has happened yet. The agent must wait for the human to approve (or deny) and then call notarize() before a receipt is minted.

Completing the flow after approval

Once a human clicks Approve (via email link or dashboard), the action moves to status=approved. The agent now has the green light to execute the real action and then notarize it.

The cleanest pattern is to drive this from a webhook. Subscribe to the action.approved event and have your handler resume execution.

Webhook-driven resume (Python)

from aira import Aira, AiraError

aira = Aira(api_key="aira_live_...")

def on_action_approved(event):
    action_uuid = event.data["action_uuid"]
    intent = aira.get_action(action_uuid)  # details from authorize() time

    # Execute the real action
    try:
        result = send_wire(intent.amount, to=intent.recipient)
    except Exception as e:
        aira.notarize(
            action_uuid=action_uuid,
            outcome="failed",
            outcome_details=f"Bank rejected: {e}",
        )
        return

    # Mint the receipt
    aira.notarize(
        action_uuid=action_uuid,
        outcome="completed",
        outcome_details=f"Sent. ref={result.id}",
    )

Denial

If the approver clicks Deny, the action moves to status=denied_by_human. No receipt is minted. The agent must not execute the action.

You will receive an action.denied webhook with the reason and the approver identity. The denial itself is recorded in the audit trail.

Default Approvers

You can configure default approvers for your organization in Settings > Approvers in the Aira dashboard.

The resolution order is:

  1. Explicit approvers on the policy. If the policy declares an approvers list, those addresses are used.
  2. Org default approvers. Otherwise, Aira emails the approvers configured in Settings.
  3. Org admins. If no default approvers are configured, all org admins receive the approval email.

Approval links are designed for security and simplicity:

  • HMAC-signed. Each link contains an HMAC signature (SHA-256) that validates the action UUID, approver email, and expiry. Tampered links are rejected.
  • 24-hour expiry. Links expire 24 hours after the approval request. After expiry, the action must be approved via the API or a new approval email must be requested.
  • No login required. Approvers click the link directly from their email. No Aira account is needed.
  • Single-use. Once an action is approved, the link cannot be used again.

If an approval link expires without action, the status remains pending_approval. You can re-request approval via the API:

POST /api/v1/actions/{action_uuid}/request-approval
Authorization: Bearer aira_live_xxxxx

Webhook Events

Subscribe to these events when creating a webhook:

EventTriggered When
action.approval_requestedauthorize() returns pending_approval.
action.approvedA human approves a held action. The agent should now execute and call notarize().
action.deniedA human denies a held action. The agent must not execute.
action.notarizedThe agent called notarize() with outcome completed. Receipt minted.
action.failedThe agent called notarize() with outcome failed. No receipt.

Cases

Cases are a separate API for multi-model decision evaluation. They have their own human-review path, triggered automatically when models disagree beyond the configured threshold. That flow is documented in Consensus Scoring and Cases. Do not confuse the case human-review flow with the held-action flow above, even though both end in a human decision.

On this page