Aira

Actions

Authorize agent actions before they run, then notarize the outcome after execution. The two-step flow is the core of Aira.

All endpoints require a Bearer token (Authorization: Bearer aira_live_xxxxx) unless marked as public. Base URL: https://api.airaproof.com/api/v1

The flow

  1. Agent calls POST /actions with the intent. Aira evaluates every active policy.
  2. Aira returns an Authorization with status of authorized or pending_approval. If a policy denies, the response is 403 POLICY_DENIED.
  3. If authorized, the agent executes the real-world action.
  4. If pending_approval, the action is held until a human approves (or denies) via /approve and /deny.
  5. Once the action has run, the agent calls POST /actions/{id}/notarize with the outcome. The receipt is minted and returned.

Status transitions:

                     +---- authorized -----+
                     |                     |
POST /actions -------+---- pending_approval ---> /approve ---> approved --+
                     |                                                    |
                     +---- denied (403 POLICY_DENIED)                     |
                                                                          v
                                                              POST /actions/{id}/notarize
                                                                          |
                                                                          v
                                                                      notarized

Authorize Action

POST /api/v1/actions
Authorization: Bearer aira_live_xxxxx

Sends the agent's intent to Aira for policy evaluation. Returns an Authorization object. No receipt is minted by this call. The receipt is minted later by POST /actions/{id}/notarize once the agent has actually executed the action.

Request Body

FieldTypeRequiredDescription
action_typestringYesType of action (e.g. wire_transfer, tool_call, email_sent).
detailsstringYesHuman-readable description of what the agent intends to do.
agent_idstringNoRegistered agent slug. Optional but recommended.
agent_versionstringNoAgent version.
model_idstringNoModel that produced the decision (e.g. claude-sonnet-4-6).
model_versionstringNoModel version.
instruction_hashstringNoSHA-256 hash of the system prompt. Omitting triggers a warning.
parent_action_uuidstringNoID of the parent action (for chaining).
endpoint_urlstringNoIf the action calls an external endpoint, Aira checks it against your whitelist.
store_detailsbooleanNoGenerate a storage key for off-DB details (default: false).
idempotency_keystringNoPrevent duplicate authorization on retry.
require_approvalbooleanNoForce the action through human approval. Even when false, policies can still require approval.
approversstring[]NoOverride the default approver list for this action only.

Example Request

curl -X POST https://api.airaproof.com/api/v1/actions \
  -H "Authorization: Bearer aira_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "action_type": "wire_transfer",
    "details": "Send 75,000 EUR to vendor X",
    "agent_id": "payments-agent",
    "model_id": "claude-sonnet-4-6",
    "instruction_hash": "sha256:e3b0c44298fc1c149afbf4c8...",
    "idempotency_key": "wire-vendor-2026-04-07-001"
  }'

Response (201 Created) — authorized

{
  "action_uuid": "9c6b...",
  "status": "authorized",
  "created_at": "2026-04-07T14:30:00.000Z",
  "request_id": "req_01J9B...",
  "warnings": null
}

The agent may now execute the action. Once done, call POST /actions/{action_uuid}/notarize with the outcome.

Response (201 Created) — pending_approval

{
  "action_uuid": "9c6b...",
  "status": "pending_approval",
  "created_at": "2026-04-07T14:30:00.000Z",
  "request_id": "req_01J9B...",
  "warnings": ["Policy 'High-value wire gate': Amount exceeds 50,000 EUR threshold."]
}

The action is held server-side. Approval emails have been sent. The agent must wait. Once a human clicks Approve via the email link or the dashboard, the action moves to approved. The agent can then call POST /actions/{id}/notarize.

Response (403 Forbidden) — POLICY_DENIED

{
  "code": "POLICY_DENIED",
  "message": "Action denied by policy 'Wire transfer hard cap': Amount exceeds 100,000 EUR absolute limit.",
  "details": {
    "action_uuid": "9c6b...",
    "policy_uuid": "7a3f...",
    "receipt_uuid": "rcpt_01JAC..."
  },
  "request_id": "req_01J9B..."
}

The action row IS persisted with status="denied_by_policy" and a signed receipt is minted for the denial, so the denial is cryptographically verifiable. The response includes a receipt_uuid. The agent must not execute the action.

Error Codes

StatusCodeDescription
403POLICY_DENIEDAction blocked by a policy. See Policies.
403ENDPOINT_TLS_MISMATCHThe TLS fingerprint of the endpoint has changed. Hard block.
403ENDPOINT_NOT_WHITELISTEDThe endpoint_url is not on your whitelist (strict mode).
404NOT_FOUNDThe parent_action_uuid does not exist in this org.
409DUPLICATE_REQUESTThis idempotency_key was already used.
422(validation)Missing or invalid fields.

Notarize Outcome

POST /api/v1/actions/{action_uuid}/notarize
Authorization: Bearer aira_live_xxxxx

Records the real-world outcome of an action and mints the cryptographic receipt. Called after the agent has executed the action. The action must be in status authorized or approved.

Request Body

FieldTypeRequiredDescription
outcomestringNocompleted (mints the receipt) or failed (audit-only, no receipt). Default: completed.
outcome_detailsstringNoHuman-readable description of what actually happened. Hashed and committed to the receipt.

Example Request

curl -X POST https://api.airaproof.com/api/v1/actions/9c6b.../notarize \
  -H "Authorization: Bearer aira_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "outcome": "completed",
    "outcome_details": "Wire sent to vendor X. Bank confirmation TXN-8821."
  }'

Response (200 OK) — completed

{
  "action_uuid": "9c6b...",
  "status": "notarized",
  "receipt_uuid": "f4a3...",
  "payload_hash": "sha256:f4a3b2c1d0...",
  "signature": "ed25519:base64url...",
  "timestamp_token": "MIIF...",
  "created_at": "2026-04-07T14:30:00.000Z",
  "request_id": "req_01J9C...",
  "warnings": null
}

The receipt commits to both the original intent (captured at authorize() time) and the reported outcome. Both are part of the signed payload, whose SHA-256 digest is exposed in the payload_hash field.

Receipt Statuses

Every terminal action state produces a signed Ed25519 receipt. The possible receipt statuses are:

StatusDescription
notarizedAction completed successfully. The receipt seals the outcome.
deniedA policy blocked the action at authorize() time. The receipt proves the system caught it.
failedThe agent executed the action but it failed. The receipt records the failure.
denied_by_humanA human reviewer rejected a held action. The receipt records who denied it and when.

Response (200 OK) — failed

{
  "action_uuid": "9c6b...",
  "status": "failed",
  "receipt_uuid": "f4a4...",
  "payload_hash": "sha256:a1b2c3d4...",
  "signature": "ed25519:base64url...",
  "timestamp_token": "MIIF...",
  "created_at": "2026-04-07T14:30:00.000Z",
  "request_id": "req_01J9C...",
  "warnings": null
}

A receipt is minted for the failure, so the failure is cryptographically verifiable.

Error Codes

StatusCodeDescription
400INVALID_OUTCOMEoutcome must be completed or failed.
404NOT_FOUNDAction does not exist.
409INVALID_ACTION_STATEAction is not in authorized or approved state. Cannot notarize a denied or already-notarized action.

List Actions

GET /api/v1/actions?page=1&per_page=20
Authorization: Bearer aira_live_xxxxx

Returns a paginated list of actions.

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1).
per_pageintegerItems per page (default: 20, max: 100).
action_typestringFilter by action type.
agent_idstringFilter by agent.
statusstringFilter by status (authorized, pending_approval, denied_by_policy, approved, denied_by_human, notarized, failed).

Response (200 OK)

{
  "data": [
    {
      "action_uuid": "9c6b...",
      "action_type": "wire_transfer",
      "agent_id": "payments-agent",
      "status": "notarized",
      "legal_hold": false,
      "created_at": "2026-04-07T14:30:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 142,
    "has_more": true
  },
  "request_id": "req_01J9D..."
}

Get Action

GET /api/v1/actions/{action_uuid}
Authorization: Bearer aira_live_xxxxx

Returns full action detail including the cryptographic receipt and human cosignatures (if any).

Response (200 OK)

{
  "action_uuid": "9c6b...",
  "org_uuid": "org_01J9A...",
  "agent_id": "payments-agent",
  "agent_version": "1.0",
  "action_type": "wire_transfer",
  "instruction_hash": "sha256:e3b0c442...",
  "action_details_hash": "sha256:f4a3b2c1...",
  "details_storage_key": null,
  "model_id": "claude-sonnet-4-6",
  "model_version": "20250514",
  "parent_action_uuid": null,
  "status": "notarized",
  "legal_hold": false,
  "created_at": "2026-04-07T14:30:00.000Z",
  "receipt": {
    "receipt_uuid": "f4a3...",
    "payload_hash": "sha256:f4a3b2c1...",
    "signature": "ed25519:base64url...",
    "public_key_id": "aira-signing-key-v1",
    "timestamp_token": "MIIF...",
    "receipt_version": "1.1",
    "verify_url": "https://api.airaproof.com/api/v1/verify/action/9c6b...",
    "created_at": "2026-04-07T14:30:01.000Z"
  },
  "authorizations": [],
  "request_id": "req_01J9E..."
}

The authorizations array contains any human cosignatures added via POST /actions/{id}/cosign.


Approve Held Action

Recommended: Use the short-code flow (approval links contain a code like APR-7Km9X2pL4nRw). The legacy token-based endpoints remain available for backward compatibility.

GET /api/v1/actions/approval/{code}

Resolves the approval code and returns the action details for review. No auth required.

POST /api/v1/actions/approval/{code}/confirm
Content-Type: application/json

{ "decision": "approve" }

Executes the approval. The decision field must be "approve". The code is single-use and expires after use.

Legacy token flow

POST /api/v1/actions/{action_uuid}/approve?token=<TOKEN>

Approves via the legacy HMAC token in the URL. No auth required.

Dashboard flow

POST /api/v1/actions/{action_uuid}/dashboard-approve
Authorization: Bearer <JWT>

JWT auth required. The user must be admin/owner or in the org's default_approvers list.

The status moves to approved. The agent (or a webhook handler) must then call POST /actions/{id}/notarize with the actual outcome to mint the receipt.

Approval by itself does not mint a receipt. The receipt is minted when the agent calls notarize() with the actual outcome.

Response (200 OK)

{
  "status": "approved",
  "action_uuid": "9c6b...",
  "approver_email": "compliance@acme.com",
  "request_id": "req_01J9F..."
}

Error Codes

StatusCodeDescription
403(forbidden)Code/token does not match this action OR (dashboard) caller is not an authorized approver.
404NOT_FOUNDAction or approval code does not exist.
409ALREADY_RESOLVEDAction is no longer in pending_approval state.
410CODE_EXPIREDApproval code has already been used or expired.

Deny Held Action

Recommended: Use the short-code flow. The legacy token-based endpoint remains available for backward compatibility.

POST /api/v1/actions/approval/{code}/confirm
Content-Type: application/json

{ "decision": "deny", "reason": "Not authorized for this amount" }

Executes the denial via the approval code. The decision field must be "deny". An optional reason field records why the action was denied.

Legacy token flow

POST /api/v1/actions/{action_uuid}/deny?token=<TOKEN>

Denies via the legacy HMAC token in the URL. No auth required.

Dashboard flow

POST /api/v1/actions/{action_uuid}/dashboard-deny
Authorization: Bearer <JWT>

Denies an action held in pending_approval status. Status moves to denied_by_human. No receipt will be minted. The agent must not execute the action.

Response (200 OK)

{
  "status": "denied_by_human",
  "action_uuid": "9c6b...",
  "approver_email": "compliance@acme.com",
  "request_id": "req_01J9G..."
}

Cosign Action

POST /api/v1/actions/{action_uuid}/cosign
Authorization: Bearer <JWT>

Adds a human co-signature to an action that already exists. JWT authentication only (API keys are not accepted; this ensures a real human identity is attached). This endpoint was previously named /authorize, but that name now refers to the initial authorization request.

Response (200 OK)

{
  "cosignature_uuid": "cs_01J9H...",
  "action_uuid": "9c6b...",
  "cosigner_email": "compliance@acme.com",
  "cosigned_at": "2026-04-07T14:35:00.000Z",
  "request_id": "req_01J9H..."
}

Error Codes

StatusCodeDescription
401UNAUTHORIZEDJWT auth required (API keys are not accepted for cosign).
404NOT_FOUNDAction does not exist.
409ALREADY_AUTHORIZEDThis user has already cosigned this action.

POST /api/v1/actions/{action_uuid}/hold
Authorization: Bearer aira_live_xxxxx

Places a legal hold on an action, preventing any retention-based deletion.

Response (200 OK)

{
  "action_uuid": "9c6b...",
  "legal_hold": true,
  "request_id": "req_01J9I..."
}

DELETE /api/v1/actions/{action_uuid}/hold
Authorization: Bearer aira_live_xxxxx

Releases a legal hold. The release itself is recorded in the audit trail.

Response (200 OK)

{
  "action_uuid": "9c6b...",
  "legal_hold": false,
  "request_id": "req_01J9J..."
}

Chain of Custody

GET /api/v1/actions/{action_uuid}/chain
Authorization: Bearer aira_live_xxxxx

Returns the parent chain of an action by walking parent_action_uuid links. Each child receipt commits to its parent's payload_hash, so the chain is cryptographically verifiable, not just a foreign-key chain.

Response (200 OK)

{
  "chain": [
    {
      "action_uuid": "9c6b...",
      "action_type": "wire_transfer",
      "agent_id": "payments-agent",
      "action_details_hash": "sha256:f4a3...",
      "status": "notarized",
      "created_at": "2026-04-07T14:30:00.000Z"
    },
    {
      "action_uuid": "8b5a...",
      "action_type": "loan_decision",
      "agent_id": "lending-agent",
      "action_details_hash": "sha256:e3b0...",
      "status": "notarized",
      "created_at": "2026-04-07T14:00:00.000Z"
    }
  ],
  "request_id": "req_01J9K..."
}

The first item is the requested action; subsequent items are its ancestors in order.

On this page