# Per-customer API call analytics

> Group api_request events by customer_id over 30 days with daily breakdown. The view usage-based pricing needs but most teams cobble together from logs.

## Agent adaptation contract

- Canonical human page: https://agentry.sh/workflows/api-usage-by-customer
- Execution mode: on_demand
- Immutable automation template: none
- Applies to: devtools-api
- Required example events: api_request
- Required Agentry resources: none declared
- Do not use when:
  - Do not use until the example events are mapped to observed project signals, the current onboarding state is verified, and live event/property reads prove the required data is present.
  - Do not present illustrative custom analysis as a built-in query blueprint or runnable Agentry product surface.
- Ask before using:
  - Which observed events map to api_request? Is the current onboarding state verified, and do live event/property reads show non-synthetic traffic for them?
  - Which live properties provide api_request.customer_id, api_request.endpoint, and which stable user or account identifier joins the signals?

This is an adaptable workflow example, not an API recipe. Map event and property names to the project's saved signal map, require status: "verified" from GET /v1/projects/:project_id/onboarding, and confirm the required signals through live event/property metadata and rows. Fetch current OpenAPI or query-blueprint details before making calls. Do not infer unattended authority from this page.

## Why this matters

If you sell a usage-based API, the per-customer usage table is your second-most-important dashboard (after MRR). Yet most teams build it badly:

- The data lives in CloudFlare logs or application logs that aren't queryable per-customer
- The "dashboard" is a quarterly SQL someone ran by hand
- CS can't pull "how much did Acme use last week?" without pinging engineering
- Sales has no idea who's growing usage (= ready to upsell) vs flatlining (= about to churn)

Agentry treats API requests as just-another-analytics-event. One HogQL pass gives you top customers, daily series, growth rate — the lot. Then render the rows in a CS-visible server-side dashboard so the team doesn't have to ping engineering for every account check.

## What you get

- Top 20 customers by 30-day call volume
- Daily breakdown per customer (sparkline data)
- Dashboard rows that CS and sales can both pull up from a server-side panel

## Walk through it

**You:**
> Group api_request events by customer_id in the last 30 days. Show me top 20 customers by call volume, with daily breakdown for each.

**Agent:**
> Two passes — first the ranked top 20, then daily series for each.

```text
Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "
    select
      properties.customer_id as customer_id,
      count() as total_calls,
      count(distinct toDate(timestamp)) as active_days,
      max(timestamp) as last_call_at
    from events
    where event = 'api_request' and timestamp > now() - interval 30 day
    group by customer_id
    order by total_calls desc
    limit 20
  "
```
```text
{
  "rows": [
    { "customer_id": "acme_corp",     "total_calls": 18420000, "active_days": 30, "last_call_at": "2026-05-16T13:42:00Z" },
    { "customer_id": "wayne_ent",     "total_calls":  9842000, "active_days": 30, "last_call_at": "2026-05-16T13:41:00Z" },
    { "customer_id": "stark_ind",     "total_calls":  6210000, "active_days": 28, "last_call_at": "2026-05-15T22:18:00Z" },
    { "customer_id": "umbrella_co",   "total_calls":  4810000, "active_days": 30, "last_call_at": "2026-05-16T13:30:00Z" },
    { "customer_id": "initech_llc",   "total_calls":   180000, "active_days":  8, "last_call_at": "2026-05-04T10:00:00Z" }
  ]
}
```

**Agent:**
> Now the daily series for the top customers so you can see the shape.

```text
Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "
    select
      properties.customer_id as customer_id,
      toDate(timestamp) as day,
      count() as calls
    from events
    where event = 'api_request'
      and timestamp > now() - interval 30 day
      and properties.customer_id in ('acme_corp', 'wayne_ent', 'stark_ind', 'umbrella_co', 'initech_llc')
    group by customer_id, day
    order by customer_id, day
  "
```
```text
{
  "rows": [
    { "customer_id": "acme_corp",   "day": "2026-04-16", "calls":  580000 },
    { "customer_id": "acme_corp",   "day": "2026-04-17", "calls":  604000 },
    { "customer_id": "acme_corp",   "day": "2026-04-18", "calls":  590000 },
    "...",
    { "customer_id": "initech_llc", "day": "2026-04-29", "calls":   12000 },
    { "customer_id": "initech_llc", "day": "2026-05-04", "calls":  168000 },
    { "customer_id": "initech_llc", "day": "2026-05-05", "calls":       0 }
  ]
}
```

**Agent:**
> Two interesting cases worth flagging to CS/Sales right now:
>
> - **`stark_ind`**: 6.2M calls / month, but `active_days = 28` and last call 2 days ago. They've gone quiet. Either weekend lull or churn risk. Worth a CS check-in.
> - **`initech_llc`**: 180k total but only 8 active days, last call 12 days ago. Classic dormant API customer — they integrated, hit it once big, then ghosted. A "did we miss anything?" email here is high-leverage.
>
> Let me prepare the rolling top-20 for a dashboard panel.

```text
Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "<first query above>"
```
```text
{ "rows": [/* dashboard rows */], "row_count": 20 }
```

**Agent:**
> Dashboard data ready. Recommended follow-ups:
>
> 1. `api-usage-as-upsell-signal` — find the customers whose calls grew >30% MoM (= ready to upsell)
> 2. `rate-limit-alert-routing` — review customers crossing 80% of their cap
> 3. Draft a weekly-window delta summary for CS to review and share

## The output

```
Top 20 API customers (30d)

Customer        Calls (30d)    Active days   Last call         Trend
─────────────────────────────────────────────────────────────────────
acme_corp       18,420,000          30        2 min ago        steady █████
wayne_ent        9,842,000          30        3 min ago        growing ███▟
stark_ind        6,210,000          28        2 days ago       ★ check in
umbrella_co      4,810,000          30        15 min ago       steady ████
initech_llc        180,000           8        12 days ago      ★★ dormant

Public view:
  Use Agentry's returned public_url after publishing a real query blueprint.

Suggested next investigations:
  - api-usage-as-upsell-signal (who's growing?)
  - rate-limit-alert-routing (who's hitting caps?)
  - on-demand weekly-window delta summary for a human to share with CS
```

## Setting it up

Emit one `api_request` analytics event per request. Don't worry about volume — Agentry's analytics ingest handles millions/day per project. The trick is **what to put in `customer_id`**:

- For API-key auth: the customer (org/project) the key belongs to, NOT the raw key
- For OAuth: the OAuth client id
- For Bearer JWT: a sub-claim you control

Tag a few useful properties for downstream segmentation:

```ts
// Hono / Express middleware after auth
app.use(async (c, next) => {
  const customer = await resolveCustomer(c.req.header("authorization"));
  c.set("customer", customer);

  // emit analytics — DO NOT await this, send fire-and-forget
  fetch(`https://api.agentry.sh/v1/analytics/`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${env.AGENTRY_SERVER_API_KEY}`,
      "Content-Type": "application/json",
      "User-Agent": "myapi-edge/1.0",  // REQUIRED — Cloudflare 403s default UAs
    },
    body: JSON.stringify({
      event: "api_request",
      distinct_id: customer.id,
      properties: {
        customer_id: customer.id,
        plan: customer.plan,
        endpoint: c.req.routePath,
        method: c.req.method,
        sdk_version: c.req.header("x-sdk-version"),
        ip_country: c.req.header("cf-ipcountry"),
      },
    }),
  }).catch(() => {}); // never block the user request on analytics
  await next();
});
```

**Per-request volume warning**: at very high RPS (>10k/sec) consider batching events client-side and POSTing a compacted batch every few seconds. The `/v1/analytics/` endpoint accepts an array of events in one POST.

## Variations

- *"Same top-20 but split by API endpoint — which endpoints are the heavy hitters?"*
- *"Hour-of-day distribution for the top 5 customers. Are they bursty or steady-state?"*
- *"Per-customer error rate (4xx + 5xx ÷ total). Sometimes a top customer's usage is mostly failing requests."*
- *"Show me customers whose usage dropped >50% in the last 7 days vs prior 7. Early churn signals."*
