---
title: Counting devices and people for a dashboard
description: If you are building an inventory dashboard, you do not need to download every device just to display a number. The API can count for you and return the totals already grouped.
---

[Skip to content](https://k12panel.com/kb/developer-api/counting-devices-for-a-dashboard#main-content)

![test-full-word-logo.png\]](https://k12panel.com/hs-fs/hubfs/test-full-word-logo.png?height=35&name=test-full-word-logo.png)

Open main navigation

Close main navigation

- [Go to k12panel.com](https://k12panel.com)

[Go to k12panel.com](https://k12panel.com?hsLang=en)

 Hello. How can we help you?

- There are no suggestions because the search field is empty.

1. [Online Help and Knowledge Base](https://k12panel.com/kb?hsLang=en)
2. [Developer API](https://k12panel.com/kb/developer-api?hsLang=en)

# Counting devices and people for a dashboard

## If you are building an inventory dashboard, you do not need to download every device just to display a number. The API can count for you and return the totals already grouped:

### Counting devices for a dashboard

If you are building an inventory dashboard, you do not need to download every device just to display a number. The API can count for you and return the totals already grouped:

```
GET /api/public/v1/assets/count?group_by=asset_class&site=38&checked_out=false 
```

```
{  "total": 15,  "results": [    { "asset_class": { "id": 3, "name": "Chromebook" }, "count": 15 }  ]}
```

This is a read-only call. It needs a key with the **assets:read** permission, the same one that lets you list devices.

#### Choosing what to group by

`group_by` is the one required parameter. It takes `asset_class`, `site`, or both:

```
group_by=asset_class          one row per device classgroup_by=site                 one row per sitegroup_by=site,asset_class     one row per class at each site
```

You can also group by health, antivirus, hardware, and activity — `group_by=site,health_state` gives you fleet health per building in one call. Those dimensions need their own permissions and report slightly differently.

Each dimension you group by appears as a member on every row, so `group_by=site,asset_class` returns rows carrying both a `site` and an `asset_class`.

#### Narrowing what is counted

| Parameter | What it does |
| --- | --- |
| `site` | Count only these sites. Ids or names: `site=15,16` or `site=Library`. |
| `asset_class` | Count only these classes: `asset_class=3,4` or `asset_class=Chromebook,Laptop`. |
| `checked_out` | `false` = still in inventory, `true` = out with a person. Omit for both. |
| `state` | Which device states to include. Defaults to devices **in inventory**. |
| `include_descendants` | Roll subclasses (or sub-sites) up into the one you asked for. |

Any of these can take several values, separated by commas, and — as everywhere else in the API — you can use a name instead of an id.

#### The zero-fill rule (the important one)

**If you name the values you want, you get all of them back — including the empty ones.**

A library that stocks Chromebooks and iPads but has run out of laptops still returns a laptop row, with a count of zero:

```
GET /api/public/v1/assets/count?group_by=asset_class&site=15&asset_class=Chromebook,Laptop,iPad&checked_out=false 
```

```
{  "total": 7,  "results": [    { "asset_class": { "id": 3, "name": "Chromebook" }, "count": 5 },    { "asset_class": { "id": 4, "name": "Laptop" },     "count": 0 },    { "asset_class": { "id": 5, "name": "iPad" },       "count": 2 }  ]}
```

That `"count": 0` is deliberate. It means your dashboard can display **"0 Available Laptops"** as an alert state without having to handle a missing entry, and it means a class you asked about never silently disappears from the response.

**If you leave a filter off, it behaves as a wildcard**, and you get back only the values actually present. Omit `asset_class` entirely and you will see every class that exists at that site — and nothing for the ones that do not.

So the rule in one line: *ask for something explicitly and it always comes back, even at zero; leave it out and you only see what is there.*

#### One call for an entire dashboard

Because the values you name are always returned, filtering several sites **and** several classes at once gives you every combination — a complete grid, zeros filled in:

```
GET /api/public/v1/assets/count?group_by=site,asset_class&site=15,16,17,18&asset_class=Chromebook,Laptop,iPad&checked_out=false 
```

That is one request for a four-building, three-class panel: 12 rows, every one of them present whether or not the building stocks that class.

#### Which devices are counted

By default, counts include devices that are **in inventory** — active and inactive ones. Archived and trashed devices are left out, so a repair-bench tile is not quietly inflated by devices that were thrown away months ago.

To count differently, name the states you want, or ask for everything:

```
state=active                  only active devicesstate=active,archived         active plus archivedstate=any                     every device, whatever its state
```

The states available to you are listed at `GET /api/public/v1/meta`.

#### Counting a class and its subclasses together

Device classes can be nested — "Laptop" might have "MacBook" and "ThinkPad" beneath it. By default a count is for the exact class you asked for, so a district with only MacBooks would see `Laptop: 0`.

Add `include_descendants=true` and each class you asked for absorbs everything beneath it:

```
GET /api/public/v1/assets/count?group_by=asset_class&asset_class=Laptop&include_descendants=true 
```

Now the MacBooks are counted under Laptop. This applies only to classes and sites you name explicitly; a wildcard always counts by exact class.

#### Reading `total`

`total` is the number of **distinct devices** matching your filters. It is usually the sum of the rows, but not always: if you ask for a class *and* one of its subclasses while `include_descendants=true` is on, a device legitimately counts in both rows. `total` never double-counts, so trust it as the true figure.

#### Devices with no class

A device that has never been assigned a class still exists, and it appears as a row with `"asset_class": null`. This is intentional — it makes unclassified devices visible rather than quietly dropping them and leaving your rows short of the total. If you don't want them on a dashboard, filter to the classes you care about.

#### Counting people

People count the same way devices do, at `GET /api/public/v1/people/count`. It needs a key with the **people:read** permission, and it follows every rule above — the same `group_by`, the same zero-fill, the same `total`.

You can group by:

```
group_by=org_unit             one row per org unitgroup_by=group                one row per person groupgroup_by=has_assets           one row for "has a device", one for "has none"group_by=org_unit,has_assets  both, per org unit
```

And narrow with:

| Parameter | What it does |
| --- | --- |
| `org_unit` | Only these org units. Ids or names: `org_unit=87919` or `org_unit=2028`. |
| `group` | Only these person groups: `group=Band,Chess Club` or `group=4,7`. |
| `has_assets` | `false` = has no device checked out, `true` = has at least one. |
| `include_descendants` | Roll an org unit's sub-units up into it. |

The same filters work on `GET /api/public/v1/people`, so you can list the people behind any number you see.

#### Counting a whole branch of your directory

Org units nest, and by default a count is for the exact unit you named. If your students sit in `Students/HighSchool/2027`, `…/2028` and so on, ask for the parent and roll the branch up:

```
GET /api/public/v1/people/count?group_by=org_unit&org_unit=HighSchool&include_descendants=true&has_assets=false 
```

```
{  "total": 41,  "results": [    { "org_unit": { "id": 87919, "name": "HighSchool" }, "count": 41 }  ]}
```

That is "high schoolers who do not have a device checked out", in one call.

Org units named after a graduating year work as you would expect: `org_unit=2028` finds the unit **named** "2028" as long as no org unit has that id. If you want to be certain which one you are asking for, use the id.

#### Working out what is left over

A common planning question — *how many devices will still be on the shelf once everyone who needs one has one* — is two counts and a subtraction:

```
GET /api/public/v1/people/count?group_by=has_assets&org_unit=HighSchool&include_descendants=trueGET /api/public/v1/assets/count?group_by=asset_class&asset_class=Chromebook&checked_out=false
```

The first tells you how many students still need a device, the second how many are available. Most dashboard tools can subtract one query from the other for you; Grafana does it with a maths expression across the two panels.

#### People in no group

Grouping by `group` behaves like grouping by device tag: a person can be in several groups and is counted under **each** of them, so the rows can add up to more than `total`. `total` is always the number of distinct people. People in no group at all appear as a row with `"group": null`, rather than dropping out of the response.

- [Getting started](https://k12panel.com/kb/getting-started?hsLang=en)
- [Menu Functions](https://k12panel.com/kb/menu-functions?hsLang=en)
- [Settings](https://k12panel.com/kb/settings?hsLang=en)
- [3rd Party Integrations](https://k12panel.com/kb/3rd-party-integrations?hsLang=en)
- [PC Health and SOAR](https://k12panel.com/kb/pc-health-and-soar?hsLang=en)
- [FAQ](https://k12panel.com/kb/faq?hsLang=en)
- [K12Panel Agent](https://k12panel.com/kb/k12panel-agent?hsLang=en)
- [Blueprints Catalog](https://k12panel.com/kb/blueprints-catalog?hsLang=en)
- [Writing your own Blueprint](https://k12panel.com/kb/writing-your-own-blueprint?hsLang=en)
- [MSP Tools](https://k12panel.com/kb/msp-tools?hsLang=en)
- [Developer API](https://k12panel.com/kb/developer-api?hsLang=en)

# K12Panel Inc

k12panel.com Help Center

Copyright © 2026, K12Panel Inc