Usage and audit through the API¶
The concept behind usage, audit and traces is explained in Insight.
Read usage and audit records through the API to track consumption and investigate activity. These endpoints are available to every key without an additional capability.
Usage records cover the whole organisation, including other keys and work started by console
users; use key_id to filter them to one key. Audit events are narrower: a key reads only its own
(Audit events).
Usage types¶
kind |
Counted as | Written when |
|---|---|---|
model_tokens |
input_tokens, output_tokens; reasoning_tokens says how many of the output tokens the model spent thinking, where the provider reports it (null on a record where it does not; already inside output_tokens, so never added) |
every model call, including one a tool or a summarisation made |
cached_tokens |
input_tokens: the part of that call's input the provider served from its prompt cache |
a model call read part of its input from the cache (prompt caching) |
cache_write_tokens |
input_tokens: the part of that call's input the provider wrote into its prompt cache |
a model call stored part of its input in the cache; of the providers Core calls, only Claude models report this |
embedding_tokens |
input_tokens |
text was embedded: ingesting, searching, writing memory, ranking a turn's skills or memory |
tool_invocation |
quantity, with tool_name |
a tool Core executed |
retrieval |
quantity |
each search step a retrieval run executed |
crawl_pages |
quantity: pages crawled |
a website connector synced |
parse_pages |
quantity |
pages a document parsing service read for an ingest pipeline |
image_generation |
quantity: 1, with model and tool_name |
the generate_image tool returned an image |
audio_transcription |
quantity: the seconds of audio, rounded up, with model; tool_name when the transcription tool made it |
a recording was transcribed: on POST /audio/transcriptions, by the transcription tool or for an input_audio part (Transcribe audio). Its price names one minute, so the cost is the seconds times that price divided by sixty |
sandbox_seconds |
quantity: the seconds a computer was ready, rounded up |
a computer stopped |
shared_bytes |
quantity: the bytes of the file sent |
a share link was opened, once per time it is opened; opening a view address writes no row |
thread_turn |
quantity: 1 |
a turn that read or wrote a thread, whatever started it |
run |
quantity: 1 |
a background run ran its first round |
agent_turn |
quantity: 1 |
a turn ran an agent, an agent called as a tool included |
pseudonymised_turn |
quantity: 1 |
a turn of a key that hides personal data |
compaction_run |
quantity: 1 |
a compaction pass asked for a summary, once per pass however many it made |
schedule_fire |
quantity: 1 |
a schedule fired and queued its turn or runs |
task_start |
quantity: 1 |
a task was started |
workflow_run |
quantity: 1 |
a workflow run was started; what its steps run is metered as it always is |
evaluation_scenario |
quantity: 1 |
an evaluation run started a scenario |
improvement_analysis |
quantity: 1 |
an improvement analysis started; its model calls are model_tokens rows with no key |
datastore_write |
quantity: 1 |
a datastore record was written |
memory_write |
quantity: 1 |
a memory record was stored |
file_upload |
quantity: 1 |
a file was uploaded; never its bytes |
One action counts on every meter it touches, each once: a scheduled turn on a thread is a
schedule_fire, a run and a thread_turn. Your operator prices each use; without a price it
costs nothing.
What you store is not one of these rows: it is a gauge, stored_bytes, on GET
/usage/summary itself (below) — read from your files and artifacts right
now, not accumulated on an event.
When interpreting usage, account for the following:
- A hybrid search records two retrieval units, one for each search node. Usage records identify the step responsible.
- Key activity records the key ID in
key_idandactor. Console activity has a nullkey_idand identifies the person inactor. - A row a turn on an agent wrote names that agent in
agent. An agent called as a tool names itself on the rows of its own turn, and the calling turn's rows, the call to the tool included, name the caller. A tool's rows name the agent whose turn called it. A turn on no agent, and every row recorded before rows named the agent, carrynull.GET /usage?agent=<name>lists one agent's rows, and the summary splits every group by it. - A row a helper did the work for names it in
helper: the name a turn onPOST /responsesgave inhelper, on every row that turn wrote, including its tools' rows and the turns of agents it called; orassist.<helper>for one of Core's own helpers, such asassist.json_schema. Work no helper did, and every row recorded before rows named the helper, carrynull.GET /usage?helper=<name>lists one helper's rows by exact name, and the summary splits every group by it. - A call that names a subject is metered under that id:
subject_idon the row,GET /usage?subject=<id>for one person's rows,GET /usage/by-subjectfor every person's totals at once. Core holds no name, e-mail or attribute for the id -- only what your application sent it as (Subjects).
Read usage¶
curl -s "$CORE_URL/usage?from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z&kind=model_tokens" \
-H "Authorization: Bearer $CORE_KEY"
| Call | Answers | Accepts |
|---|---|---|
GET /usage |
individual rows, each with what it cost, cursor-paged | kind, key_id, agent, helper, subject, request_id, response_id, from, to |
GET /usage/summary |
one row per (kind, model, tool_name, agent, helper) group, with what it cost, no paging; this is your organisation's total, over the current UTC calendar month unless you name from or to |
key_id, request_id, response_id, thread_id, from, to, interval=day |
GET /usage/by-subject |
one row per person your usage carries subject_id for, with what each cost plus their own threads_created/messages_appended, ordered by cost descending, cut to limit (default 50, max 500); subjects says how many there really were |
key_id, from, to, limit, by_kind; refused to a subject token |
GET /usage/caps |
the caps in force this month and what has been spent against them | nothing; see your monthly cap |
GET /tenants/{id}/prices |
the prices your usage is costed at, each in the currency its currency names, cursor-paged |
kind, model, tool_name; a console call, not a key's |
GET /usage/by-subject lists only people with a metered call in the window: a person who only
created a thread or was appended a message, with no priced call in the window, is not a row this
list carries. The summary's own threads_created/messages_appended still count them — those two
totals are read straight over your threads and their items, never off this by-subject list.
What each person used, kind by kind. Add by_kind=true and every row, unattributed
included, carries by_kind: one entry per usage kind the person had rows of, ordered by kind, each
with input_tokens, output_tokens, quantity (images, invocations, pages, seconds), cost_eur
and unpriced, adding up to the row's own figures. That is how a person's embedding tokens or
image count is shown apart. Without it by_kind is null. Core has no cap per person: a platform
that holds its users to fair use reads this list, and the usage events
on its feed, and acts on them itself.
curl -s "$CORE_URL/usage/by-subject?by_kind=true" -H "Authorization: Bearer $CORE_KEY"
Use timezone-aware ISO timestamps for from and to. They filter created_at, including from
and excluding to. Use the summary endpoint for organisation totals without paging through
individual usage records. With neither from nor to, the summary and GET /usage/by-subject
cover the current UTC calendar month; name either end to read another window. Narrowed to a
request_id, the summary is what that one call cost, an agent it called as a tool included; a
nested agent turn has a response_id of its own, so a whole call is request_id.
Narrowed to a thread_id, the summary is what one chat cost: the rows of every response that ran in
that thread, so a chat can show its own spend. Some limits come with it. The summary is read from
your organisation's own usage rows, as a per-person read is. A thread you deleted answers no groups, because its responses
forget it (the cost is still in your totals without the filter). A nested agent turn that ran outside the thread is not among the rows; ask for
its request_id when it should be. The stored bytes and the thread and message counts beside the
groups are not narrowed to the thread. A subject token
reads its own person's rows inside the thread only: another person's thread_id answers no groups,
never that person's spend.
What your usage cost¶
Every group on GET /usage/summary carries a cost_eur beside its volumes, and the summary as a
whole carries the total. Amounts are euro, sent as exact decimal strings — never floats — so add
them up with a decimal type and round only where you show them. An amount is always written out
in digits, such as 0.0000001, and never with an exponent.
Every row on GET /usage carries a cost_eur too: what that one row cost, at your organisation's
own price in force when it was recorded, costed exactly as the summary costs the group it falls
in. It is null where nothing prices the row, the row the summary counts in unpriced.
The summary also carries stored_bytes: what your files and artifacts hold right now, summed
from their blobs the instant the summary was built — not a total accumulated over the window
from/to narrow, which do not apply to it at all.
Beside the cost, the summary carries threads_created and messages_appended: how many
conversations and how many messages fell inside the window, counted straight over your threads
and their items, never priced or stored as a usage row. A "message" is a message item — either
side's own words — never a tool call, its result, a reasoning block or a refusal: those are how a
turn worked, not what was said. Both narrow to key_id on the same terms as items.
{
"items": [
{ "kind": "model_tokens", "model": "gpt-5.6-sol", "input_tokens": 1200000,
"output_tokens": 90000, "reasoning_tokens": 30000, "quantity": 0, "cost_eur": "3.90",
"unpriced": 0 },
{ "kind": "cached_tokens", "model": "gpt-5.6-sol", "input_tokens": 400000,
"output_tokens": 0, "reasoning_tokens": 0, "quantity": 0, "cost_eur": "-0.90", "unpriced": 0 },
{ "kind": "model_tokens", "model": "claude-sonnet-5", "input_tokens": 800000,
"output_tokens": 40000, "reasoning_tokens": 0, "quantity": 0, "cost_eur": "3.00",
"unpriced": 0 },
{ "kind": "cache_write_tokens", "model": "claude-sonnet-5", "input_tokens": 200000,
"output_tokens": 0, "reasoning_tokens": 0, "quantity": 0, "cost_eur": "0.15", "unpriced": 0 }
],
"cost_eur": "6.15",
"stored_bytes": 41943040,
"threads_created": 214,
"messages_appended": 1780
}
Three things are worth knowing about those numbers:
- A
cached_tokensgroup is a discount, not a charge. Themodel_tokensgroup already billed the whole input at the full rate; the cached group carries the difference the cheaper cached rate is worth, which is zero or negative. Add the groups up and the total is what the period really cost. - A
cache_write_tokensgroup is a surcharge. Writing input into a provider's cache costs more than sending it once, and themodel_tokensgroup billed those tokens at the plain input rate. The cache-write group carries the extra the model's cache-write rate adds, which is zero or positive: zero when the price names no cache-write rate. The total still adds up to what the period cost. A cache entry that no later call read back appears here as a cost with nocached_tokensdiscount beside it. unpricedcounts the rows nothing priced. Each of them costs zero, so a group with anunpricedabove zero is a floor on what was spent rather than the whole of it. Ask your operator to price the subject; usage already recorded is re-costed the moment the price exists, because cost is resolved at read time and never stored on the record.
Prices are per model, per tool and per unit, and each applies from an instant onward, so a price
that changed inside your window splits the cost rather than re-pricing what came before it. Your
organisation may be charged the deployment's price or an override of its own; GET /models shows
which (choosing a model).
Every amount here is in euro. A price in another currency is converted with the European Central
Bank's reference rate: in a month your operator has closed, at the rate fixed when it was closed; in
every other month, at the newest rate. So the cost of an open month changes when a new rate arrives,
and that of a closed month never does. Where Core holds no rate for a currency, a row at such a
price counts as unpriced (A price in another currency).
A daily series¶
Pass interval=day together with from and to, and the summary carries series too: one
UsageDailyTotal per UTC calendar day inside that window, with cost_eur, unpriced,
threads_created, messages_appended and by_kind -- that day's cost split by usage kind, one
{kind, cost_eur, unpriced} per kind that had rows, ordered by kind and adding up to the day's cost_eur. A day with
no usage is a zero entry with no kinds, never a gap, so you can draw a line or a stacked bar chart
straight from it. The series adds up to the same total the summary's
own cost_eur carries. from and to are then both required — "every day there has ever been
usage" is not a series — and the window is capped at 366 UTC days; beyond it Core answers 422
naming to. Left out, series is null, the smaller wire every existing caller already reads.
curl -s "$CORE_URL/usage/summary?interval=day&from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer $CORE_KEY"
{
"items": [ ... ],
"cost_eur": "6.15",
"stored_bytes": 41943040,
"threads_created": 214,
"messages_appended": 1780,
"series": [
{ "day": "2026-09-01", "cost_eur": "0.12", "unpriced": 0, "threads_created": 9, "messages_appended": 62,
"by_kind": [ { "kind": "embedding_tokens", "cost_eur": "0.02", "unpriced": 0 },
{ "kind": "model_tokens", "cost_eur": "0.10", "unpriced": 0 } ] },
{ "day": "2026-09-02", "cost_eur": "0", "unpriced": 0, "threads_created": 0, "messages_appended": 0,
"by_kind": [] }
]
}
Your monthly cap¶
Your organisation, and each of its keys, can be given a monthly cap in euro, counted per
calendar month in UTC from the first of the month at 00:00:00Z. An administrator of the
organisation above yours sets your organisation's cap, or Artific where there is none above yours.
A key's cap is set by your organisation's administrators or by another key holding management,
never by the key itself. That other key may be one the capped key registered without a cap, so the
hard ceiling over a key holding management is your organisation's cap. A cap of 0 means nothing that costs until the month turns.
A cap stops what costs, and what may cost. Once the spend has reached the cap, a call on a
model with a price above zero is refused, and so is a call on a model that has no price at all:
what it costs is unknown, and unknown is not free. A call on a model whose price is zero is
still answered, at a cap of 0 and at any other, and a tool without a price stays free.
The cap counts money over everything that has a price: tokens, generated images, tool
invocations, retrieval, parsing, crawling, computer time and the bytes a share
link served — your turns, your background runs, your
evaluations, and the work console users start. What you store is not one of them — stored_bytes
is a gauge, not a priced usage row — so it never counts against this cap. There is no cap in
tokens: a token is worth a
different amount on every model, and a budget is what you can actually plan against. A record
nothing prices costs zero against the cap, which is why the answer below also tells you how many
such records the period holds.
curl -s "$CORE_URL/usage/caps" -H "Authorization: Bearer $CORE_KEY"
{
"period_start": "2026-09-01T00:00:00Z",
"period_end": "2026-10-01T00:00:00Z",
"unpriced": 0,
"tenant": { "cost_cap_eur": "250.00", "cost_cap_until": null, "cost_eur": "241.07" },
"tenant_cost_cap_counts": "branch",
"tenant_branch_cost_eur": "248.90",
"keys": [
{ "key_id": "key_01H...", "cost_cap_eur": null, "cost_cap_until": null, "cost_eur": "12.40" }
]
}
tenant is your organisation's cap and what it has spent; keys holds your own key and no other.
A cost_cap_eur of null means no cap. cost_cap_until is set when the cap is for this month
only, and null for a cap that stands from month to month. GET /key shows the same figures for
your key on its own.
A cap for this month only. Whoever may set a cap can make it end with the month by sending
monthly_cost_cap_until beside it: the cap then lifts by itself at the start of the next month,
and nobody has to put it back. The one value accepted is the end of the current month, the first
of next month at 00:00:00Z (the same instant as period_end above); any other is 422 naming
the field, and so is an end with no cap to end. A platform that stops one of its organisations on
detection, for the rest of the month, sends:
curl -s -X PATCH "$CORE_URL/organisations/$ORGANISATION_ID" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"monthly_cost_cap_eur": "0", "monthly_cost_cap_until": "2026-11-01T00:00:00Z"}'
The same field works on a key, at POST and PATCH of the key, and on a service account's key
when the account is created. Changing the amount without the field keeps a cap for this month
only as it is; sending null makes the cap standing, and lifting the cap clears its end. Once the month has turned the
cap no longer counts anywhere -- it stops nothing and nobody is mailed about it -- while the
organisation or key still shows it, with its end, until somebody writes it again. An organisation
cannot end its own cap, just as it cannot lift it: its own administrators are refused 403.
An organisation's cap counts the organisations below it, unless it was told otherwise.
tenant_cost_cap_counts says which: branch weighs the cap against what your organisation and
every organisation below it spent, and tenant_branch_cost_eur is that figure, with what a
deleted organisation below used this month for as long as the cap keeps counting it; own weighs it
against tenant.cost_eur alone, and tenant_branch_cost_eur is null, as it is where no cap is
set and for a key without the management capability. An organisation moved under another parent
takes what it spent this month with it: from the move on that counts toward its new parent's cap,
not its old one's. A cap above yours holds you too: once an organisation above yours has reached its cap,
your calls that cost are refused as well, whatever your own cap says. This answer does not show
the caps above you.
When a cap is reached, POST /responses and POST /responses/{id}/tool_outputs answer 429
with a Retry-After header carrying the seconds until the month turns, and a detail saying which
cap stopped you — your key's, your organisation's, or that of an organisation above yours. They
are weighed in that order: your key's first, then your organisation's, then those above it. Nothing is written: no response, no conversation, no
queued run. A background run that reaches a cap between rounds ends failed with the code
usage_cap_exceeded, keeping what it had already done.
A turn can reach a cap while it runs. Core weighs the caps again before every model call and
before every tool call that has a price, and the turn stops there: the response ends incomplete with
incomplete_details.reason: "usage_cap_exceeded", a stream closes with response.incomplete, and
POST /messages answers stop_reason: "pause_turn". What the turn produced until then is in the
response and is billed. The rule behind the two endings: refused before it produced anything, a
background run's round ends the run failed with the code usage_cap_exceeded; stopped after it
produced something, a turn or a run ends incomplete with the reason usage_cap_exceeded. A
task whose run a cap stopped fails with usage_cap_exceeded either way. A tool that costs is not run and its result carries the cap's sentence; a
tool that costs nothing still runs.
The same 429 answers a search that embeds its question on an embedding model that costs money
-- POST /knowledge/indexes/{id}/search, POST /memory/stores/{id}/search and POST
/knowledge/retrieval-tools/{id}/query -- and POST /tools/{name}/call on a tool that would spend.
A search whose embedding models all cost nothing still answers, and its embedding tokens are still
counted.
A toolset's MCP endpoint has no status to give, so there the tool's result carries the cap's
sentence.
A cap is weighed against what has been spent, not against what a call is about to spend. So one turn passes its cap by at most the step that crossed it, and turns that run at the same moment can together pass it by what they have in flight: each one sees its own spend and what was already billed when it started, not what the others are spending. Plan a cap as a ceiling with that margin, not as an exact ledger.
Audit events¶
curl -s "$CORE_URL/audit/events?request_id=req_5Tn8..." -H "Authorization: Bearer $CORE_KEY"
GET /audit/events returns only the events your own key caused: what its calls did, and what
Core's background worker did at its request, such as a scheduled run or erasing a person. Another
key's events, and what people did in the console, are not in it. Each event includes the
request_id of the originating call.
| Field | Holds |
|---|---|
event |
what happened: response.created, tool.executed, policy.decision, prompt.rendered, skill.loaded, file.created, artifact.produced, memory.written, ... |
actor |
your key's id; worker when Core's background worker did something your key asked for; or subscription:<id> when a subscription your key created synced its copies |
key_id |
your key's id |
request_id, response_id |
what it belonged to |
axis, decision, reason |
on a policy.decision: which axis, allowed or denied, and why |
refs |
ids, names, versions and counts |
origin |
core for what Core recorded, product for what a product built on Core delivered (below) |
product, product_name |
on a product's event: its namespace and its name when it was delivered; empty on Core's own |
Filter audit events with event, request_id, response_id, subject (one person's rows, by
the id your application named them with), origin (core or product) and product (one
product's events, by its namespace).
The organisation's whole trail is read in two places. A key holding management reads it at
GET /organisations/{your own id}/audit/events, with the same filters and fields (there key_id
names the key behind each event, and is empty when a person or Core itself acted), and exports it
at .../audit/events/export (Manage organisations through the API); that is the route a backend uses,
since it has no console. That route answers 403 when the organisation above yours has
reserved the audit trail. An administrator reads the same trail in
the console.
An export is JSON Lines, one event per line, oldest first. If a download breaks off, ask again
with after set to the id on the last complete line you received: only the events after it
are sent, so the two files together are the whole export.
Policy checks record both allowed and denied decisions. Use these events to investigate blocked attempts as well as completed operations.
The refs field contains ids, tool names, model ids and counts. It does not contain filenames,
messages or caller-supplied content. Conversation content is stored separately.
Deliver your product's own events¶
When you build a product on Core, some of what happens in it never passes through Core: a role assigned in your product, an export someone downloaded, an audience you changed. Deliver those events to the organisation's audit trail and an administrator reads them beside Core's own, on the same routes, kept as long and exported the same way.
Two things come first. Your operator names your product on the organisation, or on one above it:
a namespace such as acme and a display name such as Acme Workspace. Only an operator can, and
every event your keys deliver then starts with that namespace. And the key that delivers holds the
product_events capability (Capabilities).
curl -s -X POST "$CORE_URL/audit/events" -H "Authorization: Bearer $CORE_KEY" \
-H "Content-Type: application/json" -d '{
"events": [{
"id": "evt_01J9",
"event": "acme.role.assigned",
"occurred_at": "2026-10-22T09:30:00Z",
"subject": "anna",
"actor": "mem_7",
"about": {"kind": "role", "id": "rol_42"},
"request_id": "req_5Tn8...",
"changes": {"role": {"from": "viewer", "to": "editor"}}
}]
}'
| Field | Holds |
|---|---|
id |
your own id for the event; deliver it again and Core keeps the first one |
event |
your namespace and at least two more parts: acme.role.assigned |
occurred_at |
when it happened, with a timezone; at most five minutes ahead of Core's clock and inside the organisation's audit retention |
about |
what it was about, as a kind and your id for it; no names or text |
subject |
optional: the person it happened for, by the id your application uses on every other call |
actor |
optional: your own id for whoever acted; shown as acme:mem_7 |
request_id |
optional: the X-Request-Id of the call it belongs to, so one search finds both; req_ and 26 upper-case ULID characters |
changes |
optional: the fields that changed, at most 32, each a short value, a short list or {from, to}, 2 KiB in all |
Send one to a hundred events at a time. Core checks them all before it records any: one event
outside your namespace, too far in the future or too old, an id that appears twice in the
delivery, and nothing of that delivery is kept, with a 422 naming each field. The answer lists one item per event in the order you sent them,
{id, audit_event_id, created}. Deliver at least once from your own outbox: an id Core already
has answers created: false and keeps the first row, even if the second delivery says something
else.
changes must not hold a secret, content or an e-mail address. Core refuses the whole delivery
with a 422 when a key contains password, passwd, secret, token, credential, api_key,
email, mail, content, body, text, message or prompt anywhere in it, so context and
email_verified are refused too, and when a value looks like an e-mail address. It names only the
field in its answer. It cannot see everything,
so the promise stays yours. Deliver only what Core does not record itself: never the calls your
product makes, which are on the trail already.
Your key reads its own deliveries at GET /audit/events?origin=product. An organisation without
a product named answers 409. So does a delivery whose earlier copy was removed while Core was
recording it: deliver it again. A key without the capability gets 404, and a person's subject
token 403.
Find a request¶
Use the X-Request-Id response header to find both usage and audit records for a call:
curl -s "$CORE_URL/audit/events?request_id=req_5Tn8..." -H "Authorization: Bearer $CORE_KEY"
curl -s "$CORE_URL/usage?request_id=req_5Tn8..." -H "Authorization: Bearer $CORE_KEY"
Store the id with your application’s request logs so you can trace activity and usage later.
Usage after deletion¶
Deleting a conversation or file retains its usage and call audit records. Deleting the organisation removes those records; its separate management audit history is retained.
View usage in the console¶
Administrators can review these records on the console’s Usage and Audit sections, using the available period and key filters.
Every summary row carries tenant_id, the organisation the total is for. The management twin of
this route also takes include_children=true, which adds the organisation and everything below it
up in one answer, told apart by that same tenant_id (interval=day together with
include_children answers 422: a daily series over a whole subtree does not exist yet). Without
management, your key always reads its own organisation and nothing else through these routes;
with management, it reads a descendant's usage and trail the same way, through
/organisations/{id}/... (Manage organisations through the API).
What you pay for one organisation below yours. GET /organisations/{id}/usage/summary of an
organisation below your key's own carries paid_eur beside cost_eur on every total and on the
summary: cost_eur is what that organisation is charged, paid_eur the same usage at your own
organisation's prices, what you pay for it. Read it organisation by organisation for your cost per
customer, without reading the whole branch at once. paid_eur is null on your own organisation,
with interval=day, and on the administrators' route /tenants/{id}/usage/summary.