Skip to content

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_id and actor. Console activity has a null key_id and identifies the person in actor.
  • 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, carry null. 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 on POST /responses gave in helper, on every row that turn wrote, including its tools' rows and the turns of agents it called; or assist.<helper> for one of Core's own helpers, such as assist.json_schema. Work no helper did, and every row recorded before rows named the helper, carry null. 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_id on the row, GET /usage?subject=<id> for one person's rows, GET /usage/by-subject for 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_tokens group is a discount, not a charge. The model_tokens group 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_tokens group is a surcharge. Writing input into a provider's cache costs more than sending it once, and the model_tokens group 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 no cached_tokens discount beside it.
  • unpriced counts the rows nothing priced. Each of them costs zero, so a group with an unpriced above 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.