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.
- 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.
- 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.
- 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_idreaches 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 thekiff_operation_idthatkiff_pendinglists 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_pendingand 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
1042is 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.
- The key is bound when you sign the phone in. The app starts KIFF’s
device sign-in (
POST /v1/auth/device/start) withpublic_key, a P-256 public key as base64 SPKI DER,device_name, andplatform(ios,ipados,macos,androidorwindows;ioswhen 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. - Every answer from that session is signed. Approve once, reject
and change Card need
"signature": {"signed_at", "value"}in the body.valueis 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
- Set what is held and for how long → KIFF Cards.
- Add room or change a limit → Limits, warnings and extra room.
- What the agent sees, field by field → Use the KIFF MCP gateway.
- Install notices and recovery in Claude Code → The KIFF skill for coding agents.