BBramaDocs

Brama HTTP API reference

Administration and operations

The /v1/admin/* family is how the console changes this deployment: the route registry, provider credentials, configuration adoption and the two deliberate actions against a named subscription. Alongside them sit the three paths an operator watches: /health, /readyz and /stats.

Who the admin family answers

Every /v1/admin/* path answers only a brama-desktop identity carrying no model allowlist; every other caller receives 403 forbidden (captured). That identity comes from the boot table alone: the launcher reads the Skarbiec item brama-desktop-model-router, field token — the same item Brama Desktop acquires through its workload key — and admits its value as brama-desktop with no agent and no allowlist. A bearer the authority resolves instead always carries the routes it was issued, so it can never reach this family. When the launcher cannot read that item it says optional client brama-desktop skipped on its stderr and starts anyway; the admin family then answers 401 to the desktop and to the console's own admin calls alike. Responses carry identifiers, usage and status only: a subscription credential is write-only and is never part of an answer, on any path, including an error path.

GET /v1/admin/snapshot

Routes and providers as this process currently holds them. Captured keys: schemaVersion, whose value is the number one; automaticRollback, which is true; boundaries ({"credentials":"skarbiec","releases":"stado","routing":"brama"}); providers[] in the same shape /stats reports; and routes, the registry document as read from the configured file. When that file will not read, the answer is 503 read route registry <path>: <cause>, retaining the actual filesystem, parser or validation refusal.

The registry document under routes carries exactly three known top-level fields — the schema version, deployments and routes — and routes maps each alias to its single destination:

{"deployments":[],
 "routes":{"weles":"best","ox-alpha":"openrouter/stealth/ox-alpha"}}

The document also carries schema_version, whose value is the number one. Any top-level field beyond those three is refused by the deserializer and the whole document is rejected, so a file written for an earlier shape is refused rather than read in part. That refusal is what puts every alias into routes_file_invalid. brama routes migrate reads the configured document, keeps the known fields, validates the result and commits it with the same atomic owner-only replacement the writes below use; it is idempotent and prints the document it committed.

The route registry

PUT /v1/admin/routes takes exactly {"alias", "primary"}: the alias, and the one route it resolves to. An unknown field is refused with the deserializer's own sentence (captured), so a body carrying anything else — including a list of further routes — does not write. Refusals:

  • 409 runtime route registry is not configured (captured) when no registry file is in effect, because there is nothing to write into;
  • 400 unknown route alias for a name this gateway does not accept as an alias;
  • a 400 route-validation refusal when the stated route is unsupported, duplicates the destination the alias already holds, or is unavailable on this deployment;
  • 503 with the credential authority's operation and cause when route inspection fails, before any registry mutation;
  • 409 update alias `<alias>` in route registry <path>: <cause> for a registry read, validation or persistence failure.

DELETE /v1/admin/routes takes exactly {"alias"} and removes that alias from the document. It answers 400 invalid route alias for a malformed name, 409 required route aliases cannot be deleted for an alias in the compiled-in contract, 404 route alias not found for one the document does not carry, and 409 delete alias `<alias>` from route registry <path>: <cause> for a registry read, validation or persistence failure. Brama Desktop receives these same messages through its existing failed-mutation and snapshot diagnostics.

Both answer {"ok": true, "routes": <document>}, the whole registry document as committed, so the caller reads the new state rather than assuming it. The write is validated first and then committed by atomic owner-only replacement: either the file is the new document or it is the old one. Nothing partial is left on disk, and a rejected write leaves the file exactly as it was.

What the resulting states look like from the client side is GET /v1/aliases.

Registry-refusal qualification: cargo test --test credential-administration -- --ignored --exact route_refusal::invalid_registry_preserves_read_and_mutation_causes. Use an isolated, quiescent real gateway whose invalid registry is readable by the qualification process. Supply TEST_SOURCE_REVISION, GATEWAY_SOURCE_REVISION, BRAMA_ADMIN_URL, BRAMA_ADMIN_BEARER, EXPECTED_REGISTRY_PATH and the independently observed EXPECTED_REGISTRY_CAUSE. The test verifies build identity and the snapshot refusal before attempting writes; it uses only a fresh alias owned by that run, checks both mutation causes and byte-for-byte unchanged registry contents, and retains HTTP evidence under target/real-tests/registry-refusal. It does not create the invalid fixture, change credentials or contact a model provider.

Model categories

PUT /v1/admin/categories takes exactly {"category", "providers", "routes", "terms"} — the name and the whole membership rule — and replaces that category in the same document the routes live in. The three lists are optional and each defaults to empty, but a category declaring none of them is refused: a rule that matches nothing means nothing. Other refusals name the member that was wrong — 400 model category 'Uncensored' must be lowercase letters, digits and hyphens, 400 model category 'x' names 'not-a-route', which is not a provider/model route — and 409 runtime route registry is not configured when no registry file is in effect.

DELETE /v1/admin/categories takes exactly {"category"} and answers 404 no model category 'x' is declared for a name nobody declared, so the caller is never told a document changed when it did not.

Both answer {"ok": true, "routes": <document>} and take the same staged, validated, owner-only replacement the route writes take. What the declarations then look like from the client side is GET /v1/categories, and what they do to the catalogue is category.

Configuration adoption

Two paths take a whole configuration document and merge the aliases an operator picks out of it. Both work on the registry shape above, so an alias adopted from a source document arrives with one destination.

POST /v1/admin/configuration-adoption/preview takes exactly {"document":"<schema-1 JSON>","source_name":"<display name>","agent_id":"wisent-app"}, where agent_id is wisent-app when the request omits it. It validates the complete source and the current destination, reads the secret-free configured acquisition and subscription identities, and returns providers, subscriptions, subscription_discovery, candidates and unreferenced_deployments. It performs no write, no credential redemption, no subscription activation and no inference.

POST /v1/admin/configuration-adoption/apply takes exactly {"document":"<schema-1 JSON>","source_name":"<display name>","agent_id":"wisent-app","selected_aliases":["<alias>"],"replace_alias_conflicts":false}. It repeats the preview validation, rejects a duplicate or unknown selection, and atomically merges only the accepted selections into the configured registry file. The answer contains the resulting secret-free routes, per-alias items, and exact imported, unchanged, conflicting and rejected counts. An existing alias keeps its route unless the request explicitly asked for replacement, and a deployment conflict is never replaced.

Both adoption paths answer 409 runtime route registry is not configured when no live registry file exists, 400 <reason> for an invalid document, identity, route, selection or destination state, and 403 forbidden for any caller other than Brama Desktop. Every error path leaves the registry file unchanged and returns no credential contents.

Provider credentials

/v1/admin/credentials administers the standalone provider credential store this host keeps, the one a deployment without Skarbiec redeems from.

  • GET answers {"providers": ["<provider>", ...]}, the providers a credential is stored for. Names only: no credential value is returned.
  • PUT takes {"provider", "credential"} and answers {"ok": true, "provider": "<provider>"}. A provider the build does not support is 400 provider must name a supported remote API provider, and a credential the store refuses is a 400 carrying the store's own sentence.
  • DELETE takes {"provider"} and answers {"ok": true, "provider": "<provider>"}. A provider with nothing stored is 404 provider credential not found.

Read and delete failures retain the failed operation and the credential store's actual cause with 409. A disabled store reports standalone credential store is not enabled; a poisoned credential lock is not misreported as a disabled store. The write retains its store refusal with 400. No refusal returns a credential value.

Qualify the refusal surface with cargo test --test credential-administration -- --ignored --nocapture, supplying BRAMA_ADMIN_URL, BRAMA_ADMIN_BEARER, TEST_SOURCE_REVISION, GATEWAY_SOURCE_REVISION and the independently known EXPECTED_STORE_REFUSAL. Use an isolated real gateway whose standalone store is unavailable. The journey verifies the serving revision and exact read refusal before attempting deletion of a fresh random provider identifier, then requires the same refusal state on a second read. It retains actual HTTP results and source identity; it installs no credential, induces no process failure and does not qualify a poisoned lock unless that is the real fixture's observed condition.

Administered subscriptions

Four paths act on a subscription the console administers. Each one happens because the console asked for it; no timer reaches any of them.

  • POST /v1/admin/subscriptions/:agent_id/:subscription_id/probe is the one path in the product that deliberately spends plan quota: one minimal completion against one named subscription, recorded as a probe with source completion. It is refused with 409 when the subscription is inside a recorded block, and 404 subscription not found when it is otherwise absent (captured). Success is {"ok": true, "probe": {...}, "subscription": {...}}.
  • POST /v1/admin/subscription-sign-in/:agent_id/:subscription_id signs in one subscription owned by the administered agent. It requires a nonempty reason and accepts login_item.
  • POST /v1/admin/subscription-pool/sign-in signs in the selected pooled account. The body states the method in by, every time: "weles" or "hand"; a body without it is refused, nothing is assumed. Both methods require a nonempty reason and subscription_id.
    • "by": "weles" accepts login_item and signs in through Weles. Both Weles sign-in paths verify current ownership, refuse a retired or mismapped account, derive the provider from the selected subscription, and return the native signed_in or failed verdict with its actual detail.
    • "by": "hand" begins a manual Claude sign-in without Weles: it verifies the account is active and not retired, draws the PKCE verifier and state, keeps them in the gateway, and answers {"sign_in_id", "provider", "subscription_id", "url"}. The url is the provider's authorize page as omp auth-broker login anthropic opens it; the operator opens it in their own browser. Refusals: 400 for a blank reason or id, or a login_item (login_item <id> names a Weles sign-in row, and by hand signs in through the operator's own browser; drop it or sign in by weles), 404 subscription not found, 409 for a retired account or a provider without a manual flow (a manual sign-in is defined for claude-code; `codex` is not it). An open sign-in stays open until it is completed or a new one is begun for the same account, which replaces it; nothing expires on a clock.
  • POST /v1/admin/subscription-pool/sign-in/:sign_in_id completes it with {"code"}: the code#state the page showed or the whole redirect URL. The gateway checks the state against the one it drew, exchanges the code at the provider's token endpoint, stores the grant as the subscription's credential and proves it with one minimal completion, answering the same signed_in or failed verdict as the CLI's login --by hand. The sign-in is spent by the first paste, whatever it comes to: 404 no manual sign-in with that id is open afterwards, 409 with the exact sentence for a paste that is empty, unreadable or carries another sign-in's state.
  • POST /v1/admin/subscription-pool/disown gives one member back: {"subscription_id", "reason"?}. The gateway retires the member, records the reason in its credential ledger, removes the stored credential and stops routing to it; any machine that signed that account in keeps its own session. This is brama subscription disown. Answers {"ok", "subscription_id", "provider", "detail"}. Refusals: 400 subscription_id is required, 404 the pool holds no member with that id, 403 for any caller but the console. There is no POST /v1/admin/subscription-pool/grant: an endpoint that takes a grant a coding harness on the console's machine already holds hands over a session, because a provider issues one OAuth pair per sign-in and revokes it when a second holder refreshes, so each adopted grant signed that machine out. A gateway signs itself in instead.
  • POST /v1/admin/subscription-pool/reinstate takes one member back: {"subscription_id", "reason"?}. A retirement used to be permanent, so a member this pool gave back could not be put back from anywhere; reinstating returns it to the rotation, and it still holds no grant of this gateway’s own, which its recorded state then says (needs_reauthorization). This is brama subscription reinstate. Answers {"ok", "subscription_id", "provider", "detail"}. Refusals: 400 subscription_id is required, 404 for a member this pool does not hold, 409 for one that is not retired, 403 for any caller but the console.
  • POST /v1/admin/subscription-pool/forget drops one id only the usage ledger remembers: {"subscription_id", "reason"}. The usage ledger keeps every id that ever ran here; one the vault no longer returns, never retired, is no pool member, counted by nothing, and the pool report answers it apart under remembered ({id, provider}); disown refuses it. Forgetting journals the reason as a forgotten record, drops any overlay entry and local credential under the id and removes the ledger row; nothing is read from or written to the vault, and discovery may enrol the account again under its own identity. This is brama subscription forget. Answers {"ok", "subscription_id", "provider", "detail"}. Refusals: 400 for an empty id or a missing reason (reason is required: say why this member is forgotten), 404 when neither the vault nor the usage history knows the id, 409 when the vault lists the member (the answer names disown) or a live session still leases it, 503 when the vault, the lease register or the journal cannot be read, 403 for any caller but the console.
  • POST /v1/admin/subscription-pool/refresh takes {"provider", "reason"} and runs the same audited refresh as brama subscription refresh, answering the verdict with status 200 (captured: {"attempted":0,"detail":"no usable `codex` subscription is in this deployment's pool, ...","provider":"codex","result":"failed"}). The verdict is the answer; only the CLI turns a failed verdict into a non-zero exit code.
  • POST /v1/admin/subscription-pool/attribute takes {"provider"} and records which account each member of that provider belongs to in the vault this gateway serves from, which is the vault Weles resolves a sign-in from. This is brama subscription attribute --gateway. Answers {"provider", "recorded": [{"member", "account"}], "unattributed": [{"member", "reason"}]} with status 200 even when members are left unattributed. Refusals: 400 for an empty provider or one this pool holds no member of, 403 for any caller but the console.

Reading the pool, rather than acting on it, is discovery and subscriptions.

GET /health

Liveness only, no request fields, no bearer. Captured:

{"build":{"built_at":"...","platform":"...","product":"brama",
 "source_revision":"...","version":"0.2.38"},
 "dependencies":"not_probed","status":"ok"}

It never redeems a credential, lists subscriptions or contacts a provider: it answers ok from a gateway whose every redemption is being refused (architecture). Treat it as evidence the process is running, and nothing else.

GET /readyz

The latest completed credential and routing check. A background sweep redeems configured provider capabilities and active subscription credentials and checks model discovery; this request returns its completed report without performing those operations again. Credential redemption waits outside the HTTP workers, so slow Skarbiec responses do not prevent the gateway answering this route.

{"build":{...},"denied":[],"operator_action_required":false,
 "providers":[{"credential":true,"provider":"openai"}],
 "ready":true,
 "reason":"every configured provider credential was obtained, every active subscription redeemed, and every vault account carries the agent tag that makes it routable",
 "routing":[],"subscriptions":[],"unredeemable":[],"unroutable":[],
 "unroutable_accounts":[]}

The status is 503 with ready: false until a check has completed or when no configured route can obtain a credential. A gateway with working routes and failed accounts reports 200 and degraded: true. Routing results appear under routing. Rows in subscriptions[] carry per-subscription redeemable and the refusal in the words of whatever refused it, and never a secret.

Every answer also carries maintenance: the last maintenance pass this process ran, as {"last_pass_unix": <seconds>, "last_pass_ok": <bool>}. The serving process runs its own passes, with no schedule to declare: one when it starts, then one at the next moment its usage ledger states something becomes due (a grant's stated expiry, a plan window's stated reset, a rate block's stated end), and one right after a request is refused subscription_reauthorization_required; each renews due grants and signs members in through Weles by itself. brama maintain asks for one more. Before the first pass ends it reads {"last_pass_unix": null, "detail": "the serving process's first maintenance pass has not finished since it started: it renews due grants and signs members in through Weles by itself, and its result appears here when it ends"}. operator_action_required is true only for what that maintenance cannot repair: a direct-provider credential the vault does not hold, a vault account this process was not started with or cannot route, or a member listed in automatic_sign_in.blocked for something other than a dependency outage. A member blocked because Weles did not answer (weles_unreachable, including a resolver route that answered a server error with no JSON) or because a provider is rate limiting is listed there and retried by the next pass without asking anyone; an unredeemable subscription alone asks nothing of anyone.

Every answer carries model, read from the usage ledger at the moment /readyz is asked: {"state": "available", "reason": null} while some signing agent's pool holds a member that can be served (live, or expired or unchecked and so renewed when a request redeems it); {"state": "none", "reason": "..."} while every member needs a new sign-in, with each member's id, provider, state, the refusal in its way and its repair (a burnt grant: the Weles sign-in of that member, which the maintenance pass orders); {"state": "unknown", "reason": "..."} with the read that failed; and {"state": "local"} on a gateway serving its own provider credentials. With none or unknown the answer is 503, ready: false, and reason carries that sentence after any cause the report was already unready for, joined by | . A routed model request to a pool every member of which needs a new sign-in is refused the same way, before any credential read, snapshot or provider call: 503 subscription_reauthorization_required naming every member, in milliseconds rather than after each member fails in turn. Clients read that refusal or the 503 readiness answer and send nothing further, naming Brama's reason.

A credential call that stands unanswered overrides the completed report. Each call to the credential source — a Skarbiec introspection over HTTP, and every vault child the gateway shells (list subscriptions, inspect credential account, read credential account, route resolve, a credential write, the account-discovery registration and routing-mark restoration, …) — is recorded while it stands. A call is measured by the refresh interval this host's Stado resolver declares, or, where no resolver declares one (a standalone or Desktop gateway Stado does not place), by BRAMA_CREDENTIAL_CALL_STANDING_SECONDS; every answer says which in standing_measure: {"seconds", "declared_by"}, or {"unbounded": "neither this host's resolver nor BRAMA_CREDENTIAL_CALL_STANDING_SECONDS declares how long a credential call may stand, so a call that never answers is listed in standing_calls and never ended"}, or {"unreadable": "<cause>"}. A reading happens when /readyz is asked, and the gateway takes one itself whenever a call begins or ends and at the moment the oldest call reaches the measure, whether or not any client is asking. When a reading finds one that has stood that long or longer, the answer is 503, ready: false, and reason and credential_source carry the credential source has not answered: <N> call(s) have stood <N>s (<who declared it>) or longer, the oldest to vault child `read credential account` (pid <N>) for <N>s; Brama serves no chat until it answers. The same reading ends every such call: the vault child is ended, the gateway's log says vault child abandoned by the readiness reading (and, for a reading the gateway took itself, standing credential calls refused by the gateway's own reading: <sentence>), and the request that waited on it is refused with its own call's sentence — the credential source has not answered <operation> to <target> (pid <N>) for <N>s, past the <N>s <who declared it> declares; Brama serves no chat until it answers — instead of standing until the client's transport gives up with an empty reply, also when nobody watches /readyz. Every answer lists each call standing now in standing_calls, the longest-standing first, as {"target", "operation", "pid", "since", "refused", "elapsed_seconds"}: operation is the vault operation (read credential account, …) or Skarbiec token introspection, pid the vault child's process (null for an HTTP call), since when it began in RFC 3339, refused whether a reading has already ended it while its process is reaped, and elapsed_seconds how long it had stood when the answer was read; the list is empty while nothing stands. Qualification against a vault that never answers: cargo test --test readiness-vault-standing -- --ignored, with the isolated gateway started with SKARBIEC_BIN naming that test's executable and BRAMA_TEST_VAULT_STANDS set.

Every answer carries digest, the SHA-256 of its status and body without the elapsed_seconds of its standing calls, so it changes when calls begin, end or are refused, not every second one stands. GET /readyz?after=<digest> answers once the readiness answer differs from the one that digest names: at once when it already does, otherwise when a sweep or a maintenance pass publishes, a credential call begins or ends, or the oldest standing call reaches the declared measure — the moment the gateway's own reading above happens. A client that watches Brama while it waits on a request asks with the digest of the answer it last read, so it makes one request per change instead of one per answer. Qualification: cargo test --test readiness-standing -- --ignored, which asks with each answer's digest and refuses two readings carrying one.

GET /stats

Bearer-protected process telemetry, with no dependency probing. Captured keys: build, configuredDirectProviders, dependencyPolicy ({"capabilityBroker":"final-use","catalog":"lazy","subscriptions":"lazy"}), models[] with per-model count, latencyMs, lastLatencyMs, tps and lastTps (a rate with no time to be measured over is null, never zero: latencyMs for a model with no dispatch counted, tps while no latency was measured, lastTps when the most recent dispatch was answered in no measurable time), perfModels, and providers[] with per-descriptor id, displayName, wireProtocol and configured. It also carries the cumulative total_requests, total_failures, total_provider_attempts, total_input_tokens, total_output_tokens and uptimeSeconds, and standingCalls[], the same records as standing_calls in /readyz, so a vault child that never answers is named with its operation, process and age by the gateway's diagnostics as well.

configured reports the declared capability or field-scoped route, not successful authentication. Neither /stats nor the admin snapshot issues credentials; use /readyz for observed redemption failures and a real completion to verify the full inference path. An unreadable declared grant-route file or failed provider inventory returns 503 with its actual operation, resource and cause rather than a successful empty provider list.

Read-only qualification: cargo test --test credential-administration -- --ignored --exact inventory_refusal::provider_inventory_failure_is_not_reported_as_an_empty_pool. Supply TEST_SOURCE_REVISION, GATEWAY_SOURCE_REVISION, BRAMA_ADMIN_URL, BRAMA_ADMIN_BEARER, EXPECTED_PROVIDER_INVENTORY_OPERATION, EXPECTED_PROVIDER_INVENTORY_RESOURCE and EXPECTED_PROVIDER_INVENTORY_CAUSE for an isolated real failing inventory. The test checks actual snapshot, stats and model-catalogue refusals and retains HTTP evidence under target/real-tests/provider-inventory; it changes no registry, credential or provider state. Pair it with the readiness journey to verify independently working subscriptions remain serviceable while inventory diagnostics are visible.

Because an alias resolves to one route, total_provider_attempts counts one attempt per request that reached a provider, plus any credential rotation the gateway made before the first byte. Process telemetry is not durable billing evidence: it resets with the process.