Analyse Production Traffic
The analytics endpoints answer “how much did this agent cost last week”. They do not answer “what happened last night, and did it work”. For that you need the rows, not the aggregates — and five calls get all of them.
The five calls
Every route below is a GET and is paginated. Four of them (/api/messages,
/api/audit-logs, /api/ai-logs, /api/pipeline-traces) default limit to 50
(maximum 200) and take an ISO-8601 from / to window. /api/conversations is
the odd one out: it defaults limit to 20 (maximum 100), and its window is
updatedSince / updatedUntil, not from / to.
| # | Route | Permission | What it gives you |
|---|---|---|---|
| 1 | /api/conversations?instanceId=&updatedSince=&updatedUntil= | conversation:read | which conversations were touched in the window |
| 2 | /api/messages?instanceId=&from=&to= | conversation:read | every message of that agent, across all conversations |
| 3 | /api/audit-logs?instanceId=&from=&to= | audit_log:read | every tool call, with its arguments, output and error |
| 4 | /api/ai-logs?instanceId=&from=&to= | analytics:read | every LLM call: tokens, cache breakdown, cost, callType, outcome |
| 5 | /api/pipeline-traces?instanceId=&from=&to= | analytics:read | every turn: total, LLM and per-phase timings |
From the CLI the same five are:
polyant-cli agent conversations list acme --limit 100 --json
polyant-cli agent messages acme --from 2026-08-31T18:00:00Z --limit 200 --json
polyant-cli monitor events --instance acme --from 2026-08-31T18:00:00Z --json
polyant-cli monitor ai-logs --instance acme --from 2026-08-31T18:00:00Z --json
polyant-cli monitor traces --instance acme --from 2026-08-31T18:00:00Z --jsonJoin them on conversationId, which every one of the five carries.
When the question starts from one conversation
Joining after the fact is for a window. Where the subject is a single conversation, three more routes narrow to it directly, and one narrows to a single turn:
| Route | Parameter | Permission |
|---|---|---|
/api/audit-logs | conversationId | audit_log:read |
/api/instances/{slug}/governance/events | conversationId, messageId | agent.governance:read |
/memories | sourceConversationId | memory:read |
/api/conversations/{id}/messages also takes around=<messageId> and returns a window centred on that message rather than the newest page, which is how a tool call in /api/audit-logs is read back in the context of the turn that issued it.
When the segment is the answer
If what you want is one row per conversation with its health, latency and cost already computed, do not compute it from these five: GET /api/conversations/export.csv streams the whole filtered segment as CSV, with the same predicates, order and columns as the conversations table, capped at 50,000 rows. The aggregates come from ai_logs, pipeline_traces, tool_audit_logs, hook_executions and governance_events — the same tables three of the routes above serve raw.
Two things that will make your numbers wrong
Failed LLM calls are in /api/ai-logs and not in the analytics aggregates.
The dashboard counts only calls that returned (outcome = 'ok'), because it
measures answers. The raw route returns the failures too and exposes the column.
So a total cost computed from these rows is legitimately higher than the one on
the dashboard. If you want the same basis, ask for it: ?outcome=ok.
Retention truncates the window from underneath. A global age-based sweep
purges ai_logs and pipeline_traces on their own schedule
(ANALYTICS_RETENTION_DAYS, default 90 days), independent of how long the
conversations they belong to are kept. A window older than that therefore looks
like conversations with no cost — which is missing data, not free traffic.
Check ANALYTICS_RETENTION_DAYS before reading an old window.
What is not exposed
The exact prompt sent to the provider is never returned by /api/messages. It
is captured only when the agent has debug capture on, and it is read one message
at a time from
/api/conversations/{conversationId}/messages/{messageId}/debug.
Scoping
Every one of the five routes is scoped to the caller’s organization and to the workspace the request names. An agent slug belonging to another tenant returns zero rows, not an error — you cannot tell an empty agent from one you may not read, and that is deliberate.