# HTTP API

> Call Kabu from any language over HTTPS. Keys, a first ask in one request, waiting with ?wait=, idempotency, paging, versions and typed clients.

Everything Kabu does is one HTTPS API, described by an OpenAPI 3.1 contract at
[`https://usekabu.com/openapi.yaml`](/openapi.yaml). The CLI, MCP and the Kabu apps all use it. The
[reference](/docs/api/messages) 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:

```sh
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](/docs/concepts/keys) says which kind to use.

## Notify, or ask, in one request

A notification is a message with a `title`:

```sh
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:

```sh
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`](/docs/errors/idempotency_key_reused);
the same key while the first request is still running is `409`
[`idempotency_key_in_use`](/docs/errors/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](/docs/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`](/docs/errors/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`](/docs/errors/rate_limited) with `Retry-After`; an exhausted monthly quota is `429`
[`quota_exceeded`](/docs/errors/quota_exceeded), with no `Retry-After`, because retrying will not help. The numbers are
in `GET /v2` and in [Limits](/docs/concepts/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`:

```sh
npx openapi-typescript https://usekabu.com/openapi.yaml -o kabu.d.ts
npm install openapi-fetch
```

```ts
import 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`:

```sh
pip install openapi-python-client
openapi-python-client generate --url https://usekabu.com/openapi.yaml --meta none --output-path kabu_client
```

Go, with `oapi-codegen`:

```sh
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
```
