{"openapi":"3.1.0","info":{"title":"Koda Rising API","version":"0.4.0","description":"HTTP API for Koda Rising — an affordable AI coding platform with local (Naira) prepaid billing.\n\n**Auth.** Browser sessions use the sealed `koda_session` cookie (`credentials: \"include\"`; obtained via `/auth/*`). Programmatic clients (CLI/CI/SDK) send an API key as `Authorization: Bearer <key>` — keys carry scopes (`agent:run`, `wallet:read`, `keys:manage`, `metrics:read`). In dev only (`KODA_DEV_AUTH=on`), a bearer of a user id authenticates as that user.\n\n**Money.** Amounts are integer **minor units** of the org currency (NGN kobo, KES cent, …) — fields are named `*Minor` (e.g. `amountMinor`, `balanceMinor`). A few convenience inputs take MAJOR units and are named for it (e.g. `capNaira`). Never floats for minor-unit fields.\n\n**Idempotency.** Wallet-mutating calls accept an `Idempotency-Key` header and are exactly-once per user.\n\n**Errors.** Non-2xx responses are `{ \"error\": \"message\" }` (some carry extra fields). Rate-limited responses are `429` with `Retry-After` + `RateLimit-*` headers.\n\n**Versioning.** This document describes **API v2**, served by platform 0.4.0. The two move independently: the platform version changes every release, while the API version changes only on an incompatible wire change — so that is the one a client should branch on. New routes and new response fields are additive and do NOT move it, so clients must tolerate fields they do not recognise. `GET /version` reports both at runtime, along with the build sha, and every response carries an `X-Koda-Version` header."},"servers":[{"url":"/","description":"Same-origin gateway"}],"tags":[{"name":"System","description":"Health + this spec"},{"name":"Auth","description":"Native auth: password, magic-link, TOTP MFA, sessions"},{"name":"Account","description":"The signed-in principal + active sessions"},{"name":"Wallet","description":"Prepaid wallet: balance, top-up, verify, ledger"},{"name":"Agent","description":"Run agent tasks (open-default routing, metered)"},{"name":"Repo","description":"Org-scoped repository index for retrieval"},{"name":"Chat","description":"Persistent, streaming assistant threads"},{"name":"Reviews","description":"The review bot — security findings, PR validation, and GitHub App installation policy"},{"name":"Research","description":"Koda Deep Research — bounded multi-phase investigations producing a cited report"},{"name":"Billing","description":"Plans, subscription, invoices"},{"name":"Teams","description":"Org members, roles, per-seat caps"},{"name":"API keys","description":"Programmatic access tokens"},{"name":"Enterprise","description":"Per-org policy (tier/residency/isolation/lanes), feature flags, audit"},{"name":"Metrics","description":"Usage/margin KPIs — operator or metrics:read"},{"name":"Operator","description":"Platform staff surfaces (platform_users.role — a separate identity space from customers since 0087). Never gated on `*` scope alone."},{"name":"Internal","description":"Machine-to-machine (shared secret) — not a user API"}],"components":{"securitySchemes":{"apiKeyAuth":{"type":"http","scheme":"bearer","description":"API key (`Authorization: Bearer <key>`), or a user id under dev auth."},"cookieAuth":{"type":"apiKey","in":"cookie","name":"koda_session","description":"Sealed browser session cookie."},"internalSecret":{"type":"apiKey","in":"header","name":"x-internal-secret","description":"Shared machine-to-machine secret (`KODA_INTERNAL_SECRET`)."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Exactly-once key for a wallet mutation (per user)."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"Money":{"type":"integer","format":"int64","description":"Amount in integer minor units of the org currency."},"Currency":{"type":"object","properties":{"code":{"type":"string","example":"NGN"},"symbol":{"type":"string","example":"₦"},"minorUnit":{"type":"integer","example":100},"minorName":{"type":"string","example":"kobo"}}},"Me":{"type":"object","properties":{"userId":{"type":"string"},"orgId":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["owner","admin","member"]},"scopes":{"type":"array","items":{"type":"string"}},"isPlatformAdmin":{"type":"boolean"},"isSuperAdmin":{"type":"boolean"},"staffRole":{"type":"string","nullable":true,"enum":["support","billing","compliance","operator","super_admin"],"description":"Platform staff tier (0073), or null when the account is not staff."},"currency":{"$ref":"#/components/schemas/Currency"},"balanceMinor":{"$ref":"#/components/schemas/Money"},"mfaEnabled":{"type":"boolean"},"plan":{"type":"string","nullable":true},"subscriptionStatus":{"type":"string","nullable":true},"includedRemainingMinor":{"$ref":"#/components/schemas/Money"}}},"Wallet":{"type":"object","properties":{"balanceMinor":{"$ref":"#/components/schemas/Money"}}},"TopupRequest":{"type":"object","required":["amountMinor"],"properties":{"amountMinor":{"$ref":"#/components/schemas/Money"},"channel":{"type":"string","enum":["card","ussd","opay","palmpay","mpesa","mtn_momo","ozow","fawry","telecel_cash","airteltigo","payshap","flutterwave"],"default":"card"},"phone":{"type":"string","description":"Required for mobile-money channels."},"email":{"type":"string"}}},"TopupResult":{"type":"object","properties":{"reference":{"type":"string"},"mode":{"type":"string","enum":["live","mock"]},"authorizationUrl":{"type":"string"},"ussdCode":{"type":"string"},"displayText":{"type":"string"}}},"LedgerEntry":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["credit","debit"]},"amountMinor":{"$ref":"#/components/schemas/Money"},"source":{"type":"string"},"ref":{"type":"string"},"balanceAfterMinor":{"$ref":"#/components/schemas/Money"},"createdAt":{"type":"string","format":"date-time"}}},"FileEdit":{"type":"object","properties":{"path":{"type":"string"},"action":{"type":"string","enum":["create","update","delete"]},"content":{"type":"string"},"description":{"type":"string"}}},"AgentRunRequest":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string"},"context":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}}}},"frontier":{"type":"boolean","description":"Hint to allow frontier escalation (still policy-gated)."}}},"AgentRunResult":{"type":"object","properties":{"plan":{"type":"string"},"edits":{"type":"array","items":{"$ref":"#/components/schemas/FileEdit"}},"summary":{"type":"string"},"lane":{"type":"string","enum":["open","frontier"]},"model":{"type":"string"},"costMinor":{"$ref":"#/components/schemas/Money"},"balanceMinor":{"$ref":"#/components/schemas/Money"},"review":{"type":"object","description":"Fintech-context findings, when applicable."}}},"Plan":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"tier":{"type":"string"},"interval":{"type":"string","enum":["month","year"]},"amountMinor":{"$ref":"#/components/schemas/Money"},"currency":{"type":"string"},"includedUsageMinor":{"$ref":"#/components/schemas/Money"},"seatAllotment":{"type":"integer"}}},"Thread":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"mode":{"type":"string","enum":["general","code"]},"updatedAt":{"type":"string","format":"date-time"}}},"Member":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["owner","admin","member"]},"seatCapMinor":{"$ref":"#/components/schemas/Money"}}},"ApiKey":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"}}},"OrgPolicy":{"type":"object","properties":{"orgId":{"type":"string"},"tier":{"type":"string","enum":["individual","team","enterprise"]},"dataIsolation":{"type":"string","enum":["shared","schema","dedicated"]},"residencyRegion":{"type":"string","nullable":true},"allowedLanes":{"type":"array","items":{"type":"string","enum":["open","frontier"]}},"ssoRequired":{"type":"boolean"},"groupRoleMap":{"type":"object","additionalProperties":{"type":"string"},"nullable":true}}},"Operator":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["support","billing","compliance","operator","super_admin"]},"isSuperAdmin":{"type":"boolean"}}},"OperatorInvite":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["support","billing","compliance","operator","super_admin"]},"expiresAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"invitedByEmail":{"type":"string","nullable":true}}},"AdminOrg":{"type":"object","properties":{"orgId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"tier":{"type":"string"},"members":{"type":"integer"},"subscriptionStatus":{"type":"string"},"kycStatus":{"type":"string"},"suspended":{"type":"boolean"}}},"KycRecord":{"type":"object","properties":{"orgId":{"type":"string"},"orgName":{"type":"string"},"status":{"type":"string","enum":["unsubmitted","pending","verified","rejected"]},"businessName":{"type":"string"},"registrationNumber":{"type":"string"},"contactEmail":{"type":"string"},"documentRef":{"type":"string"},"reviewerNotes":{"type":"string"}}},"PlatformSetting":{"type":"object","description":"A catalogued setting. `value` is the EFFECTIVE value the fleet is running on; `isDefault` says whether anyone has overridden it, and `source` says who. Everything a client needs to render an editor arrives here, so no client carries its own list of what settings mean.","properties":{"key":{"type":"string"},"group":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"type":{"type":"string","enum":["boolean","number","enum","string"]},"options":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"},"note":{"type":"string"}}}},"min":{"type":"integer"},"max":{"type":"integer"},"unit":{"type":"string","enum":["minor","ms","mb","pct","count"]},"default":{},"value":{},"isDefault":{"type":"boolean"},"source":{"type":"string","enum":["operator","env","default"]},"env":{"type":"string","description":"The environment variable that seeds this key on a fresh deployment."},"superAdminOnly":{"type":"boolean"},"updatedAt":{"type":"string","format":"date-time","nullable":true},"updatedByEmail":{"type":"string","nullable":true}}},"BuildVersion":{"type":"object","properties":{"version":{"type":"string","description":"The platform semver — for humans."},"apiVersion":{"type":"integer","description":"The HTTP contract version. THIS is the field a client branches on: it moves only on an incompatible wire change, independent of the platform version."},"commit":{"type":"string","description":"Short git sha of the build, or `unknown` outside a built image."},"builtAt":{"type":"string","nullable":true},"environment":{"type":"string"}}}},"responses":{"Unauthorized":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Authenticated but not permitted (scope / role / operator).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PaymentRequired":{"description":"Wallet balance + included allowance cannot cover the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"security":[{"apiKeyAuth":[]},{"cookieAuth":[]}],"paths":{"/health":{"get":{"tags":["System"],"summary":"Liveness","security":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}}}}},"/readyz":{"get":{"tags":["System"],"summary":"Readiness (DB reachable)","security":[],"responses":{"200":{"description":"Ready"},"503":{"description":"Not ready"}}}},"/openapi.json":{"get":{"tags":["System"],"summary":"This OpenAPI document","security":[],"responses":{"200":{"description":"The spec"}}}},"/auth/config":{"get":{"tags":["Auth"],"summary":"Public auth configuration","security":[],"responses":{"200":{"description":"mode + enabled methods","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["native","dev","none"]}}}}}}}}},"/auth/register":{"post":{"tags":["Auth"],"summary":"Register with email + password","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string","minLength":8}}}}}},"responses":{"201":{"description":"Registered (verification email sent)"},"403":{"description":"Sign-ups disabled"}}}},"/auth/login":{"post":{"tags":["Auth"],"summary":"Password login → session or MFA challenge","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string"}}}}}},"responses":{"200":{"description":"Session set (Set-Cookie) or an MFA challenge token"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/auth/mfa/challenge":{"post":{"tags":["Auth"],"summary":"Complete a TOTP/recovery MFA challenge","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["challenge","code"],"properties":{"challenge":{"type":"string"},"code":{"type":"string"}}}}}},"responses":{"200":{"description":"Session set"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/auth/magic/request":{"post":{"tags":["Auth"],"summary":"Request a magic-link email","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string"}}}}}},"responses":{"200":{"description":"Sent (non-enumerating)"}}}},"/auth/logout":{"post":{"tags":["Auth"],"summary":"Revoke the current session","responses":{"200":{"description":"Logged out"}}}},"/auth/keys":{"get":{"tags":["API keys"],"summary":"List API keys","responses":{"200":{"description":"Keys","content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["API keys"],"summary":"Create an API key (requires keys:manage)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["agent:run","wallet:read","keys:manage","metrics:read"]}}}}}}},"responses":{"200":{"description":"The created key (shown once)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"key":{"type":"string"}}}}}},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/auth/keys/{id}":{"delete":{"tags":["API keys"],"summary":"Revoke an API key","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked"},"404":{"description":"Not found"}}}},"/me":{"get":{"tags":["Account"],"summary":"The signed-in principal, wallet, plan + entitlements","responses":{"200":{"description":"Principal","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me/sessions":{"get":{"tags":["Account"],"summary":"List active sessions","responses":{"200":{"description":"Sessions"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me/sessions/{id}":{"delete":{"tags":["Account"],"summary":"Revoke a session","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked"}}}},"/me/usage":{"get":{"tags":["Account"],"summary":"Own usage series (requires wallet:read)","responses":{"200":{"description":"Usage points + rollups"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/wallet":{"get":{"tags":["Wallet"],"summary":"Wallet balance","responses":{"200":{"description":"Balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Wallet"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/wallet/ledger":{"get":{"tags":["Wallet"],"summary":"Wallet ledger entries","responses":{"200":{"description":"Entries","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/LedgerEntry"}}}}}}}}}},"/wallet/topup":{"post":{"tags":["Wallet"],"summary":"Start a top-up on a currency-appropriate rail","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopupRequest"}}}},"responses":{"200":{"description":"Redirect/USSD/prompt details; money settles via the provider webhook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopupResult"}}}}}}},"/wallet/verify":{"post":{"tags":["Wallet"],"summary":"Server-side verify a pending top-up (webhook fallback)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reference"],"properties":{"reference":{"type":"string"}}}}}},"responses":{"200":{"description":"Verification result + balance"}}}},"/wallet/refund":{"post":{"tags":["Wallet"],"summary":"Operator-only refund/adjustment (Idempotency-Key required)","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["orgId","amountMinor","reason"],"properties":{"orgId":{"type":"string"},"amountMinor":{"$ref":"#/components/schemas/Money"},"reason":{"type":"string","description":"Required audit reason."},"source":{"type":"string","enum":["refund","adjustment"],"default":"refund","description":"Defaults to `refund` if omitted."},"reference":{"type":"string","description":"Required for `refund`: the settled `payment_events` reference to refund against (capped at what it captured, net of prior refunds)."}}}}}},"responses":{"200":{"description":"Credited"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/agent/run":{"post":{"tags":["Agent"],"summary":"Run a single agent turn (metered, requires agent:run)","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRunRequest"}}}},"responses":{"200":{"description":"Structured edits + cost","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRunResult"}}}},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"503":{"description":"Inference not configured (mock lane refused in production)"}}}},"/agent/run-loop":{"post":{"tags":["Agent"],"summary":"Plan → edit → verify → retry loop (SSE, metered per iteration)","description":"Streams `text/event-stream` events: `{start}`, `{step, turn}`, then a final result frame carrying `verified`, `iterations`, `cancelled` and `totalCostMinor`. Optional `sandbox` (`none`|`local`|`docker`) + `testCmd` run the project tests each iteration; an un-runnable sandbox is rejected 400.\n\n**Cancellation.** Close the connection (abort the fetch) to stop the run — there is no cancel route and no run id. The server stops before STARTING the next iteration and the final frame reports `cancelled: true`. Iterations already dispatched to a provider cannot be un-billed: consumed tokens are always metered, so a cancelled run is charged for exactly the iterations that ran (`iterations`), never more and never fewer. A retried `Idempotency-Key` replays the cancelled outcome rather than re-running.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AgentRunRequest"},{"type":"object","properties":{"sandbox":{"type":"string","enum":["none","local","docker"]},"testCmd":{"type":"string"},"maxIters":{"type":"integer","maximum":8}}}]}}}},"responses":{"200":{"description":"SSE stream","content":{"text/event-stream":{}}},"400":{"description":"Sandbox requested but not runnable here"},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/repo/index":{"get":{"tags":["Repo"],"summary":"Index status (chunks, paths)","responses":{"200":{"description":"Status","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"chunks":{"type":"integer"},"paths":{"type":"array","items":{"type":"string"}}}}}}}}},"post":{"tags":["Repo"],"summary":"Embed + upsert files (vector mode; metered; requires agent:run)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"files":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Indexed count + cost"},"400":{"description":"Not in vector mode (KODA_RETRIEVER=vector)"}}},"delete":{"tags":["Repo"],"summary":"Clear the org index, or one ?path","parameters":[{"name":"path","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cleared"}}}},"/chat/threads":{"get":{"tags":["Chat"],"summary":"List threads","responses":{"200":{"description":"Threads","content":{"application/json":{"schema":{"type":"object","properties":{"threads":{"type":"array","items":{"$ref":"#/components/schemas/Thread"}}}}}}}}},"post":{"tags":["Chat"],"summary":"Create a thread (requires agent:run)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["general","code"]}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"mode":{"type":"string"}}}}}}}}},"/chat/threads/{id}":{"get":{"tags":["Chat"],"summary":"Thread + messages","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Thread with messages"},"404":{"description":"Not found (or not yours)"}}},"patch":{"tags":["Chat"],"summary":"Rename / set mode / archive","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"mode":{"type":"string"},"archived":{"type":"boolean"}}}}}},"responses":{"200":{"description":"OK"}}},"delete":{"tags":["Chat"],"summary":"Delete a thread","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/chat/threads/{id}/messages":{"post":{"tags":["Chat"],"summary":"Send a message; stream the reply (SSE, metered)","description":"Streams `{delta}` token frames then a final `{message}` frame with `costMinor` + `balanceMinor`. An unaffordable turn is refused `402` before any delta.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string"},"frontier":{"type":"boolean"}}}}}},"responses":{"200":{"description":"SSE stream","content":{"text/event-stream":{}}},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/plans":{"get":{"tags":["Billing"],"summary":"Available plans for the org currency","responses":{"200":{"description":"Plans","content":{"application/json":{"schema":{"type":"object","properties":{"plans":{"type":"array","items":{"$ref":"#/components/schemas/Plan"}}}}}}}}}},"/public/plans":{"get":{"tags":["Billing"],"summary":"Public rate card — no auth, used by the marketing site","parameters":[{"name":"currency","in":"query","schema":{"type":"string","default":"NGN"}}],"responses":{"200":{"description":"Plans","content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string"},"plans":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"tier":{"type":"string"},"interval":{"type":"string"},"amountMinor":{"type":"integer"},"currency":{"type":"string"},"includedUsageMinor":{"type":"integer"},"seatAllotment":{"type":"integer"},"seatPriceMinor":{"type":"integer"}}}}}}}}}}}},"/contact/lead":{"post":{"tags":["Billing"],"summary":"Submit the marketing site's Teams/Enterprise contact form","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email","message"],"properties":{"name":{"type":"string"},"email":{"type":"string"},"company":{"type":"string"},"interest":{"type":"string","enum":["teams","enterprise","general"]},"message":{"type":"string"}}}}}},"responses":{"200":{"description":"Lead recorded"},"400":{"description":"Missing or invalid fields"}}}},"/subscription":{"get":{"tags":["Billing"],"summary":"Current subscription + plan + included remaining","responses":{"200":{"description":"Subscription"}}}},"/subscription/checkout":{"post":{"tags":["Billing"],"summary":"Subscribe / change plan (owner-only)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["planCode"],"properties":{"planCode":{"type":"string"},"interval":{"type":"string","enum":["month","year"]}}}}}},"responses":{"200":{"description":"Activated, or a hosted checkout URL for a paid card plan"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Plan not found"}}}},"/metrics":{"get":{"tags":["Ops"],"summary":"Prometheus metrics (text exposition format). Auth: Bearer KODA_METRICS_TOKEN, else operator-only.","responses":{"200":{"description":"Prometheus metrics","content":{"text/plain":{}}},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/subscription/cancel":{"post":{"tags":["Billing"],"summary":"Cancel at period end (owner-only)","responses":{"200":{"description":"Cancellation scheduled"},"404":{"description":"No active subscription"}}}},"/subscription/change":{"post":{"tags":["Billing"],"summary":"Change plan (owner-only): upgrade charges the prorated delta now; downgrade is scheduled for period end","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["planCode"],"properties":{"planCode":{"type":"string"},"interval":{"type":"string","enum":["month","year"]}}}}}},"responses":{"200":{"description":"changed:true (upgraded, chargedMinor) | scheduled:true (downgrade queued) | cleared:true"},"402":{"description":"No payment method / card declined"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No active subscription / plan not found"},"409":{"description":"A plan change is already processing"}}}},"/subscription/seats":{"post":{"tags":["Billing"],"summary":"Set purchased seat count (owner-only); added seats charge the per-seat price prorated","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["seats"],"properties":{"seats":{"type":"integer","minimum":1}}}}}},"responses":{"200":{"description":"seats + chargedMinor"},"400":{"description":"Below occupied seats / invalid"},"402":{"description":"No payment method / card declined"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No active subscription"}}}},"/payment-methods":{"get":{"tags":["Billing"],"summary":"List stored cards (never exposes the authorization code)","responses":{"200":{"description":"paymentMethods[]"}}},"post":{"tags":["Billing"],"summary":"Add a card (owner-only): returns a hosted verify-charge URL; the card is stored when its webhook settles","responses":{"200":{"description":"reference + authorizationUrl"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/payment-methods/{id}":{"delete":{"tags":["Billing"],"summary":"Remove a stored card (owner-only); promotes another to default","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Removed"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found"}}}},"/payment-methods/{id}/default":{"post":{"tags":["Billing"],"summary":"Set the default card (owner-only)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found"}}}},"/invoices":{"get":{"tags":["Billing"],"summary":"Invoice history (each carries a receiptUrl)","responses":{"200":{"description":"Invoices"}}}},"/invoices/{id}/receipt":{"get":{"tags":["Billing"],"summary":"Branded printable HTML receipt for one invoice (org-scoped). ?download=1 sends it as an attachment.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"download","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Receipt","content":{"text/html":{}}},"404":{"description":"Not found"}}}},"/billing/summary":{"get":{"tags":["Billing"],"summary":"Wallet + subscription + included-usage summary","responses":{"200":{"description":"Summary"}}}},"/teams":{"post":{"tags":["Teams"],"summary":"Create an org/team","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}},"responses":{"200":{"description":"Created"}}}},"/teams/{orgId}/members":{"get":{"tags":["Teams"],"summary":"List members","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Members","content":{"application/json":{"schema":{"type":"object","properties":{"members":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}}}}}},"post":{"tags":["Teams"],"summary":"Invite/add a member (owner/admin; seat-capped)","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","role"],"properties":{"email":{"type":"string"},"role":{"type":"string","enum":["admin","member"]},"capNaira":{"type":"number","description":"Monthly spend cap in MAJOR currency units (Naira), converted to minor server-side. Omit for no cap."}}}}}},"responses":{"200":{"description":"Added"},"402":{"description":"Seat allotment exceeded"}}}},"/teams/{orgId}/members/{userId}":{"patch":{"tags":["Teams"],"summary":"Change a member role / cap","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["admin","member"]},"capNaira":{"type":"number","description":"Monthly spend cap in MAJOR units (Naira)."}}}}}},"responses":{"200":{"description":"Updated"}}},"delete":{"tags":["Teams"],"summary":"Remove a member","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Removed"}}}},"/teams/{orgId}/usage":{"get":{"tags":["Teams"],"summary":"Per-member usage (owner/admin)","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Usage by member"}}}},"/wallet/settings":{"get":{"tags":["Wallet"],"summary":"Low-balance threshold (owner/admin)","responses":{"200":{"description":"Threshold + currency"}}},"patch":{"tags":["Wallet"],"summary":"Set the low-balance threshold (owner)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["lowBalanceThresholdMinor"],"properties":{"lowBalanceThresholdMinor":{"$ref":"#/components/schemas/Money"}}}}}},"responses":{"200":{"description":"Updated"}}}},"/orgs/{orgId}/policy":{"get":{"tags":["Enterprise"],"summary":"Org policy (operator)","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Policy","content":{"application/json":{"schema":{"type":"object","properties":{"policy":{"$ref":"#/components/schemas/OrgPolicy"}}}}}},"403":{"$ref":"#/components/responses/Forbidden"}}},"put":{"tags":["Enterprise"],"summary":"Update org policy (operator). Provisions schema isolation on the shared→isolated switch.","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgPolicy"}}}},"responses":{"200":{"description":"Updated"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/audit":{"get":{"tags":["Enterprise"],"summary":"Append-only audit log","description":"Own org by default; `?scope=all` is cross-org and operator-only.","parameters":[{"name":"scope","in":"query","schema":{"type":"string","enum":["all"]}}],"responses":{"200":{"description":"Audit entries"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/admin/flags":{"get":{"tags":["Enterprise"],"summary":"Global feature flags (operator)","responses":{"200":{"description":"Flags"}}}},"/admin/flags/{key}":{"put":{"tags":["Enterprise"],"summary":"Upsert a global flag (operator)","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"defaultEnabled":{"type":"boolean"},"description":{"type":"string"},"tierOverrides":{"type":"object","additionalProperties":{"type":"boolean"}}}}}}},"responses":{"200":{"description":"Saved"}}}},"/orgs/{orgId}/flags/{key}":{"put":{"tags":["Enterprise"],"summary":"Set a per-org flag override (operator)","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Saved"}}},"delete":{"tags":["Enterprise"],"summary":"Clear a per-org flag override (operator)","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cleared"}}}},"/metrics/overview":{"get":{"tags":["Metrics"],"summary":"Fleet/org overview","responses":{"200":{"description":"Overview"}}}},"/metrics/usage":{"get":{"tags":["Metrics"],"summary":"Usage rollups","responses":{"200":{"description":"Usage"}}}},"/metrics/timeseries":{"get":{"tags":["Metrics"],"summary":"Usage/billing time series","responses":{"200":{"description":"Series"}}}},"/metrics/kpis":{"get":{"tags":["Metrics"],"summary":"KPIs","description":"`mrrMinor` is trailing recognised-usage revenue; `netCreditedMinor` is cash in (larger while wallets hold float); `subscriptionMrrMinor`/`activeSubscriptions` are distinct.","responses":{"200":{"description":"KPIs"}}}},"/metrics/orgs":{"get":{"tags":["Metrics"],"summary":"Cross-org list (operator only)","responses":{"200":{"description":"Orgs"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/admin/operators":{"get":{"tags":["Operator"],"summary":"List platform staff + pending invites (any operator)","responses":{"200":{"description":"Operators","content":{"application/json":{"schema":{"type":"object","properties":{"operators":{"type":"array","items":{"$ref":"#/components/schemas/Operator"}},"invites":{"type":"array","items":{"$ref":"#/components/schemas/OperatorInvite"}}}}}}},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Operator"],"summary":"Grant operator access to an EXISTING user (super admin only)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string"},"role":{"type":"string","enum":["support","billing","compliance","operator","super_admin"]}}}}}},"responses":{"200":{"description":"Granted"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No user with that email"}}}},"/admin/operators/invite":{"post":{"tags":["Operator"],"summary":"Invite a new operator by email (super admin only)","description":"Creates a single-use, expiring invitation and emails the accept link. Operators cannot self sign up — this and the direct grant are the only ways in.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string"},"role":{"type":"string","enum":["support","billing","compliance","operator","super_admin"],"default":"operator"}}}}}},"responses":{"201":{"description":"Invited","content":{"application/json":{"schema":{"type":"object","properties":{"invite":{"$ref":"#/components/schemas/OperatorInvite"},"devToken":{"type":"string","description":"Dev/test builds only — never returned in production."}}}}}},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/admin/operators/invite/{id}/revoke":{"post":{"tags":["Operator"],"summary":"Revoke a pending invite (super admin only)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No pending invite with that id"}}}},"/admin/operators/{userId}/role":{"put":{"tags":["Operator"],"summary":"Change a staff member's tier in place (super admin only)","description":"The supported way to move someone between tiers. A grant cannot do it: a grant only ever ADDS access, so a lateral move or a demotion made that way changes nothing. This assigns the role outright, and evaluates the last-super-admin invariant against the RESULTING fleet rather than against a removal — so super_admin to operator is allowed while another super admin exists. Your own account is allowed — DELETE already permits self-revocation, and stepping down after promoting a successor is the shape a handover takes; the invariants, not the target, are the protection. Refused for the KODA_ROOT_ADMIN_EMAIL account, because the next boot re-asserts that grant. Re-sending the tier someone already holds is a 200 with `changed: false` and no audit row. Takes effect on the target's next request; there is no session to invalidate.","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role"],"properties":{"role":{"type":"string","enum":["support","billing","compliance","operator","super_admin"]}}}}}},"responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"changed":{"type":"boolean"},"userId":{"type":"string"},"role":{"type":"string"},"previousRole":{"type":"string"}}}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not a platform operator"},"409":{"description":"The root administrator, or the last super admin stepping down"},"422":{"description":"Unknown role"}}}},"/admin/operators/{userId}":{"delete":{"tags":["Operator"],"summary":"Revoke operator access (super admin only)","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not a platform operator"},"409":{"description":"Cannot revoke the last operator / the last super admin"}}}},"/auth/operator-invite/accept":{"post":{"tags":["Auth"],"summary":"Accept a platform-operator invitation","description":"Authenticated by the emailed token alone. Single-use and expiry-checked; the promoted account and tier come from the stored invite, never the request body. A brand-new invitee sets their password here.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"},"password":{"type":"string","description":"Required only when the invited email has no account yet (min 8 chars)."}}}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"userId":{"type":"string"},"role":{"type":"string"}}}}}},"400":{"description":"Invalid, expired or already-used invitation"}}}},"/admin/orgs":{"get":{"tags":["Operator"],"summary":"Every org — plan, members, subscription, KYC, suspension","responses":{"200":{"description":"Orgs","content":{"application/json":{"schema":{"type":"object","properties":{"orgs":{"type":"array","items":{"$ref":"#/components/schemas/AdminOrg"}}}}}}}}}},"/admin/orgs/{id}/suspend":{"post":{"tags":["Operator"],"summary":"Suspend an org (blocks its paid actions)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Suspended"}}}},"/admin/orgs/{id}/unsuspend":{"post":{"tags":["Operator"],"summary":"Reinstate an org","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Reinstated"}}}},"/admin/kyc":{"get":{"tags":["Operator"],"summary":"KYC submissions","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["pending","verified","rejected","all"]}}],"responses":{"200":{"description":"Records","content":{"application/json":{"schema":{"type":"object","properties":{"records":{"type":"array","items":{"$ref":"#/components/schemas/KycRecord"}}}}}}}}}},"/admin/kyc/{orgId}/review":{"post":{"tags":["Operator"],"summary":"Verify/reject a KYC submission","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["verified","rejected"]},"notes":{"type":"string"}}}}}},"responses":{"200":{"description":"Reviewed"}}}},"/kyc":{"post":{"tags":["Account"],"summary":"Submit the org for KYC (owner/admin — NOT operator)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["businessName"],"properties":{"businessName":{"type":"string"},"registrationNumber":{"type":"string"},"contactEmail":{"type":"string"},"documentRef":{"type":"string"}}}}}},"responses":{"200":{"description":"Submitted (status: pending)"}}}},"/admin/settings":{"get":{"tags":["Operator"],"summary":"Every platform setting, with its effective value and provenance","description":"Rendered from the settings catalogue, so a key with no override row still appears at its default. `canManageRestricted` reports whether THIS operator may change the `superAdminOnly` ones; `orphans` lists stored keys this build no longer declares (inert, but clearable).","responses":{"200":{"description":"Settings","content":{"application/json":{"schema":{"type":"object","properties":{"settings":{"type":"array","items":{"$ref":"#/components/schemas/PlatformSetting"}},"orphans":{"type":"array","items":{"type":"object"}},"groups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"blurb":{"type":"string"}}}},"canManageRestricted":{"type":"boolean"}}}}}}}}},"/version":{"get":{"tags":["Meta"],"summary":"What this gateway is running","description":"Public and unauthenticated: an operator mid-rollout, a customer filing a bug and a client checking compatibility all need it, and none necessarily hold credentials. Every response also carries an `X-Koda-Version` header, which is what identifies the instance that served a particular call during a rolling deploy.","responses":{"200":{"description":"Build identity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildVersion"}}}}}}},"/admin/settings/{key}":{"delete":{"tags":["Operator"],"summary":"Reset a setting to the deployment default","description":"DELETEs the override row rather than writing the default back — the row IS the override, so removing it returns the key to its catalogue default and makes it environment-seedable again. 404 when there is no override to clear.","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","enum":["signups_enabled","maintenance_mode","runs_worker_enabled","runs_lease_seconds","runs_reap_grace_seconds","idempotency_stale_minutes","default_skin","default_mode","default_layout","price_open_kobo_per_1m","price_frontier_kobo_per_1m","rate_card_version","frontier_target_pct","default_low_balance_threshold_minor","collections_mode","collections_take_rate_bps","build_default_view","build_image_price_minor","build_image_cogs_minor","build_preview_ttl_hours","build_no_progress_rounds","build_turn_ceiling_multiple","build_reload_activity_window_ms","app_builds_per_day","app_auth_emails_per_day","app_records_per_app","astro_base_url","astro_api_key","astro_open_model","astro_frontier_model","astro_open_noprc_model","image_model_provider","image_model_base_url","image_model_api_key","image_model","inference_escalate_threshold_pct","cogs_open_kobo_per_1m","cogs_frontier_kobo_per_1m","kyc_required","review_llm_enabled","review_lane_mode","review_default_budget_minor","review_max_model_turns","review_wall_clock_ms","review_validation_max_mb","review_corpus_enabled","review_corpus_max_entries","review_corpus_promote_threshold","research_enabled","research_lane_mode","research_wall_clock_ms","direct_lane_mode","research_search_concurrency","research_extract_batch","research_extract_mode","research_fetch_pool","research_max_passes","research_search_timeout_ms","research_search_deadline_ms","research_search_retries","research_default_budget_minor","vapt_enabled","vapt_lane_mode","vapt_default_budget_minor","vapt_max_archive_mb","agent_sessions_enabled","agent_session_max_turns","events_take_rate_bps","tickets_member_wallet_pay","events_ticket_hold_minutes","recording_retention_max_days"]}}],"responses":{"200":{"description":"Reset"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Already at its default"}}},"put":{"tags":["Operator"],"summary":"Set a platform setting","description":"`review_lane_mode` decides which lane every customer's PR-review turns route to (`open` | `verify-frontier` | `frontier`); it defaults to `open` because a review runs on every push, so volume dominates and open-default routing is the margin engine. `research_lane_mode` decides which lane EVERY customer's Deep Research turns route to (`open` | `synthesis-frontier` | `frontier`). It is a platform setting rather than a request field because a research run is many turns, so the choice is a fleet margin decision. An unrecognised value resolves to `open` — a typo must fail toward the cheap lane.","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","enum":["signups_enabled","maintenance_mode","runs_worker_enabled","runs_lease_seconds","runs_reap_grace_seconds","idempotency_stale_minutes","default_skin","default_mode","default_layout","price_open_kobo_per_1m","price_frontier_kobo_per_1m","rate_card_version","frontier_target_pct","default_low_balance_threshold_minor","collections_mode","collections_take_rate_bps","build_default_view","build_image_price_minor","build_image_cogs_minor","build_preview_ttl_hours","build_no_progress_rounds","build_turn_ceiling_multiple","build_reload_activity_window_ms","app_builds_per_day","app_auth_emails_per_day","app_records_per_app","astro_base_url","astro_api_key","astro_open_model","astro_frontier_model","astro_open_noprc_model","image_model_provider","image_model_base_url","image_model_api_key","image_model","inference_escalate_threshold_pct","cogs_open_kobo_per_1m","cogs_frontier_kobo_per_1m","kyc_required","review_llm_enabled","review_lane_mode","review_default_budget_minor","review_max_model_turns","review_wall_clock_ms","review_validation_max_mb","review_corpus_enabled","review_corpus_max_entries","review_corpus_promote_threshold","research_enabled","research_lane_mode","research_wall_clock_ms","direct_lane_mode","research_search_concurrency","research_extract_batch","research_extract_mode","research_fetch_pool","research_max_passes","research_search_timeout_ms","research_search_deadline_ms","research_search_retries","research_default_budget_minor","vapt_enabled","vapt_lane_mode","vapt_default_budget_minor","vapt_max_archive_mb","agent_sessions_enabled","agent_session_max_turns","events_take_rate_bps","tickets_member_wallet_pay","events_ticket_hold_minutes","recording_retention_max_days"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["value"],"properties":{"value":{}}}}}},"responses":{"200":{"description":"Saved"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Unknown key, or a value the catalogue rejects — the error names the legal values or bounds."}}}},"/review/precision":{"get":{"tags":["Reviews"],"summary":"False-positive rate from the adversarial-verification record","description":"The rate is taken ONLY over ADJUDICATED findings (`confirmed` + `refuted`). Deterministic catalogue findings are never verified — a rule provable from the text has nothing for a verifier to add — so they are reported as `unadjudicated` and excluded; counting them as successes would make the number look better the more deterministic rules exist. `unproven` means the verifier could not substantiate the claim from the code available, so it is excluded from the rate and reported on its own. `falsePositiveRate` is **null**, never 0, when nothing has been adjudicated. `scope=all` is a cross-org read and requires an operator.","parameters":[{"name":"scope","in":"query","schema":{"type":"string","enum":["org","all"]}},{"name":"days","in":"query","schema":{"type":"integer","default":90}},{"name":"minSample","in":"query","description":"Minimum adjudications before a rule may be ranked in `noisiestRules`.","schema":{"type":"integer","default":5}}],"responses":{"200":{"description":"Precision summary"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/vcs/installations/{id}/settings":{"patch":{"tags":["Reviews"],"summary":"Per-installation review policy (owner/admin)","description":"`blockingPolicy` decides what may gate a merge: `off` never blocks; `errors` (default) blocks only on a finding that is BOTH error-severity AND high-confidence, or a failed required validation step; `strict` blocks on any error/warn. `validationEnabled` asks to run the repository's own install/build/test — it returns **409** unless the operator also set `KODA_ENABLE_PR_VALIDATION=1`, because a customer toggle must never be able to switch on code execution by itself.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"blockingPolicy":{"type":"string","enum":["off","errors","strict"]},"inlineComments":{"type":"boolean"},"validationEnabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Saved"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found"},"409":{"description":"PR validation is not enabled on this deployment"}}}},"/vcs/installations/{id}/permissions":{"get":{"tags":["Reviews"],"summary":"Granted GitHub App permissions vs what each feature needs","description":"Returns the contract (`required`) alongside what the installation actually granted, plus any `gaps` with a user-facing reason. The bot DEGRADES rather than failing: without `pull_requests: write` there are no inline comments, without `checks: write` there is no blocking, without `contents: read` there is no validation. Only `metadata` and the `pull_request` event are fatal.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Permission check"},"404":{"description":"Not found"}}}},"/research/config":{"get":{"tags":["Research"],"summary":"Whether Deep Research is usable here, and what is missing","description":"Deep Research needs a web search provider AND object storage. `missing` names each unmet dependency so a client can explain the situation instead of showing a control that 503s. `laneMode` and `budgetMinor` are the operator policy this org is subject to (read-only here; changed via `PUT /admin/settings/{key}`).","responses":{"200":{"description":"Config","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean"},"missing":{"type":"array","items":{"type":"string"}},"canFetchPages":{"type":"boolean"},"laneMode":{"type":"string","enum":["open","synthesis-frontier","frontier"]},"budgetMinor":{"type":"integer"},"depths":{"type":"array","items":{"type":"object"}}}}}}}}}},"/research":{"post":{"tags":["Research"],"summary":"Start a research run (async, metered per model turn)","description":"Plans the question, buys web searches, reads sources and writes a CITED report saved as downloadable `generated` attachments.\n\n**The run outlives this request.** It returns `202` with an id and continues server-side; follow it with `GET /research/{id}/stream`, which replays a durable event log rather than piping the live loop — so a dropped connection loses nothing and does NOT stop the run (unlike `/agent/run-loop`). Stop one with `POST /research/{id}/cancel`.\n\n**Money.** Every model turn is metered like `/agent/tools/run`; per-search vendor COGS is folded into the next turn's margin and never billed to the user. The run additionally stops at `budgetMinor`, which a caller may lower below the operator ceiling but never raise above it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["question"],"properties":{"question":{"type":"string","maxLength":2000},"depth":{"type":"string","enum":["quick","standard","exhaustive"]},"projectId":{"type":"string","description":"Write target for the report attachments — requires editor access."},"conversationId":{"type":"string"},"budgetMinor":{"type":"integer","description":"Spend ceiling for this run; clamped to the operator ceiling."}}}}}},"responses":{"202":{"description":"Accepted — the run is executing","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"depth":{"type":"string"},"budgetMinor":{"type":"integer"},"laneMode":{"type":"string"}}}}}},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"description":"Too many runs in flight for this organization"},"503":{"description":"Deep research unavailable (search / storage not configured, or disabled by the operator)"}}},"get":{"tags":["Research"],"summary":"List research runs","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","maximum":100}}],"responses":{"200":{"description":"Runs"}}}},"/research/{id}":{"get":{"tags":["Research"],"summary":"A run with its plan, sources, findings and report files","description":"Every source carries the citation number `n` the report refers to; sources that failed or were skipped are returned WITH their reason, because a research result is only as trustworthy as its gaps are visible.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Run detail"},"404":{"description":"Not found"}}},"delete":{"tags":["Research"],"summary":"Delete a finished run","description":"Sources and events cascade. The report attachments are deliberately kept — they are ordinary files the user may have shared.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"409":{"description":"Cancel the run before deleting it"}}}},"/research/{id}/cancel":{"post":{"tags":["Research"],"summary":"Stop a running investigation","description":"Cooperative: the run stops before its NEXT model turn. The turn already dispatched to a provider still completes and is still billed — those tokens are consumed whatever we do here.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled"},"409":{"description":"Already finished"}}}},"/research/{id}/stream":{"get":{"tags":["Research"],"summary":"Follow a run (SSE, replayable)","description":"Replays `research_events` from `after` (omit for the whole run) and then follows live until the run ends, closing with a `{kind:\"done\", run}` frame. Each frame carries `seq`; pass the last one back as `after` to resume a reconnect cheaply. Closing this stream does NOT cancel the run.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"after","in":"query","schema":{"type":"integer"},"description":"Resume cursor — only events with a greater `seq` are sent."}],"responses":{"200":{"description":"SSE stream","content":{"text/event-stream":{}}},"404":{"description":"Not found"}}}},"/internal/billing/run-renewals":{"post":{"tags":["Internal"],"summary":"Charge due card-subscription renewals","description":"Called by the scheduled cron, not a user. Charges due paystack subscriptions, settles success, duns declines. No-op without a live Paystack key.","security":[{"internalSecret":[]}],"responses":{"200":{"description":"`{ charged, declined, skipped }`"},"403":{"$ref":"#/components/responses/Forbidden"},"503":{"description":"Not configured (KODA_INTERNAL_SECRET unset)"}}}}}}