# Public API and CLI

This is the complete API/CLI reference, published at
[kbdrop.io/docs/api/reference.md](https://kbdrop.io/docs/api/reference.md).
For a guided introduction, start with the [quickstart](https://kbdrop.io/docs/api).

kbDrop exposes ready knowledge bases through ordinary HTTPS/JSON. The supported
first-party `kb-drop` CLI owns OAuth, credential storage, retries, idempotency,
and response parsing. Direct API callers receive one complete JSON answer by
default; server-sent events (SSE) remain an explicit compatibility option.

## Recommended: `@kbdrop/cli`

Node.js 24 or newer is supported on current macOS, Windows, and Linux releases.
OAuth requires an available, unlocked native credential store: macOS Keychain,
Windows Credential Manager, or Linux Secret Service with a working user D-Bus
session. Device login changes the browser flow; it still needs that store.
An environment API key does not require a credential store.

Run without installing globally and inspect the active credential source:

```bash
npx @kbdrop/cli@latest auth status --json
```

`auth status` reports local credential availability, not a server-side token
validation. It exits `0` with `ok: true` even when logged out. Read its data:

| Status result | Next action |
| --- | --- |
| `data.authenticated: true`, `data.source: "environment"` | Asking/searching uses `KB_DROP_API_KEY`, then `KB_DROP_MANAGEMENT_KEY`, before saved OAuth. A management key needs `knowledge:query`. Use the existing credential; do not launch OAuth. |
| `data.authenticated: true`, `data.source: "oauth"` | Use the saved login; the CLI refreshes it when needed. |
| `data.authenticated: false` | Complete browser or device login below. |
| Exit `3`, `keychain_unavailable` | Configure/unlock the native store and its session, or use an existing environment API key supplied by a secret manager. Retrying login/device login does not fix a missing store. |
| Exit `3`, `api_key_invalid` | Correct the environment credential through the secret manager; OAuth cannot override an invalid environment key. |
| Exit `3`, `management_key_invalid` | Correct `KB_DROP_MANAGEMENT_KEY` through the secret manager. A saved login does not override an invalid environment key. |
| `data.management: null` | Needed only to [create knowledge bases](#create-knowledge-bases): run `auth login --manage`, or use a `KB_DROP_MANAGEMENT_KEY` supplied by a secret manager. |

For an unauthenticated session, `auth login` opens the system browser and uses
Authorization Code with S256 PKCE and an exact
`http://127.0.0.1:<port>/callback` loopback callback:

```bash
npx @kbdrop/cli@latest auth login
```

For a remote terminal with a native credential store, open the displayed URL
in a browser on another device and complete sign-in and consent:

```bash
npx @kbdrop/cli@latest auth login --device --no-browser
```

The device flow honors the authorization server's pending and slow-down
responses. A plain login can only read and query knowledge bases in the
signed-in account. `auth login --manage` also requests `knowledge_bases:read`
and `knowledge_bases:write`, which let it create knowledge bases and follow
their ingestion once the account owner approves them on the consent screen. No
login can delete knowledge bases or administer keys. Access tokens last at
most 15 minutes. Rotating refresh tokens and their
grant expire within the configured server bounds; replay of an already-used
refresh token revokes the complete grant family.

Interactive credentials are saved only in the native operating-system
credential store (macOS Keychain, Windows Credential Manager, or Linux Secret
Service). The CLI fails explicitly if no native store is available; it does not
fall back to a plaintext token file.

For CI or unattended jobs, have the runner's secret manager inject an existing
API key as `KB_DROP_API_KEY`. Set the non-secret `KB_DROP_KNOWLEDGE_BASE_ID` to
the UUID of a knowledge base plugged into that key's interface. Never paste an
actual key into chat, a command argument, or a shell `export` recorded in history.

For asking/searching, `KB_DROP_API_KEY` takes precedence over
`KB_DROP_MANAGEMENT_KEY`, which takes precedence over a saved OAuth login.
Management commands use the management key or a saved login with approved
management scopes; they never send an API key. The CLI has no
secret-bearing `--api-key` option. `auth status --json` reports the active
source without displaying a credential; `auth logout` removes the local OAuth
credential and attempts server revocation.

### Private input and resumable questions

Save your question as UTF-8 in `question.txt` with an editor or file tool.
Use a tool's stdin channel or an existing file to keep its contents out of
process arguments and shell history. A literal `printf`/`echo` pipeline still
puts the question in the shell command; stdin only protects the receiving
process's arguments.

Generate and save a non-secret UUID **once per new logical question**:

```bash
export KB_DROP_REQUEST_ID="$(node -p 'crypto.randomUUID()')"
```

Run the question with that saved ID:

```bash
npx @kbdrop/cli@latest ask \
  --knowledge-base "$KB_DROP_KNOWLEDGE_BASE_ID" \
  --input-file ./question.txt \
  --idempotency-key "$KB_DROP_REQUEST_ID" \
  --json
```

If a response is lost, rerun **only the `ask` command**, keeping the same ID,
knowledge base, input, conversation, and response options. Restore that saved
ID when resuming from a different shell; do not rerun UUID generation. A new
invocation without `--idempotency-key` creates a new UUID and can submit another
turn even if the earlier request completed on the server.

The CLI bounds its automatic retries. After it exhausts them, retain the key
and report the error instead of looping. A deliberately resumed attempt must
use the same key and respect `Retry-After`. `operation_in_progress` can be
retried with that key; `operation_failed` is terminal for it, and
`idempotency_mismatch` means the request changed. Do not automatically use a
fresh key to bypass either terminal response. A fresh key starts a separate
logical turn and requires an intentional new request.

### Commands and stable output

```text
kb-drop auth login [--manage] [--device] [--no-browser]
kb-drop auth status [--json]
kb-drop auth logout
kb-drop ask --knowledge-base <uuid> --input <text|-> [controls]
kb-drop ask --knowledge-base <uuid> --input-file <path> [controls]
kb-drop search --knowledge-base <uuid> --query <text|-> [--json]
kb-drop knowledge-bases create --file PATH|--zip PATH|--url URL|--video-url URL [options]
kb-drop knowledge-bases status ID [--watch]
kb-drop knowledge-bases list [--limit N] [--cursor CURSOR]
kb-drop knowledge-bases retry ID [--wait]
kb-drop knowledge-bases recrawl ID [--wait]
kb-drop completion bash|zsh|fish
```

CLI 0.2.0 requires `--knowledge-base` (or `KB_DROP_KNOWLEDGE_BASE_ID`). In
later releases it is optional: without it, `ask` and `search` call
[`/v1/messages`](#ask-every-plugged-in-knowledge-base) and
[`/v1/search`](#search-every-plugged-in-knowledge-base), which use everything
plugged into the credential's interface.

Human-readable output is the default. `--json` writes exactly one versioned
object to stdout on success:

```json
{
  "schema_version": "1",
  "ok": true,
  "command": "ask",
  "data": { "type": "response.completed", "answer": "..." }
}
```

Structured failures are written only to stderr. Exit statuses are stable:

| Status | Meaning |
| ---: | --- |
| `0` | Completed with an answer/result |
| `2` | Invalid command, option, or input |
| `3` | Authentication or authorization failure |
| `4` | Terminal API request failure |
| `5` | Retry exhausted or service unavailable |
| `6` | Network failure or timeout |
| `7` | Invalid/unexpected response representation |
| `8` | Valid completion with `insufficient_evidence: true` |
| `9` | Ingestion failed or was cancelled (`knowledge-bases` with `--wait` or `--watch`) |
| `10` | Stopped waiting before ingestion finished. If `next_action` is `review`, answer the review; otherwise run the same command again (without `--idempotency-key`, `create` and `recrawl` resume only within 24 hours) |
| `70` | Unexpected local client failure |

Unless `--idempotency-key` is supplied, `ask` generates one UUID per invocation
and reuses it for that invocation's retries only. The default per-attempt
timeout is 30 seconds and the default
retry count is two; use `--timeout 1000..120000` and `--retries 0..5` to bound
them. The CLI honors `Retry-After` on `429`, retryable `503`, and in-progress
idempotent replays, and uses capped exponential backoff with jitter for
transport failures. A server-directed wait is capped at 30 seconds so the retry
budget remains finite. It never retries authentication, validation, or a
completed operation. `--stream-compat` uses SSE internally but produces the
same finalized human or `--json` output.

Both commands accept `--input -`, automatic piped stdin, and `--input-file`;
search also accepts `--query -`. The input producer must avoid putting the
question in the shell command. Error rendering excludes credentials,
authorization headers, request bodies, excerpts, signed URLs, and input paths.

Install a chosen version globally with `npm install --global @kbdrop/cli@<version>`.
Upgrade with `npm install --global @kbdrop/cli@latest`. Generate shell completion
with `kb-drop completion bash`, `zsh`, or `fish` and source the result according
to that shell's normal completion setup.

### Create knowledge bases

`kb-drop knowledge-bases` (alias `kb`) creates a knowledge base from a file, a
ZIP archive, a public website, or a video URL, and follows its ingestion.
Creation uses the account's ingestion quota, so run it only when the user asks.
It authenticates with `KB_DROP_MANAGEMENT_KEY`, a `kb_mgmt_...` key from the
Management keys page supplied by a secret manager, or a login from
`auth login --manage`. `KB_DROP_API_KEY` is never sent to these endpoints.

Save one UUID per knowledge base and pass it with `--idempotency-key`:

```bash
export KB_DROP_CREATE_ID="$(node -p 'crypto.randomUUID()')"
npx @kbdrop/cli@latest knowledge-bases create \
  --idempotency-key "$KB_DROP_CREATE_ID" \
  --zip ./product-docs.zip --name "Product docs" --wait --json
```

The server keeps that key for as long as the knowledge base exists, so running
the same command with it again, at any later time, resumes the same knowledge
base and uploads only the parts not yet confirmed. If the upload has since
expired, failed, or been cancelled, the command fails with
`upload_unavailable` (exit `4`) rather than creating a second knowledge base.

Without `--idempotency-key`, the CLI saves a key of its own before sending
anything and keeps it only while the command is unfinished. Running an
interrupted or timed-out command again within 24 hours resumes the same way,
with one exception: if the interrupted file or ZIP upload has since expired,
failed, or been cancelled, the CLI starts a new knowledge base under a new key
and reports `data.resumed: false`. An upload with no newly confirmed part for
`idle_timeout_seconds` (30 minutes by default) can be cancelled when the
account starts another upload. A command that finished forgets its key,
including a run without `--wait`, so running it again creates another
knowledge base. After 24 hours, an interrupted command also starts over, so
look for the earlier knowledge base first with `kb-drop knowledge-bases list`
or `status ID`.

`recrawl` resumes the same way, and `retry` follows the attempt it already
queued; each reports `data.resumed`. On success, `data.knowledge_base.id` is
the ID to query. Exit `9` carries the failure in `data.ingestion_job.failure`.
On `5`, `6`, or `10`, reuse the same command and key, but answer any pending
review first. The published CLI 0.2.0 can keep waiting at a paused review;
inspect `status ID --json` without `--watch` and follow the job's
`links.review_page` or use the HTTP review endpoint. Every option is listed by
`kb-drop knowledge-bases --help`, and the same operations are available over
HTTPS through the [management API](https://kbdrop.io/docs/api/management.md).

## Direct API authentication

Direct callers may send:

- a saved OAuth access token issued to `kb-drop-cli`, with the required
  `knowledge:read` or `knowledge:query` scope;
- a `kb_live_...` API key. Each key belongs to one interface on the signed-in
  home page's switchboard, made with New API key there or with a knowledge
  base's Developers tab, and is valid only for the knowledge bases plugged into
  that interface; or
- an account-scoped `kb_mgmt_...` management key with `knowledge:query` for
  messages/search, or `knowledge:read` for source access.

The endpoints that create knowledge bases and report ingestion take a
`kb_mgmt_...` management key or an OAuth token granted `knowledge_bases:read`
or `knowledge_bases:write` instead; see the
[management API](https://kbdrop.io/docs/api/management.md).

```http
Authorization: Bearer <oauth-access-token-or-api-key>
```

OAuth metadata is published at
`/.well-known/oauth-authorization-server`. The authorization server supports
Authorization Code + S256 PKCE, Device Authorization, refresh-token rotation,
and revocation for the public client ID `kb-drop-cli`.

All three credential types have a PostgreSQL-backed rolling one-minute rate limit.
Responses include the applicable `X-RateLimit-Limit`,
`X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. A rejected request
returns `429` and `Retry-After`.

For the curl examples below, have a secret manager supply a protected
`authorization.headers` file containing the Authorization header. Prepare the
JSON request files with an editor or file tool. Keep both out of repositories
and logs; only file paths appear in these commands.

## Ask a question: JSON by default

```http
POST /v1/knowledge-bases/{knowledge_base_id}/messages
Authorization: Bearer ...
Content-Type: application/json
```

No `Accept` header, `Accept: */*`, `Accept: application/json`, or
`stream: false` selects buffered JSON. `Accept: text/event-stream` or `stream: true`
selects SSE. When both selectors are present they must agree; ambiguous or
conflicting selectors return a structured `406` before a turn begins.

```bash
curl --fail-with-body --request POST \
  --url 'https://kbdrop.io/v1/knowledge-bases/<knowledge-base-id>/messages' \
  --header @./authorization.headers \
  --header 'Content-Type: application/json' \
  --data-binary @./message.json
```

Example `message.json` (generate your own UUID once, then preserve it on retry):

```json
{
  "input": "How do I reset the product?",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}
```

The body is strict; unknown fields fail validation:

| Field | Type | Default / limit |
| --- | --- | --- |
| `input` | string, required | 1–4,000 trimmed characters |
| `conversation_id` | UUID | Omit to create a conversation |
| `idempotency_key` | UUID | Omit for a server ID; reuse across ambiguous retries |
| `stream` | boolean | `false`/buffered when no selector is supplied |
| `citation_detail` | `compact` or `full` | `compact` for JSON; legacy `full` for SSE |
| `include_excerpts` | boolean | `false` for JSON; legacy `true` for SSE |
| `include_usage` | boolean | `false` for JSON; legacy `true` for SSE |
| `include_latency` | boolean | `false` for JSON; legacy `true` for SSE |
| `max_answer_tokens` | integer | Optional; 128–4,096 |
| `response_schema` | strict JSON Schema object | Optional; see below |

A buffered completion is the same authoritative `response.completed` object
used by SSE:

```json
{
  "type": "response.completed",
  "request_id": "logical-operation-uuid",
  "operation_id": "logical-operation-uuid",
  "conversation_id": "conversation-uuid",
  "knowledge_base_id": "knowledge-base-uuid",
  "answer": "Hold reset for ten seconds [S1].",
  "answer_model": "google/gemini-3.5-flash-lite",
  "citations": [
    {
      "id": "S1",
      "source_id": "source-uuid",
      "display_title": "Reset guide",
      "modality": "text",
      "source_url": "https://docs.example.com/reset",
      "deep_link": "https://kbdrop.io/v1/knowledge-bases/.../sources/...?chunk_id=...",
      "locator": { "pages": [12], "sections": ["Reset"] },
      "locator_text": "page 12 · Reset"
    }
  ],
  "insufficient_evidence": false
}
```

Compact citations always retain the citation ID, source ID, display title,
modality, canonical/deep links, machine locator, and human locator. `full` adds
diagnostic fields such as `chunk_id`, filename, relative path, host, media
type, asset availability, and similarity. `include_excerpts: true` adds only
the bounded excerpt already attached to a citation used by the answer.

`source_url` is the canonical original HTTP(S) URL only when safe. URLs with
credentials, signatures, access tokens, or expiring storage parameters become
`null`. `deep_link` is a stable authenticated API link to the exact evidence
chunk. Fetch it with the same credential to receive source metadata and the
cited content/locator; it never redirects to a signed private object URL.
Links from answers stay valid after the knowledge base is replaced by a new
upload or recrawl. A deployment may retire the text of a replaced version after
a retention period; a link returned only by search then answers `404
citation_source_not_found`.
Locators cover lines, pages, slides, sheets, sections/anchors, image regions,
audio/video time ranges, and video keyframes.

### Ask every plugged-in knowledge base

```http
POST /v1/messages
Authorization: Bearer ...
Content-Type: application/json
```

This takes the same body, selectors, and controls, and answers from every
knowledge base plugged into the credential's interface. An API key uses its own
interface; an OAuth token or management key uses the account's `kb-drop` CLI
interface. Plug knowledge bases into either on the switchboard. It needs
`knowledge:query`.

```bash
curl --fail-with-body --request POST \
  --url 'https://kbdrop.io/v1/messages' \
  --header @./authorization.headers \
  --header 'Content-Type: application/json' \
  --data-binary @./message.json
```

The completion adds `interface_id` and `knowledge_base_ids`, and each citation
names its `knowledge_base`. When several knowledge bases answered,
`knowledge_base_id` is `null`:

```json
{
  "type": "response.completed",
  "conversation_id": "conversation-uuid",
  "knowledge_base_id": null,
  "interface_id": "interface-uuid",
  "knowledge_base_ids": ["handbook-uuid", "runbook-uuid"],
  "answer": "Hold reset for ten seconds [S1].",
  "citations": [
    {
      "id": "S1",
      "display_title": "Reset guide",
      "knowledge_base": { "id": "handbook-uuid", "name": "Handbook" }
    }
  ],
  "insufficient_evidence": false
}
```

With one knowledge base plugged in, the answer is that knowledge base's own,
labelled with these fields. A knowledge base that is not ready yet is left out.
A question searches at most 10 plugged-in knowledge bases.

`/v1/knowledge-bases/{knowledge_base_id}/messages` still asks one knowledge
base. An API key reaches only those plugged into its interface.

### Strict structured output

`response_schema` must describe a strict root object. It is limited to 16 KiB,
eight levels, and 64 total properties. Every object must set
`additionalProperties: false` and list every property in `required`; remote
references, combinators, nullable unions, and unsupported keywords are rejected
before provider or billing work. Generation also receives
`max_answer_tokens`.

```json
{
  "input": "Return the reset duration and action.",
  "response_schema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "action": { "type": "string" },
      "seconds": { "type": "integer", "minimum": 0 }
    },
    "required": ["action", "seconds"]
  }
}
```

A validated structured completion uses a discriminated output and JSON Pointer
claim map instead of stringified JSON in `answer`:

```json
{
  "type": "response.completed",
  "output": {
    "type": "json",
    "value": { "action": "Hold reset", "seconds": 10 },
    "claims": [
      { "pointer": "/action", "citation_ids": ["S1"] },
      { "pointer": "/seconds", "citation_ids": ["S1"] }
    ]
  },
  "citations": [{ "id": "S1", "source_id": "..." }],
  "insufficient_evidence": false
}
```

Every claim pointer must resolve in `value`, every returned leaf value must be
covered by a claim at that field or an ancestor, and every referenced citation
must be selected evidence. Invalid or incompletely grounded provider output
fails explicitly with `502 answer_structured_output_invalid`; it is never
returned as a successful completion. Insufficient evidence is a successful
structured completion with `output.value: null`, no claims/citations, and
`insufficient_evidence: true`.

### Idempotency, headers, and errors

`X-Request-ID` identifies the HTTP exchange. `X-Operation-ID` identifies the
durable turn and matches the body's historical `request_id`/`operation_id`.
`X-Operation-Trace-ID`, when present, opens the durable operation trace.
Responses use `Cache-Control: no-store` and `Vary: Authorization, Accept`.

Repeating a completed `idempotency_key` returns the saved completion and
`X-Idempotent-Replay: true` without another provider request or charge. A
still-pending operation returns `409 operation_in_progress` and `Retry-After`;
a saved failed operation returns `409 operation_failed`. Reusing a key with a
different question or structured-output shape returns
`409 idempotency_mismatch` and never creates a second turn.

Buffered provider failures return JSON and no partial answer:

| Error | HTTP |
| --- | ---: |
| `answer_rate_limited`, `answer_failed`, `answer_incomplete` | `503` + `Retry-After` |
| `answer_uncited`, `answer_structured_output_invalid` | `502` |
| Authentication/scope failure | `401` / `403` |
| Readiness or conversation conflict | `409` |
| `no_knowledge_bases_connected`, `knowledge_bases_not_ready`, or `interface_over_capacity` from `/v1/messages` or `/v1/search`: plug one in, wait for one, or unplug some on the switchboard | `409` |
| Billing quota | `429` |
| Payment required | `402` |

## Explicit SSE compatibility

Existing clients that explicitly request `text/event-stream` retain the ordered
contract and legacy full-detail completion defaults:

1. `response.started`
2. zero or more `citation`
3. zero or more `response.delta`
4. exactly one `response.completed` or `error`

Save `stream-message.json` with the same fields as `message.json` and its own
new `idempotency_key`. Reusing a completed key replays the saved completion in
the requested representation with `X-Idempotent-Replay: true`, so it never
produces a new stream of deltas.

```bash
curl --no-buffer --request POST \
  --url 'https://kbdrop.io/v1/knowledge-bases/<knowledge-base-id>/messages' \
  --header @./authorization.headers \
  --header 'Accept: text/event-stream' \
  --header 'Content-Type: application/json' \
  --data-binary @./stream-message.json
```

`/v1/messages` streams the same events; `response.started` and
`response.completed` carry its interface fields.

The completion is self-contained and authoritative; clients must not treat a
partial delta as complete. A client disconnect aborts generation and preserves
the existing failed/disconnected turn accounting. Explicit response controls
can reduce or expand the terminal completion without changing event order.

## Search ranked evidence

Search returns JSON evidence without creating a conversation or calling an
answer model:

```bash
npx @kbdrop/cli@latest search \
  --knowledge-base "$KB_DROP_KNOWLEDGE_BASE_ID" \
  --input-file ./query.txt \
  --language typescript --language javascript \
  --path-prefix src/ \
  --json
```

Only `--language` is repeatable. Supply `--path-prefix` at most once; repeating
it exits `2` with `duplicate_option`. To search multiple subtrees, use a common
prefix or make separate searches. `--top-k` accepts 1–20 results, default five.

For the equivalent direct request, save this as `search.json`:

```json
{
  "query": "Where is upload quota enforced?",
  "top_k": 8,
  "filters": { "path_prefix": "src/", "languages": ["typescript"] }
}
```

```bash
curl --fail-with-body --request POST \
  --url 'https://kbdrop.io/v1/knowledge-bases/<knowledge-base-id>/search' \
  --header @./authorization.headers \
  --header 'Content-Type: application/json' \
  --data-binary @./search.json
```

`query` is 1–4,000 characters, `top_k` is 1–20 (default five), languages are a
1–10 element normalized list, and `path_prefix` is a traversal-free relative
path up to 512 characters. Results contain bounded source/chunk text, scores,
paths, symbols, and line locations. The response stays below 256 KiB. Empty
results identify `no_matches`, `low_confidence`, or `response_size_limit`; do
not invent evidence.

### Search every plugged-in knowledge base

`POST /v1/search` takes the same body and searches every knowledge base plugged
into the credential's interface, as [`/v1/messages`](#ask-every-plugged-in-knowledge-base)
does, then merges the results by rank. Each result names its `knowledge_base`,
and the response adds `interface_id` and `knowledge_base_ids`. With several
plugged in, `knowledge_base_id` is `null`, `knowledge_bases` lists each one's
version, embedding, retrieval profile and threshold, and `empty.threshold` is
`null`. Language and path filters apply only to code knowledge bases, so a
filtered search leaves the others out; with no code knowledge base plugged in it
returns `400 filters_not_supported`.

A nonempty `results` array means only that those chunks cleared the retrieval
threshold. It does not mean that any returned chunk contains enough evidence
to answer the query. The messages endpoint performs a separate grounded-answer
check and may correctly return `insufficient_evidence: true` for the same query.
