Billing Namespace
- Category
- core
- Namespace
- billing
Read the billing accounts you belong to and what one of them has used in the current period, in dollars, and change which billing account pays for an organization, so an agent can answer "what does this cost us" and put an organization on the right account without opening the web app.
| Access | Method | Side Effect | Description |
|---|---|---|---|
| read | listMyAccounts | No | Lists every billing account you hold a role on, your personal account first, with your role ( |
| read | getAccountUsage | No | Reads one account's usage in the plan's current period: the priced ledger per kind and organization that the invoice is built from, the budget's month-to-date estimate, the budget standing, and for a personal account without a plan its free credit. Every amount is in microdollars. |
| write | attachOrganization | Yes | Makes one of your shared billing accounts pay for an organization, as the organization's billing page does. From then on the organization's usage is billed to that account, and a plan the account holds or a sponsorship it carries applies to the organization. Repeating the call for the account that already pays changes nothing. |
| write | detachOrganization | Yes | Stops a shared billing account from paying for an organization. You name the account you believe pays; if another account pays, the call is refused and nothing changes. Afterwards no account pays, and the organization cannot grow until someone attaches it again or takes it over. |
| write | takeOverOrganization | Yes | Moves an organization onto your own personal billing account and makes you the person who carries it: from a shared account, from no account, or from an owner who left. The result's |
Checkout, the plan, the card, invoices, the budget setting and the members of a shared account stay in the web app's billing settings; there is no method to change any of them. The ledger runs a few hours behind (pricedThrough) and stored documents are priced once a day (storedDocumentsPricedThrough), so recent usage is missing from it; the estimate is closer to now but is one sampled total without a split. A sponsored account, and any account without a paid plan, has no period, so its plan, period, ledger, estimate and budget are all null. After any change of who pays for an organization, through these methods or the web app, its billing cannot change again for up to a day: the next change is refused with cooldown and changes nothing. An organization that is being deleted refuses all three changes. The rest of the run that made a change still sees the previous account; the change shows from the next run. An organization's API key acts as a machine account that holds no billing role: listMyAccounts returns an empty list, getAccountUsage fails with a permission error, and the three changes are refused. Persistence failures are answered with a generic line, so a failed read tells you to retry, not why.
Call listMyAccounts first and pass one of its billingAccountId values to getAccountUsage, attachOrganization or detachOrganization. Your personal account's id is your user id, so getAccountUsage({ billingAccountId: (await orientation.whoami()).userId }) also works. Divide microdollar amounts by 1,000,000 for US dollars; plan.monthlyBasePriceUsdMinor is in cents. The two reads are idempotent and cheap; there is no pagination. The three changes take an organizationId from organizations.listMine, return { organizationId, lifecycleStatus, billingAccountId } with the account that pays afterwards (null after a detach), and change nothing when repeated for a state that already holds.
Every method needs the mcp:read scope. getAccountUsage also needs the owner, admin or viewer role on the account, or the account must be your own personal one. The three changes also need mcp:write, a connection that includes the organization, and the Admin role in that organization; takeOverOrganization needs an Admin role without an expiry date. attachOrganization and detachOrganization also need the owner or admin role on the shared account. Any other account id, existing or not, fails the same way with BillingAccountPermissionError, so the error never reveals whether an account exists. Billing roles decide money only and open no organization.
organizationId values in the ledger are the same ids organizations.listMine returns, so you can join usage to organizations you work in. The ledger's lifecycleStatus tells you when a row belongs to an organization that is being deleted or is gone. For what a kind costs per call or per unit, ask guide about pricing; this namespace reports amounts, not prices.
Billing
What it does
The billing namespace answers three questions: which billing accounts can I see, what has one of them used this period, and which account pays for an organization. A billing account pays for organizations. Every person has a personal account; a shared account belongs to a team and carries its own owners, admins and viewers. listMyAccounts and getAccountUsage only read; attachOrganization, detachOrganization and takeOverOrganization change who pays, with the same rules as the organization's billing page.
listMyAccounts
billing.listMyAccounts({}) returns { accounts }. Each row carries billingAccountId, kind (personal or shared), name, your role, organizationCount (how many organizations the account pays for) and sponsored (Oddlynew pays for it). The personal account is always first and is always there for a person, even when it pays for nothing.
getAccountUsage
billing.getAccountUsage({ billingAccountId }) returns one flat object. Read planStanding first, because it decides which other fields are filled:
paid: the account holds the plan.plan,period,ledgerandbudgetare filled;estimateis filled once the budget aggregator has recorded this period.sponsored: Oddlynew pays. Everything but the account fields is null.no_plan: no plan pays. For a personal accountfreeCreditis filled; for a shared account everything else is null.unknown_plan: the account holds a plan version this deployment does not know.plan,period,ledgerandbudgetare null.
plan names the plan version and name, the monthlyBasePriceUsdMinor in cents, and the includedUsageMicrousd the base price covers each month. period is the current period's start and end.
ledger is the priced usage the invoice is built from. totalMicrousd is the whole period so far; kinds splits it by kind (writes, reads, search, file_transfers, stored_documents, stored_files, invitation_mails, deletes, run_starts, decisions, decision_tokens, content_api, other); organizations splits it again per organization, each with its own kinds. includedKindsMicrousd is the part that draws on the included usage: every kind except decisions, decision_tokens and content_api, which are billed per call and per 1,000 tokens outside it. other is usage priced under a kind not split out yet, so the kinds always add up to the total.
An organization row whose organizationName is null is one the account no longer carries, or one outside the organizations your connection may see. Its id and amounts stay, so the rows still add up to the total.
estimate is the budget's month-to-date spentMicrousd with its asOf time. It is closer to now than the ledger, which is why it can be higher. budget holds the monthlyBudgetUsd setting (null when the stop is off), the effectiveMonthlyBudgetUsd the stop compares with, and standing: ok, warning or stopped.
freeCredit is a personal account's free credit while no plan pays: balanceMicrousd now, capacityMicrousd, refillMicrousdPerDay, and exhaustedUntil when the balance is spent and refilling.
attachOrganization
billing.attachOrganization({ organizationId, billingAccountId }) makes a shared account pay for the organization. The account must be shared and you need the owner or admin role on it; a personal account takes an organization only through takeOverOrganization. An organization that another person carries on their personal account moves only when that person hands it over.
detachOrganization
billing.detachOrganization({ organizationId, billingAccountId }) stops the named shared account from paying. Name the account you believe pays, for example the one whose getAccountUsage ledger lists the organization. When another account pays, the call is refused with billing_changed and nothing changes, so trying your managed shared accounts in turn is safe. Afterwards billingAccountId is null and the organization cannot grow until it is attached again or taken over.
takeOverOrganization
billing.takeOverOrganization({ organizationId }) moves the organization onto your personal account and makes you the person who carries it. It works from a shared account, from no account, and from an owner who left. An organization another active person carries on their personal account is refused; that person hands it over in the web app.
When a change is refused
A refused change leaves everything as it was. The error names its reason in its message:
cooldown: the organization's billing changed recently; it can change again within a day.billing_changed: another account than the one you named pays for the organization now.not_attached: no shared account pays for the organization, so there is nothing to detach.personal_account: a personal account was passed toattachOrganization; usetakeOverOrganization.carried_by_another: another person carries the organization on their personal account.no_owner: the organization has no owner; take it over before attaching it.BillingAccountPermissionError: you lack the role the call needs, or the account does not exist.OrganizationLifecycleFencedError: the organization is being deleted.
Read what an account costs
const { accounts } = await billing.listMyAccounts({})
const usd = microusd => microusd / 1_000_000
return await Promise.all(accounts.map(async account => {
const usage = await billing.getAccountUsage({ billingAccountId: account.billingAccountId })
return {
account: account.name ?? account.kind,
role: account.role,
planStanding: usage.planStanding,
ledgerUsd: usage.ledger ? usd(usage.ledger.totalMicrousd) : null,
estimateUsd: usage.estimate ? usd(usage.estimate.spentMicrousd) : null,
budget: usage.budget?.standing ?? null,
freeCreditUsd: usage.freeCredit ? usd(usage.freeCredit.balanceMicrousd) : null,
}
}))The example shows the flow; live server validation inside run is authoritative for exact parameters.
Move an organization to a shared account
const organizationId = "<organization id from organizations.listMine>"
const { accounts } = await billing.listMyAccounts({})
const target = accounts.find(account => account.kind === "shared" && account.role !== "viewer")
if (!target) return "No shared account you manage"
try {
return await billing.attachOrganization({ organizationId, billingAccountId: target.billingAccountId })
} catch (error) {
// A refusal changes nothing; its message names the reason, for example cooldown.
return String(error.message)
}When billing is not the answer
To change the plan, the card or the budget, or to download an invoice, send the person to the billing settings in the web app. To learn what Oddlynew charges for a kind, ask guide about pricing. To see which organizations you can work in, call organizations.listMine.