App Usage Dashboard — API Spec
This document describes approved API changes to support the App Usage Dashboard. Endpoints marked New require implementation from scratch. Endpoints marked Modified require changes to existing, shipped endpoints.
This spec maps every UI section of the App Usage Dashboard mockup to the exact API endpoints, query parameters, and response fields needed to render it.
All endpoints below are prefixed with /clients/{client_id}/analytics/apps. Standard date parameters (granularity, start_date, end_date, compare_start_date, compare_end_date) apply to every call and are omitted from the parameter columns for brevity. For full parameter and response definitions, click through to the linked endpoint docs.
Endpoint Index
Every endpoint the dashboard calls, grouped by when it fires.
Page load (9 parallel calls):
| # | Endpoint | Key Params | Feeds | Status |
|---|---|---|---|---|
| 1 | GET /utilization/summary | — | Summary card: Total Apps + New/Unused counts | Modified |
| 2 | GET /utilization/summary | is_approved=false | Summary card: Un-Approved + Section 4 tab headers | Modified |
| 3 | GET /utilization/summary | category_ids=<ai_id> | Summary card: AI Apps + Section 3 tab headers | Modified |
| 4 | GET /utilization/timeseries | granularity=monthly | Stacked bar chart | New |
| 5 | GET /utilization | category_ids=<ai_id>&page_size=5 | AI Apps "Apps" tab (default) | Modified |
| 6 | GET /utilization | is_approved=false&page_size=5 | Un-Approved "Apps" tab (default) | Exists |
| 7 | GET /utilization/rankings | — | Top Apps: all 3 tabs | Exists |
| 8 | GET /utilization | filter=discovered&page_size=5 | New Apps list | Exists |
| 9 | GET /utilization | filter=dropped_off&page_size=5 | Unused Apps list | Exists |
Tab switches (lazy-loaded on click):
| # | Endpoint | Key Params | Feeds | Status |
|---|---|---|---|---|
| 12 | GET /utilization/categories | category_ids=<ai_id> | AI "Categories" tab | New |
| 13 | GET /utilization/users | category_ids=<ai_id> | AI "Users" tab | New |
| 14 | GET /utilization/departments | category_ids=<ai_id> | AI "Departments" tab | New |
| 15 | GET /utilization/categories | is_approved=false | Un-Approved "Categories" tab | New |
| 16 | GET /utilization/users | is_approved=false | Un-Approved "Users" tab | New |
| 17 | GET /utilization/departments | is_approved=false | Un-Approved "Departments" tab | New |
DTO changes (no new endpoints):
| Change | Where | Status |
|---|---|---|
Add is_approved query parameter | App Utilization Summary | Modified |
Add categories_used, users, departments response fields | App Utilization Summary | Modified |
Return full App DTO (includes catalog_app.categories[]) instead of AppRef | App Utilization response | Modified |
Add usage_level field | App Utilization response | New |
Add risk_level field | App Utilization response | New |
Add compare_value field to MetricWithDeltaDto | All analytics endpoints | New |
Page Layout
┌────────────────────────────────────────────────────────────────────────┐
│ SECTION 1: Top Summary Cards │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Total Apps Used │ │ Un-Approved Apps │ │ AI Apps Used │ │
│ │ 107 ↓12 +2 New │ │ 12 ↑3 +2 New │ │ 7 ↓2 +3 New │ │
│ │ │ │ │ │ │ │
│ │ Call #1 │ │ Call #2 │ │ Call #3 │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
├────────────────────────────────────────────────────────────────────────┤
│ SECTION 2: Stacked Bar Chart ── Call #4 │
│ │
│ 12-month time-series: Un-Used | Un-Approved | AI | Other │
├──────────────────────────────────┬─────────────────────────────────────┤
│ SECTION 3: AI Apps Used │ SECTION 4: Un-Approved Apps Used │
│ │ │
│ Tabs: Apps | Cat | Users | Dept │ Tabs: Apps | Cat | Users | Dept │
│ Header counts ── Call #3 │ Header counts ── Call #2 │
│ Default tab ─── Call #5 │ Default tab ─── Call #6 │
│ Other tabs ──── Calls #12-14 │ Other tabs ──── Calls #15-17 │
│ │ │
├──────────────────────────────────┤ │
│ SECTION 5: Top Apps │ │
│ │ │
│ Tabs: By Users | By Time | │ │
│ By Interactions │ │
│ All tabs ── Call #7 │ │
│ │ │
├──────────────────────────────────┤ │
│ SECTION 6: New Apps This Month │ │
│ Count ── Call #1 │ │
│ List ─── Call #8 │ │
│ │ │
├──────────────────────────────────┤ │
│ SECTION 7: Unused Apps │ │
│ Count ── Call #1 │ │
│ List ─── Call #9 │ │
└──────────────────────────────────┴─────────────────────────────────────┘
Section-by-Section Mapping
Section 1 — Summary Cards
Three cards across the top. Each card is one call to the same endpoint with different filters.
Card: Total Apps Used
GET /utilization/summary · docs · Modified
| UI Element | Mockup | Response Field |
|---|---|---|
| App count | 107 | apps_used.value |
| Change vs last month | ↓12 | apps_used.change_pct |
| "+N New" badge | +2 New | apps_discovered.value |
Card: Un-Approved Apps Used
GET /utilization/summary?is_approved=false · docs · Modified
| UI Element | Mockup | Response Field |
|---|---|---|
| App count | 12 | apps_used.value |
| Change vs last month | ↑3 | apps_used.change_pct |
| "+N New" badge | +2 New | apps_discovered.value |
Card: AI Apps Used
GET /utilization/summary?category_ids=<ai_id> · docs · Modified
| UI Element | Mockup | Response Field |
|---|---|---|
| App count | 7 | apps_used.value |
| Change vs last month | ↓2 | apps_used.change_pct |
| "+N New" badge | +3 New | apps_discovered.value |
Section 2 — Stacked Bar Chart
GET /utilization/timeseries?granularity=monthly · docs · New
12-month time-series. Each bar is one month split into 4 segments.
| UI Element | Response Field |
|---|---|
| X-axis month labels (Jun–May) | data[].date |
| "Un-Used Apps" segment | data[].unused_apps |
| "Un-Approved Apps" segment | data[].unapproved_apps |
| "AI Apps" segment | data[].ai_apps |
| "Other Apps" segment | data[].other_apps |
Section 3 — AI Apps Used
Four tabs: AI Apps Used · Categories · Users · Departments
Tab header counts
GET /utilization/summary?category_ids=<ai_id> · same as Call #3 (reuse response)
Each tab label shows a count and a change indicator. All four come from one response.
| Tab Label | Mockup | Count Field | Change Field |
|---|---|---|---|
| AI Apps Used | 7 ↑3 | apps_used.value | apps_used.change_pct |
| Categories | 5 ↑3 | categories_used.value (new) | categories_used.change_pct |
| Users | 41 ↑3 | users.value (new) | users.change_pct |
| Departments | 3 ↑3 | departments.value (new) | departments.change_pct |
Tab: AI Apps Used (default — page load)
GET /utilization?category_ids=<ai_id>&page_size=5 · docs · Modified
| UI Element | Example | Response Field |
|---|---|---|
| App name | ChatGPT | data[].app.canonical_name |
| App logo / initial | C | data[].app.logo (fallback to initial) |
| Category badge | AI, Productivity, Design | data[].app.catalog_app.categories[].name |
| User count | 52 Users | data[].unique_users.value |
| Usage level badge | Heavy / Medium / Light | data[].usage_level (new) |
| Approval status | ✓ Approved / ✕ Not Approved | data[].app.is_approved |
Tab: Categories (lazy — on click)
GET /utilization/categories?category_ids=<ai_id>&page_size=5 · docs · New
| UI Element | Example | Response Field |
|---|---|---|
| Category name | AI, Productivity, Design | data[].category.name |
| User count | 71 Users | data[].user_count.value |
The endpoint also returns data[].app_count but the mockup does not display it.
Tab: Users (lazy — on click)
GET /utilization/users?category_ids=<ai_id>&page_size=5 · docs · New
| UI Element | Example | Response Field |
|---|---|---|
| User name | Sarah Chen | data[].user.full_name |
| User initials | SC | Derived from data[].user.full_name |
| Department | Engineering | data[].department.name |
| App count | 5 Apps | data[].app_count.value |
Tab: Departments (lazy — on click)
GET /utilization/departments?category_ids=<ai_id>&page_size=5 · docs · New
| UI Element | Example | Response Field |
|---|---|---|
| Department name | Engineering | data[].department.name |
| User count | 34 Users | data[].user_count.value |
| App count | 6 Apps | data[].app_count.value |
Section 4 — Un-Approved Apps Used
Same 4-tab structure as Section 3. The only difference is the filter: is_approved=false instead of category_ids=<ai_id>.
Four tabs: Un-Approved Apps · Categories · Users · Departments
Tab header counts
GET /utilization/summary?is_approved=false · same as Call #2 (reuse response)
| Tab Label | Mockup | Count Field | Change Field |
|---|---|---|---|
| Un-Approved Apps | 12 ↑4 | apps_used.value | apps_used.change_pct |
| Categories | 5 ↑2 | categories_used.value (new) | categories_used.change_pct |
| Users | 58 ↑6 | users.value (new) | users.change_pct |
| Departments | 5 ↑1 | departments.value (new) | departments.change_pct |
Tab: Un-Approved Apps (default — page load)
GET /utilization?is_approved=false&page_size=5 · docs · Exists
| UI Element | Example | Response Field |
|---|---|---|
| App name | ChatGPT | data[].app.canonical_name |
| App logo / initial | C | data[].app.logo (fallback to initial) |
| Category badge | AI, Collaboration | data[].app.catalog_app.categories[].name |
| User count | 23 users | data[].unique_users.value |
| Risk level | Critical / Medium / Low | data[].risk_level — see open question |
Tab: Categories (lazy — on click)
GET /utilization/categories?is_approved=false&page_size=5 · docs · New
| UI Element | Example | Response Field |
|---|---|---|
| Category name | AI, Collaboration | data[].category.name |
| User count | 31 Users | data[].user_count.value |
Tab: Users (lazy — on click)
GET /utilization/users?is_approved=false&page_size=5 · docs · New
| UI Element | Example | Response Field |
|---|---|---|
| User name | James Wilson | data[].user.full_name |
| User initials | JW | Derived from data[].user.full_name |
| Department | Product | data[].department.name |
| App count | 6 Apps | data[].app_count.value |
Tab: Departments (lazy — on click)
GET /utilization/departments?is_approved=false&page_size=5 · docs · New
| UI Element | Example | Response Field |
|---|---|---|
| Department name | Engineering | data[].department.name |
| User count | 28 Users | data[].user_count.value |
| App count | 8 Apps | data[].app_count.value |
Section 5 — Top Apps
Three tabs: By Users · By Time · By Interactions
All three tabs are powered by a single call. The frontend selects which ranking to display based on the active tab.
GET /utilization/rankings · docs · Exists
Tab: By Users (default)
| UI Element | Example | Response Field |
|---|---|---|
| "Top App this Month" heading | — | (static label) |
| Leader app name + logo | MS Teams | by_active_users.leader.app.canonical_name, .logo |
| Leader metric | 32 Users | by_active_users.leader.metric.value |
| Leader change | ↑4 vs last month | by_active_users.leader.metric.change_pct |
| Biggest Increase app | Google Calendar | by_active_users.biggest_increase.app.canonical_name, .logo |
| Biggest Increase metric | 8 Users | by_active_users.biggest_increase.metric.value |
| Biggest Increase change | ↑11 | by_active_users.biggest_increase.metric.change_pct |
| Biggest Decrease app | Vercel | by_active_users.biggest_decrease.app.canonical_name, .logo |
| Biggest Decrease metric | 2 Users | by_active_users.biggest_decrease.metric.value |
| Biggest Decrease change | ↓7 | by_active_users.biggest_decrease.metric.change_pct |
Tab: By Time
| UI Element | Example | Response Field |
|---|---|---|
| Leader app name + logo | Figma | by_time.leader.app.canonical_name, .logo |
| Leader metric | 1h 32m Avg/Per Day | by_time.leader.metric.value (ms → formatted) |
| Leader change | ↑18 | by_time.leader.metric.change_pct |
| Biggest Increase app | Zoom | by_time.biggest_increase.app.canonical_name, .logo |
| Biggest Increase metric | 54m Avg/Per Day | by_time.biggest_increase.metric.value (ms → formatted) |
| Biggest Increase change | ↑22 | by_time.biggest_increase.metric.change_pct |
| Biggest Decrease app | Google Calendar | by_time.biggest_decrease.app.canonical_name, .logo |
| Biggest Decrease metric | 12m Avg/Per Day | by_time.biggest_decrease.metric.value (ms → formatted) |
| Biggest Decrease change | ↓31 | by_time.biggest_decrease.metric.change_pct |
Tab: By Interactions
| UI Element | Example | Response Field |
|---|---|---|
| Leader app name + logo | Slack | by_interactions.leader.app.canonical_name, .logo |
| Leader metric | 842 Avg/Per Person | by_interactions.leader.metric.value |
| Leader change | ↑56 | by_interactions.leader.metric.change_pct |
| Biggest Increase app | MS Teams | by_interactions.biggest_increase.app.canonical_name, .logo |
| Biggest Increase metric | 614 Avg/Per Person | by_interactions.biggest_increase.metric.value |
| Biggest Increase change | ↑124 | by_interactions.biggest_increase.metric.change_pct |
| Biggest Decrease app | Vercel | by_interactions.biggest_decrease.app.canonical_name, .logo |
| Biggest Decrease metric | 187 Avg/Per Person | by_interactions.biggest_decrease.metric.value |
| Biggest Decrease change | ↓93 | by_interactions.biggest_decrease.metric.change_pct |
Section 6 — New Apps This Month
Summary count reuses Call #1; the app list is a separate call.
Header
GET /utilization/summary · same as Call #1 (reuse response)
| UI Element | Mockup | Response Field |
|---|---|---|
| Count heading | 8 | apps_discovered.value |
| Change | ↑2 vs last month | apps_discovered.change_pct |
App list
GET /utilization?filter=discovered&page_size=5 · docs · Exists
| UI Element | Example | Response Field |
|---|---|---|
| App name | Slack | data[].app.canonical_name |
| Category badge | CRM | data[].app.catalog_app.categories[].name |
| User count | 8 | data[].unique_users.value |
Section 7 — Unused Apps This Month
Summary count reuses Call #1; the app list is a separate call.
Header
GET /utilization/summary · same as Call #1 (reuse response)
| UI Element | Mockup | Response Field |
|---|---|---|
| Count heading | 4 | apps_dropped_off.value |
| Change | ↓1 vs last month | apps_dropped_off.change_pct |
App list
GET /utilization?filter=dropped_off&page_size=5 · docs · Exists
| UI Element | Example | Response Field |
|---|---|---|
| App name | Quora | data[].app.canonical_name |
| Category badge | CRM | data[].app.catalog_app.categories[].name |
| Previous user count | 8 | See open question: compare_value |
For dropped-off apps, unique_users.value is 0 (no usage this period). The number shown in the mockup (8, 6, 4, 4) represents the previous period user count. The current MetricWithDeltaDto does not expose this directly.
Detailed Change Specs
1. Modify: App Utilization Summary
Endpoint: GET /clients/{client_id}/analytics/apps/utilization/summary
Add query parameter:
| Parameter | Type | Required | Description |
|---|---|---|---|
is_approved | boolean | No | Filter by approval status: true (approved only), false (unapproved only) |
Add response fields:
| New Field | Type | Description |
|---|---|---|
categories_used | MetricWithDeltaDto | Distinct catalog categories across apps with activity in the period |
users | MetricWithDeltaDto | Distinct users with app activity in the period |
departments | MetricWithDeltaDto | Distinct departments with app activity in the period |
These are backwards-compatible additive fields. Existing consumers are unaffected.
Updated response example:
{
"period": {
"start_date": "2026-02-01",
"end_date": "2026-02-28",
"compare_start_date": "2026-01-01",
"compare_end_date": "2026-01-31"
},
"apps_used": { "value": 107, "change_pct": -10.1, "compare_value": 119 },
"apps_discovered": { "value": 2, "change_pct": 100.0, "compare_value": 1 },
"apps_dropped_off": { "value": 1, "change_pct": null, "compare_value": null },
"categories_used": { "value": 15, "change_pct": -6.3, "compare_value": 16 },
"users": { "value": 87, "change_pct": -3.3, "compare_value": 90 },
"departments": { "value": 8, "change_pct": 0.0, "compare_value": 8 }
}
DB source for new fields:
| DTO Field | DB Aggregation |
|---|---|
categories_used.value | Count of distinct catalog_categories.id joined through catalog_application_catalog_categories for apps with activity in the period |
users.value | COUNT(DISTINCT app_usage_reports.user_id) in the period |
departments.value | Count of distinct departments via user-department assignments for users with activity in the period |
2. Modify: App Utilization — return full App DTO
The app field in the /utilization response will return the full App DTO instead of AppRef. The full App DTO includes catalog_app.categories[], which the UI uses to render category badges next to app names.
No changes to AppRef are needed.
3. Modify: App Utilization
Endpoint: GET /clients/{client_id}/analytics/apps/utilization
Add response fields:
| New Field | Type | Nullable | Description |
|---|---|---|---|
usage_level | string | Yes | Usage intensity: heavy, medium, or light. Derived from the Usage Score formula (35% adoption rate + 65% activity rate). null when insufficient data. |
risk_level | string | Yes | Risk classification: critical, medium, or low. null for approved apps. |
These are backwards-compatible additive fields. Existing consumers are unaffected.
Updated response row example:
{
"app": {
"id": "...",
"catalog_app": { "id": "...", "canonical_name": "ChatGPT", "logo": "...", "categories": [{ "id": "...", "name": "AI" }] },
"is_approved": false
},
"unique_users": { "value": 23, "change_pct": 15.0 },
"active_time_ms": { "value": 4320000, "change_pct": 8.2 },
"avg_time_ms_per_day": { "value": 144000, "change_pct": 8.2 },
"session_count": { "value": 412, "change_pct": 5.1 },
"usage_level": "heavy",
"risk_level": "critical"
}
risk_level is always null for approved apps. usage_level is null when there is insufficient data to compute the usage score.
4. Deprecate: apps/ai/ Endpoint Family
All six apps/ai/ endpoints are deprecated. usage_level is now available directly on GET /utilization, making the purpose-built AI routes redundant.
| Deprecated Endpoint | Replacement |
|---|---|
GET /apps/ai/users | GET /utilization?category_ids=<ai_id> |
GET /apps/ai/recent | GET /utilization?category_ids=<ai_id>&filter=discovered |
GET /apps/ai/users/summary | GET /utilization/summary?category_ids=<ai_id> |
GET /apps/ai/recent/summary | GET /utilization/summary?category_ids=<ai_id>&filter=discovered |
GET /apps/ai/departments | GET /utilization/departments?category_ids=<ai_id> |
GET /apps/ai/departments/summary | GET /utilization/summary?category_ids=<ai_id> |
The only breaking change is a field rename: user_count → unique_users. All other fields on the replacements are a superset of the deprecated responses.
5. Modify: MetricWithDeltaDto — add compare_value
compare_value is added as a nullable field to MetricWithDeltaDto. It is present on every analytics endpoint that returns MetricWithDeltaDto fields.
Updated shape:
{ "value": 0, "change_pct": -100.0, "compare_value": 8 }
| New Field | Type | Nullable | Description |
|---|---|---|---|
compare_value | number | Yes | The raw metric value for the comparison period. null when no comparison period was requested, matching change_pct. |
Motivation:
- Unused Apps list: For dropped-off apps,
unique_users.valueis0andchange_pctis always-100%. The previous-period user count shown in the mockup (8, 6, 4, 4) cannot be derived fromvalueandchange_pctalone.compare_valueexposes it directly. - Top Apps movers: The UI shows absolute change numbers (↑4, ↑11, ↓7). Computing these from
valueandchange_pctis lossy due to rounding.compare_valuemakes them exact:value - compare_value.
This is a backwards-compatible additive field — existing consumers that ignore unknown fields are unaffected.
6–9. New Endpoints
See the individual endpoint docs for each of the four new routes:
- App Utilization Timeseries — Stacked bar chart time-series
- App Utilization Categories — Categories dimension list
- App Utilization Users — Users dimension list
- App Utilization Departments — Departments dimension list
Complete App Analytics Endpoint Family
After these changes, the /analytics/apps/ namespace covers three analytical lenses:
/analytics/apps/utilization ← list: apps with metrics [EXISTS]
/analytics/apps/utilization/summary ← aggregate counts (enriched) [MODIFIED]
/analytics/apps/utilization/rankings ← leader + movers per metric [EXISTS]
/analytics/apps/utilization/timeseries ← time-series by classification [NEW]
/analytics/apps/utilization/categories ← categories dimension [NEW]
/analytics/apps/utilization/users ← users dimension [NEW]
/analytics/apps/utilization/departments ← departments dimension [NEW]
/analytics/apps/ai/users ← AI apps with usage_level [DEPRECATED → use /utilization?category_ids=<ai_id>]
/analytics/apps/ai/recent ← new AI apps with usage_level [DEPRECATED → use /utilization?category_ids=<ai_id>&filter=discovered]
/analytics/apps/ai/users/summary ← AI apps summary [DEPRECATED → use /utilization/summary?category_ids=<ai_id>]
/analytics/apps/ai/recent/summary ← new AI apps summary [DEPRECATED → use /utilization/summary?category_ids=<ai_id>&filter=discovered]
/analytics/apps/ai/departments ← AI departments [DEPRECATED → use /utilization/departments?category_ids=<ai_id>]
/analytics/apps/ai/departments/summary ← AI departments summary [DEPRECATED → use /utilization/summary?category_ids=<ai_id>]
The utilization endpoints accept composable filter parameters (is_approved, category_ids, vendor_ids, department_ids) and now include usage_level and risk_level on every row.
Design Decisions
Why generic utilization endpoints instead of dedicated apps/ai routes
The AI and Unapproved dashboard sections have identical structures (4 tabs: Apps, Categories, Users, Departments). Rather than using the existing /analytics/apps/ai/ endpoints for one section and creating a parallel /analytics/apps/unapproved/ family for the other, this dashboard uses only the generic /utilization/ endpoints with different filter values:
- AI section →
?category_ids=<ai_id> - Unapproved section →
?is_approved=false - Future sections (e.g. "Favorite Apps", "Browser Apps") → same endpoints, different filter
This keeps each endpoint with a single responsibility (one dimension view) and composable filters. Filters narrow the dataset; they don't change the response shape. This is different from a "god endpoint" where a ?view= parameter changes the entire DTO structure.
Relationship to existing /analytics/apps/ai/ endpoints
Adding usage_level to GET /utilization makes all six AI-specific endpoints redundant. The AI Apps Used tab switches from GET /apps/ai/users to GET /utilization?category_ids=<ai_id>, reading usage_level from the generic response. The only field rename is user_count → unique_users. See Detailed Change Specs: 4. Deprecate apps/ai/ Endpoint Family for the full migration table.
Why enrich the existing summary instead of a new endpoint
Adding categories_used, users, and departments to the existing utilization/summary response is a backwards-compatible additive change. It avoids creating a new endpoint with an awkward URL (the /summary path is already taken) and keeps the "one summary call per filter permutation" pattern clean.
Classification priority in timeseries
The stacked bar chart needs mutually exclusive buckets. The priority is: Unused > AI > Unapproved > Other. An app that is both AI-categorized and unapproved is classified as AI (the more specific classification wins). See App Utilization Timeseries for details.
Related Documents
- Design Guidelines — API-wide conventions all endpoints follow
- App Usage Report Review — Review of the original App Metrics proposal
- AI App Report Review — Review of the original AI App Report proposal
- Analytics Domain Overview — Endpoint pattern, data sources, constraints
- Open Questions — Existing design questions pending discussion