Docs

How approvals work

Follow one approval end to end: what triggers it, who can approve, how the agent waits and resumes, and why a request expires or goes stale.

An approval is K-Block's way of holding one exact tool call until a person signs off on it. When the agent's policy says a call needs approval, the Gateway records the call, signs its exact terms, and opens an approval request. Nothing runs upstream until an eligible member approves, and then only that exact call runs, once.

This page follows a request from the rule that creates it to the moment the approval is used up. To review a request in the dashboard, see Review and decide. For the concept in brief, see Approvals.

  1. Agent calls a tool

    through the Gateway

  2. Policy: require_approval

    approvers resolved, terms signed

  3. Request pending

    dashboard inbox, webhook

  4. Approver signs

    Approve and sign, or Reject and sign

  5. Agent resumes

    same call

  6. Gateway executes once

    result and receipt

The life of an approval (concept)

What causes an approval

The agent's active policy decides every call. A request is created only when the effect that applies to the call is require_approval. That happens when:

  • a rule with "effect": "require_approval" matches the call and no matching rule denies it (when several rules match, the most restrictive effect wins: deny, then require_approval, then allow), or
  • no rule matches and the policy's default is "require_approval".

The call must also pass the other checks of a decision first. A call whose amount is invalid (INVALID_AMOUNT) or whose action no valid delegation of the agent grants (NO_DELEGATION) is denied, and nobody is asked.

A require_approval rule must say who may approve it, in approvers:

{
  "id": "refund-approval",
  "actions": ["payments.refund"],
  "effect": "require_approval",
  // Who may approve: approval-capable roles, and/or specific members by user ID.
  "approvers": { "roles": ["owner", "admin"] },
  // Optional: how long the request stays open (60 to 2,592,000 seconds).
  "approval_ttl_seconds": 3600
}

When the policy default applies instead, the approvers are members with the roles owner, admin or approver. See Rules, amounts and currencies for the full rule format.

An observe window can relax a require_approval decision into an allow. A relaxed call creates no approval request.

Who can approve

When the Gateway records the call, it resolves the approvers once and signs their key IDs (KIDs) into the call's evidence. An approver must be:

  • a member whose role is owner, admin or approver, and
  • admitted by the rule: their role is in approvers.roles, or their user ID is in approvers.users, and
  • holding at least one active personal K-Block identity. See Identities and KIDs and Your account.

Disabled users, revoked identities and identities with conflicting documents are left out. Each member counts with at most their 5 most recently bound identities, and one request lists at most 32 approver KIDs.

Owners and admins are not approvers automatically. A rule with "approvers": { "roles": ["approver"] } admits only members with the approver role. On the free Developer plan, whose only member is the owner, such a rule finds nobody: there, the rule must admit the owner role or the owner's user ID.

The list is fixed when the request is created. A member promoted later, or a key bound later, cannot decide that request. A member demoted below an approval-capable role loses the right to decide it, because the role is checked again when they sign.

NO_APPROVERS and TOO_MANY_APPROVERS

If nobody matches, the call is not held. It is recorded as denied with one of these codes, and nothing runs:

CodeMeaningWhat to do
NO_APPROVERSNo member with an approval-capable role and an active K-Block identity matches the approver rule.Make sure the members the rule names have the right role and have created their K-Block identity, or widen the rule.
TOO_MANY_APPROVERSMore than 32 members match; one request can list at most 32.Narrow the rule to fewer roles, or list specific users.

An observe window never relaxes these two denials.

The request

The Gateway signs the call's terms (see What the approval is bound to), records a protected action with status pending_approval, and opens an approval request with status pending. K-Block records all of this together, so a request always has its signed evidence.

The agent gets a refusal, not an error. Its structured content carries the identifiers the agent needs to resume:

{
  "kblock": {
    "status": "approval_pending",
    "code": "APPROVAL_PENDING",
    "detail": "the call requires approval; approval request <approval-request-id> is pending — retry the same call to resume it",
    "protected_action_id": "<protected-action-id>",
    "approval_request_id": "<approval-request-id>",
    "expires_at": "<ISO 8601 time>"
    // "policy": { … } when the organization discloses decisions to agents
  }
}

What the agent is told about the policy depends on your disclosure setting. See What agents are told.

Notification

A new request appears in the dashboard under Approvals, on the Pending tab. K-Block does not send approval e-mails in this version. To alert people elsewhere, subscribe a webhook to the approval events:

EventWhen
approval.requestedA request is created. The data includes approval_request_id, expires_at and approver_kids.
approval.approvedAn approver signed an approval.
approval.rejectedAn approver signed a rejection.
approval.expiredThe request expired undecided, or it was closed as stale (then the data has "reason": "SUPERSEDED").

There is no webhook event for a cancellation (only the receipt.created event of its receipt), and none when an approved call expires or is closed as superseded before the agent runs it. See Webhook events for payloads.

How the agent waits and resumes

The agent resumes a held call by repeating it: the same tool with the same arguments. Which open action a repeat continues depends on the idempotency key:

  • Without a key, the repeat continues the open action of that exact call: the one recorded without a key, or else the oldest one recorded with a key.
  • With a key, the repeat continues only the action recorded with that same key. A call with a new key is a new operation and gets its own decision and its own approval request.

The repeat must come from the same agent, but not necessarily with the same gateway API key: a repeat with any of the agent's keys continues the call, and can execute it once it is approved.

While the request is pending, a repeat returns approval_pending again. Once it is approved, the repeat executes the call. For a decision-only tool, the repeat (POST /v1/authorize with the same idempotency key) is answered authorized instead: K-Block authorizes the approved terms once, and the caller executes them; the receipt covers the approval, not an execution.

To avoid polling, the agent can ask the Gateway to hold the call until a decision lands, with the kblock/wait-seconds entry in the call's _meta. The Gateway caps the wait at 60 seconds by default; a longer wait is cut to the cap. If the approver decides during the wait, the same request executes and returns the result. If the wait ends first, the agent gets approval_pending and repeats. The Gateway holds at most 20 waiting calls per API key (and 1,000 in total on each Gateway); beyond that it answers approval_pending at once without waiting.

The TypeScript client does this for you. callTool sends a fresh idempotency key by default, and waitForApproval repeats the call with that same key until it is decided. It gives up after timeoutSeconds (300 by default) and then returns the result that is still pending. This program reads the Gateway URL and the agent's gateway API key from KBLOCK_GATEWAY_URL and KBLOCK_API_KEY, two environment variables of its own:

agent.ts
import { connectKBlock, explainResult } from "@kblockai/client";

const client = await connectKBlock({
  gatewayUrl: process.env.KBLOCK_GATEWAY_URL ?? "",
  apiKey: process.env.KBLOCK_API_KEY ?? "",
});

const first = await client.callTool("refund", { order_id: "ord_123", amount: "250.00", currency: "USD" });
if (first.status === "approval_pending") {
  console.log("approval request", first.approvalRequestId, "expires", first.expiresAt);
  // Repeats the same call (same key) and asks the Gateway to hold each repeat until a decision.
  const done = await client.waitForApproval(first, { timeoutSeconds: 600 });
  console.log(explainResult(done));
} else {
  console.log(explainResult(first));
}
await client.close();

Deciding

Any one listed approver decides. There is no quorum: the first signed decision closes the request, and a second decision is refused with ALREADY_DECIDED.

  • Approve. The approver's browser signs an approval of the exact signed proposal. The action becomes approved and waits for the agent's repeat.
  • Reject. The approver's browser signs a rejection. The action becomes rejected, the agent's next repeat gets rejected / REJECTED, and K-Block issues a signed receipt of the rejection.

An optional note (approval) or reason (rejection) is signed into the decision. See Review and decide.

Expiry

Every request has an expires_at, set when the call is decided:

  • the rule's approval_ttl_seconds, or else the organization's Default approval lifetime (seconds) (86,400 seconds, 24 hours, by default; owners and admins change it under Settings);
  • never later than the expiry of the delegation the call acts under.

The same time limits the whole call. It ends in one of two ways:

SituationResultCode
Nobody decided before expires_atRequest and action expiredAPPROVAL_EXPIRED
Approved, but the agent did not repeat the call before expires_atAction expired; the request stays approvedTASK_EXPIRED

K-Block marks a call expired within a few seconds (about 15) after expires_at, so a request past its expiry can stay under Pending in the dashboard for that long; it can no longer be decided. If the agent repeats the call after expires_at, the call is marked expired at once. Expiry issues no receipt. After expiry, an identical call without a key gets a new decision. Approvals cannot be extended: if the call is still wanted, the agent makes it again.

Cancellation

An owner or admin can cancel a call that is pending approval or approved but not yet executed. Open the action (from the request, Action and K-Block chain), then use Cancel this action: an optional reason, Cancel action…, then Sign cancellation.

The cancellation is signed by the organization principal, so the principal's key must be in your browser. The action becomes cancelled, a pending request becomes cancelled, the agent's next repeat gets cancelled / CANCELLED, and K-Block issues a receipt of the cancellation. A call that is already executing cannot be cancelled.

What the approval is bound to

An approval covers one exact call, not a tool or a category of calls. The Gateway signs these terms into the call's evidence before anyone sees the request, and the approver's signature covers them:

  • the arguments, verbatim, and their digest;
  • the upstream tool name and the action;
  • the amount and currency (or no amount, when the tool has no amount mapping);
  • the upstream MCP server (its ID, name and URL) and the executing server's KID;
  • the delegation and the principal the agent acts for;
  • the listed approver KIDs;
  • the agent, its gateway API key, the environment and the exposed tool name;
  • the policy, its version and digest, and the deciding rule;
  • the expiry.

Different arguments make a different call. If the agent changes anything, even one character, the call gets a new decision and the old approval still runs only its own exact call.

K-Block also keeps, in its own records, the tool binding the call was decided under: the server's URL, KID and credential version, and the tool's upstream name, action, amount mapping and input schema. This record is not part of the signed evidence, but the Gateway checks it before it executes.

Why approvals become stale

Between the decision and the execution, things can change. The Gateway checks before it executes, and refuses rather than run a call under conditions nobody approved. In every case below, nothing is executed and the approval is not used up.

What changedA pending requestAn approved call
The MCP server's URL or server KID, or the tool's upstream tool or actionClosed as superseded on the agent's next identical callRefused denied / UPSTREAM_CHANGED on every repeat
The server's credential, or the tool's amount mapping or input schemaStays pending; repeats get approval_pendingRefused denied / TOOL_REAUTHORIZATION_REQUIRED on every repeat
The tool was re-authorized after any of these changes, even one changed backClosed as superseded on the agent's next identical callClosed as superseded on the agent's next identical call
The tool or its MCP server was disabledRepeats get UNKNOWN_TOOL; the request stays openRepeats get UNKNOWN_TOOL; the call stays approved
The delegation was revoked or expiredClosed as superseded on the agent's next identical callRefused denied / VERIFICATION_FAILED on every repeat
The organization principal was replacedClosed as superseded at onceClosed as superseded at once
A rollback or emergency stop closed open actionsSee Policy changes and POLICY_CHANGEDSee the same section

A call refused on every repeat stays approved until it expires or an owner or admin cancels it. Re-authorizing the tool does not make an approval given before the change runnable again.

A superseded action gets the status expired with the code SUPERSEDED, and its pending request becomes expired too. After that, an identical call without a key gets a new decision (while the tool waits for re-authorization, that call is refused denied / TOOL_REAUTHORIZATION_REQUIRED and nothing is recorded); a repeat with the old idempotency key gets expired / SUPERSEDED.

The dashboard shows a changed tool binding on the request itself, and does not offer Approve and sign. See Review and decide.

Policy changes and POLICY_CHANGED

Activating a new policy version does not close pending requests. A request decided under an older version stays open and can still be approved; the inbox marks it policy changed. A person's approval is not re-evaluated against the new version.

A rollback or emergency stop can close open actions decided under the replaced versions:

  • An emergency stop asks what to do with them: close pending approvals and approved calls that have not run (the default when it denies everything), close pending approvals only, or keep them (the default when it requires approval for everything).
  • A rollback keeps them by default. In the dashboard you can tick the option to also close the approvals still pending; closing approved calls as well is available only through the Cloud API.

Once such a change takes effect, an approver who tries to approve a request it closes gets 409 POLICY_VERSION_REPLACED. When approved calls are closed too, they are closed as superseded; an agent that repeats one before it is closed gets denied / POLICY_CHANGED.

Calls the policy allowed without approval are treated differently: when they are executed, the Gateway checks that the policy in force still allows them, and refuses them POLICY_CHANGED if it does not.

Consumption

An approval is used up the moment the Gateway starts executing the call. In one step that either completes entirely or not at all, it re-checks the API key, agent, tool, server, delegation and tool binding, checks that the call has not expired, moves the action from approved to executing, and records the execution. K-Block allows exactly one execution per action and per approval request, so an approval runs at most once, even when the agent sends the same call many times at once.

After that:

  • The upstream result is recorded with a signed result and a receipt, whether it succeeded or failed. A failed upstream call still uses up the approval.
  • A repeat with the same idempotency key returns the stored outcome without calling the upstream again.
  • An identical call without a key, or with a new key, is a new request and needs a new decision.
  • The approval request keeps the status approved. The action's status (succeeded, failed, expired) tells you what happened to the call.

See Action statuses for every status and Execution and results for what happens upstream.

Next steps