Everything the product does, callable
The dashboard is one client of a documented API. 27 operations, a closed set of 83 stable error codes, two generated clients and an MCP server that dispatches through the same handlers rather than reimplementing them.
// VERSIONS
The current API version is v2
v2 is the same handler group under the nested error envelope this project's standard prescribes, plus permission-filtered capability discovery. It is stable and has no sunset date.
v2
CurrentNested error envelope, capability discovery filtered by what your credential may actually do. Start here for anything new.
v1
DeprecatedDeprecated on 2026-09-07 and sunsets on 2027-09-07. It keeps its flat error envelope unchanged until then — both generated clients read it directly, so it is never mutated in place. Every response carries Deprecation, Sunset and a successor-version Link header.
// CLIENTS
Two clients, generated from the document
Neither client is hand-written, and neither is allowed to drift: a build gate fails when the OpenAPI document moves and the committed clients do not. The snippets below are slices of the examples CI runs against a live API, so they are the calls that actually work rather than the calls someone once documented.
@ai-stack-qr/sdkPython ai-stack-qrOne thing these snippets keep from their source: the imports resolve against the client in this repository rather than the published package, because that is how the file CI runs is written. Swap the import for the package name above and everything else is unchanged.
import { AiStackQrClient, ApiError } from "../src/index";
const apiKey = process.env["AI_STACK_QR_API_KEY"];
const baseUrl = process.env["AI_STACK_QR_BASE_URL"] ?? "http://localhost:18005";
if (!apiKey) {
console.error("Set AI_STACK_QR_API_KEY to an sk_ key with the write scope.");
process.exit(1);
}
const client = new AiStackQrClient({ apiKey, baseUrl });const created = await client.createQr(
{
payloadType: "url",
payload: { value: "https://example.com/quickstart" },
accessMode: "link",
title: "SDK quickstart",
},
{ idempotencyKey },
);
console.log("created", created.id, created.gate_url);from ai_stack_qr import AiStackQrClient, ApiError # noqa: E402
api_key = os.environ.get("AI_STACK_QR_API_KEY")
base_url = os.environ.get("AI_STACK_QR_BASE_URL", "http://localhost:18005")
if not api_key:
print("Set AI_STACK_QR_API_KEY to an sk_ key with the write scope.", file=sys.stderr)
raise SystemExit(1)
client = AiStackQrClient(api_key=api_key, base_url=base_url)payload = {
"payloadType": "url",
"payload": {"value": "https://example.com/quickstart"},
"accessMode": "link",
"title": "SDK quickstart",
}
created = client.create_qr(body=payload, idempotency_key=idempotency_key)
print("created", created["id"], created["gate_url"])Retries are safe, and a wrong retry is refused
Send an idempotency key per intent and reuse it on every retry of that intent: the replay returns the original response instead of creating a second code. Sending the same key with a different body is a client bug, and it is answered as one rather than silently doing the wrong thing.
try {
await client.createQr(
{
payloadType: "url",
payload: { value: "https://example.com/different" },
accessMode: "link",
},
{ idempotencyKey },
);
throw new Error("expected a 409 for a reused Idempotency-Key");
} catch (err) {
if (!(err instanceof ApiError) || err.status !== 409) throw err;
console.log("reusing the key for different content was refused:", err.reason);
}// MCP
An MCP server that inherits its own authorization
Tools dispatch back through the published API rather than reimplementing handlers, so scopes, tenant isolation, rate limits, idempotency and the audit trail apply to a tool call exactly as they apply to an HTTP one. There is no second code path to keep in step and no second place for a permission check to be forgotten.
Scopes, not roles
A key carries action scopes — 8 of them. Reading a payload is its own scope and is deliberately outside the legacy read/write expansion, so an older key never gained plaintext access on the day it shipped.
Refusals bind the agent
No tool can create a public code or weaken an existing gate. The refusal lives in the tool layer, so a human can still make that call in the dashboard while an automated caller cannot make it for them.
A pinned tool contract
Tool names, versions, required arguments, enum values, output fields, scopes and effect annotations are compared against a committed snapshot on every build. An agent's stored configuration does not break because a release renamed something.
The rules an agent should read before its first call are published at /agents.md, and the site's machine index is at /llms.txt.
// CONTRACTS
Everything above is machine-readable
- /api/v1/openapi.json — the authoritative contract. It supersedes anything written on this page.
- /api/v1/errors — the closed error catalogue as JSON. Branch on
code, never onmessage. - /agents.md — preconditions, what is refused, how to read a failure, when to stop.
- /llms.txt — the index a model follows to find the rest.