Read your spend over the API
Read your organisation's spend over HTTP and show it beside your own figures: in your product's console, a warehouse, a spreadsheet or a board report.
Get a key
On the API keys page, create a key for A dashboard. Send its name as X-Api-Key and its secret as X-Api-Secret on every request.
Every path starts with https://gateway.agentspulse.ai, the address on the Connect page. The examples call it $AP_GATEWAY.
Keep reading and recording apart. A key used by your agents only needs to record, and a dashboard only needs to read. A read key that leaks shows everyone's spend in the organisation, so give one both only where a process does both.
Pick the right method
| You want | Use |
|---|---|
| A total, a chart or a table: spend by agent, customer, model or day | Aggregate spend |
| The individual calls behind a figure, such as one request | List activities |
| Names for the user identifiers in your rows | List users |
| Your agents and how they are listed | List agents |
| Business results counted against requests | List outcomes |
| The prices calls are charged at | List prices |
Aggregate spend adds up on the server, over any window. If you find yourself paging through List activities and adding rows up, ask Aggregate spend for the same figure instead.
Ask for what a panel draws
Group by as many dimensions as the panel needs. groupBy=date&groupBy=model is a stacked daily chart; groupBy=user is a cost-per-person table; no groupBy is one total.
curl -G "https://$AP_GATEWAY/v1/organisations/<organisation>/activities:aggregate" \
-H "X-Api-Key: $AP_API_KEY" \
-H "X-Api-Secret: $AP_API_SECRET" \
--data-urlencode 'groupBy=date' \
--data-urlencode 'groupBy=model' \
--data-urlencode 'startTime=2026-09-01T00:00:00Z' \
--data-urlencode 'endTime=2026-10-01T00:00:00Z'
The dimensions are workspace, project, agent, request, model, provider, user, caller_service, caller_component, sub_agent, skill, framework, tool, status, error_code, error_class, kind, agent_kind, environment, date and hour. Each also works in camelCase, such as errorCode, and a row's dimensions always come back keyed in snake_case, such as error_code.
- Days and hours are cut in the
timeZoneyou send, such astimeZone=Africa/Nairobi, and in UTC if you send none. A day reads2026-10-14and an hour2026-10-14T12:00, with no offset. - Rows come most expensive first. Sort them yourself for a chart by day.
- Times are RFC 3339. Send an offset or
Z, such as2026-10-01T00:00:00+03:00. The window includesstartTimeand excludesendTime. - A blank value groups the calls that carried none. A
userof""is work done for no signed-in person. A call that did not fail has theerror_classERROR_CLASS_UNSPECIFIED. - A day with no calls has no row. Fill the gaps in your chart yourself.
readMaskpicks the figures to work out, such asreadMask=estimatedCostMicros,activityCount, and a narrow one answers faster. The figures left out still appear, as"0", and their times asnull. Leave it out for all of them.
Narrow it with a filter
A filter keeps only matching calls: terms of field = value joined by AND. The fields are workspace, project, agent, request, model, provider, user, caller_service, caller_component, sub_agent, skill, framework, tool, status, error_code, error_class, kind and environment, in either spelling. A workspace or agent can be its short id, such as default or atlas, or its full name.
| Filter | With | Gives |
|---|---|---|
user = "8c21e0b4" |
groupBy=date |
One person's spend by day |
agent = "organisations/acme/agents/atlas" AND status = FAILED |
groupBy=error_class |
That agent's failures by kind |
Only = and AND are accepted. Anything else is refused rather than ignored, so a figure never silently leaves out a term you wrote. status, error_class and kind take a fixed set of values, matched in any case, and a value outside the set is refused.
Read one customer
Use the workspace path to read one of your customers only, for example to show them their own usage:
/v1/organisations/<organisation>/workspaces/brightline/activities:aggregate
A filter can never widen a read past its path: filtering a workspace by another workspace returns nothing.
Show money
Costs are whole millionths of a US dollar, sent as strings. Divide by 1,000,000 when you display them, not before you add them up.
estimatedCostMicros is priced from the call's tokens when it was recorded. billedCostMicros is the call's share of the Google bill you reported, filled only where its hour and model matched. See check your bill. See how cost is calculated.
Show names
Rows carry user identifiers. Join them with List users, or with your own user table. A user is listed only once your code has sent their name or email, see show names instead of identifiers.
Read cost per outcome
- List every outcome. The list has no window or filter, so page through it all and keep the ones whose
occurredAtfalls in the period you report on. - Ask Aggregate spend for the same period with
groupBy=request, which gives every request's spend in one call. - Join the two on
request, and divide each request's spend by its outcomes'quantity.
Page through a list
A list returns 100 items unless you ask for up to 1,000 with pageSize. Pass nextPageToken back as pageToken until it comes back empty. Each item comes once, whatever the filter. An empty list is [] and a zero figure is "0", so neither field is ever missing.
When something is refused
A refusal has the HTTP status and a body naming what to change:
{"error": {"code": "InvalidArgument", "reason": "UNKNOWN_DIMENSION", "message": "\"colour\" is not a dimension that can be grouped by; allowed: ...", "field": "groupBy", "requestId": "4f1c9e2a7b3d4e5f8a9b0c1d2e3f4a5b"}}
Check reason in your code, show message to a person, and quote requestId when you contact us. Every reply also carries the id as X-Request-Id. Every read your key is allowed to make also says where your rate limit stands:
| Header | Means |
|---|---|
RateLimit-Policy |
The limit and its window in seconds, such as 6000;w=60: 6,000 reads a minute. |
RateLimit-Limit |
The reads the window holds. |
RateLimit-Remaining |
How many are left now. |
RateLimit-Reset |
Seconds until the full limit is back. |
A 429 says how many seconds to wait in Retry-After. A read refused before it is counted, a 401 or 403, carries none of them. See error codes for every reason.