Docs Cards

Held calls and approvals

When an agent asks for something outside its Card, and the Card says Ask, KIFF holds the call for an authorized approver. Nothing runs until someone who may approve answers. If nobody answers before the hold expires, the call is refused; silence never approves. A Card that says Stop refuses the call instead, and nothing is held.

Where the owner hears about it

A held call reaches the owner in three places. All three lead to the same page, and the answer is given there.

  1. Needs you, in KIFF Cloud, and the bell. Every held call is listed with the agent, the action, the Card and how far over it is.
  2. Email. One email per held call, to the account owner, with what the agent wanted, which Card, how far over, and when the hold expires. Each button opens the call’s Needs you page.
  3. The agent’s chat, for clients KIFF has verified (Claude Code 2.1.287 or later). The chat shows a prompt to open the Needs you link. This is on by default for a personal account and off for a company’s, where the person running the agent is usually not the owner. Change it in Settings → Account → Held calls.

Your own app can hear about held calls too, through an alert target (POST /v1/me/alert-targets). A target with outcome owner_required receives each Card hold. Its payload’s hold has the held call’s id, its Needs you review_url and expires_at. Add hold_answered to also hear when a held call is answered, as type: "kiff.hold.answered" with the hold’s new status. That event goes only to targets that list it. It is also sent when a held call stops waiting without an answer: its agent withdrew it (status: "invalidated"), or nobody answered before it expired (status: "expired", within about two minutes of the expiry). Payloads carry an event_id to drop duplicates, and never the call’s arguments: read those through the API with your own credential.

Every delivery is signed. Creating a target returns its signing_secret once. Each request carries KIFF-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<body> with that secret. Recompute it, compare in constant time, and refuse a t more than a few minutes old. A target made before signing existed is sent unsigned until you call POST /v1/me/alert-targets/{id}/rotate-secret, which also replaces a leaked secret.

If a delivery fails, KIFF sends the same payload again after 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours and 12 hours, signing it afresh each time. Drop duplicates by event_id. If the last try fails too, about a day after the first, KIFF switches the target off. The list then shows enabled: false with a disabled_reason. POST /v1/me/alert-targets/{id}/enable switches it back on for the next event. A delivery refused because the address is private or internal is not retried.

Who can answer

A signed-in person with approval authority, or an external approver explicitly granted bounded authority by an admin.

External approval is optional behind Ask. Connect a separate approver from the Card. An unavailable service or an out-of-scope request leaves human review available until the original deadline. External answers record the service’s name and reason separately from the tool’s outcome.

  • The email and the chat only carry a link to the Needs you page. Neither can approve anything, so a forwarded email or a copied link answers nothing without a signed-in session that may approve.
  • The agent’s key cannot answer, and a “yes” typed in the chat is not an approval.
  • KIFF approval links always go to app.kiff.dev. A link to answer from anywhere else is not from KIFF.

What the approver sees, and what to rely on

The Needs you page shows the held call from KIFF’s own record of it, not from the agent’s description:

  • the agent, the tool, and each Card the call is outside of, with how far over it is;
  • every argument the call will be sent with, and amounts in the unit the tool declares. Long values are shortened in the list and shown in full on the call’s own page;
  • once answered, who answered and how.

Text the agent wrote, inside an argument or in the chat, is shown as a value, never as part of the page, and it decides nothing. If KIFF cannot read the call’s arguments, the page says so. Don’t approve a call on the agent’s account of what it does.

Approve once answers that one held call. The gateway ties the call to its exact arguments: an operation id reused with different arguments is refused, and a call without one is matched by its arguments for a limited time (see Duplicate below). Changing the order, the amount or any other value makes it a different call, which is held or decided on its own.

Stale, duplicate and lost requests

  • Stale. An unanswered hold expires and is refused. An approval is rechecked when the agent collects it: if the Card was revoked, has expired or now belongs to someone else, the approval no longer applies.
  • Duplicate. A call repeated with the same key, tool, arguments and kiff_operation_id reaches the same hold and the same answer, and an approved call is sent at most once. Without an operation id, an identical call counts as the same call only within the 10-minute identical-call window, or, for a read-only tool, only while the earlier call is unfinished (being decided, held or sent). After that it is a new call: held or decided on its own, and sent again if allowed. To collect an earlier held call after the window, use the kiff_operation_id that kiff_pending lists for it. Give every call that moves money an operation id.
  • Lost session. The answer is kept by KIFF, not by the agent’s session. A restarted agent reads it with kiff_pending and collects it with the original arguments and operation id; see below.

Limits of the approval channel

  • Keys. A key bound to an agent can never answer. An admin key that is not bound to an agent can, through the API. Don’t give one to an agent or put it where an agent can read it.
  • The approver’s own session. Anyone signed in as a person with approval authority can answer. Protect those accounts as you would any account that can move money.
  • Task context. A Card says what an agent may do, not which business task a call belongs to. Needs you shows the agent and the Card, not the task, so the approver judges the call on its own.
  • What the arguments mean. KIFF shows the values the tool will receive. Whether order 1042 is the right order is for the approver and the tool to judge; KIFF does not check business facts it is not given.

Answering from a device with a device key

An app on a phone, tablet or computer can hold a key that only that device can use, kept in its secure hardware behind its biometric or PIN: an iPhone’s or Mac’s Secure Enclave behind Face ID or Touch ID, Android’s Keystore, or a Windows TPM behind Windows Hello. KIFF then accepts its answers only with that key’s signature, so a stolen sign-in token approves nothing.

A device signed in without a key (such as the kiff CLI, or a computer with no secure hardware) can still read held calls, but it doesn’t answer them: KIFF refuses with device_required. Answer from a device with a key, or in KIFF Cloud.

  1. The key is bound when you sign the phone in. The app starts KIFF’s device sign-in (POST /v1/auth/device/start) with public_key, a P-256 public key as base64 SPKI DER, device_name, and platform (ios, ipados, macos, android or windows; ios when a key comes without one). When you approve the sign-in in your browser, KIFF shows the device’s name and the key’s fingerprint. Approving binds the key to that phone’s session. Nothing else can add a key to a session.
  2. Every answer from that session is signed. Approve once, reject and change Card need "signature": {"signed_at", "value"} in the body. value is a base64 DER ECDSA P-256 signature over the SHA-256 of this text, one field per line:
   kiff.answer.v1
   <tenant_id>
   <exception_id>
   <answer>                approve-once, reject or change-card
   <next_step>             empty unless a refusal names one
   <sha256 of the note>    hex; empty when there is no note
   <accounting_version>    the hold's, from GET /v1/me/card-exceptions/{id}
   <signed_at>             RFC 3339, the same string as in the body

KIFF refuses an answer from that session without a signature (signature_required), with one that doesn’t verify for this hold, answer, next step, note and accounting version (signature_invalid), or with signed_at more than 5 minutes from KIFF’s clock (signed_at_out_of_range). A hold is answered only once, so a signature can’t be replayed. Change Card still needs the right to manage Cards, and what it changes depends on why the call was held: - a total was reached: the Card gets room for this call in its current period only; the total stays as it was; - a per-action limit was passed: that limit is raised to this call’s amount, and any total the call would pass is raised to fit it. This lasts: later calls get the new limits; - a resource outside the Card’s scope: that resource is added to the scope. This lasts too.

A call that both reaches a total and passes a per-action limit takes two answers. The first adds the room only, and the call is held again for the per-action limit alone; changing the Card on that hold raises the limit. KIFF doesn’t relax both from one answer.

So an app should say “change the Card” for the lasting cases, not “allow this once”, and expect the call it changed the Card for to be held again when both limits were in the way.

The settled receipt keeps the proof: under the answer, device holds the phone’s name, its public key, the exact signed text (signed_message) and the signature. Anyone with the receipt can check that the phone signed that answer, without asking KIFF. 3. Sessions without a key answer as before. The dashboard and the CLI have no device key and send no signature. 4. A phone’s session does only a phone’s work. It reads your Cards and held calls, answers with its signature, revokes a Card, reads the receipt of a call (GET /v1/receipts/{trace_id}: a held call’s proposal_id is its receipt’s trace id), lists the agent drafts waiting for confirmation (GET /v1/me/agent-drafts, for someone who may issue Cards and give keys), and lists or signs out your devices. On the gateway it can list your connected tools but not change them. Everything else is refused (phone_session_forbidden): approver grants, moving or archiving Cards or changing who holds them, reconciling a call, and approving another device’s sign-in, which only your browser can do. Without this, a stolen phone token could mint a credential that answers without the phone. 5. Starting an agent from the phone is signed too. Creating the agent (POST /v1/me/agents), issuing its Card (POST /v1/me/mandates), minting its key (POST /v1/keys), and giving a Card extra room (POST /v1/me/mandates/{id}/extra-room) go through only with KIFF-Device-Signature: t=<signed_at>,v1=<signature>. The signature is base64 DER ECDSA P-256 over the SHA-256 of:

   kiff.request.v1
   <tenant_id>
   <METHOD>
   <path and query, as sent>
   <sha256 of the body>    hex
   <signed_at>             RFC 3339, the same string as in the header

A call without a signature is refused (signature_required), as is one more than 5 minutes old (signed_at_out_of_range) or one already made (signature_replayed). From a phone, a key must be bound to an agent: the phone never mints a key that acts for the account. It revokes one the same way, signed (DELETE /v1/keys/{id}), when its agent is removed; an account key or a connector key is revoked only in KIFF. Extra room takes a revision_id, so a retry grants it once, and expected_base_limit, so it isn’t given against a Card that changed since the phone read it. 6. A phone can turn on held-call alerts for its app. A signed POST /v1/me/alert-targets registers the app’s backend as the account’s alert target, if KIFF lists that backend for phone apps; any other address is refused (phone_alert_url_forbidden). The target receives owner_required and hold_answered only, and signing_secret comes back once, to the phone. Registering the same address again replaces the target and its secret. From the phone, GET /v1/me/alert-targets lists only that app’s targets, and a signed DELETE /v1/me/alert-targets/{id} removes one.

GET /v1/me/devices lists your devices with a key on this account: each one’s id (dev_…, made by KIFF), name, platform and key fingerprint. DELETE /v1/me/devices/{id} removes one: its key stops counting at once, and every sign-in from it ends.

The three answers

  • Approve once: waives the reviewed per-action threshold for this call only. Collection charges its full quantities against every applicable Card. Approval cannot waive a cumulative ceiling.
  • Revise the Card: a cumulative ceiling needs a separate management decision. Review extra room shows the quantity, measure and fixed end time before confirmation. The default is This window only; an end date or a permanent base revision must be selected explicitly. Recorded usage stays in place when the allowance expires.
  • Reject: the call is refused, and the agent is told not to retry it.

What the agent does

The agent gets a result that says the call is waiting for the owner, with the link and when the hold expires. Nothing has been sent to the tool.

To get the answer, the agent retries the same call: the same tool, the same arguments, and the same kiff_operation_id if it used one. Through the MCP gateway, an approved call is then forwarded once; a rejected or expired one is refused. A program can read the same facts in the result’s _meta (state, retry, retry_after_s, hold_expires_at).

If the run that made the call is cancelled, its runtime withdraws the held call with the agent’s own key: POST /v1/me/holds/{exception_id}/withdraw, where exception_id is in the held result’s _meta. The hold stops waiting, a later answer from the owner is refused, and nothing is sent. A retry of the same call is refused for good (mandate_withdrawn), even if it would fit a Card by then. A call the owner approved but the agent hasn’t collected is withdrawn too: KIFF sends an approved call only when the agent collects it, and a cancelled run never will. The approval stays in the call’s history. Withdrawing it again answers the same. A call already sent, refused or expired is not withdrawn: the reply is 409 already_answered, with where the call stands, so the runtime can record what KIFF did. Only the agent that made the call can withdraw it.

If other spending uses the available room before collection, the call stays Approved, waiting for room (approved_no_room). It has not been sent or refused. The owner can revise the applicable budget; collection then rechecks all Cards under the authorization locks. A new or tighter per-action threshold needs another review, rather than inheriting the earlier waiver. Previous approvals remain in the record.

Read the answer without executing the call

The gateway’s read-only kiff_pending tool lists this credential’s held, sending and unknown calls. A held entry includes the owner’s answer, the original tool arguments, and the kiff_operation_id used to collect it. Reading the list sends nothing to the business tool and uses no approval. Follow next_cursor as cursor to read older pages.

An answer of approved_no_room in kiff_pending is not a collectible approval (owner_answer on the API’s hold reads). Keep the original arguments and operation id and wait for room. An unavailable answer means the read failed, not that the owner refused.

Collect an approved call with the same tool and original arguments. Use its original operation id, or KIFF’s listed id if the original call had none. The latter lets the agent collect that held call after the usual 10-minute identical-call window. An unknown outcome is for reconciliation: the gateway does not automatically resend it.

Bring the answer back into Claude Code

The optional kiff plugin reads kiff_pending while this session has a held call. It shows the owner’s answer once and submits a turn telling the agent; the agent decides whether to collect an approved call. The plugin does not approve or execute the call itself.

The plugin shows an approved call waiting for room without starting a collection turn. When the call becomes collectible, the usual single notice and turn apply. After background checks stop, /kiff performs another read; it does not resend the business action.

If permissions stopped the reads, allow kiff_card and kiff_pending and run /kiff again. Checks continue through older pages and preserve recovery when KIFF cannot read the owner’s answer. An answer given in time still counts when read after an outage. Install steps and exact permission rules are in the plugin guide.

Approval and the tool’s outcome

Approval records who authorized the request. It does not establish that the tool performed it. KIFF records the tool’s reported outcome beside the authorization decision:

  • Tool reports success: the delivered call has a successful response. This is what the tool reported, rather than independent verification of the external action.
  • Tool reports an error: the call was authorized and delivered, but the tool returned an error. Do not describe it as done or assume that the error proves there were no partial effects.
  • Outcome unknown: the call was sent but its response was lost or timed out. Reconcile with the tool before deciding what to do next.
  • Outcome not recorded: an older call has no explicit outcome record. While its stored tool response is retained, KIFF uses that response; after it is purged, an approved call is shown with an outcome-not-recorded warning. Missing evidence does not establish success.

For example, a person can approve a €25 refund and the provider can refuse it because only €20 remains refundable. The approval and the refusal both belong in the record. Recording the outcome does not itself restore the Card’s budget.

How long a call waits

Each Card sets how long a held call waits: 10 minutes by default, from 1 minute to 7 days. A held call never runs on silence:

  • if nobody answers in time, the call is refused and nothing is sent;
  • a late answer is not accepted;
  • the agent has to ask again, as a new call.

The prompt in the chat does not extend this. An approval given before expiry remains valid when the agent collects it later; a late read is different from a late approval.

Next