Owners and admins can mint API keys at /api-keys. The plaintext is shown exactly once at creation — copy it into your secret store immediately. After that, only the prefix is visible.
Authentication: every request carries Authorization: Bearer k_<prefix>_<secret>. The API-key principal binds to the user who minted it, so the integration sees what that person sees — there's no privilege escalation through an integration. Permission-trimming, held-doc deny-wins, and audit-event emission all fire identically to a UI call.
Scopes (opt-in at creation via checkboxes): - search:read — always granted. Covers hybrid search, list documents, read documents, list collections. - documents:write — rename, change sensitivity (refuses to lower on held docs), patch metadata jsonb. - documents:delete — soft-delete (tombstone) and restore. Held docs still refuse. - collections:write — create collections, add or remove pinned members.
Read endpoints: - GET /api/v1/me — identity probe + endpoint catalog with required scope per route - POST /api/v1/search — hybrid retrieval, body { query, limit } - GET /api/v1/documents — list newest-first with cursor pagination - GET /api/v1/documents/{id} — metadata + character-paginated extracted text - GET /api/v1/collections — list collections in the tenant
Write endpoints (require the matching scope): - POST /api/v1/documents/{id}/rename — body { displayName } - POST /api/v1/documents/{id}/sensitivity — body { sensitivityLabel, reason } - POST /api/v1/documents/{id}/metadata — body { patch, reason? }; pass null on a key to delete it - DELETE /api/v1/documents/{id} — body { reason }; refuses on active legal hold - POST /api/v1/documents/{id}/restore — body { reason }; owner/admin-only - POST /api/v1/collections — body matches the createCollection MCP tool input - POST /api/v1/collections/{id}/members — body { documentId } - DELETE /api/v1/collections/{id}/members/{documentId} - POST /api/v1/documents — direct binary upload + register; raw bytes in the request body, headers carry display-name / sensitivity / collection / metadata. See /help/bulk-ingest-api for the watch-folder + ETL story.
Drop /api/openapi.json into Postman, Insomnia, or Stoplight Studio for typed request building. The full request and response schemas — including the held-doc deny-wins error envelope — are documented there.
**Model Context Protocol — same key, all 75+ tools**
The same API key also authenticates against /api/mcp, Kodori's public MCP server. Any conformant MCP client (Claude Desktop, Cursor, ChatGPT desktop, Kodokyo's agent, custom integrations) connects with the bearer token and calls the full agent tool catalog — search, list, read, write, governance triage. Tools are filtered by your key's scopes the same way the REST endpoints are. See /help/mcp-server for the connection guide and per-client examples.
For push notifications when events happen in your tenant, set up webhooks: see /help/webhooks-overview. Pick "Slack" as the format on a webhook subscription and Kodori renders each event as Block Kit at the destination URL — see /help/webhook-format-slack.
## Rate limits
Every API key carries a per-minute request cap. The default is 600 requests / minute (10 / second), enough for typical integrations. Per-key overrides are settable on api_keys.requests_per_minute (UI editor lands in a follow-up; for now request a custom cap from support).
Every successful response carries the standard rate-limit headers:
- X-RateLimit-Limit — the cap that applied to this request - X-RateLimit-Remaining — quota left in the current 1-minute window AFTER this request - X-RateLimit-Reset — seconds until the current bucket rolls over
When you exceed the cap, Kodori returns 429 with Retry-After: <seconds> and the same X-RateLimit-* headers. Implement client-side back-off using Retry-After — wait the given seconds, then retry. Same shape Stripe and GitHub return; existing client libraries that respect Retry-After work without changes.
The window is a fixed 1-minute bucket, not a true sliding window — a key can theoretically burst at 2× the cap if all requests land near a minute boundary. The cap is meant to defend Kodori from runaway integrations; if you need a hard SLA, put your own rate limiter in front of your client.
## Per-key cap override
Owners and admins can set a per-key cap from /api-keys/<id>/usage. Click "Usage →" on the key in /api-keys, then use the Rate limit form at the top of the page:
- Type a positive integer (max 60,000 rpm = 1000 rps) and click Save - Or click "Reset to tenant default" to revert to the 600 rpm default
Changes take effect on the next request. Each change emits an `api-key.rate-limit-set` event on the tenant audit stream — chain records who changed which key when. Hard cap of 60,000 rpm prevents fat-finger entries; customers with documented higher throughput needs contact support.
## Lifetime usage stats
Every accepted API call bumps a per-key counter so /api-keys can answer "is this still being used?" without grepping audit. Each row shows two adjacent stats —
- **X calls** — lifetime count of successful auth (NOT a per-day window). A key showing "no calls yet" with a 30-days-old creation date is a safe-to-revoke candidate; one with "12,847 calls · last call 2m ago" is load-bearing. - **last call <relative>** — most recent accepted request, formatted as "2m ago" / "3h ago" / "5d ago" or YYYY-MM-DD past 30 days. UTC-stable (uses absolute deltas, not localized weekdays) so the rendering doesn't change on page refresh.
Both stats are best-effort — auth correctness does NOT depend on the counter writes succeeding. The counter is a bigint (effectively unbounded) and is incremented atomically via SQL `total_requests + 1` inside the same UPDATE that touches lastUsedAt, so concurrent requests don't race-lose updates.
If you need per-call audit trail (compliance / forensics), this counter is the wrong layer — open a support ticket; we can enable a per-tenant rolling-log table behind a feature flag (off by default to avoid the 100x audit-volume blow-up).
## Bulk-revoke for offboarding
Departing employee, compromised user, or rotation cycle? The "Offboarding — bulk-revoke all keys created by a user" form on /api-keys (admin / owner only) lets you pick a workspace user from a dropdown and revoke every active key they created in one action. Each revoke emits `api-key.revoked` on the audit chain with `bulk: true` + the offboarded user's id and email in the payload — distinguishable from individual revokes for "which keys came down in our offboarding sweep?" queries. Refuses to revoke your own keys (a guarantee against accidental self-lockout); for that case, use the per-row Revoke button instead.
## Key expiration + rotation reminders
Keys can carry an explicit expiration. Set on creation via the "Expiration" picker (Never / 30 / 90 / 180 / 365 days, capped at 365), or edit on an existing key from /api-keys/[id]/usage. NULL = no expiration; manual revoke is the only deactivation path.
The auth path refuses expired keys with a 401 — auto-revocation without a separate cron flip. Reason=key-expired in the response logs distinguishes this from reason=bad-secret or reason=unknown-key.
A daily Inngest cron (api-key-expiration-sweep at 03:00 UTC) finds active keys whose expiration is within 7 days and emails workspace owners + admins with the key name, prefix, expiry date, and a /api-keys deep link. Throttled to one email per ~6 days inside the window so the same key doesn't generate a daily reminder. The cron also emits `api-key.expiration-warning-sent` (inside window) and `api-key.expired` (post-expiry) events on the audit chain so you can filter "every key that expired this quarter" with one /audit query.
Per-row expiration badge on /api-keys list: - "no expiration" (gray) — null expires_at - "expires YYYY-MM-DD" (gray) — > 7 days out - "expires in Nd" (amber) — ≤ 7 days - "expired Nd ago" (red) — past expiry
Standard rotation policies: 90 days for high-volume integrations (Make.com flows, scheduled syncs), 365 days for read-only / low-velocity (dashboards, reports). Editing the expiration re-arms the warning throttle so a freshly-rotated expiry generates a fresh 7-day-out reminder later.
## Per-key usage audit
Every external API call lands on the hash-chained audit log with actorId="apikey:<prefix>". The "Usage →" link on each active key in /api-keys opens /api-keys/<id>/usage which surfaces:
- Total all-time calls, last-30-days call count, last-used stamp - A 30-day daily activity bar chart (one bar per day, height proportional to call count, tooltip shows the exact count) - The 10 most-frequent event types in the last 30 days with counts + share-of-traffic percentages - A 30-row paginated recent-calls table — timestamp, event type, stream id
For full payload + actor-kind filtering, the page links to /audit?actor=apikey:<prefix> as the deep dive. The usage panel is the at-a-glance triage; /audit is the forensic trail.