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

# Confirmations — recording that your app asked first

> Use confirmToolCall / confirm_tool_call to tell Zespan your own app recorded a confirmation for a tool call, so a require_confirmation rule can tell a confirmed call from an unconfirmed one.

Some tools should not run unless your app asked first: a refund, a transfer, a delete. If you declared such a tool destructive, a [`require_confirmation`](https://docs.zespan.com/policies/file-reference#require_confirmation) rule can flag a call to it that has **no confirmation recorded by your app**. `confirmToolCall` (TypeScript) and `confirm_tool_call` (Python) are how your app records one.

> [!WARNING]
>
> Zespan checks that **your app recorded** a confirmation for the call. It does **not** verify what the end user saw or agreed to, who the end user is, or that your confirmation step was a real one. An app that records a confirmation without asking, for example straight from a model's output or from a tool result, defeats the rule, and the SDK cannot tell. Call it only after your own confirmation step has happened (a person pressed confirm in your UI).

## Record a confirmation

```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { zespan } from "@zespan/sdk";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

// ...your own confirm step: the person pressed "Refund $50" in your UI...
const result = zespan.confirmToolCall({
  toolName: "issue_refund",
  args: { orderId: "4412", amount: 50 }, // the arguments that were confirmed
  ref: confirmCardId, // optional: your own id for this confirmation
});
// result: { recorded: true } or { recorded: false | true, reason: "..." }
```

```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import zespan

zespan.init(api_key=os.environ["ZESPAN_API_KEY"])

# ...your own confirm step: the person pressed "Refund $50" in your UI...
result = zespan.confirm_tool_call(
    "issue_refund",
    args={"order_id": "4412", "amount": 50},  # the arguments that were confirmed
    ref=confirm_card_id,                      # optional: your own id for this confirmation
)
# result: {"recorded": True} or {"recorded": ..., "reason": "..."}
```

It never throws into your code. It records a `human_in_loop` approval span (kind `confirmation`, outcome `approved`, actor role `user`) with the tool name and a hash of the arguments, never the arguments themselves, and puts an entry in a small in-memory ledger. The next tool-call check for that tool sends the matching entries.

| Option | Default | Meaning |
| - | - | - |
| `toolName` / `tool_name` | required | The tool the confirmation is for |
| `args` | none | The exact arguments confirmed. With them the confirmation is tied to **these arguments** (a yes to 50 does not unlock 500). Without them it is tied to the tool only |
| `ref` | made up | An opaque id of your own (letters, digits, `. _ : -`, at most 128 characters; no free text) |
| `kind` | `confirmation` | `confirmation`, `human_review`, `identity_verified` or `risk_profile` |
| `outcome` | `approved` | `approved`, `rejected` or `pending`. Only `approved` can satisfy a call |
| `actorRole` / `actor_role` | `user` | `user`, `agent_operator`, `reviewer` or `system`. You assert it; Zespan cannot verify it |
| `bindFields` / `bind_fields` | none | Hash only these argument paths (a rule that sets `bindFields` must use the same paths). Not available when tool capture is off |

The return value is `{ recorded, reason? }`. A `reason` of `no_context` means the span was recorded but nothing was put in the ledger. **This is what a top-level call returns when it runs outside any context** (no `withZespanContext` / `with_zespan_context`, no session): wrap the confirmation and the model call in the same context (see below), `ledger_off` that the ledger is switched off, `unhashable_args` that the arguments could not be hashed (a cycle, deeper than 32 levels, or more than 256 KiB once written out), so the confirmation is tied to the tool only. `invalid_input` and `no_client` mean nothing was recorded.

## How a confirmation is used

* **One confirmation unlocks one call.** When the check says a call was confirmed, the entry is used up. Two calls in one model reply need two confirmations, and two checks running at the same time cannot both use one.
* **It expires.** The rule decides how long (default 10 minutes). The SDK keeps an entry for an hour at most.
* **It is tied to the session.** The ledger is scoped by the current context's `sessionId`, or its trace id when there is no session. Call `confirmToolCall` inside the same `withZespanContext` / `with_zespan_context` that sets the session, and set a `sessionId` when the confirmation and the model call happen in different traces. With no context at all, nothing is ledgered, because an entry no session owns could unlock another user's call.
* **It lives in this client, in this process.** The ledger is in memory, per client instance, and bounded (50 entries per session, 1,000 sessions, 10 entries sent per check). **After a fork** (a pre-fork server such as gunicorn, or `multiprocessing`), child processes inherit a copy of the ledger and of the key: create a new client in each child.
* **A server that does not know the rule ignores it.** An older server, or a project without a `require_confirmation` rule, receives two extra fields (`ledger` and `confirmations`) and ignores them. An older SDK sends neither, and the rule then reports **no confirmation data** and blocks nothing.

## When the confirmation and the tool call are in different processes

The ledger does not cross processes. If a web handler records the confirmation and a queue worker makes the tool call, pass the confirmation to the worker yourself and hand it to the SDK when you run the model call:

```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
// in the worker, with the data your web handler stored with the job
await zespan.withContext(
  {
    sessionId: job.sessionId,
    confirmations: [{ toolName: "issue_refund", args: job.confirmedArgs, ref: job.confirmId }],
  },
  () => runAgent(job),
);
```

```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
# in the worker, with the data your web handler stored with the job
with zespan.with_zespan_context(
    session_id=job["session_id"],
    confirmations=[{"tool_name": "issue_refund", "args": job["confirmed_args"], "ref": job["confirm_id"]}],
):
    run_agent(job)
```

Pass `args` (not a hash) so this client hashes them the same way it hashes the call. Each entry works once. `ageMs` / `age_ms` (or `at`) says how old the confirmation is, so the rule's expiry still applies; without it the entry counts as just recorded. **An explicit confirmation is used once per context object, so a job that is retried and rebuilds the same confirmation each time can reuse it on every retry: record one confirmation per attempt, with a new `ref`.** Until you do this, a rule in observe mode reports the call as having no confirmation.

## Argument hashes, tool capture and redaction

When tool capture is on, the hash is the SHA-256 of a canonical form of the arguments (keys sorted, numbers and text written the same way in TypeScript and Python), computed from the arguments as your code holds them, before any redaction. `confirmationArgsHash(args)` / `confirmation_args_hash(args)` gives the same hash.

When **tool capture is off, or PII redaction is on** (`redactPii` / `redact_pii`), a plain hash of small arguments could be guessed back, so the SDK uses a keyed hash instead (HMAC-SHA256 with a random key made when the client starts, kept private in the process and never sent anywhere). A keyed hash can be compared only inside the same client: it works for confirmations recorded by this client, but never for a hash made in another process (pass `args`, as above). A rule that sets `bindFields` cannot be applied to a keyed hash and reports that it could not compare, blocking nothing.

## The approval queue

`client.awaitApproval()` (TypeScript) and `client.await_approval()` (Python) gate a call on a person's decision in the dashboard. When the request is approved, the SDK also records it as a `human_review` entry from a `reviewer`, tied to the tool and arguments you passed. **A `require_confirmation` rule counts it only if the rule names both `human_review` and `reviewer` in `accept`.** By default it does not: a decision by someone on your team is not a confirmation recorded by your app at the point of use, and the two should stay apart.

```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
await client.awaitApproval("issue_refund", { orderId: "4412", amount: 50 }, { sessionId });
```

```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
client.await_approval("issue_refund", {"order_id": "4412", "amount": 50}, session_id=session_id)
```

`await_approval` needs a `project_id` (`zespan.init(project_id=...)`; `projectId` in TypeScript); without one it raises `ValueError`. Both return when approved, raise `ApprovalRejectedError` when rejected or expired, and raise `ApprovalTimeoutError` when no decision arrives in time (default wait 5 minutes, polling every 3 seconds). The Python call blocks; in async code run it with `asyncio.to_thread`. Both record a `human_review` approval span for every outcome.

## Where the check runs

A call is checked **before your code runs the tool** only where the SDK sends the tool-call check after the model replies and before your tool code runs: the wrapped clients (`wrapOpenAI`, `wrapAnthropic`, `wrapGoogle`, `wrapGoogleGenAI`, `wrapMistral`, `wrapCohere`, `wrapOpenRouter` in TypeScript; `patch_openai`, `patch_anthropic`, `patch_google`, `patch_google_genai`, `patch_mistral`, `patch_cohere`, `patch_groq`, `patch_bedrock`, `patch_litellm` in Python) with `guardrails` turned on. Anywhere else (a framework that runs the tool for you, or an integration that does not send the check), no ledger is sent: the rule reads **no confirmation data** and blocks nothing.

## Language

The confirmation itself is language-independent: your app records it, in any language. What reads text is only the proof Zespan runs when it drafts a rule for you, and that reading is **English only**: for a conversation it cannot read it says so and does not count the call. It never decides whether a live call runs.

## Switching it off

Set `ZESPAN_CONFIRMATION_LEDGER=off` in the environment to stop the ledger and the `ledger` field. The rule then reports **no confirmation data** and blocks nothing. The approval span from `confirmToolCall` is still recorded.

## Next steps

* [Policy file reference: `require_confirmation`](https://docs.zespan.com/policies/file-reference#require_confirmation) — the rule, its fields and its reason codes
* [Guardrails](https://docs.zespan.com/sdk/guardrails.md) — turning on tool-call checks in a wrapper
* [Agent tracing](https://docs.zespan.com/sdk/agent-tracing.md) — `recordApproval` and tool roles

> [!NOTE]
>
> Arguments should be plain JSON values (objects, arrays, strings, numbers, booleans, null). A `Date`, `Map`, `Set` or class instance, which JSON cannot hold, cannot be hashed, so the confirmation is tied to the tool only.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.