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
- Agent calls
POST /actionswith the intent. Aira evaluates every active policy. - Aira returns an
Authorizationwithstatusofauthorizedorpending_approval. If a policy denies, the response is403 POLICY_DENIED. - If
authorized, the agent executes the real-world action. - If
pending_approval, the action is held until a human approves (or denies) via/approveand/deny. - Once the action has run, the agent calls
POST /actions/{id}/notarizewith 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
notarizedAuthorize Action
POST /api/v1/actions
Authorization: Bearer aira_live_xxxxxSends 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
| Field | Type | Required | Description |
|---|---|---|---|
action_type | string | Yes | Type of action (e.g. wire_transfer, tool_call, email_sent). |
details | string | Yes | Human-readable description of what the agent intends to do. |
agent_id | string | No | Registered agent slug. Optional but recommended. |
agent_version | string | No | Agent version. |
model_id | string | No | Model that produced the decision (e.g. claude-sonnet-4-6). |
model_version | string | No | Model version. |
instruction_hash | string | No | SHA-256 hash of the system prompt. Omitting triggers a warning. |
parent_action_uuid | string | No | ID of the parent action (for chaining). |
endpoint_url | string | No | If the action calls an external endpoint, Aira checks it against your whitelist. |
store_details | boolean | No | Generate a storage key for off-DB details (default: false). |
idempotency_key | string | No | Prevent duplicate authorization on retry. |
require_approval | boolean | No | Force the action through human approval. Even when false, policies can still require approval. |
approvers | string[] | No | Override 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
| Status | Code | Description |
|---|---|---|
| 403 | POLICY_DENIED | Action blocked by a policy. See Policies. |
| 403 | ENDPOINT_TLS_MISMATCH | The TLS fingerprint of the endpoint has changed. Hard block. |
| 403 | ENDPOINT_NOT_WHITELISTED | The endpoint_url is not on your whitelist (strict mode). |
| 404 | NOT_FOUND | The parent_action_uuid does not exist in this org. |
| 409 | DUPLICATE_REQUEST | This idempotency_key was already used. |
| 422 | (validation) | Missing or invalid fields. |
Notarize Outcome
POST /api/v1/actions/{action_uuid}/notarize
Authorization: Bearer aira_live_xxxxxRecords 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
| Field | Type | Required | Description |
|---|---|---|---|
outcome | string | No | completed (mints the receipt) or failed (audit-only, no receipt). Default: completed. |
outcome_details | string | No | Human-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:
| Status | Description |
|---|---|
notarized | Action completed successfully. The receipt seals the outcome. |
denied | A policy blocked the action at authorize() time. The receipt proves the system caught it. |
failed | The agent executed the action but it failed. The receipt records the failure. |
denied_by_human | A 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
| Status | Code | Description |
|---|---|---|
| 400 | INVALID_OUTCOME | outcome must be completed or failed. |
| 404 | NOT_FOUND | Action does not exist. |
| 409 | INVALID_ACTION_STATE | Action 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_xxxxxReturns a paginated list of actions.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1). |
per_page | integer | Items per page (default: 20, max: 100). |
action_type | string | Filter by action type. |
agent_id | string | Filter by agent. |
status | string | Filter 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_xxxxxReturns 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.
Approval code flow (recommended)
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
| Status | Code | Description |
|---|---|---|
| 403 | (forbidden) | Code/token does not match this action OR (dashboard) caller is not an authorized approver. |
| 404 | NOT_FOUND | Action or approval code does not exist. |
| 409 | ALREADY_RESOLVED | Action is no longer in pending_approval state. |
| 410 | CODE_EXPIRED | Approval 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.
Denial code flow (recommended)
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
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED | JWT auth required (API keys are not accepted for cosign). |
| 404 | NOT_FOUND | Action does not exist. |
| 409 | ALREADY_AUTHORIZED | This user has already cosigned this action. |
Set Legal Hold
POST /api/v1/actions/{action_uuid}/hold
Authorization: Bearer aira_live_xxxxxPlaces a legal hold on an action, preventing any retention-based deletion.
Response (200 OK)
{
"action_uuid": "9c6b...",
"legal_hold": true,
"request_id": "req_01J9I..."
}Release Legal Hold
DELETE /api/v1/actions/{action_uuid}/hold
Authorization: Bearer aira_live_xxxxxReleases 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_xxxxxReturns 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.