Docs

Quickstart

Make three protected tool calls through the K-Block Gateway (allowed, held for approval, denied) and verify a receipt offline.

This page takes you from three values K-Block gives you to three protected tool calls and a receipt verified on your own machine. You install the client package from your package manager (and @kblockai/sdk to verify receipts); you need no K-Block source code.

The Gateway decides every call with your organization's policy: it executes an allowed call once, holds a call that needs a person's approval until someone decides it, and refuses a denied call with an explanation. It records a signed receipt of every executed call, which you can verify offline.

What you need

From K-Block:

ValueWhat it looks likeWhere to get it
Gateway URLhttps://…The dashboard shows the Gateway's MCP endpoint (…/mcp) with a copy button right after you create a gateway API key, and in the last step of the setup guide on Overview. If it does not show it, contact K-Block support. See API keys.
Gateway API keykb_live_… or kb_test_…API keys, New API key, kind Gateway key, bound to your agent. The full key is shown once. See API keys.
Gateway KIDkid:kb:…The Gateway's identity, to check that a receipt comes from your Gateway. The dashboard shows it with a copy button right after you create a gateway API key. It also shows it to owners and admins as the delegate when they sign a delegation on an agent's page (Agents). If the dashboard does not show it after you create a key, contact K-Block support.

Your organization must already be set up in the dashboard. The programs on this page assume this setup, which Your first protected action walks through:

  • your MCP server's tools exposed as lookup_order (action orders.read) and refund (action refund.issue, amount pointer /amount, currency USD, currency pointer /currency);
  • an active policy created from the template Support refunds with its default parameters: refunds in USD are automatic up to 100 and need approval up to 1000; larger refunds are denied;
  • the agent of your API key assigned to that policy, with a delegation that grants these two actions and has no spending limit. The template's delegation hint suggests a maximum amount, but with a spending limit every call of a tool without an amount mapping, such as lookup_order, is denied (CONSTRAINT_VIOLATION);
  • a member with the owner, admin or approver role whom the policy's approval rule admits (the template's rule admits all three; on the free Developer plan, that member is you, the owner) and who has set up a personal K-Block key in the dashboard before you run the program (the approvers of a request are fixed when the call is decided). Approvals are signed in the browser with that key. Without such a member, a call that needs approval is denied with NO_APPROVERS instead of held;
  • dashboard access for whoever downloads the receipt.

The template decides by action, not by tool name. With other tools, actions or limits, change the tool names, arguments and amounts in the programs to calls your policy allows, holds for approval and denies.

On your machine: Node.js 22.6 or later for TypeScript, or Python 3.10 or later. Verifying a receipt needs Node.js 22 or later in both cases: the Python package has no verifier.

Install

In a directory of its own:

mkdir my-agent
cd my-agent
npm init -y
npm pkg set type=module
npm install --save-exact @kblockai/client@0.1.0-rc.1 @kblockai/sdk@0.1.0-rc.1

npm init -y gives the directory its own package.json. Without one, npm install installs into the nearest package.json above the directory and reports no error. Both packages are ESM only, hence type=module. @kblockai/client calls the Gateway; @kblockai/sdk verifies receipts.

Configure the Gateway URL and the API key

The client takes both values as arguments: connectKBlock({ gatewayUrl, apiKey }) in TypeScript, connect_kblock(gateway_url, api_key) in Python. The URL can be the Gateway's base URL or its MCP endpoint (…/mcp). The client reads no environment variable itself. The programs below read them from KBLOCK_GATEWAY_URL and KBLOCK_API_KEY, two environment variables of their own, which the commands that run them set; your application can take them from any configuration or secret store.

Keep the key out of source code and logs; the client never prints its secret part. The Gateway URL must be https:. Plain http: is accepted only for a loopback host (the machine the program runs on), unless you set the allowInsecureHttp option (allow_insecure_http=True in Python), which sends the key unencrypted. The TypeScript client lists the exact rule.

Make three protected calls

quickstart.ts
import { randomUUID } from "node:crypto";
import { connectKBlock, explainResult } from "@kblockai/client";

const gatewayUrl = process.env.KBLOCK_GATEWAY_URL ?? "";
const apiKey = process.env.KBLOCK_API_KEY ?? "";
if (gatewayUrl === "" || apiKey === "") {
  console.error("Set KBLOCK_GATEWAY_URL and KBLOCK_API_KEY to the Gateway URL and the agent's gateway API key that K-Block gave you.");
  process.exit(2);
}

const client = await connectKBlock({ gatewayUrl, apiKey });

// 1. Allowed: the Gateway executes the call once and records a receipt.
const r = await client.callTool("lookup_order", { order_id: `ord_${randomUUID()}` });
if (r.status === "completed") console.log("1. completed:", r.structuredContent, "receipt", r.receiptId, `(${r.receiptObjectId})`);
else console.log("1.", explainResult(r));

// 2. Approval required: nothing runs until a person approves the call in the K-Block dashboard.
//    waitForApproval repeats the same call until the decision lands; it then executes once.
const pending = await client.callTool("refund", { order_id: `ord_${randomUUID()}`, amount: "250.00", currency: "USD" });
console.log("2.", explainResult(pending));
if (pending.status === "approval_pending") {
  console.log(`   waiting for a person to approve request ${pending.approvalRequestId} in the K-Block dashboard`);
  const done = await client.waitForApproval(pending, { timeoutSeconds: 600, waitSecondsPerRequest: 30 });
  if (done.status === "completed") console.log("   completed:", done.structuredContent, "receipt", done.receiptId, `(${done.receiptObjectId})`);
  else console.log("  ", explainResult(done));
}

// 3. Denied: the policy's explanation; nothing was executed.
const denied = await client.callTool("refund", { order_id: `ord_${randomUUID()}`, amount: "2500.00", currency: "USD" });
console.log("3.", explainResult(denied));

await client.close();

Run it with Node.js 22.6 or later, which runs TypeScript by stripping the types:

export KBLOCK_GATEWAY_URL="<the Gateway URL from K-Block>"
export KBLOCK_API_KEY="<the agent's gateway API key>"
node --experimental-strip-types quickstart.ts

In PowerShell:

$env:KBLOCK_GATEWAY_URL = "<the Gateway URL from K-Block>"
$env:KBLOCK_API_KEY = "<the agent's gateway API key>"
node --experimental-strip-types quickstart.ts

What each call does

The Gateway decides each call with your organization's policy; the client decides nothing.

CallOutcomeWhat happens
lookup_ordercompletedThe policy allows orders.read. The Gateway executes the call once; the result carries the tool's answer and the IDs of the call's receipt.
refund of 250.00 USDapproval_pendingNothing runs yet. The program prints the approval request's ID and waits.
refund of 2500.00 USDdeniedNothing runs. The code is POLICY_DENIED: the rule refund-over-limit denies refunds above 1000 USD.

For the second call, a member who may approve it (on the free Developer plan, you, the owner) opens Approvals in the dashboard, opens the request for refund, checks the exact call and chooses Approve and sign (or Reject and sign). Meanwhile waitForApproval repeats the same call with the same arguments and the same idempotency key, asking the Gateway to hold each request up to 30 seconds. Once the call is approved, the Gateway executes it once and the result is completed. A rejected call ends as rejected; one nobody decided in time ends as expired.

A program that stops waiting can keep the call (pending.call holds the tool, the arguments and the idempotency key) and resume it later with client.resume(pending) (kb.resume(pending) in Python). It never runs twice. Resume it before the approval request expires (4 hours for the template's approval rule): an approved call that is not repeated by then expires without running. Calling callTool again instead sends a new idempotency key, which makes a new call that needs its own approval.

explainResult(result) (explain_result in Python) turns any outcome into text you can show a person or hand to a model. For a denial it gives the code, the policy's explanation and "Nothing was executed." Checks that K-Block makes outside the policy deny with their own codes, for example INVALID_AMOUNT when the amount cannot be read or the call names another currency than the tool's. How much of the policy an agent sees is your organization's choice: Settings, What agents see of a decision.

Get the receipt

Every executed call has a receipt: a portable .kbundle file with every signed object of the call and the identity documents needed to check them. The program printed the receipt's ID and, in parentheses, its object ID (ko_…), which the dashboard shows.

In the dashboard, open Receipts, then the receipt, then Download .kbundle. The browser saves it as <object ID>.kbundle. From an action, Open and verify receipt leads to the same page. See Receipts.

A program can download it from the Cloud API instead, with a management API key that has the receipts:read scope:

GET /api/v1/orgs/<organization ID>/receipts/<receipt ID>/bundle HTTP/1.1
Host: <Cloud API host>
Authorization: Bearer <management API key>

Send the request to the Cloud API base URL that K-Block gives you, which can differ from the dashboard's address. If a request with a valid key is refused with HTTP 401 (UNAUTHENTICATED) and the message sign in required, the address you used does not pass API keys on to the Cloud API: use the Cloud API base URL from K-Block, and contact K-Block support if you do not have it. The organization ID is in the dashboard's addresses (/orgs/<organization ID>/…). The agent's gateway API key cannot download receipts.

Rejected and cancelled calls also end with a receipt. A call denied when it was decided, and a call that expired, end without one.

Verify the receipt offline

Verification uses @kblockai/sdk and Node.js 22 or later, also for Python agents. The TypeScript directory above has the package already; otherwise install it in a directory of its own:

mkdir kblock-verify
cd kblock-verify
npm init -y
npm pkg set type=module
npm install --save-exact @kblockai/sdk@0.1.0-rc.1
verify-receipt.ts
import { readFile } from "node:fs/promises";
import { renderText, verifyBundleJson } from "@kblockai/sdk/verify";

const [file, gatewayKid] = process.argv.slice(2);
if (!file) {
  console.error("usage: node --experimental-strip-types verify-receipt.ts <receipt.kbundle> [<gateway KID>]");
  process.exit(2);
}

const report = verifyBundleJson(await readFile(file, "utf8")); // never throws on an invalid bundle
console.log(renderText(report)); // the seven checks, then RESULT: VALID or RESULT: INVALID

// VALID means the bundle is complete and consistent, and every object in it is signed by the keys
// its identity documents list. Whether YOUR Gateway issued it is a separate question: compare the
// issuer with the Gateway KID that K-Block gave you.
const fromGateway = gatewayKid === undefined || report.issuer === gatewayKid;
if (!fromGateway) console.log(`issuer ${report.issuer} is not the Gateway identity ${gatewayKid}`);
process.exit(report.valid && fromGateway ? 0 : 1);
node --experimental-strip-types verify-receipt.ts "<path of the downloaded .kbundle file>" "<the Gateway KID from K-Block>"

It prints the receipt's text report: the receipt and its issuer, the seven checks (Bundle format, Identity chains, Signature chain, Receipt coverage, Graph integrity, Workflow & authorization, Receipt summary), who requested, approved and executed the call, then RESULT: VALID and a note on what a receipt does not prove. It exits with 0 (1 when the receipt is invalid or its issuer is not the KID you passed). It needs no network and no K-Block account. The dashboard's Verify in this browser button on a receipt runs the same verifier and also compares the issuer with your Gateway: VALID means your Gateway issued the receipt, NOT THIS GATEWAY that it did not. CONSISTENT means every check passed but the dashboard could not establish which Gateway issued the receipt: compare the issuer KID with the Gateway KID that K-Block gave you, and contact K-Block support if the dashboard keeps showing CONSISTENT.

A valid receipt from your Gateway proves that the request, the execution and the result were signed by your Gateway on behalf of your organization, under the delegation your organization signed with its own key; that an approval was signed with the approver's personal key, for exactly the approved call; and that nothing was altered after signing. Which agent and which API key made the call is the Gateway's own signed statement: the agent signs nothing. A receipt does not prove that the real-world effect happened (the result is a signed claim), and on its own it does not prove which Gateway issued it, which is why the program compares the issuer with your Gateway's KID. See What a receipt proves.

Troubleshooting

What you seeCause and fix
KBlockAuthError with code MALFORMED_API_KEYThe key is not in the kb_<live or test>_<prefix>_<secret> format; nothing was sent. Check that the whole key was copied.
KBlockAuthError with code UNAUTHORIZED (HTTP 401)The Gateway refused the key: not a gateway API key (a management key, for example), unknown, revoked or expired, its agent is disabled, or the member who created it left the organization or lost the role to create keys. Ask whoever manages your organization's keys.
TypeError (TypeScript) or ValueError (Python) about the Gateway URLThe URL is not https: (plain http: only for a loopback host), or it contains a user name, a query or a fragment.
KBlockHttpError (for example HTTP 404, or a redirect, which is never followed)The URL is not the Gateway's. Use the Gateway URL that K-Block gave you (see What you need).
KBlockProtocolErrorThe URL points at an MCP server that is not a K-Block Gateway.
KBlockUnavailableError with code NETWORK_ERRORThe Gateway could not be reached, or the connection broke. When outcomeUnknown (Python: outcome_unknown) is false, nothing was sent. When it is true, the Gateway may have processed the call: repeat it only with the same idempotency key, never as a new call: the error's call holds it (client.resume(error.call) / await kb.resume(e.call); a call sent without a key has none, so check its effect first).
denied with code NO_APPROVERSThe policy requires approval, but no member who may approve has set up a personal K-Block key in the dashboard.
denied with code NO_DELEGATIONNo valid delegation grants the tool's action to this agent: sign one on the agent's page. A delegation whose Valid from is still in the future does not count yet.
denied with code CONSTRAINT_VIOLATIONThe delegation's spending limit refused the call: its amount is above the limit or in another currency, or the tool has no amount mapping. Sign a delegation without a spending limit for such tools.
ERR_UNKNOWN_FILE_EXTENSION ".ts"You ran a TypeScript file without type stripping. Use node --experimental-strip-types (Node.js 22.6 or later), tsx, or compile it.
UnicodeEncodeError: 'charmap' codec can't encode character (Python on Windows)The program printed a character such as ≤ to a file or a pipe in the ANSI code page. Set PYTHONUTF8=1, or reconfigure the output to UTF-8 as quickstart.py does.

More in Common problems and Gateway and tools.

Next steps