Documentation / Reference / Admin API
Use this reference to automate dashboard operations. For an illustrated introduction, start with the dashboard tour; for model requests from your application, use the separate application API.
Endpoints below describe request shapes, authorization and refusal conditions. Field lists use ? for optional values. Errors use the envelope {"error": {"message", "type", "param", "code"}}.
Authentication, once#
| Surface | Credential | Mutation requirements |
|---|---|---|
/admin/** |
Admin HTTP Basic or a login session | Session requests need X-XSRF-TOKEN; Basic without a session cookie does not |
/v1/** |
Virtual-key bearer token | Admin credentials do not authorize application traffic |
/internal/**, writable /actuator/env and /actuator/refresh |
ADMIN HTTP Basic | Operator-controlled surfaces |
/actuator/health, /actuator/info, /actuator/prometheus |
Open in the shipped application configuration | Restrict exposure at the deployment boundary |
POST /admin/auth/login sets an HttpOnly JSESSIONID and an XSRF-TOKEN cookie. The CSRF gate applies when a session cookie accompanies the request; a stale cookie can therefore make a Basic-auth mutation fail with 403 forbidden_error.
Coarse /admin/** access requires a control-plane role (ADMIN, TENANT_ADMIN or VIEWER). Each endpoint then enforces its own permissions. Operator-only endpoints refuse TENANT_ADMIN with 403; scoped cross-tenant reads use 404 to avoid confirming existence. See policy authorization for scopes and grants.
Two wire conventions worth reading before copying anything#
Pricing fields spell per-1k without an underscore before the digit: input_per1k,
output_per1k, cache_read_per1k, cache_write_per1k - Jackson's SNAKE_CASE does not
split before digits, while the database columns underneath are _per_1k. Copy the wire
spelling, not the column. One surface disagrees: GET /v1/models exposes cost_per_1k
(explicitly annotated), so the public listing and the admin registry spell the same unit two
ways.
Replacement semantics#
PUT is usually a full replace. Registry provider and model PUTs refuse a partial body
outright (run: {"enabled": false} alone answers 400: openai_compatible providers need a
base_url). The org/app/user/key limits PUTs replace all five fields - an omitted field
becomes null, and null means uncapped, so a partial limits PUT silently removes caps.
The exceptions are stated per entry below: guardrail settings PUT is a patch, key policy
PUT treats null as clear-to-global, and scope policy has no PUT at all (delete, then
create).
Sessions#
| Endpoint | Contract |
|---|---|
POST /admin/auth/login |
Verify credentials, start the session. Body {username, password}; returns the identity payload (accountId, username, role, orgId, workspaces) and sets the cookies. Every failure is the same 401 invalid_credentials - wrong password and unknown user are indistinguishable on purpose. |
POST /admin/auth/logout |
End the session. Needs the CSRF token. 204. |
GET /admin/auth/me |
The same identity payload, for whoever is authenticated - session or Basic. |
Organizations, keys, and their limits#
| Endpoint | Contract |
|---|---|
POST /admin/organizations |
Create an org (the tree mirrors it as a TENANT scope automatically). Body {name}. |
GET /admin/organizations |
Paginated roster: search, sort (name / created_at), page, size. |
GET /admin/organizations/{orgId} |
One org. ADMIN, or TENANT_ADMIN/VIEWER reading their own. |
PUT /admin/organizations/{orgId}/limits |
Full five-field replace: {rpm_limit?, tpm_limit?, max_budget?, concurrency_limit?, budget_window?} - null or omitted means uncapped. Refuses budget_window with a named 400: the window rides the budget rule (monthly by default), not the limits row. Operator-only. |
GET /admin/organizations/{orgId}/default-model |
The tenant default and its reach over the org's active keys: keys_active, keys_inheriting (no default of their own), keys_own_default, keys_refusing (their own omitted-model override is refuse), plus gateway_omitted_model and known (whether the default names a registered model, alias or routing group; null without a default). One indexed count. Read like the org (VIEWER of the org and up). |
PUT /admin/organizations/{orgId}/default-model |
{default_model} - the tenant's default for requests that name no model, used when the key has none of its own; null or blank clears it. At most 200 characters. Resolved like any name, inside the key's allowed set. Every key of the tenant is re-read on its next request. Audit org.update_default_model. Same authorization as retention. |
PUT /admin/organizations/{orgId}/retention |
{retention_days} - null returns to the gateway default. Floor 30; out-of-range is refused, never clamped. |
GET /admin/organizations/{orgId}/retention/effective |
What is actually kept, by whom, for how long: configured/defaulted/floor/max, spend rollups, routing decisions (with the cap state resolved server-side), operator windows - one field per knob, always. |
GET /admin/organizations/{orgId}/effective-limits |
The org's own values beside what actually binds it. |
POST /admin/organizations/{orgId}/keys |
Issue a key. Body {label?, kind, allowed_models?, budget_usd?, rate_limit_rpm?, rate_limit_tpm?, expires_at?, default_model?, scope_id?}. A scope_id must lie inside the org's subtree (outside answers 404 unknown scope), and on the app and user doors inside that app's group (400, naming why). A key's own limits attach at its chain mirror and bind in both flag states; what it inherits from a deeper home binds flag-on. kind is required (service or personal) with no default. scope_id homes the key at an authored tree scope: refused for the platform root (400), and an unknown or out-of-subtree scope answers 404 unknown scope identically, so the door confirms nothing about other tenants' trees; absent means the derived chain mirror. Response carries the raw key once, plus scope_id disclosing the home. |
GET /admin/organizations/{orgId}/keys |
Paginated: search (prefix/label), sort, page, size. |
DELETE /admin/keys/{keyId} |
Revoke. Terminal. |
PUT /admin/keys/{keyId}/kind |
Reclassify: {owner_account_id?} with the kind implied by ownership rules. |
POST /admin/keys/{keyId}/rotate |
New key with the old key's full chain and settings, including its authored scope home (a key with no authored home derives its chain mirror); body {grace_hours?} - both keys work through the grace window, then the old one expires. Only ACTIVE keys rotate (409 otherwise). Raw key returned once, as at issuance. |
PUT /admin/keys/{keyId}/limits |
Same five-field replace semantics as the org door, clamped restrict-only against the chain above. |
GET /admin/keys/{keyId}/effective-limits |
Resolved through user, app, org, global - the tightest wins and is named. |
PUT /admin/keys/{keyId}/policy |
Per-key resolution policy: {default_model?, unknown_model?, on_denied?, omitted_model?} (unknown_model is reject, resolve-only or fallback-to-allowed; on_denied is reject or substitute; omitted_model is route or refuse, overriding pointsman.resolution.omitted-model for this key; anything else is a 400) - null clears the field back to the global default (this PUT is the null-clears exception). Scoped like the key's limits (16 October): ADMIN, or a TENANT_ADMIN of the key's own org; a foreign-org key answers 404 like a missing one. Read the current values first (GET .../resolution): the write replaces every field. |
GET /admin/keys/{keyId}/resolution |
What a request on this key gets with no model, an unknown model or a model outside its allowed set. Each of default_model, omitted_model, unknown_model and on_denied reports stored (on the key), effective (what requests get, through the request path's own precedence), source (key, tenant, gateway, or none) and inherited (the value with nothing stored on the key). Also returns fallback_mode, route_preference, successor_inherits_allowlist and tenant_default_model, so an editor can send the full-replace policy write without clearing anything. Read like the key's effective limits. |
PUT /admin/allow-policy/{level}/{id} |
The allow-list at any level (org/app/user/key): {allow_policy, allowed_models} where policy is explicit or inherit (any other value is a 400; absent means inherit). Returns the stored set plus hygiene warnings (shadowed grants, dangling model names) rather than refusing them. |
GET /admin/usage?org= |
Spend and token summary for one org. org is required - missing answers 400 invalid_request_error naming the parameter. |
GET /admin/audit |
Committed mutations only ("who changed what"; denied actions are a filed roadmap item). Params target?, since? (ISO instant), page, size (max 200). Omitting since leaves the lower time bound open (the query uses the epoch sentinel). |
Workspaces (apps), members, users#
| Endpoint | Contract |
|---|---|
POST /admin/organizations/{orgId}/apps |
Create a workspace: {name, max_budget?}. Mirrors as a GROUP scope. |
GET /admin/organizations/{orgId}/apps / GET /admin/apps/{appId} |
Roster (paginated, searched) and detail. |
GET/POST/DELETE /admin/apps/{appId}/members[...] |
Workspace membership: {account_id, workspace_role}; delete by account id. |
POST /admin/apps/{appId}/keys / GET .../keys |
Issue and list workspace-scoped keys - same body and same refusals as the org key door. |
PUT /admin/apps/{appId}/limits / GET .../effective-limits |
The five-field replace and its resolved view, at app level. |
POST /admin/apps/{appId}/users / GET .../users |
Create ({email}) and list users in a workspace; GET /admin/organizations/{orgId}/users lists across the org. |
GET /admin/users/{userId} /keys / POST .../keys / PUT .../limits / GET .../effective-limits |
The user tier: detail, attributed keys (issue resolves app and org from the user authoritatively - contradicting ids are refused as a chain violation), limits, effective view. |
POST /admin/users / GET /admin/users |
Admin accounts (operators and tenant admins), distinct from hierarchy users. |
Limit rules#
One rule shape everywhere - {applies_when, count_by, amount, budget, window, action,
action_duration_seconds?, degrade_to?} - documented with its vocabularies in
gateway-controls.md. Unknown words in any closed vocabulary are
refused at the door naming the legal set. Responses add id and inherited.
| Endpoint | Contract |
|---|---|
GET/POST /admin/organizations/{orgId}/rules / PUT/DELETE .../rules/{ruleId} |
Rules at the org. |
GET/POST /admin/apps/{appId}/rules / PUT/DELETE .../rules/{ruleId} |
The same door at app level. |
POST/GET /admin/scopes/{id}/rules / DELETE .../rules/{ruleId} |
The same machinery addressed by tree scope - dedup by counter shape, restrict-only clamp against every ancestor. ADMIN at the scope. |
Budget trust (the budget fence)#
Budget trust belongs to a tenant slot (tag) and generation. Counter-store restart, data loss or rollback can fence the slot; budgeted traffic then receives 503 budget_recovering until recovery establishes a trusted generation. These endpoints are operator-only (root ADMIN). Follow the recovery procedure, not a counter reset.
| Endpoint | Purpose |
|---|---|
GET /admin/budget/trust |
List slots and their generation, state and activation; fenced slots first |
GET /admin/budget/trust/{tag}/evidence |
Read the current generation's evidence and unresolved reservations |
POST /admin/budget/trust/{tag}/fence |
Fence a trusted slot whose balance is known to be wrong |
POST /admin/budget/trust/{tag}/attest |
Establish reviewed balances in a new generation |
GET /admin/budget/held |
List held settlement records and their inclusion/archive status |
POST /admin/budget/held/{event_id}/archive |
Preserve an included held record in the journal archive |
Trust and activation states#
| Field | Values | Meaning |
|---|---|---|
state |
TRUSTED, FENCED, RECOVERING |
Trust state of the slot's generation |
activation |
NONE, ACTIVATING, ACTIVE |
Whether that Redis epoch is durably activated and admitting traffic |
generation |
Generation number | Version reviewed before a conditional recovery write |
Fence a known-invalid balance#
Request body: {expected_generation, evidence_ref, reason}.
- Match the reviewed generation; a stale value returns 409.
- Fence PostgreSQL state first, then that generation's Redis epoch, then this instance's cache. The Redis epoch closes admission across replicas.
- Let already-admitted requests settle normally. Repeating the operation on an already fenced slot changes nothing.
The operation audits budget.fence. Invalid input returns 400; an unknown slot returns 404.
Attest reviewed balances#
Request body: {expected_generation, evidence_ref, balances: {counter_key: micros}}.
| Condition | Result |
|---|---|
Positive expected_generation matches under the transaction lock |
A fenced/recovering slot can open generation + 1 |
| Missing or invalid required fields, foreign-slot counter key or negative balance | 400 |
| Unknown slot | 404 |
| Stale generation or slot not fenced/recovering | 409, without recovery writes |
Balance below a known evidence lower_bound |
409 names the floor; nothing is written |
Evidence floor is UNKNOWN |
Attestation can be accepted; evidence_status is recorded in the audit |
| Corrupt or conflicting journal archive evidence | 409 until archive problems are resolved |
Each named counter begins at its attested balance. An unnamed counter whose period began before the attestation stays refused; a new period begins at zero. The audit action is budget.attest.
Evidence completeness: a known lower bound is not proof that all spend is known. With an open recovery boundary,
backup_gapidentifies spend after the backup that the gateway cannot observe. Establish the balance from the required independent evidence.
Read counter evidence#
GET /admin/budget/trust/{tag}/evidence is read-only and returns 404 for an unknown slot.
| Field | What it represents |
|---|---|
init |
Initial balance and its basis |
settled |
Recorded charges |
held |
Journal settlements whose database rows were lost and which an attestation has not included for this counter |
outstanding |
Admitted, unsettled reservations |
uncertain |
Prepared/unknown reservations or pending releases; contribution is bounded by [0, reserved] |
deferred |
Owed adjustments already included in settled |
redis |
Live counter value for information; not independent evidence |
lower_bound, upper_bound |
Evidence bounds |
status |
EXACT, BOUNDED or UNKNOWN |
backup_gap |
An open recovery boundary with unobservable post-backup spend |
archiveProblems |
Corrupt/conflicting archive files; a nonempty list blocks attestation |
Held evidence includes archived records whose inclusion was lost in a restore.
Unresolved reservations#
unresolvedReservations lists rows marked reserve_unresolved:* in the slot's current or a later generation. It corresponds to pointsman_budget_reservation_unresolved and returns total, limit (100) and the oldest items.
Each item includes {requestId, counterKey, generation, state, reservedMicros, reason, createdAt, dispatches, dispatchOutcomes}.
- A crash-orphaned row is
ADMITTED, withinterrupted_dispatchedorinterrupted_no_dispatch_record. Boot recovery records503 accounting_interruptedfor it. - Its reservation remains in
outstanding, so the floor retains the hold. reservedMicrosis the counter hold, not a maximum provider bill.- Zero recorded dispatches is missing evidence, not proof that nothing was sent.
Preserve held settlement records#
GET /admin/budget/held returns event ID, slot, generation, cost in micros, SHA256, counters, includedFor, archivable and archived.
| State | Meaning |
|---|---|
includedFor |
Counters whose attested balances include this record (budget_held_inclusion) |
archivable=true |
Included for every counter the record names |
archived=true, inclusion lost by restore |
Re-include it through attestation; do not archive it again |
| Not yet included | Its cost remains in the relevant evidence floor |
Archive with POST /admin/budget/held/{event_id}/archive, body {evidence_ref}. It moves the record intact to a/<event>.<sha256>.held.json, ending retries/replay without deleting the evidence.
- 409 until every named counter includes the record through attestation.
- 404 if the event is not held.
ALREADY_ARCHIVEDfor a repeat.- Audit action:
budget.held.archive.
Scopes, grants, and policy (the tree)#
| Endpoint | Contract |
|---|---|
GET /admin/scopes/root |
The platform root. |
GET /admin/scopes/{id}/tree |
The subtree from a node: children with ids, names, slugs. |
POST /admin/scopes |
Author a scope: {parent_id, name, slug}. Refuses a missing parent_id by name (the root exists once and is never created here), an unknown parent (404), a sibling slug collision, and depth beyond the configured maximum (POINTSMAN_SCOPES_MAX_DEPTH_BELOW_TENANT, default 2 - a number an operator raises, not a schema anyone migrates). Kind comes from position - a child of the root is a TENANT, everything else a GROUP; the caller never says. OWNER at the parent: handing out structure is delegation-shaped power, so it sits above ADMIN. |
GET/POST /admin/scopes/{id}/grants / DELETE /admin/scopes/grants/{grantId} |
Grants: {principal_id, role, expires_at?} with roles ordered VIEWER < DEVELOPER < ADMIN < OWNER. The door enforces the delegation rule through the domain function itself: a grantor confers STRICTLY below their own strength, never at or above it - an ADMIN attempting to grant OWNER is refused, verbatim, and so is a VIEWER granting VIEWER or an ADMIN granting ADMIN; an OWNER may grant any role including OWNER, since nothing sits above it . Revocation takes the standing to grant, an actor may also revoke an authored grant when their current applicable role is at least that grant's role. Authorship alone does not bypass lost authority. |
GET /admin/scopes/{id}/limits/preview |
Every level from the target to the root, each level's rules, and per amount WHICH level binds - named by the server, not inferred. |
POST /admin/scopes/{id}/policy |
A policy value at a scope: {subject_kind, subject_id?, key, value}. Refuses by name: unknown key or subject kind (closed vocabularies, the refusal lists them); a PRINCIPAL or CREDENTIAL subject id with no backing row; entries the path's composed policy already excludes (storing them would create a control that looks set and does nothing); and a duplicate (scope, subject, key) as 409 - an edit is delete-then-create; there is deliberately no PUT. |
GET /admin/scopes/{id}/policy / DELETE .../policy/{policyId} |
List and remove. |
GET /admin/scopes/{id}/policy/preview |
The composed policy along the real path with per-level provenance (what each level removed), and enforced_on_request_path reporting only the data-path flag. The preview can compose subject rows the live request path does not consume in the same way; see live composition limits. |
The registry: providers, models, credentials#
| Endpoint | Contract |
|---|---|
GET/POST /admin/registry/providers / PUT/DELETE .../{id} |
Provider rows: {name, adapter_type, base_url?, credential_ref?, extra_headers?, quirks?, timeout_ms?, enabled}. PUT is a full replace and refuses partial bodies by naming the missing field. Responses always carry credential_resolves. Refusals worth knowing: credential-shaped values in extra_headers (credentials belong in credential_ref); reserved header names in extra_headers (Authorization, Content-Type, X-Pointsman-Dispatch-Id; a row stored before the last was reserved has its value dropped at load, with a WARN and gateway_registry_reserved_header_stripped_total); unknown quirk flags (a quirk you believe is active while the provider gets spec-violating requests is worse than a rejected config). |
POST /admin/registry/providers/{id}/probe |
A real one-token call through the real adapter and credential; verdict recorded on the row (probe_status, probe_at). Advisory - enabling is just the PUT. |
GET/POST /admin/registry/models / PUT/DELETE .../{id} |
Read, create, fully replace or remove a model row. Fields and serving-identity rules are described under Model fields and activation. |
GET/POST /admin/registry/providers/{providerId}/credentials / PUT .../{id} / POST .../{id}/rotate / DELETE .../{id} |
Sibling credentials for one provider (the pool). Create: {label, credential_ref, enabled?, tpm_limit?}. PUT is a patch over label, enabled and the TPM ceiling: a field the body does not carry keeps its value, tpm_limit: null clears the ceiling ; it refuses a changed ref - swapping the secret is the rotation door, which replaces the secret behind a live key while draining the old client. Reads never expose a secret: the env:NAME ref plus credential_resolves, exactly as the primary. Refuses (400) a sibling on a code-backed provider, a changed ref on PUT, a rotation to the same ref, the label primary, a raw secret, a non-positive tpm_limit; refuses (409) any change that would leave an enabled provider with no live key. The behaviours: provider credential pools. |
POST /admin/registry/import / GET /admin/registry/export |
The one config pipeline: all-or-nothing (one transaction, one version bump, one audit row), accepting JSON or YAML; export is its round-trip. |
Model fields and activation#
Model rows use {provider_name, model_id, aliases?, input_per1k?, output_per1k?, cache_read_per1k?, cache_write_per1k?, context_window?, max_output?, capabilities?, fallback_model?, tags?, lifecycle?, accepted_response_models?, enabled, ...}.
Accepted response identities#
accepted_response_models declares additional exact native model IDs that a deployment accepts as its serving identity. Its own model_id is always accepted.
| Rule | Contract |
|---|---|
| Length and count | At most 16 IDs; each 1–96 characters from [A-Za-z0-9._:/@+-] |
| Uniqueness | No duplicates, the row's own ID, or another row's ID/alias; invalid declarations return 400 |
| Omitted on PATCH | Preserve the current list |
| Omitted on PUT, POST or import | Use an empty list |
| Cache identity | A separate registry-wide trust-policy digest partitions exact/semantic entries and in-flight sharing for new requests, including fallback routes |
| Existing requests | Keep their admitted policy; reordering the same accepted-ID set does not invalidate reuse |
| Quality identity | Does not change generation settings or the quality qualification digest |
Model write refusals#
| Input | Refusal or load behavior |
|---|---|
Retired routing_patterns field |
Refused rather than silently ignored |
| Enabling a null-priced model with budget enforcement | Declare explicit rates; zero means deliberately free |
| Retiring model without a successor | Refused unless acknowledged |
| Model ID already registered under another provider | 409; model IDs must be globally unique |
| Capability unsupported by a code-backed adapter | 400 names the capability and adapter. Anthropic/Ollama adapters carry text only; unsupported stored declarations are stripped on load, warned and counted by gateway_registry_capability_stripped_total |
Reasoning effort#
capabilities.reasoning_effort accepts none, minimal, low, medium or high only on an openai_compatible row. The value is sent verbatim; Pointsman does not translate vendors' reasoning levels. Unsupported adapters or values are refused on write and stripped/countable on load (capability=reasoning_effort). The upstream model must support the value or can return its typed 400.
The setting participates in the quality registry digest and response-cache identity. It is fixed by the operator's model row, not an arbitrary caller body override.
Admission snapshots and cache activation#
- Admission pins a registry snapshot. Routing, quality decisions and serialized attempts use that snapshot.
- If a row changes before an attempt sends, the attempt is refused as
configuration_changed: no send, retry, breaker fault or arm penalty. The fallback ladder may move within the decision's constraints. - Cache lookup, coalescing and publication use the generation identity computed at admission: a registry-wide digest of declared generation settings.
- Changing any model's generation settings invalidates reusable work across routes, including fallback answers. Non-generation edits such as prices or aliases do not.
- A response is published, or accepted by a coalesced waiter, only when the serving model belongs to the route (or nothing involved declares a setting). Otherwise the waiter executes its own call.
This provider-state snapshot binding applies to openai_compatible rows. Code-backed adapters retain their own provider resolution.
Cache namespace and rollback: generation identity uses
gen/v3. Deploying that namespace starts routes cold; older entries are unreachable. An older binary can read its own earlier namespace after rollback. Invalidate the cache withDELETE /internal/cacheif generation settings or accepted-response-ID policy changed in the meantime.
Paged registry browsing#
The dashboard uses these ADMIN-only additions. The existing unpaged endpoints above keep their array responses.
| Endpoint | Contract |
|---|---|
GET /admin/registry/providers/page |
page=0, size=25, search=. Literal case-insensitive substring over provider name, adapter and endpoint. |
GET /admin/registry/models/page |
Same parameters, plus enabledOnly=false. Search covers model ID, provider name and family. |
POST /admin/registry/models/availability |
Read-only JSON array of at most 1000 exact model IDs (each at most 255 characters); returns the enabled IDs found. Missing/disabled IDs are omitted. Normal admin authentication and POST CSRF rules apply. This is registration availability, not tenant authorization. |
Page responses are { "items": [...], "total": 123, "page": 0, "size": 25, "modelCounts": {} }. total counts all matching rows. Provider pages include per-provider registered-model counts keyed by the returned provider names; model pages leave that map empty. Page must be 0..100000, size 1..100, search at most 200 characters; invalid bounds return 400. Out-of-range pages return an empty items array with the matching total.
Ordering is provider name/UUID for providers and provider name/model ID/UUID for models. Each response is read in one repeatable-read transaction. Offset positions can shift when the registry changes between requests; this API is live browsing, not a snapshot export. Use the configuration export endpoint for a complete configuration document. Model page responses preserve the existing successor-narrowing warnings. These routes are control-plane reads and do not dispatch provider calls.
Paged browsing is a control-plane capability; its availability does not expand the qualified runtime scope.
Operator-fixed thinking mode#
An openai_compatible model row may declare capabilities.thinking_mode as enabled or disabled. Pointsman sends the corresponding native thinking: {"type":"enabled"} or thinking: {"type":"disabled"} on both sync and stream requests. An absent setting preserves the provider default. Kimi K2.6 defaults to thinking enabled, so use disabled to select its non-thinking profile. This is an operator model-row control, not a caller-controlled chat field or a generic body-injection map.
The admin API refuses other values, a non-openai_compatible adapter, and a row that declares both thinking_mode and reasoning_effort. A hand-edited invalid row fails registry reload and leaves the prior good snapshot in service. A supported setting participates in the quality registry digest, the registry-wide cache and in-flight generation identity (including fallback routes), and the served-generation response header. A change does not alter requests already admitted under their pinned snapshot. See the Kimi setup example.
Guardrails and control#
| Endpoint | Contract |
|---|---|
GET /admin/control/effective |
Reported configuration/policy values with source and mutability, plus the guardrail catalog and parameter descriptors. Global RPM/TPM/spend read the constructed platform-rule snapshot (source: runtime-holder, mutability: boot); null means no loaded ceiling, including a non-positive boot target. Rule windows are described from that snapshot and spend stays in exact micro-dollars. Current property targets for those three are not separately projected. Global concurrency retains its separate mutable-property read/refreshable metadata. These observations are not proof of traffic enforcement. See ownership. Source and focused API checks. |
PUT /admin/control/guardrails/{ruleKey} |
A patch, not a replace: {enabled?, mode?, params?, tenant_policy?}. An absent enabled or mode keeps the rule's current value: the stored setting, else the boot configuration. Until 2026-09-25, a mode-only patch switched the rule off, and an enabled-only patch was refused. Rule parameters nest under params - a flat body is accepted with 204 and the parameters silently ignored, so read the effective surface back after writing. Secrets are write-only: absent preserves, explicit null clears. Boot-badged parameters (compiled regex) refuse live edits. |
GET /admin/control/guardrails/tenant?org= / PUT /admin/control/guardrails/{ruleKey}/tenant?org= |
Tenant tightening - enable what is off globally, push monitor to enforce. Loosening below the global setting is refused by the resolver; the operator can lock a rule against tenant changes entirely. TENANT_ADMIN may write their own org. From code (P5c proved the loosening refusal live). |
GET /admin/control/guardrails/workspace /workspace-effective / PUT .../{ruleKey}/workspace |
The same tightening one level down, and the workspace-wide effective view with binding levels. |
POST /admin/guardrails/dry-run |
{messages, response_text?} through the real chain: per-rule would pass / would block / would redact: N / not reached verdicts with zero side effects - no audit rows, no metrics, and externals are never called with your text (external - not evaluated). Operator-only, deliberately: it evaluates the global chain, and a tenant reading it would trust a result that describes someone else's configuration. |
Observe, ops, and the bandit#
| Endpoint | Contract |
|---|---|
GET /admin/observe/requests?org= |
The request ledger, newest first; org is required (missing answers 400 naming it). Rows carry model, provider, tokens, cost, cache_hit, status, error_type, latency. provider is the deployment the request's send went to (since 17 October 2026: one model behind two providers is two deployments, never the registry's owner of the model): a coalesced delivery names its leader's, a cache hit names none (null: no provider served it), a failure names the last deployment a send went to, or none when nothing was sent. GET .../requests/{id} is one row's detail. |
GET /admin/observe/metrics/overview |
The golden metrics, ADMIN-proxied. |
GET /admin/observe/analytics/overview /groupby /cache /errors |
Ledger-backed analytics per org: KPIs, group-bys, cache hit rate with hit-vs-miss latency and savings estimate, error breakdowns. |
GET /admin/ops/spend?org= |
The flat ops spend view with as-of rollups. |
GET /admin/ops/decisions?org= |
The routing decision log - arm chosen, provenance, propensity. provenance filters by posterior, exploration_floor, uniform_prior or cost (16 October: a lowest-cost bandit group's decision, logged under its tenant with no posterior and no propensity); any other value is no filter. feedback_score, feedback_revision and feedback_state (rated or withdrawn; 18 October) are the caller's feedback as the gateway accepted it. Only a higher revision replaces them, and a withdrawal keeps its revision with no score. They are not the learner's application, which the learner records itself. Rows rated before revisions were kept have no revision or state. Null fields are omitted. |
GET /admin/ops/convergence |
Registry convergence: whether every instance serves the current version. |
GET /admin/ops/guardrails/health |
Per rule: enabled/mode, fail posture, and degradation counts (failed_open / failed_closed) in 3-hour buckets - counts are audit-backed and exact, latency is instance-local. |
GET /admin/bandit/state |
The belief surface: per sampling key (tenant + task type, with its learning_semantics: ts2, or ts1 for frozen legacy keys), every arm's posterior, mean, seeded-vs-live observation split and, for ts2 keys, three separate quantities - effective_mass (the discounted evidence the sampler draws from), observed_decisions and rated_decisions (null for ts1), plus fetch staleness (fetched, version, age_seconds) and lints_key_count, the number of sampling keys routed by the contextual (LinTS) sampler, which are disclosed as a count rather than drawn as Beta rows. |
GET /admin/bandit/cost-state[?org=&page=&size=] |
What the in-process cost sampler has learned (16 October): per state (a strategy: bandit, objective: cost group of one tenant, kind: group; or a quality profile with selection: cost, kind: profile), per member - one per deployment, provider and model (17 October) - observations (decayed; 0 = the declared price alone), cost_vs_declared (the multiplier selection centres on: the posterior of the member's own observations shrunk toward the state's pooled multiplier, so a member with no evidence of its own reads at the group's multiplier rather than 1.0; 1.0 = as declared, which is day one; null for a free or unpriced model, which has none), with the state's prior_cost_vs_declared (the pooled multiplier itself), completed (share of calls that returned) and price (declared, free for declared zero rates, unknown for no declared price - never treated as free - or null before the member was first priced). Each member also has priced_updates, reliability_updates and failure_updates (18 October): updates applied since the sampler started, cumulative and never discounted. observations ages and can hold steady under steady traffic, so it cannot show that learning continues; these counts can. The response's sampler_instance names the sampler that counted them, and a new value means a restart, with the counts at zero again. Also learn_from_direct (the operator's setting). An ADMIN sees every tenant; anyone else sees their own org's states only, server-injected; a profile serving every organisation reaches an ADMIN only. In-memory on the instance: a restart starts again from declared prices. |
Quality-guarded routing (experimental)#
Authorization is decided in each handler: operator endpoints need root ADMIN (gate admin.quality);
organization endpoints need that organization's ADMIN (legacy TENANT_ADMIN) or root ADMIN (gate
admin.org.quality). Every write is audited; writes return 503 quality_disabled while
pointsman.quality.enabled is false. How-to.
| Endpoint | Behaviour |
|---|---|
GET /admin/quality/status |
What this instance enforces: enabled, policy version, expiry, last refresh problem, every profile's baseline chain, baseline evidence (baseline_unqualified when none) and qualified models with evidence expiry. Each profile also reports selection (cheapest, bandit or cost) and feedback_floor (16 October). |
POST /admin/quality/orgs/{org}/validators |
Register a key of that organization as a trusted validator for listed rubrics: key_id, name, rubrics, gold_allowed. A key of another organization is 404; a second registration 409. Audit quality.validator.register. |
GET /admin/quality/orgs/{org}/validators |
The organization's validators. |
POST /admin/quality/orgs/{org}/validators/{id}/deactivate |
Deactivate (the row and its evidence stay). Audit quality.validator.deactivate. |
GET /admin/quality/orgs/{org}/evidence/{evidence_key} |
Derived counts per namespace (trusted, proxy, voluntary): independent, successes, failures, severe, conflicts, overridden. |
POST /admin/quality/epochs |
Register a frozen evaluation spec (body = spec). 201 new, 200 same spec again; 400 spec_invalid / 422 spec_unsupported with every reason. Audit quality.epoch.register. |
POST /admin/quality/epochs/{id}/adjudicate |
The one sealed batch for that epoch; the result is stored forever. A second batch is 409 epoch_already_adjudicated; over max-batch-rows is 413. Audit quality.epoch.adjudicate. |
GET /admin/quality/epochs[?profile_id=&limit=], GET /admin/quality/epochs/{id} |
Every epoch, failed ones included, with spec, result and respecified_after_look. Each epoch also reports latency_gate (quantile, ratio_max, max_ms), null when its spec gates on quality only (16 October). |
POST /admin/quality/policy/preview |
Validate a draft and bind its evidence: valid, errors, warnings, the bound document and the change against the active version. Writes nothing. |
POST /admin/quality/policy/promote |
document, expected_version (the active version, or null for the first), reason. Invalid: 400 policy_invalid; stale expectation: 409 stale_expected_version. Audit quality.policy.promote. |
POST /admin/quality/policy/rollback |
target_version, expected_version, drop_expired, reason: a NEW version from the old document, re-validated today; expired evidence or registry drift is 409 rollback_refused unless dropped. Audit quality.policy.rollback. |
GET /admin/quality/policy/versions[?limit=], GET /admin/quality/policy/versions/{v} |
Every version, the document on the single read. |
GET /admin/quality/audit |
The audit lane: enabled, sample rate, queued, dropped since this process started, and jobs by status. |
GET /admin/quality/audit-window-corpus?profile_id=&from=&to= |
The identity to pin as a spec's corpus_sha256 BEFORE sealing that window. |
POST /admin/quality/epochs/{id}/seal-audit-window |
from, to (closed, half-open): every selection becomes a row, unlabelled ones missing; refused when the window was not the pinned one (409 window_not_preregistered), is still open, or had dropped selections (409 audit_window_incomplete). Audit quality.epoch.seal_audit_window. |
GET /admin/quality/auxiliary |
The operator account: calls, spent, reserved and unknown exposure against their limits; billing_owner: operator. |
GET /admin/quality/observation |
Decision-only observations (served vs would-serve, bounded) and alternative-answer attempts (status and cost only). |
POST /admin/quality/auxiliary/attempts/{id}/reconcile |
actual_micros, note: settles one UNKNOWN attempt with a reviewed amount. Anything else is 409 not_unknown. Audit quality.aux.reconcile. |
Internal and actuator#
| Endpoint | Contract |
|---|---|
DELETE /internal/cache?tenant= |
Invalidate the semantic cache - one tenant's entries, or everything when tenant is omitted. The tenant is a literal id: a value carrying *, ?, [, ] or \ is refused (400) rather than matched, because until 2026-09-05 ?tenant=* flushed every tenant while the response reported one. Fans out: the L2 flush is shared, and every other instance drops its L1 on illg:cache:invalidate (M-3). Basic ADMIN. |
GET /internal/thread-info |
Virtual-thread diagnostics. Basic ADMIN - run: 401 without credentials, despite an older code comment claiming this surface is open. |
POST /actuator/env / POST /actuator/refresh |
The writable environment (Basic ADMIN): how runtime-tunable values move without a restart - the cache threshold retune is the worked example in the cache how-to. |
GET /actuator/health /info /prometheus |
Open on purpose: an unauthenticated 401 probe reads as a dead instance, and the scrape endpoint feeds Prometheus. |
Correlated provider dispatch history#
GET /admin/observe/requests/{id}/dispatches?page=0&size=25 returns physical provider
sends for an authorized ledger record. {id} is the numeric ledger ID, not a caller-supplied
request header. Authorization and foreign/missing-ID behavior match the ledger detail
endpoint. The query uses the stored internal request UUID and organization together.
The response carries request_id, correlation_available, items, page, and has_next.
Page size is bounded to 1–100. Each item includes dispatch UUID, sequence/rung/attempt,
provider/model, recorded outcome, optional HTTP status/error class/cost, usage provenance,
dispatch/finish timestamps, and reported_model (V68: the identity the provider reported for that
send, sanitized; null when it named none). model is the dispatched deployment - the identity the
tariff belongs to. Credential IDs and secrets are not returned. Unknown usage
is not zero. A legacy row without the internal UUID returns correlation_available: false;
a currently empty list can also reflect journal-delivery lag and must not be treated as proof
that no provider send occurred. Ledger list/detail entries additionally expose nullable
request_id for this correlation.
Account update safety#
PATCH /admin/users/{userId} retains its existing root-owner authorization. Disabling the
signed-in account or the final enabled global administrator returns 409. Competing global
administrator deactivations serialize on ordered row locks before checking remaining access.
Password replacements must contain at least 12 characters and at most 72 UTF-8 bytes;
invalid replacements return 400. Password values are never written into the audit detail.
These rules affect account updates, not the separate account-creation contract.
Paged keys, accounts and quality history#
These additive browse endpoints preserve the authorization of their corresponding legacy list APIs. Legacy endpoints and their response shapes remain available.
| Endpoint | Authorization | Literal case-insensitive search | Order |
|---|---|---|---|
GET /admin/organizations/{orgId}/keys/browse |
Existing organization visibility gate | Key prefix or label | Created time descending, UUID descending |
GET /admin/users/page |
Existing root account-list ADMIN gate | Username or account UUID | Username, UUID |
GET /admin/quality/epochs/page |
Existing root quality ADMIN gate | Profile ID or epoch UUID | Registered time descending, UUID descending |
GET /admin/quality/policy/versions/page |
Existing root quality ADMIN gate | Version number, reason or author | Version descending |
All accept page (zero-based, 0..100000), size (1..100, default 25), and search (up to 200 characters, trimmed). Invalid bounds return 400. % and _ are literal text. Search, totals and paging execute in the database. Key browsing also accepts unclassified=true and returns {items,total,page,size,unclassified_total}. The unclassified total covers the entire authorized organization regardless of the search or kind filter. It is not a count of just the visible page. Responses expose prefixes, never raw keys or key hashes.
Account and quality pages use {items,total_items,total_pages,page,size}. Account responses omit password hashes. Quality history queries select summary metadata without loading sealed policy documents, epoch specs or adjudication results. Full details remain available through the exact-ID endpoints. GET /admin/quality/policy/versions/{version} now resolves that version directly, including versions older than the latest 200.
Counts and rows share a repeatable-read transaction in each inventory read. Pages are live offset views, not multi-request snapshots or export/completeness proofs. Concurrent changes can shift later pages. The dashboard resets its page when the scope or filter changes, keeps mutations tied to explicit selected IDs, and refreshes totals after mutations. No access grants, key-kind semantics, promotion checks, rollback checks or quality verdicts are changed by browsing.
These routes are included in the matching gateway/dashboard candidate accepted on
28 October (e71e5da8); see launch status for combined source,
package and endurance evidence. Use the matched release artifacts rather than
assuming an older gateway implements these APIs. Bounded output is not evidence of
constant-time substring search or a throughput benchmark.
See Dashboard configuration ownership for conditional registry edits, Jev partial deployment drafts, operational observations and intentional API/deployment-only scope.