# Activity & stats

> Read your timeline of everything that happened, and the dashboard totals behind it, from code.


Two read-only surfaces: the **activity timeline** (what happened, event by event) and the **statistics** behind the dashboard (how big your farm is and how it is trending).

Scopes: `activity.read` for the timeline, `stats.read` for the numbers. They are separate on purpose. A reporting integration can be handed the totals without also being handed a blow-by-blow account of every ban, sale and failed task.

Nothing here writes. In particular, reading your timeline does **not** mark it seen: the unread badge in the dashboard only clears when a person opens the page, so a script polling every minute never hides new activity from you.

## The timeline

Every event is durable and immutable, kept for 90 days. The list is newest first.

```endpoint
method: GET
path: /api/v1/activity
description: Your activity timeline, newest first.
auth: bearer
```

| Filter | Values |
| --- | --- |
| `category` | `account`, `wallet`, `task`, `workflow`, `trade`, `market`, `profile`, `billing` |
| `type` | Array of event types, e.g. `task.failed`, `market.listing_sold` |
| `severity` | Array of `info`, `success`, `warning`, `danger`, `gray` |
| `problems_only` | `true` keeps only `danger` and `warning`, the dashboard's toggle |
| `since`, `before` | Timestamps. `since` is inclusive, `before` exclusive |
| `account_ids` | Up to 1,000 of your Steam account IDs |
| `accounts` | A filter object, using the same keys [GET /accounts](/docs/api/en/endpoints/accounts) accepts |

```bash
curl "https://dashboard.steamlabs.dev/api/v1/activity?problems_only=1&per_page=20" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

```json
{
    "data": [
        {
            "id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
            "type": "task.failed",
            "title": "Task failed",
            "category": "task",
            "severity": "danger",
            "summary": null,
            "happened_at": "2026-07-30T09:14:22+00:00",
            "day": "2026-07-30",
            "steam_account": { "id": "019fb42e-9a7e-728d-b960-8b4c2162898c", "username": "farm_014" },
            "account_username": "farm_014",
            "subject": { "type": "task", "id": "019fb431-2c88-71ea-b0a3-8ce2f5d19b07" },
            "data": { "task_type": "login" }
        }
    ],
    "meta": { "page": 1, "per_page": 20, "total": 1, "last_page": 1 }
}
```

`day` is the calendar date of `happened_at` in **your** timezone, which is the value the dashboard groups its day cards on. Group on it and you get the same timeline the page draws, without having to guess at a timezone.

`account_username` is the username captured when the event was written, so it survives the account being deleted. `steam_account` is `null` once that happens, and for events that were never about one account.

`subject` is the record the event is about, as a type and an ID, so you can link straight to it. It is `null` when the event has no subject or the subject has since been pruned.

### Paging a long timeline

For a few pages, `page` is fine. To walk a long history, page with `before` instead: pass the oldest `happened_at` you have seen and read the next batch. That stays fast at any depth, while a high page number makes the database count and throw away everything above it.

```bash
curl "https://dashboard.steamlabs.dev/api/v1/activity?before=2026-07-30T09%3A14%3A22%2B00%3A00" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

### Narrowing to accounts

Naming IDs is capped at 1,000; over that you get `422 bulk_limit_exceeded` with the ceiling in `max`. To follow more accounts than that, describe them instead:

```bash
curl "https://dashboard.steamlabs.dev/api/v1/activity?accounts[login_state][]=banned&problems_only=1" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

The `accounts` object takes the same filters the accounts list does and is resolved server-side, so "everything that happened to every banned account" is one call regardless of how many accounts that is.

### One event

```endpoint
method: GET
path: /api/v1/activity/{id}
description: One activity event.
auth: bearer
```

Same shape as a list row. The timeline is denormalized, so a row already carries everything the event knows.

### The summary strip

```endpoint
method: GET
path: /api/v1/activity/summary
description: Counts for today and the trailing week, with problems and accounts touched.
auth: bearer
```

```json
{
    "window_days": 7,
    "timezone": "Europe/Amsterdam",
    "today": 34,
    "week": 214,
    "problems": 6,
    "accounts": 41
}
```

`today` starts at midnight in your timezone. The other three cover the trailing seven days.

## Statistics

```endpoint
method: GET
path: /api/v1/stats
description: Your dashboard totals: accounts, items, inventory value, wallet money, boosted time.
auth: bearer
```

```json
{
    "accounts": 412,
    "banned_accounts": 3,
    "item_count": 8241,
    "inventory_value_cents": 1284530,
    "inventory_value_currency": "USD",
    "wallet_cents": { "EUR": 41250, "USD": 9900 },
    "boost_minutes": 184320
}
```

Wallet money is **never** summed across currencies. Balances live in each account's own currency, and adding 15 PLN to 15 EUR gives a number that is wrong in both, so you get the whole map and convert with rates you trust. Inventory value is the Steam Market price snapshot, which is USD.

`banned_accounts` counts VAC, community and game bans. A trade-banned account is not counted, matching the dashboard card.

`boost_minutes` is wall-clock boosted time across your accounts, the same figure the Live page reports as hours all time. It is not the per-game ledger, which counts one hour of a ten-game plan ten times.

### Inventory value over time

```endpoint
method: GET
path: /api/v1/stats/inventory-value
description: Daily inventory value, from the nightly snapshots plus a live point for today.
auth: bearer
```

`days` defaults to 30 and accepts up to 365.

```json
{
    "days": 30,
    "currency": "USD",
    "series": [
        { "day": "2026-07-29", "inventory_value_cents": 1279900 },
        { "day": "2026-07-30", "inventory_value_cents": 1284530 }
    ]
}
```

History starts the day your account was first snapshotted; days before that are absent rather than zero, because you did not hold an inventory worth nothing, you held no inventory. Today is always present and always read live, so it agrees with `GET /stats` even before the night's snapshot run.

### Tasks completed per day

```endpoint
method: GET
path: /api/v1/stats/tasks
description: Completed tasks per day over a trailing window.
auth: bearer
```

`days` defaults to 14 and accepts up to 90. The series is zero-filled, so a quiet day is a zero rather than a gap.

```json
{
    "days": 14,
    "retention_days": 14,
    "series": [
        { "day": "2026-07-29", "completed": 512 },
        { "day": "2026-07-30", "completed": 344 }
    ]
}
```

`retention_days` is how long finished tasks are kept. Ask for a window longer than that and the extra days will read as zeros because the rows are gone, which is why `days` is refused outright above 90 rather than quietly shortened.

## Polling these endpoints

All five are cheap by construction: the totals and both series sum columns that are kept up to date as you go, never the item table, and the answers are cached for a minute per account. Polling once a minute costs about as much as polling once an hour.

The one thing worth knowing is that the numbers can be up to a minute behind a change you just made, which is the same freshness the dashboard shows. If you need the exact moment something happened, read the timeline, not the totals.
