Skip to main content

App Usage Dashboard — API Spec

Approved

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):

#EndpointKey ParamsFeedsStatus
1GET /utilization/summarySummary card: Total Apps + New/Unused countsModified
2GET /utilization/summaryis_approved=falseSummary card: Un-Approved + Section 4 tab headersModified
3GET /utilization/summarycategory_ids=<ai_id>Summary card: AI Apps + Section 3 tab headersModified
4GET /utilization/timeseriesgranularity=monthlyStacked bar chartNew
5GET /utilizationcategory_ids=<ai_id>&page_size=5AI Apps "Apps" tab (default)Modified
6GET /utilizationis_approved=false&page_size=5Un-Approved "Apps" tab (default)Exists
7GET /utilization/rankingsTop Apps: all 3 tabsExists
8GET /utilizationfilter=discovered&page_size=5New Apps listExists
9GET /utilizationfilter=dropped_off&page_size=5Unused Apps listExists

Tab switches (lazy-loaded on click):

#EndpointKey ParamsFeedsStatus
12GET /utilization/categoriescategory_ids=<ai_id>AI "Categories" tabNew
13GET /utilization/userscategory_ids=<ai_id>AI "Users" tabNew
14GET /utilization/departmentscategory_ids=<ai_id>AI "Departments" tabNew
15GET /utilization/categoriesis_approved=falseUn-Approved "Categories" tabNew
16GET /utilization/usersis_approved=falseUn-Approved "Users" tabNew
17GET /utilization/departmentsis_approved=falseUn-Approved "Departments" tabNew

DTO changes (no new endpoints):

ChangeWhereStatus
Add is_approved query parameterApp Utilization SummaryModified
Add categories_used, users, departments response fieldsApp Utilization SummaryModified
Return full App DTO (includes catalog_app.categories[]) instead of AppRefApp Utilization responseModified
Add usage_level fieldApp Utilization responseNew
Add risk_level fieldApp Utilization responseNew
Add compare_value field to MetricWithDeltaDtoAll analytics endpointsNew

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 ElementMockupResponse Field
App count107apps_used.value
Change vs last month↓12apps_used.change_pct
"+N New" badge+2 Newapps_discovered.value

Card: Un-Approved Apps Used

GET /utilization/summary?is_approved=false · docs · Modified

UI ElementMockupResponse Field
App count12apps_used.value
Change vs last month↑3apps_used.change_pct
"+N New" badge+2 Newapps_discovered.value

Card: AI Apps Used

GET /utilization/summary?category_ids=<ai_id> · docs · Modified

UI ElementMockupResponse Field
App count7apps_used.value
Change vs last month↓2apps_used.change_pct
"+N New" badge+3 Newapps_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 ElementResponse Field
X-axis month labels (Jun–May)data[].date
"Un-Used Apps" segmentdata[].unused_apps
"Un-Approved Apps" segmentdata[].unapproved_apps
"AI Apps" segmentdata[].ai_apps
"Other Apps" segmentdata[].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 LabelMockupCount FieldChange Field
AI Apps Used7 ↑3apps_used.valueapps_used.change_pct
Categories5 ↑3categories_used.value (new)categories_used.change_pct
Users41 ↑3users.value (new)users.change_pct
Departments3 ↑3departments.value (new)departments.change_pct

Tab: AI Apps Used (default — page load)

GET /utilization?category_ids=<ai_id>&page_size=5 · docs · Modified

UI ElementExampleResponse Field
App nameChatGPTdata[].app.canonical_name
App logo / initialCdata[].app.logo (fallback to initial)
Category badgeAI, Productivity, Designdata[].app.catalog_app.categories[].name
User count52 Usersdata[].unique_users.value
Usage level badgeHeavy / Medium / Lightdata[].usage_level (new)
Approval status✓ Approved / ✕ Not Approveddata[].app.is_approved

Tab: Categories (lazy — on click)

GET /utilization/categories?category_ids=<ai_id>&page_size=5 · docs · New

UI ElementExampleResponse Field
Category nameAI, Productivity, Designdata[].category.name
User count71 Usersdata[].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 ElementExampleResponse Field
User nameSarah Chendata[].user.full_name
User initialsSCDerived from data[].user.full_name
DepartmentEngineeringdata[].department.name
App count5 Appsdata[].app_count.value

Tab: Departments (lazy — on click)

GET /utilization/departments?category_ids=<ai_id>&page_size=5 · docs · New

UI ElementExampleResponse Field
Department nameEngineeringdata[].department.name
User count34 Usersdata[].user_count.value
App count6 Appsdata[].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 LabelMockupCount FieldChange Field
Un-Approved Apps12 ↑4apps_used.valueapps_used.change_pct
Categories5 ↑2categories_used.value (new)categories_used.change_pct
Users58 ↑6users.value (new)users.change_pct
Departments5 ↑1departments.value (new)departments.change_pct

Tab: Un-Approved Apps (default — page load)

GET /utilization?is_approved=false&page_size=5 · docs · Exists

UI ElementExampleResponse Field
App nameChatGPTdata[].app.canonical_name
App logo / initialCdata[].app.logo (fallback to initial)
Category badgeAI, Collaborationdata[].app.catalog_app.categories[].name
User count23 usersdata[].unique_users.value
Risk levelCritical / Medium / Lowdata[].risk_level — see open question

Tab: Categories (lazy — on click)

GET /utilization/categories?is_approved=false&page_size=5 · docs · New

UI ElementExampleResponse Field
Category nameAI, Collaborationdata[].category.name
User count31 Usersdata[].user_count.value

Tab: Users (lazy — on click)

GET /utilization/users?is_approved=false&page_size=5 · docs · New

UI ElementExampleResponse Field
User nameJames Wilsondata[].user.full_name
User initialsJWDerived from data[].user.full_name
DepartmentProductdata[].department.name
App count6 Appsdata[].app_count.value

Tab: Departments (lazy — on click)

GET /utilization/departments?is_approved=false&page_size=5 · docs · New

UI ElementExampleResponse Field
Department nameEngineeringdata[].department.name
User count28 Usersdata[].user_count.value
App count8 Appsdata[].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 ElementExampleResponse Field
"Top App this Month" heading(static label)
Leader app name + logoMS Teamsby_active_users.leader.app.canonical_name, .logo
Leader metric32 Usersby_active_users.leader.metric.value
Leader change↑4 vs last monthby_active_users.leader.metric.change_pct
Biggest Increase appGoogle Calendarby_active_users.biggest_increase.app.canonical_name, .logo
Biggest Increase metric8 Usersby_active_users.biggest_increase.metric.value
Biggest Increase change↑11by_active_users.biggest_increase.metric.change_pct
Biggest Decrease appVercelby_active_users.biggest_decrease.app.canonical_name, .logo
Biggest Decrease metric2 Usersby_active_users.biggest_decrease.metric.value
Biggest Decrease change↓7by_active_users.biggest_decrease.metric.change_pct

Tab: By Time

UI ElementExampleResponse Field
Leader app name + logoFigmaby_time.leader.app.canonical_name, .logo
Leader metric1h 32m Avg/Per Dayby_time.leader.metric.value (ms → formatted)
Leader change↑18by_time.leader.metric.change_pct
Biggest Increase appZoomby_time.biggest_increase.app.canonical_name, .logo
Biggest Increase metric54m Avg/Per Dayby_time.biggest_increase.metric.value (ms → formatted)
Biggest Increase change↑22by_time.biggest_increase.metric.change_pct
Biggest Decrease appGoogle Calendarby_time.biggest_decrease.app.canonical_name, .logo
Biggest Decrease metric12m Avg/Per Dayby_time.biggest_decrease.metric.value (ms → formatted)
Biggest Decrease change↓31by_time.biggest_decrease.metric.change_pct

Tab: By Interactions

UI ElementExampleResponse Field
Leader app name + logoSlackby_interactions.leader.app.canonical_name, .logo
Leader metric842 Avg/Per Personby_interactions.leader.metric.value
Leader change↑56by_interactions.leader.metric.change_pct
Biggest Increase appMS Teamsby_interactions.biggest_increase.app.canonical_name, .logo
Biggest Increase metric614 Avg/Per Personby_interactions.biggest_increase.metric.value
Biggest Increase change↑124by_interactions.biggest_increase.metric.change_pct
Biggest Decrease appVercelby_interactions.biggest_decrease.app.canonical_name, .logo
Biggest Decrease metric187 Avg/Per Personby_interactions.biggest_decrease.metric.value
Biggest Decrease change↓93by_interactions.biggest_decrease.metric.change_pct

Section 6 — New Apps This Month

Summary count reuses Call #1; the app list is a separate call.

GET /utilization/summary · same as Call #1 (reuse response)

UI ElementMockupResponse Field
Count heading8apps_discovered.value
Change↑2 vs last monthapps_discovered.change_pct

App list

GET /utilization?filter=discovered&page_size=5 · docs · Exists

UI ElementExampleResponse Field
App nameSlackdata[].app.canonical_name
Category badgeCRMdata[].app.catalog_app.categories[].name
User count8data[].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 ElementMockupResponse Field
Count heading4apps_dropped_off.value
Change↓1 vs last monthapps_dropped_off.change_pct

App list

GET /utilization?filter=dropped_off&page_size=5 · docs · Exists

UI ElementExampleResponse Field
App nameQuoradata[].app.canonical_name
Category badgeCRMdata[].app.catalog_app.categories[].name
Previous user count8See 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:

ParameterTypeRequiredDescription
is_approvedbooleanNoFilter by approval status: true (approved only), false (unapproved only)

Add response fields:

New FieldTypeDescription
categories_usedMetricWithDeltaDtoDistinct catalog categories across apps with activity in the period
usersMetricWithDeltaDtoDistinct users with app activity in the period
departmentsMetricWithDeltaDtoDistinct 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 FieldDB Aggregation
categories_used.valueCount of distinct catalog_categories.id joined through catalog_application_catalog_categories for apps with activity in the period
users.valueCOUNT(DISTINCT app_usage_reports.user_id) in the period
departments.valueCount 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 FieldTypeNullableDescription
usage_levelstringYesUsage intensity: heavy, medium, or light. Derived from the Usage Score formula (35% adoption rate + 65% activity rate). null when insufficient data.
risk_levelstringYesRisk 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 EndpointReplacement
GET /apps/ai/usersGET /utilization?category_ids=<ai_id>
GET /apps/ai/recentGET /utilization?category_ids=<ai_id>&filter=discovered
GET /apps/ai/users/summaryGET /utilization/summary?category_ids=<ai_id>
GET /apps/ai/recent/summaryGET /utilization/summary?category_ids=<ai_id>&filter=discovered
GET /apps/ai/departmentsGET /utilization/departments?category_ids=<ai_id>
GET /apps/ai/departments/summaryGET /utilization/summary?category_ids=<ai_id>

The only breaking change is a field rename: user_countunique_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 FieldTypeNullableDescription
compare_valuenumberYesThe 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.value is 0 and change_pct is always -100%. The previous-period user count shown in the mockup (8, 6, 4, 4) cannot be derived from value and change_pct alone. compare_value exposes it directly.
  • Top Apps movers: The UI shows absolute change numbers (↑4, ↑11, ↓7). Computing these from value and change_pct is lossy due to rounding. compare_value makes 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:


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_countunique_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.