# Ask before a sensitive action

Bind a token to one action, ask AuthFI whether the agent may do it, and wait for a person to approve when a rule requires one.

Before an AI agent does something that matters, such as sending money or merging code, it asks AuthFI. The answer is `allow`, `deny` or `approval_required`. Use this guide to add that question to your agent, and to wait for a person when the answer is `approval_required`.

## Before you begin

You need:

- A registered agent and its access token. See [Give an AI agent an identity](/quickstart/agent).
- The permission for the action it asks about, given to the agent through a role, and a capability granted for it. The console shows a capability as a permission. See [Limit what an agent may do](/agents/limits).

<Steps>

### Bind a token to the action

Exchange the agent's access token for a _transaction token_. A transaction token names one action and its details, and it lasts 60 seconds.

<Endpoint method="POST" path="/apis/regional/v1/ai/auth/transaction-token" />

```bash
curl https://acme.authfi.io/apis/regional/v1/ai/auth/transaction-token \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "transfer:accounts",
    "resource": "account:4821",
    "context": { "amount": 1200, "currency": "USD", "account_type": "business" }
  }'
```

`$ACCESS_TOKEN` is the access token the agent received when it signed in.

```json
{
  "data": {
    "transaction_token": "eyJhbGciOiJFZERTQSIs…",
    "txn": "0b7d6a52-…",
    "token_type": "N_A",
    "expires_in": 60
  }
}
```

The action, resource and context are frozen inside the token. It cannot be reused for a different amount or a different account.

Before it binds a token, AuthFI checks the details against the limits on the agent's capability for the action. Details outside a limit are refused with `403` and `constraint_violated`, and no token is issued. See [How a limit is evaluated](/agents/limits#how-a-limit-is-evaluated).

```bash
export TRANSACTION_TOKEN=<the transaction_token from the response>
```

### Ask whether the action is allowed

Send the transaction token with the same action, resource and context.

<Endpoint method="POST" path="/apis/regional/v1/agents/authorize" />

```bash
curl https://acme.authfi.io/apis/regional/v1/agents/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_7f3a9c21",
    "transaction_token": "'"$TRANSACTION_TOKEN"'",
    "action": "transfer:accounts",
    "resource": "account:4821",
    "context": { "amount": 1200, "currency": "USD", "account_type": "business" }
  }'
```

`agt_7f3a9c21` is the agent ID. The console shows it as "Client id" on the agent's **Connect** tab.

With the rule from [Limit what an agent may do](/agents/limits#add-a-rule-for-the-whole-project), a transfer of 1,000 or more waits for a person:

```json
{ "data": { "decision": "approval_required", "approval_id": "3f0c2b6e-…", "reason": "policy_step_up" } }
```

| Decision            | What the agent does                              |
| ------------------- | ------------------------------------------------ |
| `allow`             | Goes ahead                                       |
| `deny`              | Stops. `reason` says why.                        |
| `approval_required` | Waits for a person, and polls with `approval_id` |

The limits on the capability are checked here again. If a limit was lowered after the token was bound, the answer is `deny`. Each reason is listed in [The decision and its reason](/agents/limits#the-decision-and-its-reason).

### Wait for a person

When the decision is `approval_required`, the agent's owner or its approval group is asked. The person answers in the console:

1. Open **Approvals** in the sidebar.
2. Find the request under "Agents waiting to do something". The card shows the agent that asked and the details of the action.
3. Choose **Allow** to approve, or **Refuse** to deny.

**Allow** asks for confirmation: the agent is told yes and goes ahead straight away, with the arguments shown. The console then shows "Approved" or "Refused".

Meanwhile the agent polls for the result, using the `approval_id` from the previous step.

<Endpoint method="GET" path="/apis/regional/v1/agents/approvals/{approval_id}" />

```json
{
  "data": {
    "approval_id": "3f0c2b6e-…",
    "status": "approved",
    "requesting_action": "transfer:accounts",
    "approver_user_id": "5a1e…",
    "created_at": "2026-10-09T10:02:11Z",
    "resolved_at": "2026-10-09T10:02:48Z",
    "expires_at": "2026-10-09T10:17:11Z"
  }
}
```

| Status     | Meaning                 | What the agent does |
| ---------- | ----------------------- | ------------------- |
| `pending`  | Nobody has answered yet | Keeps polling       |
| `approved` | A person approved       | Goes ahead          |
| `denied`   | A person denied         | Stops               |
| `expired`  | Nobody answered in time | Stops               |

</Steps>

## Check that it works

Run the three steps with an action that one of your [project rules](/agents/limits#add-a-rule-for-the-whole-project) sends to a person. That is a policy with the effect "Require step-up".

1. The authorize response has `"decision": "approval_required"` and an `approval_id`.
2. Poll the approval. Its `status` is `pending`.
3. In the console, open **Approvals**. Under "Agents waiting to do something", choose **Allow** on the request and confirm.
4. Poll again. The `status` is `approved`, and `approver_user_id` and `resolved_at` are set.

## Open an approval request directly

An agent can also open an approval request without the authorize step. Use this for an action that your own code decides is sensitive.

<Endpoint method="POST" path="/apis/regional/v1/agents/request-approval" />

| Field                                       | Meaning                                                  |
| ------------------------------------------- | -------------------------------------------------------- |
| `agent_id`                                  | The agent                                                |
| `client_assertion`, `client_assertion_type` | A fresh signed assertion, as at sign-in                  |
| `requesting_action`                         | What it wants to do, in words the approver will read     |
| `context`                                   | Details to show the approver                             |
| `timeout_seconds`                           | How long to wait, up to 3600. The default is 15 minutes. |

A _client assertion_ is the short statement the agent signs with its private key to prove who it is. Build one as you do in [Give an AI agent an identity](/quickstart/agent).

The response is `202 Accepted` with the approval, in the `pending` status. Poll it as in [Wait for a person](#wait-for-a-person).

## How an approval is decided

- **A person decides.** The decision is made by a signed-in person on the **Approvals** page. An agent cannot approve through its own credentials.
- **A policy decides when to ask.** An action needs approval when a project policy with the effect "Require step-up" matches it.
- **No answer is a denial.** A request that nobody answers expires. An expired request can never be approved.
- **Everything is recorded.** The request, who decided, and when are in the [audit record](/security/audit).

## Troubleshooting

| What you see                         | Cause                                                    | Fix                                                                                     |
| ------------------------------------ | -------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `400` with `invalid_body`            | The request body is missing a field or is not valid JSON | Send every field shown in the step, as JSON                                             |
| `401` with `unauthorized`            | The access token is missing, expired or revoked          | Sign in again and send the new access token                                             |
| `401` from the authorize endpoint    | The agent is unknown or revoked                          | Check `agent_id`. A revoked agent stays revoked.                                        |
| `deny` with a `reason`               | A limit, a rule or a missing grant stops the action      | Look up the reason in [the list of reasons](/agents/limits#the-decision-and-its-reason) |
| The approval's `status` is `expired` | Nobody answered in time                                  | Start again from _Bind a token to the action_                                           |

## Next steps

<Cards>
  <Card href="/agents/ciba" title="Ask a named person to approve">
    Ask one person, and receive an access token that names them.
  </Card>
  <Card href="/agents/mcp-gateway#when-a-call-needs-approval" title="Approve a tool call">
    How a tool call that needs approval is retried through the MCP gateway.
  </Card>
  <Card href="/agents/verify" title="Accept agents at your own API">
    Check a transaction token in the service that performs the action.
  </Card>
  <Card href="/agents/how-it-works" title="How agent sign-in works">
    The model behind access tokens, transaction tokens and approvals.
  </Card>
</Cards>
