Analytics API overview

The Analytics API is experimental. Its routes, query semantics, and response shapes may change or be removed without notice, and it is not covered by the v2 backwards-compatibility guarantees. Avoid building production-critical integrations against it yet.

The Analytics API lets you run read-only SQL queries directly against your Paid data — usage signals, AI costs, revenue, credits, and delivered value — so you can build your own dashboards, reports, and in-product analytics without exporting data first.

Every query is automatically scoped to your organization: you can only ever see your own data, and no organization filter is needed (or possible) in your SQL.

Prerequisites

The data model

Your data is exposed as a small star schema of dimension tables (dim_*, describing entities) and fact tables (fact_*, describing events and transactions). Facts join to dimensions on *_id columns.

GroupTablesWhat they hold
Dimensionsdim_customer, dim_product, dim_plan, dim_credits_currency, dim_organizationYour customers, products/agents, plans, and credit currencies
Usagefact_signalOne row per signal (event), including its raw JSON data payload
Costfact_costOne row per AI/LLM call: vendor, model, tokens, cost
Valuefact_delivered_valueBusiness value delivered per signal
Revenuefact_invoice, fact_invoice_line, fact_order, fact_order_line, fact_payment, fact_usage_summaryContracts, invoices, payments, and billed usage
Creditsfact_credit_transactionCredit grants and spends

See the Schema reference for every column, and the Query cookbook for ready-to-use examples.

Endpoints

The base URL is https://api.agentpaid.io/api/v2/experimental/analytics. All endpoints authenticate with your API key via the Authorization: Bearer header.

POST /query

Run a single SQL statement. The request body is { "query": "<sql>" }.

$curl -X POST https://api.agentpaid.io/api/v2/experimental/analytics/query \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{"query": "SELECT signal_name, count() AS signals FROM fact_signal WHERE created_at > now() - INTERVAL 7 DAY GROUP BY signal_name ORDER BY signals DESC"}'

The response gives you typed columns, rows in column order, and metadata:

1{
2 "columns": [
3 { "name": "signal_name", "type": "String" },
4 { "name": "signals", "type": "UInt64" }
5 ],
6 "rows": [
7 ["addin-chat", "4514784"],
8 ["email-triage", "1379812"]
9 ],
10 "meta": { "rowCount": 2, "elapsedMs": 190, "truncated": false }
11}

GET /schema

Returns the queryable views with their columns, types, and descriptions — the same information as the Schema reference, live from your account. Useful for discovery and for building tooling.

$curl https://api.agentpaid.io/api/v2/experimental/analytics/schema \
> -H "Authorization: Bearer YOUR_API_KEY"

GET /signals/metadata

Signals carry a free-form JSON data payload whose shape you define. This endpoint lists the JSON paths (and their observed types) present in your signals, grouped by signal name, so you know what you can filter and group on.

Query parameterDescription
signalNameRestrict discovery to a single signal name. Optional.
fromDateStart of the window (ISO 8601). Defaults to 30 days ago.
toDateEnd of the window (ISO 8601). Defaults to now.
$curl "https://api.agentpaid.io/api/v2/experimental/analytics/signals/metadata?signalName=email-triage" \
> -H "Authorization: Bearer YOUR_API_KEY"
1{
2 "signals": [
3 {
4 "signalName": "email-triage",
5 "paths": [
6 { "path": "account_id", "types": ["String"] },
7 { "path": "tool_name", "types": ["String"] }
8 ]
9 }
10 ],
11 "meta": { "fromDate": "2026-05-13T00:00:00.000Z", "toDate": "2026-06-12T00:00:00.000Z" }
12}

Writing queries

The API speaks ClickHouse SQL. A few rules and conventions:

  • Read-only. Only a single SELECT (or WITH … SELECT) statement is accepted. Anything else is rejected.
  • No org filter needed. Queries are automatically restricted to your organization. Reference tables by their unqualified name (FROM fact_signal), not analytics.fact_signal or default.*.
  • Money is in cents. All monetary columns are integer cents — divide by 100 for display (e.g. total_incl_tax / 100).
  • 64-bit integers come back as strings. Counts, sums, amounts, and large ids (UInt64/Int64) are returned as JSON strings to preserve precision beyond 2^53. Parse them client-side as needed. Smaller integers, floats, booleans, and dates serialize natively.

Limits

These limits are guardrails for interactive analytics. The API is not a bulk export mechanism — for large extracts, aggregate or paginate with LIMIT/WHERE on an indexed column such as created_at.

LimitValue
Execution time30 seconds per query
Result rows10,000 (responses set meta.truncated: true when capped)
StatementsOne SELECT/WITH per request
ConcurrencyA small number of simultaneous queries per organization

Errors

Errors return a JSON body with an error, a machine-readable code, and usually a details message.

HTTPcodeMeaning
400INVALID_QUERYNot a single read-only SELECT/WITH statement
400QUERY_ERRORThe query was rejected by the database (syntax, unknown column, type mismatch, …); details explains why
400INVALID_DATE_RANGEfromDate is after toDate (metadata endpoint)
403ACCESS_DENIEDThe query referenced something outside the analytics views
408QUERY_TIMEOUTThe query exceeded the 30-second limit
429CONCURRENCY_LIMIT_EXCEEDEDToo many concurrent queries; retry shortly
401Missing or invalid API key