---
title: Approvals
description: A human decision bound to one specific request, with quorum, escalation and expiry — never a standing permission.
---

When policy returns `REQUIRE_APPROVAL`, the action stops and an approval request
is created. A human decides. Only then does the action proceed.

:::security An approval authorises one request, not a permission
An approval is bound to the exact request that created it — the agent, the tool,
the parameters, the amount. Approving does not grant the agent a standing
permission, and an approval cannot be replayed against a different request.

Present an approval id with a different payload and the gateway refuses with
`APPROVAL_NOT_GRANTED`. This is what stops "approved once" from becoming
"approved forever", which is the failure mode every approval system that keys on
the approver rather than the request eventually has.
:::

## The flow

1. Policy returns `REQUIRE_APPROVAL`.
2. Veefore creates the approval, routes it to approvers, and returns `APPROVAL_REQUIRED` with an `approvalId` and a `pollAfterMs`.
3. An approver sees the request with full decision context and approves or denies.
4. The agent retries with the `approvalId`. The pipeline runs again, the approval validates against this exact request, and the action proceeds.

Step 4 matters: the pipeline runs **again**. An approval does not bypass
anything. If the agent was revoked between the approval and the retry, the
retry is refused.

## Handling it in code

The SDK gives you three levels, from most to least control.

:::tabs

:::tab TypeScript
```ts
import { VeeforeApprovalRequiredError, VeeforeApprovalDeniedError } from '@veefore/sdk';

// 1. Wait for the decision and then proceed. Good for a background worker.
const decision = await agent.veefore.actWithApproval(
  { tool: 'refund_customer', amount: 250_00, currency: 'INR' },
  { timeoutMs: 15 * 60 * 1000 },
);

// 2. Return control to a caller while the human decides.
try {
  await agent.act({ tool: 'refund_customer', amount: 250_00, currency: 'INR' });
} catch (error) {
  if (error instanceof VeeforeApprovalRequiredError) {
    return { status: 'waiting', approvalId: error.approvalId };
  }
  if (error instanceof VeeforeApprovalDeniedError) {
    return { status: 'denied', reason: error.message };
  }
  throw error;
}

// 3. Decide as data, no exceptions.
const result = await agent.evaluate({ tool: 'refund_customer', amount: 250_00 });
if (result.approval?.required) {
  await persist(result.approval.approvalId);
}
```
:::

:::tab Python
```python
from veefore import (
    ActionRequest,
    VeeforeApprovalDeniedError,
    VeeforeApprovalRequiredError,
)

request = ActionRequest(tool="refund_customer", amount=250_00, currency="INR")

# 1. Wait for the decision and then proceed.
decision = agent.veefore.act_with_approval(request, timeout_seconds=900)

# 2. Return control to a caller while the human decides.
try:
    agent.act(request)
except VeeforeApprovalRequiredError as error:
    return {"status": "waiting", "approval_id": error.approval_id}
except VeeforeApprovalDeniedError as error:
    return {"status": "denied", "reason": str(error)}

# 3. Decide as data, no exceptions.
result = agent.evaluate(request)
if result.approval and result.approval.get("required"):
    persist(result.approval["approval_id"])
```
:::

:::

:::warning Respect `pollAfterMs`
The response tells you when to poll next. Honour it rather than busy-looping —
polling faster will not make a human decide faster, and it will earn you
`RATE_LIMITED`. `waitForApproval` already does this correctly.
:::

## Routing

The policy effect controls who decides and how many of them.

```json
{
  "then": {
    "decision": "REQUIRE_APPROVAL",
    "reason": "Refunds above ₹1,000 require a human approver.",
    "approval": {
      "routingMode": "QUORUM",
      "requiredApprovals": 2,
      "approverRoleKeys": ["APPROVER", "SECURITY_ADMIN"],
      "ttlSeconds": 900,
      "requireStepUp": true
    }
  }
}
```

| Mode | Behaviour |
| --- | --- |
| `SINGLE` | One named approver decides |
| `GROUP` | Anyone in the group may decide |
| `QUORUM` | `requiredApprovals` distinct approvers must agree |
| `SEQUENTIAL` | Approvers decide in order; a denial at any step ends it |

`requireStepUp` forces the approver to re-authenticate at the moment of
approving. Use it for anything irreversible: it means an unattended, still-valid
browser session is not enough to approve a destructive action.

When several rules both require approval, the effects merge conservatively —
the maximum `requiredApprovals`, the minimum `ttlSeconds`, the union of approver
roles, and `requireStepUp` true if any rule asked for it.

## Expiry

An approval has a TTL. When it passes, the status becomes `EXPIRED` and the
action is refused with `APPROVAL_EXPIRED`.

Expiry is not optional. A pending approval is a half-finished action holding an
agent's attention, and an approval queue that only grows is one nobody reads.
Default TTL is 900 seconds; tune it per rule to match how fast a human can
realistically respond to that particular thing.

Statuses: `PENDING`, `APPROVED`, `DENIED`, `EXPIRED`, `CANCELLED`.

## What the approver sees

The approval card carries the full decision context, not just a yes/no prompt:

- Which agent, under which delegation chain, acting for whom
- The tool, the capability, the parameters, the amount
- The policy and rule that required approval, with the conditions that matched
- The risk level, score and the signals that fired, each with its explanation
- The agent's recent action history
- The blast radius if it is a destructive tool

An approver without that context is guessing, and an approver who is guessing
approves everything. Which is worse than having no approval step, because now
there is a record saying a human reviewed it.

## Denying

```bash
curl -X POST https://veefore.example.com/v1/approvals/apr_01ABC/deny \
  -H "Authorization: Bearer $VEEFORE_API_KEY" \
  -H 'content-type: application/json' \
  -d '{ "reason": "Order was already refunded on 1 Oct" }'
```

A reason is required. It reaches the agent as the message on
`APPROVAL_DENIED`, so the agent can report something meaningful upstream instead
of "the request failed".

## Permissions

| Permission | Who |
| --- | --- |
| `approval:read` | Every built-in role |
| `approval:approve` | `APPROVER`, `ADMIN`, `OWNER` |
| `approval:deny` | `APPROVER`, `ADMIN`, `OWNER` |
| `approval:delegate` | `APPROVER`, `ADMIN`, `OWNER` |
| `approval:configure` | `ADMIN`, `OWNER` |

Notably `SECURITY_ADMIN` does not hold `approval:approve` by default, and
`DEVELOPER` does not either. Separation of duties: the person who wrote the rule
that requires approval should not be the person satisfying it. Grant it
explicitly if your organization genuinely wants that overlap.

## Related

:::cards

:::card Handle approvals | /build/handle-approvals
The full integration, including resuming work after a wait.
:::

:::card Approvals queue | /operate/approvals-queue
Operating the queue, SLAs and escalation.
:::

:::card Risk engine | /concepts/risk
The usual reason an approval appears.
:::

:::card Approvals API | /reference/api/approvals
List, read, approve, deny.
:::

:::
