> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kordio.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Require a person above a threshold

> Hold spend over a limit, and let the agent wait instead of failing.

The useful thing about an approval is that it is not a refusal. The action does not fail, the
agent does not retry, and the budget it asked for is held for it while a person decides. Getting
that right is mostly about what your agent does with a `202`.

## Before you start

<Snippet file="env-agents.mdx" />

```bash Identifiers theme={"dark"}
export KORDIO_AGENT_ID="7d2a6c15-3f89-4b02-9e64-8a15d7c0b3f2"
export KORDIO_BUDGET_ID="3f1c8a92-5d41-4c8e-9f2b-7a6e1d0c4b83"
```

The amounts here assume the agent's starter policy is disabled, as the
[quickstart](/agents/quickstart) does in step 2. Leave it on and it holds anything over \$100 for
a person before your own rules get a say.

## 1. Set the threshold

`approval_threshold_cents` is the shorthand. It compiles to an `approval_threshold` rule and
reports as one:

```bash theme={"dark"}
curl -s -X POST https://api.kordio.io/control/v1/workspaces/$KORDIO_WORKSPACE/policies \
  -H "Authorization: Bearer $KORDIO_DASHBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"agent_id\": \"$KORDIO_AGENT_ID\",
    \"approval_threshold_cents\": 20000,
    \"velocity_cap_cents\": 200000,
    \"velocity_window_seconds\": 3600
  }"
```

```json 201 Created theme={"dark"}
{
  "data": {
    "id": "a05f8c37-6b12-4d94-8207-c3e1b9d6a4f8",
    "agent_id": "7d2a6c15-3f89-4b02-9e64-8a15d7c0b3f2",
    "status": "active",
    "mode": "blocklist",
    "approval_threshold_cents": 20000,
    "velocity_cap_cents": 200000,
    "velocity_window_seconds": 3600,
    "rules": [],
    "created_at": "2026-08-22T13:02:55Z"
  }
}
```

To hold on something other than an amount, write a `condition` rule with
`"effect": "require_approval"` instead. [Passing facts from your own
service](/agents/guides/metadata-facts) holds a payment on how new the buyer is, and reaches the
same `202`.

## 2. Your agent gets a 202

```bash theme={"dark"}
ACTION=$(curl -s -X POST https://api.kordio.io/control/v1/agent/actions \
  -H "Authorization: Bearer $KORDIO_AGENT_KEY" \
  -H "Idempotency-Key: order-9120-attempt-1" \
  -H "Content-Type: application/json" \
  -d "{
    \"budget_id\": \"$KORDIO_BUDGET_ID\",
    \"action_type\": \"payment.create\",
    \"resource\": \"acme-supplies.example\",
    \"cost_cents\": 24000
  }")

export KORDIO_ACTION_ID=$(echo "$ACTION" | jq -r '.data.id')
```

```json 202 Accepted theme={"dark"}
{
  "data": {
    "id": "9b2ef0c1-6a3d-4e75-8c19-2f4b7d5a0e63",
    "budget_id": "3f1c8a92-5d41-4c8e-9f2b-7a6e1d0c4b83",
    "action_type": "payment.create",
    "resource": "acme-supplies.example",
    "cost_cents": 24000,
    "currency": "USD",
    "state": "requires_approval"
  },
  "decision": {
    "outcome": "requires_approval",
    "rule": "approval_threshold",
    "detail": { "above_cents": 20000 },
    "headroom": { "session_remaining_cents": 50000 }
  }
}
```

There is no `cosignature`, because nothing is authorized yet. The 24000 is already reserved
against the budget, so the next action in the same run cannot quietly spend it while this one
waits.

## 3. Wait, do not retry

A `202` is not a transient failure. Retrying it under a fresh `Idempotency-Key` creates a second
held intent, reserves the budget twice, and asks a person the same question twice.

<Warning>
  Reuse the same `Idempotency-Key` if you must retry the call itself. Kordio replays the original
  decision rather than holding the spend again.
</Warning>

In production, subscribe to `action.requires_approval` and `approval.resolved` and let the
[webhook](/agents/webhooks) wake the run back up. In a script, read the intent back:

```bash theme={"dark"}
curl -s https://api.kordio.io/control/v1/agent/actions/$KORDIO_ACTION_ID \
  -H "Authorization: Bearer $KORDIO_AGENT_KEY"
```

```json 200 OK theme={"dark"}
{
  "data": {
    "id": "9b2ef0c1-6a3d-4e75-8c19-2f4b7d5a0e63",
    "cost_cents": 24000,
    "state": "requires_approval",
    "resolved_by_sub": null,
    "resolved_at": null
  }
}
```

## 4. What the approver sees

An approver looking at "approve \$240?" is rubber-stamping. One call answers what approving would
actually do, and it is the same shape for a payment intent:

```bash theme={"dark"}
curl -s "https://api.kordio.io/control/v1/workspaces/$KORDIO_WORKSPACE/action_intents/$KORDIO_ACTION_ID/impact" \
  -H "Authorization: Bearer $KORDIO_DASHBOARD_TOKEN"
```

```json 200 OK theme={"dark"}
{
  "data": {
    "amount_cents": 24000,
    "currency": "USD",
    "rule": "approval_threshold",
    "executed": false,
    "session": {
      "id": "3f1c8a92-5d41-4c8e-9f2b-7a6e1d0c4b83",
      "budget_cents": 50000,
      "spent_cents": 24000,
      "remaining_cents": 26000,
      "returns_on_deny_cents": 24000,
      "open_holds": 1
    },
    "runtime": {
      "id": "7d2a6c15-3f89-4b02-9e64-8a15d7c0b3f2",
      "name": "procurement agent",
      "mode": "test",
      "scopes": ["payment.*", "tool.invoke"]
    },
    "windows": [
      { "kind": "velocity", "window_seconds": 3600, "cap_cents": 200000,
        "spent_cents": 24000, "remaining_cents": 176000 }
    ],
    "spend_token": null
  }
}
```

`session` is the budget and `runtime` is the agent, in the wire names the API has always used.
Two numbers carry most of the decision: `returns_on_deny_cents` is what denying gives back, and
`open_holds` counts every other intent competing for the same budget, which is how an approver
notices that three pending approvals total more than the budget can cover.

The console shows the same facts on the Approvals page, where the queue can be narrowed to live
or test agents.

## 5. Resolve it

```bash theme={"dark"}
curl -s -X POST \
  "https://api.kordio.io/control/v1/workspaces/$KORDIO_WORKSPACE/action_intents/$KORDIO_ACTION_ID/approve" \
  -H "Authorization: Bearer $KORDIO_DASHBOARD_TOKEN"
```

```json 200 OK theme={"dark"}
{
  "data": {
    "id": "9b2ef0c1-6a3d-4e75-8c19-2f4b7d5a0e63",
    "cost_cents": 24000,
    "state": "pending",
    "resolved_by_sub": "user_01J8XQ2M4K7T",
    "resolved_at": "2026-08-22T13:19:40Z"
  }
}
```

The intent is now `pending`, exactly as if it had come back `201`, and `approval.resolved`
fires. Your agent executes and reports the outcome with `complete` or `fail`, and the
reservation is only released when it does.

Denying instead moves the intent to `denied` and returns the reservation:

```bash theme={"dark"}
curl -s -X POST \
  "https://api.kordio.io/control/v1/workspaces/$KORDIO_WORKSPACE/action_intents/$KORDIO_ACTION_ID/deny" \
  -H "Authorization: Bearer $KORDIO_DASHBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "duplicate of order 9119"}'
```

```json 200 OK theme={"dark"}
{
  "data": {
    "id": "9b2ef0c1-6a3d-4e75-8c19-2f4b7d5a0e63",
    "state": "denied",
    "failure_reason": "duplicate of order 9119",
    "resolved_by_sub": "user_01J8XQ2M4K7T",
    "resolved_at": "2026-08-22T13:21:02Z"
  }
}
```

Resolving an intent that is not in `requires_approval` is a `422`, so a double click cannot
double execute.

## What approvals do not do

Approving is a capability, not a queue with routing. Owners and admins hold `approve`; members
hold `read` only. Say this out loud to whoever runs the process, because the product does not
enforce any of it for you:

* Kordio does not route a hold to a named person, a group, or an on-call rota. Anyone with
  `approve` in the workspace can resolve any hold in it.
* There is no quorum. One approval resolves the intent, and `resolved_by_sub` records who.
* There is no escalation and no expiry. A hold waits until somebody answers it, and its
  reservation waits with it.
* An approved intent carries no cosignature. Cosignatures are issued on the authorize call and
  only for an `allowed` decision, so an executor that gates on one should read the intent state
  for this path instead.

## Next steps

<CardGroup cols={2}>
  <Card title="Roll a policy out safely" icon="flask-conical" href="/agents/guides/policy-rollout">
    Prove the threshold on a test agent before it holds real money.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/agents/webhooks">
    Wake the run on `approval.resolved` instead of polling.
  </Card>

  <Card title="Holding spend for a person" icon="user-check" href="/agents/approvals">
    The reference for states, capabilities, and the payment path.
  </Card>

  <Card title="Verifying an authorization" icon="signature" href="/agents/cosignatures">
    Why `approval.resolved` is not itself permission.
  </Card>
</CardGroup>
