HTTP API
Everything Kabu does is one HTTPS API, described by an OpenAPI 3.1 contract at
https://usekabu.com/openapi.yaml. The CLI, MCP and the Kabu apps all use it. The
reference has a page per area, generated from that contract; this page is what they share.
Base URL and keys
The base URL is https://usekabu.com/api, and every path starts with /v2. Send an API key as a bearer token:
curl https://usekabu.com/api/v2 -H "Authorization: Bearer $KABU_API_KEY"GET /v2 answers what the key may do: its scopes, its limits, the org’s API version and links to these docs. A live
key starts kabu_live_sk_; a test key, kabu_test_sk_, never reaches a phone. Create keys in the console, or let
kabu login --device mint one for a computer; Keys and scopes says which kind to use.
Notify, or ask, in one request
A notification is a message with a title:
curl https://usekabu.com/api/v2/messages \
-H "Authorization: Bearer $KABU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Build finished", "body": "All 412 tests passed."}'An ask is a message with an ask. Add ?wait=25 and the request holds until the human answers or 25 seconds pass:
curl "https://usekabu.com/api/v2/messages?wait=25" \
-H "Authorization: Bearer $KABU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: deploy-web-run-77" \
-d '{"title": "Deploy web to production?", "ask": {"type": "confirm"}}'The answer is 201 with the message. If the human answered in time, status is answered and
resolution.decision says what they decided (approve, reject, …). If not, status is pending: send the same
request again, with the same Idempotency-Key, or GET /v2/messages/{id}?wait=25, until it settles. That loop is the
whole integration, and it survives your process restarting.
Waiting
?wait=N (0 to 50 seconds) is the only way a request holds: on creating and reading a message, on the event list and
on claims. It returns as soon as the state settles, or when the time is up, with the current object either way. A
connection that drops never cancels an ask.
Idempotency
Every POST takes an Idempotency-Key. Retrying with the same key returns the same result instead of doing it again.
For messages, sessions and responses the key stays bound to the object for its whole life; for everything else, for
24 hours. The same key with a different body is 422 idempotency_key_reused;
the same key while the first request is still running is 409
idempotency_key_in_use.
Errors
An error is application/problem+json (RFC 9457) with a stable code, the status, a detail, a hint (one
sentence saying what to do), param (the field at fault, as a JSON pointer), retryable and the request_id. Its
doc_url is the code’s page here, under Errors. Branch on code; handle a code you do not know by
its status.
A request body is closed: a field the contract does not define is 400
unknown_parameter, which catches typos. A response is open: new fields and new
enum values appear without notice, so ignore what you do not know.
Paging
Lists take limit (up to 100) and return data, has_more and next_cursor. Pass next_cursor as after for the
next page.
Limits
Every response carries RateLimit-Policy and RateLimit headers. A rate limit is 429
rate_limited with Retry-After; an exhausted monthly quota is 429
quota_exceeded, with no Retry-After, because retrying will not help. The numbers are
in GET /v2 and in Limits.
Versions
Paths carry the major version, /v2, and the contract’s version is in its info.version, printed on every reference
page. Your org is pinned to an API version (api_version in GET /v2), and changes that only add fields, values or
event types never move it. Webhook deliveries carry their endpoint’s version in a Kabu-Version header.
Typed clients
The contract describes every operation and type, so a generator can write a client for your language. There is no
Kabu SDK: generate a typed client from the contract instead. Its servers entry is the base URL, so a generated
client needs nothing but your key.
TypeScript, with openapi-typescript and openapi-fetch:
npx openapi-typescript https://usekabu.com/openapi.yaml -o kabu.d.ts
npm install openapi-fetchimport createClient from "openapi-fetch";
import type { paths } from "./kabu";
const kabu = createClient<paths>({
baseUrl: "https://usekabu.com/api",
headers: { Authorization: `Bearer ${process.env.KABU_API_KEY}` },
});
// Ask, and ask again with the same key until the human answers.
export async function approved(runId: string): Promise<boolean> {
for (;;) {
const { data, error } = await kabu.POST("/v2/messages", {
params: { query: { wait: 25 }, header: { "Idempotency-Key": `deploy-${runId}` } },
body: { title: "Deploy web to production?", ask: { type: "confirm" } },
});
if (error) throw new Error(`${error.code}: ${error.detail ?? error.title}`);
if (data.status !== "pending") return data.resolution?.decision === "approve";
}
}Python, with openapi-python-client:
pip install openapi-python-client
openapi-python-client generate --url https://usekabu.com/openapi.yaml --meta none --output-path kabu_clientGo, with oapi-codegen:
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
curl -fsSO https://usekabu.com/openapi.yaml
oapi-codegen -generate types,client -package kabu openapi.yaml > kabu.gen.go