---
title: "Summary"
method: GET
path: "/tenants/summary"
tags: ["tenants"]
---

# Summary

`GET /tenants/summary`

## Request

- Base URL: `https://api.instruxi.dev/api/v1/enforcer`
- URL: `https://api.instruxi.dev/api/v1/enforcer/tenants/summary`
- Auth: one of:
  - API key in header `Authorization`
  - API key in header `X-API-Key`

One metric row per tenant the caller may read: accounts, active accounts, KYC-verified accounts, groups, pending invites and last signup — the platform-console view that replaces a list-then-N-counts loop. Scoped by the SAME compiled row filter as GET /tenants, so a tenant admin sees exactly their own tenant's row and a platform admin sees their own tenant's row by default — all_tenants=true for one row per tenant on the platform, tenant_id for one other tenant; a non-admin caller simply gets one row (their own), never an error. `unowned` on each row means the workspace has no owner and so counts against nobody's workspace plan, and the top-level `unowned` counts those across the whole visible set (ignoring paging, like `total`) — the operator's answer to "how many live workspaces is nobody being billed for"; a tenant_admin can fix their own with `claimWorkspace`.

## Query parameters

- `limit` integer
- `offset` integer
- `tenant_id` string, uuid
- `all_tenants` boolean

## Response `200`

OK

- InternalDeliveryHttpHandlerTenantSummaryResponse
  - `data` EnforcerV3InternalDomainTenantSummary
    - `rows` EnforcerV3InternalDomainTenantSummaryRow[]
      - `accounts` integer
      - `active_accounts` integer
      - `agent_count` integer
      - `groups` integer
      - `last_signup_at` string: LastSignupAt is the newest account created_at, nil for an empty tenant.
      - `member_count` integer: MemberCount / AgentCount split Accounts by kind (AGENTS_CONTRACT §8): MemberCount is humans, AgentCount is service accounts. MemberCount + AgentCount == Accounts; Accounts itself stays unsplit for backward compatibility with existing consumers.
      - `name` string
      - `pending_invites` integer
      - `status` string
      - `tenant_id` string
      - `unowned` boolean: Unowned reports that this workspace has no owner_person_id (000104), so it counts against nobody's workspace plan. Not an error state — an operator-created or pre-000104 workspace is legitimately unowned until a tenant_admin claims it (POST /tenants/{id}/claim) — but it IS the state nothing anywhere reported, which is how four live tenants sat unattributed with the first symptom being a paying customer reading "0 of 2 used". A boolean rather than the person id: who owns a workspace is a cross-tenant identifier this row deliberately does not carry (see Tenant.OwnerPersonID's json:"-"); whether anyone does is the operator's question.
      - `verified` integer: Verified counts ACCOUNTS with a verified kyc/kyb session, not sessions — the same predicate the contacts KYC gate and referral stats use, so the console and the gate agree on who is verified.
    - `total` integer
    - `unowned` integer: Unowned is how many of those Total visible workspaces have no owner — counted over the whole filtered set, independent of paging, exactly like Total, so a console shows "4 of 37 unowned" without walking the pages. This is the smallest thing that would have surfaced the gap: the operator console already renders this table, so the number arrives where someone is already looking, with no new route, no new operationId and no new permission to reason about. It is scoped by the SAME authz row filter as the rows, so a tenant_admin sees only their own workspace's contribution and nothing platform-wide leaks.
  - `success` boolean

## Other responses

- `401`: Unauthorized
- `403`: forbidden: tenant_id/all_tenants beyond your own tenant without the cross_tenant grant
- `500`: Internal Server Error

---

[API](https://skmtc.dev/instruxi/apis/enforcer-api.md) · [All operations](https://skmtc.dev/instruxi/apis/enforcer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/instruxi/apis/enforcer-api/revisions/fa82bc1becc0?raw)
