{"openapi":"3.1.0","info":{"title":"The Bot Forum — platform API","version":"0.3.0","description":"The platform plane on api.forum.bot: accounts, keys, payments, usage and discovery.\nEvery error is a dictionary envelope; every 402/403/409 carries next_action; every\nresponse carries X-Request-Id.\nThis holds for paths and methods NOT described here, not only for the routes below —\nthere is no unenveloped wall on this host. An unrouted path answers 404 `not_found`\n(\"re-read https://api.forum.bot/openapi.json\" — this very document, which lists every\nroute); a described path called with an undescribed method answers\n400 `invalid_params` naming the methods it serves. Both match the data plane's shipped\nbehaviour, and neither invents a code: the published dictionary is closed and has no 405.\nTwo strings, both minted by the platform at signup and shown once (the credentials\nsitting, 2026-09-04): the SECRET (bfa_…, one per account, no id, no name, listed by\nno route) runs the account on this host; the DATA KEY (bf_live_…) fetches data on\ndata.forum.bot and can do nothing here — presented here it answers 403\nwrong_credential with next_action switch_credential, and the secret presented on the\ndata plane answers the same. Signup takes one field, the email, and no header: the\nTerms version is recorded and returned, and no route reads a client-made\nIdempotency-Key any more. A data key is never returned before its entitlement record\nis replicated. A data key carries no scopes (the keys sitting, 2026-08-29): any key of\nthe account reaches every served product, funded from the account's one balance —\ngranted plus bought credits, minus spend. The free calls are the 1,000-credit signup\ngrant, booked once into that balance before the 201; issuing or rotating a key never\ngrants anything.\n"},"servers":[{"url":"https://api.forum.bot","description":"The Forum's own name."},{"url":"https://agents.openweathermap.org","description":"OpenWeather's agent lane — the same deployment, the same accounts and keys, under OpenWeather's name (ratified 2026-09-02). A key issued on either name works on both."}],"paths":{"/":{"get":{"summary":"The machine root — the catalogue index this host serves verbatim; no auth","responses":{"200":{"description":"the index document. for_agents.tos_version names the current Terms version — the version an account accepts by signing up; signup asks for no string and returns the version it recorded. The same index is served on the bare domain at https://forum.bot/index.json, byte-identical.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"status":{"type":"string"},"for_agents":{"type":"object","properties":{"tos_version":{"type":"string","description":"the current Terms version string — recorded for every signup and returned in its 201"}}}}}}}}}}},"/tos":{"get":{"summary":"The Terms of service as JSON — current by default, any published version by ?version=; no auth","parameters":[{"name":"version","in":"query","required":false,"schema":{"type":"string"},"description":"an exact published version string; omitted serves the current version. Every published version stays fetchable forever."}],"responses":{"200":{"description":"the Terms document; its version field equals what was asked for","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string"}}}}}},"404":{"description":"not_found — no such Terms version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/healthz":{"get":{"summary":"Liveness probe","responses":{"200":{"description":"process is up","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}}}}}}}}},"/v1/products":{"get":{"summary":"The product catalogue — the authority on price; the quoted price is the charged price","responses":{"200":{"description":"current products with lifecycle, fulfillment, pricing and licence — a canonical assembly of the catalogue product records (vendored contracts), fixed for the life of the process","content":{"application/json":{"schema":{"type":"object","required":["products"],"properties":{"products":{"type":"array","items":{"type":"object","required":["slug","title","status"],"properties":{"slug":{"type":"string"},"title":{"type":"string"},"status":{"type":"string","description":"catalogue lifecycle (e.g. active) — live health is /v1/status"},"fulfillment":{"type":"string"},"pricing":{"type":"object","description":"unit, credits per unit and the pricing note; the quoted price is the charged price. Every product is funded from the account's one balance — the signup grant spends here like bought credits do; no per-product trial exists (the keys sitting, 2026-08-29)."},"licence":{"type":"object","description":"licence id, provider and attribution rules, as published in the catalogue"},"sub_spec":{"type":"string","format":"uri","description":"the product's data spec — routes, request contract, response envelope and a sample"}}}}}}}}}}}},"/v1/status":{"get":{"summary":"Live product health — silence never reads healthy","responses":{"200":{"description":"one row per product. A product with no fresh fulfillment report answers degraded with detail stale_report — absence of evidence is never presented as health. Catalogue lifecycle (/v1/products) and live health are distinct.","content":{"application/json":{"schema":{"type":"object","required":["products"],"properties":{"products":{"type":"array","items":{"type":"object","required":["slug","status"],"properties":{"slug":{"type":"string"},"status":{"type":"string","enum":["operational","degraded","outage"]},"detail":{"type":"string"},"note":{"type":"string"}}}}}}}}}}}},"/v1/accounts":{"post":{"summary":"Signup — one field, the email; no header; the secret and the first data key, both shown once; a 1,000-credit grant","description":"The platform mints everything a caller holds (the credentials sitting, 2026-09-04). The Terms need no field: the account accepts the version this host serves at this moment, recorded and returned. No Idempotency-Key is read. A second signup for the same address answers 409; a reply lost on the wire is recovered at the owner console by the account's email, where a new secret is minted.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"},"example":{"email":"agent-owner@example.com"}}}},"responses":{"201":{"description":"account created; the secret and the data key are shown exactly once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResponse"}}}},"400":{"description":"invalid_params — malformed email, or a field beyond email (the Terms version and the identity object left the body; a field the platform would drop is refused)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"email_exists — next_action login_to_link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"rate_limited with retry_after — the per-source signup throttle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"internal_error — nothing was issued (no string is returned before its record is on the master); retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/account":{"get":{"summary":"Who am I — tier, scope, budget; reading also re-checks unfinished payments","security":[{"forumSecret":[]},{"forumSession":[]}],"responses":{"200":{"description":"the account view","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountView"}}}},"401":{"description":"invalid_key / key_revoked (a replaced secret) / session_expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"wrong_credential — a data key was presented; this host takes the secret (next_action switch_credential)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/account/secret":{"post":{"summary":"Replace the secret — a new one shown once; the old one is dead before the answer","description":"Under the secret, or under a console session — the recovery door: an owner who lost the secret logs in by mail and mints a new one here. Data keys are untouched. A retry after a lost answer mints again and kills the first.","security":[{"forumSecret":[]},{"forumSession":[]}],"responses":{"201":{"description":"the new secret, shown exactly once","content":{"application/json":{"schema":{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","pattern":"^bfa_[A-Za-z0-9_-]{43}$","description":"the account's secret — runs the account on this host; fetches nothing"}}}}}},"401":{"description":"invalid_key / key_revoked / session_expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"wrong_credential — a data key was presented","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/account/usage":{"get":{"summary":"The account's balance and its spend over time — the Ledger's owner-visible view","security":[{"forumSecret":[]},{"forumSession":[]}],"responses":{"200":{"description":"usage aggregates (last 30 days) + the one balance (granted + bought − spend)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageView"}}}},"401":{"description":"invalid_key / key_revoked / session_expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"wrong_credential — a data key was presented","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/account/verify":{"post":{"summary":"Attach a card — the human checkpoint that upgrades the tier","description":"Opens a verification payment through the Billing Bridge and returns the provider's hosted page; the owner types the card there, and card data never touches the Forum. The step places a $0 HOLD and never charges. When it succeeds, the account switches from untrusted to verified — which Core learns by re-checking on the next account read, because the Bridge never calls Core. Calling this on an already-verified account answers 200 with the current tier and opens nothing. No header: one open verification page per account — asking again returns it (2026-09-04).","security":[{"forumSecret":[]},{"forumSession":[]}],"responses":{"201":{"description":"opened — the payment and its hosted page; the same page again while it is open","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntentProjection"}}}},"200":{"description":"already verified — nothing was opened"},"403":{"description":"wrong_credential — a data key was presented","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"502":{"description":"upstream_error — the payment service is unavailable; the call was not charged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/account/topup":{"post":{"summary":"Buy credits — callable at any time, before or after the balance runs out","description":"Takes an amount in the currency's minor unit and returns the provider's hosted page for the owner. Callable while UNTRUSTED: the first successful payment is what verifies the account, and nothing is unlocked early by that, because the verified tier and the first bought credits land on the same event. The $10 minimum and the accepted currency are checked HERE and nowhere else, from contracts/money.json. No header: one open top-up page per account — asking again while it is open returns the same page, whatever the amount; a new page opens only after it is paid or has expired (2026-09-04).","security":[{"forumSecret":[]},{"forumSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount_minor","currency"],"properties":{"amount_minor":{"type":"integer","minimum":1,"description":"whole minor units — cents, for dollars"},"currency":{"type":"string","pattern":"^[A-Z]{3}$"}}}}}},"responses":{"201":{"description":"opened — the payment, its amount echoed, and its hosted page; the same page again while it is open. Credits land about 15 s after the payment completes; poll GET /v1/account/usage.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntentProjection"}}}},"400":{"description":"invalid_params — below the minimum (with next_action top_up), an unaccepted currency, or a malformed amount","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"wrong_credential — a data key was presented","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"502":{"description":"upstream_error — the payment service is unavailable; the call was not charged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/keys":{"get":{"summary":"List the data keys (masked — the eight visible letters, never a key string; the secret is listed by no route)","security":[{"forumSecret":[]},{"forumSession":[]}],"responses":{"200":{"description":"the account's keys","content":{"application/json":{"schema":{"type":"object","required":["keys"],"properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/KeyMasked"}}}}}}},"401":{"description":"invalid_key / key_revoked / key_expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"summary":"Issue a data key — a pure credential over the account's one budget; the key shown exactly once","description":"A key carries no scopes and no rights of its own (the keys sitting, 2026-08-29): every key of the account reaches every served product, funded from the account's one balance, so the body takes an optional name only. Issuing a key never grants anything — the account's balance is exactly the same after this call as before it. Two fields are refused rather than ignored, in this order after the name type check: a `scopes` field, whatever its value (400 — keys carry no scopes; the account's scope is every served product), then a `budget_cap_credits` field (400 — this platform enforces no spending cap per key; the account balance is the bound). A field the platform would silently drop is a promise it never made.","security":[{"forumSecret":[]},{"forumSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyCreateRequest"}}}},"responses":{"201":{"description":"the new data key, shown once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyIssued"}}}},"400":{"description":"invalid_params — a scopes field was sent (keys carry no scopes: the account's scope is every served product, funded from its one budget), budget_cap_credits was sent (no per-key spending cap exists; the account balance is the bound), or name is not a string","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"invalid_key / key_revoked / key_expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/keys/{key_id}":{"delete":{"summary":"Revoke — effective within seconds on a healthy replication link","security":[{"forumSecret":[]},{"forumSession":[]}],"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"revoked; the inline master write happened before this answer"},"401":{"description":"invalid_key / key_revoked / key_expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"not_found — no such key on this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/keys/{key_id}/rotate":{"post":{"summary":"Rotate — a new key and key_id, same name; the old key turns terminal (key_revoked) within the same SLO","security":[{"forumSecret":[]},{"forumSession":[]}],"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"the rotated key, the new key shown once, same name. Rotation is a pure credential swap: the account's balance is the same before and after, and the operator flag — the one right a key can carry — never survives it (a fresh host-command grant is the only way back).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyIssued"}}}},"400":{"description":"invalid_params — the key is not active","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"invalid_key / key_revoked / key_expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"not_found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/login":{"post":{"summary":"Request a mailed login link (the consoles sitting) — no key, no marker, the same empty 202 for every address","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string"}}}}}},"responses":{"202":{"description":"always, known address or not — this route confirms no email's existence; for a known one a mail follows, its one-time link minted at the moment of sending"},"400":{"description":"invalid_params — malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"rate_limited with retry_after — the flood guard (per address per hour and per UTC day), counted identically for unknown addresses","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/sessions":{"post":{"summary":"Exchange the mailed link's token for a session — the browser's credential, not a key; shown exactly once","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"}}}}}},"responses":{"201":{"description":"the session (contracts_vendor/schemas/session.schema.json) — 24 hours, no renewal","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionIssued"}}}},"400":{"description":"invalid_params — malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"invalid_key — the link is invalid, spent, expired or superseded by a newer one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/session":{"delete":{"summary":"Log out — session-authed; the record dies and answers session_expired until its natural expiry","security":[{"forumSession":[]}],"responses":{"204":{"description":"logged out"},"400":{"description":"invalid_params — the caller presented a key; only a session can end itself","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"session_expired / invalid_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}},"components":{"securitySchemes":{"forumSecret":{"type":"http","scheme":"bearer","description":"The account's secret (bfa_… — reserved in contracts_vendor/key-format.json): the one string that runs the account on this host, minted at signup, shown once, hash only at rest. A data key presented here answers 403 wrong_credential."},"forumSession":{"type":"http","scheme":"bearer","description":"The browser session of the login flow (contracts_vendor/schemas/session.schema.json) — its own credential, never a key; accepted wherever the secret is; a key or the secret presented where a session is required answers invalid_params."}},"schemas":{"SessionIssued":{"description":"Mirror of contracts_vendor/schemas/session.schema.json — a session is its own credential, hash-only at rest in Core's schema, never on the entitlement master, never valid on the data plane. The token never matches the key format.","type":"object","required":["session"],"properties":{"session":{"type":"object","required":["token","account_id","created_at","expires_at"],"properties":{"token":{"type":"string"},"account_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"}}}}},"IntentProjection":{"description":"What an agent sees of a payment — never card data, never a provider object's id, never a provider's error text. hosted_url is the provider's own page and is SHORT-LIVED; the stable address a wall hands an owner is action_url on the error envelope, which never expires.","type":"object","required":["intent"],"properties":{"intent":{"type":"object","required":["intent_id","type","state","created_at"],"properties":{"intent_id":{"type":"string"},"type":{"type":"string","enum":["verification","topup"]},"state":{"type":"string","enum":["created","pending","succeeded","failed","expired"]},"hosted_url":{"type":"string","format":"uri"},"amount_minor":{"type":"integer"},"currency":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}}},"ErrorEnvelope":{"type":"object","description":"Mirror of contracts_vendor/schemas/error-envelope.schema.json — the suite enforces field equality.","required":["type","code","message"],"properties":{"type":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"},"docs_url":{"type":"string"},"next_action":{"type":"string"},"action_url":{"type":"string","format":"uri","description":"where a HUMAN completes next_action — the stable payment landing address on a money or verification wall, the support destination on key_blocked. Never a per-payment provider link."},"retry_after":{"type":"integer"},"request_id":{"type":"string"}}},"SignupRequest":{"description":"One field and nothing else — a body naming any other field is refused (400).","type":"object","required":["email"],"additionalProperties":false,"properties":{"email":{"type":"string","format":"email","description":"the account's identity — one account per address"}}},"SignupResponse":{"type":"object","required":["account","secret","data_key","grant"],"properties":{"account":{"type":"object","required":["account_id","tier","tos_version"],"properties":{"account_id":{"type":"string"},"tier":{"type":"string","enum":["untrusted"]},"tos_version":{"type":"string","description":"the Terms version this account accepted by signing up — the one served at that moment"},"created_at":{"type":"string","format":"date-time"}}},"secret":{"type":"string","pattern":"^bfa_[A-Za-z0-9_-]{43}$","description":"the account's secret — the one string that runs the account on this host, shown exactly once; one per account, no id, no name, listed by no route. Store it where account credentials live; it fetches nothing."},"data_key":{"$ref":"#/components/schemas/KeyIssued"},"grant":{"type":"object","description":"the signup grant — one account-level sum of credits, booked into the account's one balance before this response and spendable on any served product. The number is the contracts money law's signup_grant record fact.","required":["credits"],"properties":{"credits":{"type":"integer"}}}}},"KeyCreateRequest":{"description":"An optional name, and nothing more. A key is a pure credential (the keys sitting, 2026-08-29): it carries no scopes — the account's scope is every served product, funded from its one budget — so a `scopes` field is refused rather than ignored, whatever its value. A `budget_cap_credits` field is refused the same way: this platform enforces no spending cap per key, and the account balance is the bound. An `expires_at` sent today is ignored — no key expires yet. Nothing in this body grants anything.","type":"object","properties":{"name":{"type":"string","description":"an optional label, echoed on the key and in the masked listing"}}},"KeyIssued":{"description":"A data key as issued — the whole string, shown exactly once; it fetches data on the data plane and can do nothing on this host. The eight visible letters the masked listing shows are the start of the string itself, so no prefix field is repeated here.","type":"object","required":["key_id","key"],"properties":{"key_id":{"type":"string","description":"the handle for revoke and rotate"},"name":{"type":"string"},"key":{"type":"string","pattern":"^bf_live_[0-9A-Za-z]+_[0-9A-Za-z]{43}_[0-9A-Za-z]{12}$","description":"the full data key, shown exactly once (format = contracts_vendor/key-format.json)"},"expires_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"KeyMasked":{"type":"object","required":["key_id","prefix","state"],"properties":{"key_id":{"type":"string"},"prefix":{"type":"string","description":"the key's eight visible letters — how an owner tells keys apart without seeing one"},"name":{"type":"string"},"state":{"type":"string","enum":["active","blocked","revoked","expired"]},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"}}},"UsageView":{"type":"object","required":["usage","balance"],"properties":{"usage":{"type":"array","description":"per key × product × day × outcome, newest first (30 days)","items":{"type":"object","required":["day","product","key_id","outcome","calls","credits"],"properties":{"day":{"type":"string","format":"date"},"product":{"type":"string"},"key_id":{"type":"string"},"outcome":{"type":"string"},"calls":{"type":"integer"},"credits":{"type":"integer"}}}},"balance":{"type":"object","description":"The account's one balance — granted plus bought credits, minus the whole spend (the keys sitting, 2026-08-29). Granted credits are told apart from bought ones by each entry's provenance; this is how an owner confirms from OUTSIDE that a payment arrived. Deliberately absent: the provider's own identifiers — a payment reference is evidence in the book, not an agent's business.","required":["granted_credits","bought_credits","remaining_credits","entries"],"properties":{"granted_credits":{"type":"integer","description":"the signup grant — booked once, provenance 'granted'"},"bought_credits":{"type":"integer","description":"the sum of what was bought, refund corrections netted off"},"remaining_credits":{"type":"integer","description":"granted + bought − spend; may be negative — absorbed overshoot, never billed"},"entries":{"type":"array","description":"the 50 most recent money and grant movements, newest first","items":{"type":"object","required":["credits","provenance","pricing_version","occurred_at"],"properties":{"credits":{"type":"integer","description":"negative for a refund correction"},"provenance":{"type":"string","enum":["bought","earned","granted"]},"pricing_version":{"type":"string","description":"the engine build's commit id — the catalogue that priced it"},"occurred_at":{"type":"string","format":"date-time"}}}}}}}},"AccountView":{"type":"object","required":["account_id","tier","tos_version","entitlements"],"properties":{"account_id":{"type":"string"},"tier":{"type":"string","enum":["untrusted","verified"]},"tos_version":{"type":"string"},"entitlements":{"type":"object","description":"The account's rights, whole (the keys sitting, 2026-08-29): scope — every product the platform serves, for every account — and budget, the one balance every key of the account spends from. No per-key rights exist.","required":["scope","budget"],"properties":{"scope":{"type":"array","description":"the active catalogue slugs — what any of this account's keys may call","items":{"type":"string"}},"budget":{"type":"object","required":["remaining_credits"],"properties":{"remaining_credits":{"type":"integer","description":"granted + bought − spend, in whole credits; the balance detail is /v1/account/usage"}}}}}}}}}}