Costs
Use this API when you want to answer four questions:
- How much did the company spend?
- Which agents, models, providers, or projects are driving that spend?
- Are budgets about to warn or hard-stop work?
- What non-token financial events were recorded alongside token spend?
When you need to check that the ledger itself is sound — runs still waiting to be counted, usage with no reliable price, or a provider invoice that doesn't match — use the Accounting endpoints at the end of this page.
All of these endpoints are company-scoped.
Money values. Every response that carries a cents amount as a number (for example
costCentsorspendCents) can also carry an exact string twin ending inExact(for examplecostCentsExact: "12.3456789"). The string keeps seven decimal places of a cent, so small per-request charges don't round away. Use theExactfield when you reconcile; use the number for display.
Reporting Usage
Cost events
POST /api/companies/{companyId}/cost-events
This is the main endpoint for token spend. Adapters typically call it after a heartbeat or other AI operation finishes.
Required fields:
agentIdprovidermodelcostCentsoccurredAt
Common optional fields:
issueIdwhen the spend came from a specific taskprojectIdwhen the spend clearly belongs to one projectgoalIdwhen you can attribute the spend higher up the treeheartbeatRunIdwhen the spend came from a specific runbillingCodefor your own accounting labelbillerif the charge came from a billing entity different fromproviderbillingTypeif you want to distinguishmetered_api,subscription_included,subscription_overage,credits,fixed, orunknowncostStatusto mark how the usage was priced —reportedfor a provider-reported amount,estimatedwhen the amount was calculated from token counts and a published rate card, orunpricedwhen you recorded token usage but no dollar cost was availableinputTokens,cachedInputTokens,outputTokensfor token-level reporting (whole numbers up to2147483647)idempotencyKey(up to 200 characters) so a retried report doesn't create a second chargeproviderRequestId(up to 250 characters) — the provider's own request ID, which lets an imported invoice line match this event laterpricingProvenance— where the price came from:source(provider_reported,provider_invoice,operator,rate_card, orunknown), plus optionalversion,evidence,serviceTier,contextTier(shortorlong), and per-million-token ratesinputCentsPerMillion,cachedInputCentsPerMillion,cacheWriteCentsPerMillion,outputCentsPerMillion
Rules from the implementation:
- The agent must belong to the company.
- Board users can report any company agent’s costs.
- Agent-authenticated calls can only report that agent’s own costs.
billerdefaults toproviderwhen omitted.billingTypedefaults tounknownwhen omitted.costStatusdefaults toreportedwhen omitted.costCentscan be a number or a decimal string (for example"0.0425"), and can't be negative. Fractions of a cent are kept.occurredAtmust be an ISO datetime string.
Priced, estimated, and unpriced usage. Most cost events carry a real
costCentsamount, so they stayreported. Some runs have no provider-reported price but use a model ThinkingMach has a published rate for; those are recorded asestimated, and the provider's bill may differ. When an agent run uses tokens but ThinkingMach can't determine a dollar cost at all — for example a local CLI pointed at an OpenAI-compatible endpoint other than OpenAI or OpenRouter — the usage is recorded withcostStatusset tounpricedinstead of being logged as if it cost nothing. That keeps the token counts on the record while flagging that the money figure is an unknown rather than a real zero. Unpriced usage can block budgeted work; see What happens at the thresholds.
When the event is accepted, ThinkingMach:
- stores the event
- recalculates
spentMonthlyCentsfor the agent and company - evaluates budget policies for soft warnings and hard stops
- writes an activity log entry
curl -X POST "http://localhost:3100/api/companies/company-1/cost-events" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"agentId": "agent-1",
"issueId": "issue-1",
"projectId": "project-1",
"heartbeatRunId": "run-1",
"provider": "anthropic",
"biller": "anthropic",
"billingType": "metered_api",
"model": "claude-sonnet-4-20250514",
"inputTokens": 15000,
"cachedInputTokens": 2000,
"outputTokens": 3000,
"costCents": 12,
"occurredAt": "2026-04-15T12:30:00.000Z"
}'
await fetch("http://localhost:3100/api/companies/company-1/cost-events", {
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
},
body: JSON.stringify({
agentId: "agent-1",
projectId: "project-1",
heartbeatRunId: "run-1",
provider: "anthropic",
billingType: "metered_api",
model: "claude-sonnet-4-20250514",
inputTokens: 15000,
cachedInputTokens: 2000,
outputTokens: 3000,
costCents: 12,
occurredAt: "2026-04-15T12:30:00.000Z",
}),
});
import requests
requests.post(
"http://localhost:3100/api/companies/company-1/cost-events",
headers={"Authorization": "Bearer <token>"},
json={
"agentId": "agent-1",
"projectId": "project-1",
"heartbeatRunId": "run-1",
"provider": "anthropic",
"billingType": "metered_api",
"model": "claude-sonnet-4-20250514",
"inputTokens": 15000,
"cachedInputTokens": 2000,
"outputTokens": 3000,
"costCents": 12,
"occurredAt": "2026-04-15T12:30:00.000Z",
},
)
Tip: If you already know the issue, project, or run that caused the spend, send those IDs. They make the breakdown views much more useful later.
Cache-adjusted cost
Prompt caching means the amount a provider actually bills for a run is often well below what its raw token counts imply. ThinkingMach records the billed amount rather than the nominal one, so the dollars in the ledger match the dollars on your invoice.
Adapters express this through their execution result:
costUsd— the cost the adapter reports for the run, in US dollars.cacheAdjustedCostUsd— the provider-billed cost after prompt-cache discounts, in US dollars. Adapters set this when they expose it separately fromcostUsd.
ThinkingMach resolves the two into a single billed figure:
- If
cacheAdjustedCostUsdis a finite number that is zero or greater, that value is the billed cost. - Otherwise, if
costUsdis a finite number that is zero or greater, that value is the billed cost — a provider-reportedcostUsdis treated as already cache-adjusted. - Otherwise there is no billed cost for the run.
That resolved figure is what ThinkingMach converts into the costCents of the cost event it records for the run, so it is the number that flows into every summary, breakdown, and budget policy described below. It is also the figure used to decide costStatus: a run with token usage but no resolvable cost is recorded as unpriced rather than as a real zero.
The run keeps both numbers. The heartbeat run's usageJson carries costUsd when the adapter reported one and cacheAdjustedCostUsd when a cache-adjusted amount was resolved, alongside the token counts and the provider, biller, model, costStatus, and billingType fields. Read them from either of:
GET /api/companies/{companyId}/heartbeat-runsGET /api/heartbeat-runs/{runId}
Note:
cacheAdjustedCostUsdandcostUsdare in dollars, whilecostCentson cost events is in cents. Don't mix the two units when you reconcile a run against its ledger entry.
Reading Spend
Who can read spend
Every read endpoint in this section needs company access and permission to read company-wide data (company_scope:read). A caller without that permission gets 403 with Costs are outside this actor's authorization boundary.
Choosing a date range
The summary, breakdown, and finance read endpoints all share the same date filters:
from— optional start, either an ISO datetime with an offset (2026-04-01T00:00:00.000Z) or a plain date (2026-04-01)to— optional end, in the same formatsperiod— optional;allfor all-time totals, ormonth
What you get depends on what you send:
- Nothing — the current UTC calendar month, from the 1st up to now. This matches the window monthly budgets use.
fromand/orto— exactly that range, with both ends included.period=all— everything ever recorded. You can't combine it withfromorto.
The server answers 400 for an unknown period, a date it can't parse, or a from that falls after to.
Note: Leaving out the dates used to return all-time totals. It now returns the current month, so add
period=allif your script relied on the old behavior.
Company summary
GET /api/companies/{companyId}/costs/summary
This returns the company’s total spend for the selected date range, the current company budget, and the utilization percentage. It also tells you whether that total is complete.
Response fields:
spendCents(andspendCentsExact) — known spend in the rangebudgetCentsutilizationPercenteventCount— cost events in the rangeestimatedEventCount— how many of those were priced from a rate card rather than reported by the providerunpricedEventCount— usage with no reliable price (subscription-included usage isn't counted here)pendingRunCount— finished runs whose cost hasn't been recorded yetunknownBillingEventCount— usage whose API-or-subscription billing category wasn't established. These charges are still counted inspendCents.pricingComplete—trueonly when there's no unpriced usage and no pending run, sospendCentsis the whole story
curl "http://localhost:3100/api/companies/company-1/costs/summary?from=2026-04-01T00:00:00.000Z&to=2026-04-30T23:59:59.999Z" \
-H "Authorization: Bearer <token>"
const res = await fetch(
"http://localhost:3100/api/companies/company-1/costs/summary?from=2026-04-01T00:00:00.000Z&to=2026-04-30T23:59:59.999Z",
{ headers: { Authorization: "Bearer <token>" } },
);
const summary = await res.json();
import requests
summary = requests.get(
"http://localhost:3100/api/companies/company-1/costs/summary",
headers={"Authorization": "Bearer <token>"},
params={
"from": "2026-04-01T00:00:00.000Z",
"to": "2026-04-30T23:59:59.999Z",
},
).json()
Breakdown views
These endpoints use the same date filters as the summary endpoint:
GET /api/companies/{companyId}/costs/by-userGET /api/companies/{companyId}/costs/by-agentGET /api/companies/{companyId}/costs/by-agent-modelGET /api/companies/{companyId}/costs/by-providerGET /api/companies/{companyId}/costs/by-billerGET /api/companies/{companyId}/costs/by-project
What each one is for:
by-userhelps you see which people the spend was run on behalf of.by-agenthelps you find which employees are expensive overall.by-agent-modelhelps you spot a specific agent/model combination that is burning tokens.by-providerhelps you compare Anthropic, OpenAI, and any other provider you ingest.by-billerhelps when the provider name and the billable entity are not the same.by-projecthelps you connect cost back to project work rather than just an agent.
In by-agent, charges that don't belong to an agent — such as background decision model requests — come back as a row with agentId: null. The UI labels it ThinkingMach services.
Rows from by-agent and by-agent-model include eventCount (ledger events in the group, not distinct runs) and estimatedEventCount, so you can tell when a total is partly or fully an estimate.
Spend by user
GET /api/companies/{companyId}/costs/by-user
Each run records the person it was done for — its responsible user. This endpoint adds up cost events by that person. The response has two fields:
activeUserCount— active human members of the company. The built-in local board login isn't counted.rows— one row per user, sorted by spend. Every active member appears, even with zero spend.
Each row carries:
userId,userName,userImage—userIdisnullfor the Unattributed row, which collects spend with no recorded user (including anything recorded against the local board login). Members who are no longer active keep their past spend in their own row; if their name can't be found, the row is labelledFormer user.runCount— distinct runs with cost events in the rangeeventCount,estimatedEventCount,unpricedEventCountcostCents,costCentsExactinputTokens(excluding cached input),cachedInputTokens,outputTokens
Because unattributed spend has its own row, the rows always add up to the company total.
Cost gaps
GET /api/companies/{companyId}/costs/data-quality
When the summary shows unpriced, pending, or unknown-billing usage, this endpoint tells you which runs and events are behind it. It powers the expandable cost-gap line on the Costs page. It takes the same date filters as the summary.
The response has:
eventsandruns— up to 25 of the most recent entries each. Every entry hasid,runId,agentId,agentName,occurredAt,provider,model, and areason.hasMoreEventsandhasMoreRuns—truewhen a list was cut off; narrow the date range to see the rest.canRetry— whether you're allowed to call the retry endpoint below.
reason is one of:
missing_price— no reliable price for this usagemissing_model— tokens were recorded without a modelunknown_billing— the run didn't establish API or subscription billingwaiting_for_receipt— the run stopped and its final usage hasn't arrived yetreceipt_unavailable— the run stopped a while ago without final usage data; a late receipt is still acceptedretryable— saved usage is ready to be recorded
The details contain no provider errors or credentials.
POST /api/companies/{companyId}/costs/retry-accounting
{ "runId": "<run uuid>" }
Records a run's saved usage now instead of waiting for the automatic retry. It never reruns the agent, and recording the same run twice is safe. The response is { "accounted": … }. Board members with view-only access get 403 with Viewers cannot retry cost recording.
Subscriptions
GET /api/companies/{companyId}/costs/subscriptions
Reports monthly subscription fees beside API spend. It takes the same date filters as the summary, but only the token totals follow them — the fees are the current monthly commitment (asOf).
Top-level fields:
accounts— one entry per subscription account you're allowed to see (your own personal accounts, shared accounts you manage, and shared accounts you're in the audience for)monthlyTotals— one entry per currency, each withcurrency,amountCents, andestimatedCount. Currencies are never combined.activeCount,unknownPriceCount,unidentifiedAccountCountapi,subscription,unknown,unattributedSubscription— company-wide usage for the range, split by billing type. Each hasinputTokens,cachedInputTokens,outputTokens,costCents,eventCount,estimatedEventCount, andunpricedEventCount.unknowncovers credits, fixed-price, and unidentified billing.canRefresh— whether you can trigger account discovery
Each account carries id, provider, name, ownerUserId, ownerName, shared, identityVerified, detectedPlan, observedAt, lastCheckedAt, refreshStatus (ok or unavailable), canEdit, its usage for the range, the agents that used it, and a price:
revision,plan,amountCents,monthlyCents,currency,effectiveAtcadence—monthoryear(annual amounts are divided by 12 formonthlyCents)status—active,ended, orexcludedsource—catalog(a published list price, so an estimate),user, orunknown
POST /api/companies/{companyId}/costs/subscriptions/refresh
Starts a background check of the connected subscription accounts visible to you, to pick up account identity and plan. It answers 202 with { "status": "accepted" } right away. It only uses credentials you're already allowed to use. When too many checks are running it answers 429 with Subscription account checks are busy. Try again shortly. Viewers get 403.
PATCH /api/companies/{companyId}/costs/subscriptions/{subscriptionId}
Records the price you actually pay. Body:
expectedRevision— theprice.revisionyou read. If someone changed it since, you get409withThis subscription changed. Reload it before saving.plan— 1–100 charactersamountCents— a number or decimal string, ornullfor unknowncurrency—USD,EUR,GBP,CAD,AUD, orCHFcadence—monthoryearstatus—active,ended, orexcluded
The response is { "id": …, "revision": … }. Each update adds a new price revision from now on; earlier revisions are kept. Owners can edit their personal accounts; shared accounts need permission to manage AI connections. Anyone else gets 403 with Only subscription owners or managers of shared accounts can edit prices. Setting status here doesn't cancel anything with the provider.
POST /api/companies/{companyId}/costs/subscriptions/{subscriptionId}/link
{ "targetId": "<subscription uuid>", "expectedRevision": 3, "targetRevision": 1 }
Marks two accounts from the same provider as the same subscription or seat, so the fee counts once and usage is combined. The account in the path is merged into targetId, and the target's price is kept. You must be able to edit both, or you get 403. You get 409 if both IDs are the same account, the accounts come from different providers, or either revision changed since you read it.
Fast moving spend
GET /api/companies/{companyId}/costs/window-spend
This returns rolling spend for the last 5h, 24h, and 7d. It is useful for spotting sudden spikes without having to choose a date range yourself.
Budget Controls
Set company budget
PATCH /api/companies/{companyId}/budgets
This is the simplest way to set the monthly company budget. The implementation also syncs a matching company budget policy behind the scenes.
Set agent budget
PATCH /api/agents/{agentId}/budgets
This sets the monthly budget for a specific agent and also syncs the corresponding agent budget policy.
The request body for both endpoints is the same:
{ "budgetMonthlyCents": 5000 }
The budget window is calendar-month UTC for company and agent monthly budgets. Sending 0 turns the matching policy off rather than setting a zero-dollar cap.
curl -X PATCH "http://localhost:3100/api/companies/company-1/budgets" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "budgetMonthlyCents": 100000 }'
await fetch("http://localhost:3100/api/companies/company-1/budgets", {
method: "PATCH",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
},
body: JSON.stringify({ budgetMonthlyCents: 100000 }),
});
import requests
requests.patch(
"http://localhost:3100/api/companies/company-1/budgets",
headers={"Authorization": "Bearer <token>"},
json={"budgetMonthlyCents": 100000},
)
Budget overview
GET /api/companies/{companyId}/budgets/overview
Use this when you want the board-level view of budget health. It returns:
- the current policies
- active incidents
- paused agent and project counts
- pending approval count
Each policy summary also reports unpricedEventCount and pendingRunCount for its window, its reservationCents, and its unpricedUsagePolicy, so you can see why a scope is blocked even when spend is under the cap.
Policy upsert
POST /api/companies/{companyId}/budgets/policies
This is the general budget policy API. Use it when you need a policy for a company, agent, or project rather than just a monthly company budget.
Request fields:
scopeTypeandscopeId— required; which company, agent, or project the policy coversamount— the cap in whole cents. Required when you create a policy; optional when you update one.0turns enforcement off.reservationCents— optional per-run hold (see Budget reservations); a number or decimal string, zero or moreunpricedUsagePolicy— optional;blockorallow(see below)metric,windowKind,warnPercent(1–99),hardStopEnabled,notifyEnabled,isActive— optional
Important defaults from the implementation:
metricdefaults tobilled_centswindowKinddefaults tocalendar_month_utcfor company and agent scopeswindowKinddefaults tolifetimefor project scopeswarnPercentdefaults to80hardStopEnableddefaults totruenotifyEnableddefaults totrueunpricedUsagePolicydefaults toblockreservationCentsdefaults to0isActivedefaults totrue
These defaults apply when a policy is first created. When you update an existing policy, any field you leave out keeps its current value — so you can change just unpricedUsagePolicy or reservationCents without resending the rest.
Budget reservations
A reservation is an estimate ThinkingMach holds against a budget before a run starts. It stops several runs from all starting at once and together overshooting a cap that each of them would have fit under alone.
- Set it per policy with
reservationCents.0(the default) holds nothing. - When a run starts, ThinkingMach holds the largest
reservationCentsamong the active hard-stop policies that cover it. - If recorded spend plus the amounts already held would reach the cap, the run isn't started, with the reason
Available budget is reserved by unfinished runs. - The hold is released once the run's cost has been recorded. It survives timeouts and server restarts until then.
A reservation only gates when work can start. It can't cap what the provider actually charges for a run, which may come in higher than the estimate.
Budget incidents
POST /api/companies/{companyId}/budget-incidents/{incidentId}/resolve
Allowed actions:
keep_pausedraise_budget_and_resume
For a soft (warning) incident, keep_paused dismisses the warning; the scope was never paused. raise_budget_and_resume raises the cap either way.
If you raise the budget, you must supply a new amount that exceeds the current observed spend. Raising the budget is also refused, with 422, while:
- finished runs in the scope are still waiting for their cost to be recorded (
Completed runs must finish accounting before resuming), or - the scope has unpriced usage and its policy is set to
block(Resolve unpriced usage or explicitly allow it in the budget policy before resuming).
Warning: Hard-stop budget enforcement pauses the affected scope and cancels work for that scope. A budget increase only resumes the scope if the new budget is above current observed spend and the scope's accounting is complete.
What happens at the thresholds
- At the warn threshold, the service creates a soft incident when notifications are enabled. A soft incident is a warning only — it doesn't pause anything.
- At 100 percent, the service creates a hard incident when hard-stop is enabled.
- A hard-stop policy also blocks new work before 100 percent when its spend figure can't be trusted yet: when a finished run in its window is still waiting for its cost to be recorded, or when its window has unpriced usage and
unpricedUsagePolicyisblock. Set the policy toallowif you'd rather keep working and treat unpriced usage as unknown. - Hard-stop policies pause the affected company, agent, or project and cancel work for that scope.
- Budget windows reset on the first day of each month in UTC.
- Project policies default to lifetime windows unless you choose a different window kind explicitly.
Finance Events
These endpoints track non-token accounting events alongside cost events. They are useful for credits, refunds, platform fees, manual adjustments, and other finance records that are not raw token usage.
Create finance event
POST /api/companies/{companyId}/finance-events
This endpoint is board-only.
amountCents accepts a number or a decimal string and can't be negative. currency is any three-letter code (it's stored in upper case, and defaults to USD). Send an idempotencyKey (up to 200 characters) if you might retry the same write.
Read finance data
GET /api/companies/{companyId}/costs/finance-summaryGET /api/companies/{companyId}/costs/finance-by-billerGET /api/companies/{companyId}/costs/finance-by-kindGET /api/companies/{companyId}/costs/finance-events
The finance list endpoint supports:
fromtolimitfrom1to500with a default of100
Useful event kinds include things like inference_charge, platform_fee, credit_purchase, credit_refund, and manual_adjustment. The API also stores direction, amountCents, currency, estimated, and optional metadata.
ThinkingMach never converts between currencies:
finance-summarykeeps its headlinedebitCents,creditCents,netCents, andestimatedDebitCentsin US dollars (currency: "USD"), and adds acurrenciesarray with one set of totals per currency.- Each
finance-by-billerandfinance-by-kindrow carries its owncurrency. - Amounts brought in from a provider cost report are left out of those totals, because they overlap invoices and run estimates.
finance-summaryreports them separately asproviderReportedCentsandproviderReportedCentsExact— don't add them to the other totals.
Accounting
These endpoints help you check that the cost ledger is complete and matches what your providers actually billed. They are board-only: the caller must be a board user with access to the company. Agents can't call them, even for their own runs.
Accounting health
GET /api/companies/{companyId}/accounting/health
Start here. It returns a quick status:
pendingRunCountandoldestPendingAt— finished runs whose cost hasn't been recorded yetunpricedEventCount— usage with no reliable pricependingCancellationCount— budget stops that haven't finished cancelling work yetheldReservationCents— the total currently held by budget reservationsitems— up to 100 pending runs plus up to 100 unpriced events. Each hasrunId,costEventId(unpriced events only),agentId,state,lastError,attempts,since, andlastAttemptAt.stateis one of:waiting_for_receipt— the run's usage report hasn't arrived yetretryable— recording failed and can be tried againunpriced— the event needs a price
Retry a run's accounting
POST /api/companies/{companyId}/accounting/retry
{ "runId": "<run uuid>" }
Tries again to record the cost of one run. The response is { "accounted": … }. If it fails again, the error is saved on the run so it shows up in health with a higher attempts count.
Inspect and repair totals
GET /api/companies/{companyId}/accounting/inspect
ThinkingMach keeps running totals (company and agent month-to-date spend, and per-agent lifetime totals) alongside the individual cost events. Inspect recomputes those totals from scratch and lists every place they disagree. The response has companyId, checkedAt, a fingerprint, and findings. Each finding has a kind, the entityId it concerns, actual and expected values, and repairable:
company_projection,agent_projection,runtime_projection— a running total doesn't match its events. These can be repaired.missing_receipt,missing_acknowledgement,receipt_mismatch,legacy_runtime— a run's records are incomplete or don't match its usage report. These are reported for you to investigate; repair won't change them.
POST /api/companies/{companyId}/accounting/repair
{ "fingerprint": "<64-character fingerprint from inspect>", "reason": "Month-end check" }
Repair resets every repairable total to its recomputed value and returns a fresh inspection. reason is required (up to 1,000 characters) and is recorded in the activity log. If anything changed since your inspection, the fingerprint no longer matches and the request fails with 409 — inspect again and repeat.
Import provider invoices
POST /api/companies/{companyId}/accounting/invoices
Bring a provider invoice into ThinkingMach so you can compare it with what was recorded. Request body:
biller— who issued the invoice (up to 150 characters)externalId— the invoice's own ID (up to 200 characters)currency— a three-letter codelines— 1 to 2,000 lines, each with:externalId— unique within the invoicekind—inference(default),fee, orcreditamountCents— a number or decimal string, zero or moreoccurredAt— ISO datetime with an offset- optional
costEventId,runId,providerRequestId, andmodelto help match the line to a recorded charge - optional
pricing— the same shape aspricingProvenanceon cost events
The response is 201 with the stored invoice. Each line also becomes a finance event — inference lines as charges, fee lines as platform fees, and credit lines as refunds — unless the matching charge is already in the finance ledger, or the line could match more than one charge. Importing the same invoice again returns the existing one; reusing an invoice ID with different contents fails with 409.
GET /api/companies/{companyId}/accounting/invoices lists the 100 most recent imported invoices.
Reconcile an invoice
GET /api/companies/{companyId}/accounting/invoices/{invoiceId}
Returns the invoice and its lines. Each line shows amountCents, the matchedEventId and recordedCents of the charge it matched, the differenceCents between the two, and a status:
matched— the invoice and the ledger agreedifference— matched, but the amounts differ (or the recorded charge was unpriced)unmatched— no recorded charge fitsambiguous— more than one charge fits, or several lines point at the same chargenon_inference— a fee or credit line, which isn't comparedunsupported_currency— the invoice isn't in USD, so it can't be compared with the USD ledger
Correct a charge
POST /api/companies/{companyId}/accounting/events/{eventId}/adjustments
Use this to fix the amount on a recorded cost event — for example to price an unpriced charge, or to apply a difference from a reconciled invoice. Request body:
idempotencyKey— required, up to 200 charactersexpectedCents— the amount you believe is recorded now. If the charge has changed since you looked, the request fails with409so you can review it again.correctedCents— the new amountreason— required, up to 1,000 characterspricing— required; where the new price came from (same shape aspricingProvenance)invoiceLineId— optional; ties the correction to an imported invoice line. The line must be a USDinferenceline from the same biller, match this event in reconciliation, carry exactlycorrectedCents, and not have been used before.
The response is 201 with the adjustment. ThinkingMach keeps the provider's original amount, updates the event's effective cost, recalculates month-to-date spend, and re-checks budgets. The event's costStatus becomes estimated when pricing.source is rate_card, and reported otherwise — so correcting an unpriced charge clears it from the unpriced count.
GET /api/companies/{companyId}/accounting/events/{eventId}/adjustments lists the corrections made to one event, newest first. Each has previousCents, correctedCents, previousStatus, previousPricing, pricing, reason, actorId, and createdAt.
Import provider cost reports
POST /api/companies/{companyId}/accounting/provider-costs/import
Pull daily cost totals straight from the OpenAI or Anthropic organization cost report. This sends a stored admin credential to the provider, so on top of board access it needs a company owner or admin, an instance admin, or a local trusted install. Others get 403 with Company admin access required to import provider billing.
Before you start, store the provider's admin key as a company secret and mark it for billing use by setting its providerMetadata field when you create or update the secret through the Secrets API:
{ "providerBilling": { "provider": "openai", "accountId": "<organization id>" } }
ThinkingMach only uses a company secret marked for the same provider and account.
Request body:
provider—openaioranthropicsecretId— the marked secretaccountId— the provider organization ID (letters, digits,_, and-)scopeIds— 1 to 25 unique OpenAI project IDs or Anthropic workspace IDs to importfromandto— plain dates.tois exclusive, the range can be at most 31 days, and it must end on or before today (UTC), so only completed days are imported.
The response is { "daysRead": …, "eventsCreated": … }. Each day and project or workspace becomes a finance event. Importing the same days again only records the difference, as a debit or credit marked as a correction. Only USD reports are accepted. If someone else imported newer figures while yours were loading, the request fails and asks you to fetch the report again.
Internal Diagnostics
GET /api/companies/{companyId}/costs/quota-windows
This is a board-only endpoint that powers the subscription quota panels on the Costs page. The company must exist and the caller must be board-authenticated.
It returns one entry for each Anthropic or OpenAI subscription account connected in AI connections that the calling user is allowed to see. It reads each account's own stored credential — never the sign-in on the machine running ThinkingMach. Each entry includes provider, accountKey (a stable ID for that account and credential version — never the credential itself), accountLabel, ok, windows, and capturedAt when the read succeeded. A failed read returns ok: false with an errorFamily such as credentials_unavailable, authentication_required, permission_denied, or provider_unavailable, and a general message rather than the provider's raw error. When the user has no such accounts, you get one placeholder entry each for anthropic and openai with errorFamily: "credentials_unavailable".
Practical Reading Order
If you are trying to understand a company’s spending, start here:
GET /costs/summaryGET /costs/subscriptionsif any agents run on subscriptionsGET /costs/by-agentGET /costs/by-projectGET /budgets/overviewGET /costs/window-spend
That sequence usually tells you whether the problem is a single agent, a single project, a provider mix issue, or a real budget policy problem. If the summary says pricingComplete: false, start with GET /costs/data-quality to see which runs are affected. If a scope is blocked while under its cap, follow up with GET /accounting/health.