# licenses (/docs/reference/api/licenses)

> License management: products, pools of seats, assignments to people and groups, and seats with a waiting list.



License management: products, pools of seats, assignments to people and groups, and seats with a waiting list.
A product (a SKU, such as a plan tier or an add-on) is defined by the platform or by a tenant, pools give each tenant a
number of seats of it, and the product is assigned to people and groups. Better IAM keeps one seat per person, active
while capacity lasts and otherwise waiting in order, and moves seats the moment anything changes. What a license
unlocks is expressed with existing governance: policies on `principal.licenses`, birthright package rules on
`identity.licenses`, and the product's feature keys. The repository guide is `docs/licenses.md`.

## Products and where they apply [#products-and-where-they-apply]

A product defined on the root tenant is a platform product: every tenant sees and assigns it, and only root
administrators define, change, or retire it. A product defined by an organization or project is visible to that tenant
and every tenant below it. Keys are permanent, unique in the defining tenant, and a tenant cannot repeat a key an
enclosing tenant defines; when an enclosing tenant later introduces a key a tenant below already uses, the enclosing
product wins and the lower one is listed as `shadowed`: its seats stay but no longer grant the key or its feature keys.
Only the defining tenant changes a product. Retiring a product releases its seats in every tenant and leaves its pools
and assignments as read-only history.

Capacity is granted by whoever owns the product: root administrators grant seats of platform products, and a tenant's
managers grant seats of its own products to itself or to any tenant below it. Pool changes are therefore authorized in
the product's defining tenant, while their `license:pool-*` events are recorded in the receiving tenant.

## Seats and the waiting list [#seats-and-the-waiting-list]

The claimants of a product in a tenant are its direct assignees and the live members of assigned groups; only active,
unexpired identities of the tenant claim a seat, of any kind (people, service accounts, agents, guests). They are
ranked by seniority, the first time each person claimed the product: the first `capacity` hold an active seat (the sum
of the tenant's live pools) and the rest wait in order. Within one millisecond, existing active seats rank before
existing waiting seats, which rank before new claimants, so a newcomer never takes a seat someone holds. Capacity is
never over-allocated: a cut moves the newest active seats to the head of the waiting list instead of failing.

Seats move in the same transaction as assignments, pool changes, retirement, group membership changes from any
writer, group deletion, and identity deletion; disabling and offboarding release seats right after the change. Pool
terms and expiring memberships or accounts are applied by the hourly job
[`iam.licenses.reconcile`](/docs/reference/api#license-jobs-and-helpers). Each change is audited on the person as
`license:seat-activate`, `license:seat-waiting`, or `license:seat-release`, with `from`, `to`, and a `reason`, and
methods that move seats return them as `seats` (`tenantId`, `productId`, `productKey`, `identityId`, `from`, `to`).

## What a license unlocks [#what-a-license-unlocks]

* **Policies.** `principal.licenses` lists the keys of the products the principal holds an active seat for in the
  decision's tenant (see [principal keys](/docs/authorization/conditions#principal-keys)). Waiting seats,
  retired products, and seats held only through a lapsed group membership never count; assumed roles hold none.
  Simulations, who-can reviews, and access invariants see the real seats, and `policies.test` defaults the key to an
  empty list.
* **Birthright packages.** Package rules test `identity.licenses` with product keys (see
  [automatic assignment](/docs/features/privileged-access/automatic-assignment)), so a seat brings the roles and groups
  a product needs and takes them away when it goes. Seats held only through package-granted memberships do not count.
* **Feature keys.** `iam.licenses.features` returns the feature keys a person's active seats unlock for server code,
  counting only platform products (defined by the root tenant) unless you pass `trustedTenantId`, so no tenant can
  mint a paid feature for itself by defining a product that lists its key. [`mine`](#mine) returns them to the person,
  with the platform's keys in `featureKeys` and every product's in `allFeatureKeys`.

Assigning, unassigning, and pool changes run the receiving tenant's enforced
[access invariants](/docs/features/governance/change-safety#access-invariants), so a seat change that newly breaks one
fails with `INVARIANT_VIOLATION`.

## Who may call what [#who-may-call-what]

Reading needs `iam:licenses:read`, handing out seats `iam:licenses:assign`, and defining products, adding capacity,
and settings `iam:licenses:manage`. They are checked on `iam/licenses/products` (with `/{id}` for one product),
`iam/licenses/pools` (with `/{id}` for one pool), `iam/licenses/assignments`, `iam/licenses/seats`, and
`iam/licenses/settings`. Every call is audited under its permission (reads included), and changes also record the
`license:*` event each method names. [`mine`](#mine) needs no permission and is not audited.

A product assigned to a group goes to whoever joins it, so every other API that puts people into a licensed group, or
keeps them there longer, asks for `iam:licenses:assign` on `iam/licenses/assignments` too: the groups API,
`identities.createMany` and `invite` (checked again at acceptance and when resent), guest invitations and
`guests.convertToMember` with `clearExpiry`, access packages and their rules, onboarding completion groups, the teams
API for administrators, and configuration sync. A team's maintainers, directory sync (SCIM), and renewing a guest
(`guests.attest`) are exempt by design.

| Method                                | What it does                                                                                                           | Access     |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------- |
| [`addPool`](#addpool)                 | Gives a tenant seats of a product: a quantity, optionally from a start until an end.                                   | Credential |
| [`assign`](#assign)                   | Gives a product to a person or a group of the tenant; the person, or each live member of the group, claims a seat.     | Credential |
| [`assignMany`](#assignmany)           | Gives a product to up to 100 people in one transaction.                                                                | Credential |
| [`configure`](#configure)             | Turns inactive-seat reclaim and waiting-list email on or off for the tenant.                                           | Credential |
| [`createProduct`](#createproduct)     | Defines a product: a permanent key, a name, and the feature keys a seat unlocks.                                       | Credential |
| [`getProduct`](#getproduct)           | Returns one product visible to the tenant, with where it is defined and whether it is shadowed.                        | Credential |
| [`getSettings`](#getsettings)         | Returns the tenant's license settings, with the defaults when they were never configured.                              | Credential |
| [`listAssignments`](#listassignments) | Lists the tenant's assignments, newest first, optionally of one product or subject.                                    | Credential |
| [`listPools`](#listpools)             | Lists the tenant's pools and, for products it defines, the pools it granted to tenants below it, newest first.         | Credential |
| [`listProducts`](#listproducts)       | Lists the products the tenant can use, its own and its ancestors', sorted by key.                                      | Credential |
| [`listSeats`](#listseats)             | Lists the tenant's seats: who holds each product and who waits, in order.                                              | Credential |
| [`mine`](#mine)                       | Returns the caller's own licenses in the tenant, waiting-list places included, and the feature keys they unlock.       | Credential |
| [`removePool`](#removepool)           | Removes a pool; the newest active seats beyond the remaining capacity move to the waiting list.                        | Credential |
| [`retireProduct`](#retireproduct)     | Retires a product for good, releasing its seats in every tenant.                                                       | Credential |
| [`unassign`](#unassign)               | Removes an assignment; seats it carried are released unless another assignment still covers the person.                | Credential |
| [`updatePool`](#updatepool)           | Changes a pool's quantity, its end, or its note; seats follow the new capacity at once.                                | Credential |
| [`updateProduct`](#updateproduct)     | Renames a product, changes its description, or replaces its feature keys; the key never changes.                       | Credential |
| [`usage`](#usage)                     | Reports, per product the tenant uses, its capacity and how many seats are active, waiting, available, and reclaimable. | Credential |

## addPool [#addpool]

Gives a tenant seats of a product: a quantity, optionally from a start until an end.

**HTTP:** `POST /api/iam/licenses/addPool` (requires a credential) · **Browser client:** `client.licenses.addPool()`

* **Permission:** `iam:licenses:manage` on `iam/licenses/pools` in the product's defining tenant; for a platform
  product, a root administrator.
* **Audited as:** `iam:licenses:manage` (where the permission was checked), `license:pool-add` in the receiving
  tenant (metadata: `poolId`, `productId`, `productKey`, `quantity`, `startsAt`, `endsAt`, `source`,
  `subscriptionId`), and the seat changes.
* **Errors:** `ACCESS_DENIED` for anyone but the defining tenant's managers (the receiving tenant's own
  administrators included); `INVALID_INPUT` for a `quantity` outside 1 to 1,000,000, an `endsAt` that is not in the
  future and after `startsAt`, a note over 512 characters, or a `tenantId` that is neither the defining tenant nor
  below it; `NOT_FOUND` for an unknown product or a `subscriptionId` that is not a subscription of the receiving tenant
  or an enclosing tenant; `INVALID_TRANSITION` (409) for a retired product; `LIMIT_EXCEEDED` (409) past 100 pools of the
  product in the tenant; `INVARIANT_VIOLATION` when the seats it activates would break an enforced invariant of the
  receiving tenant.

`tenantId` is the tenant that receives the seats. A pool counts while it is live: from `startsAt` (or its creation)
until `endsAt` (or indefinitely); one that starts later adds nothing until the hourly reconcile after its start.
Waiting people take the new seats in order, and the result carries the pool and those `seats`. `subscriptionId`
records which billing subscription paid for the seats (`source: 'subscription'`); nothing is metered.

```ts
await iam.api.licenses.addPool(credential, {
  tenantId,
  productId,
  quantity: 25,
  endsAt: Date.parse('2027-01-01T00:00:00Z'),
  note: 'PO 2026-114',
});
```

```ts title="Signature"
iam.api.licenses.addPool(
  credential: CredentialInput,
  input: {
    tenantId: string;
    productId: string;
    quantity: number;
    startsAt?: number;
    endsAt?: number;
    note?: string;
    subscriptionId?: string;
  },
): Promise<LicensePoolResult>
```

## assign [#assign]

Gives a product to a person or a group of the tenant; the person, or each live member of the group, claims a seat.

**HTTP:** `POST /api/iam/licenses/assign` (requires a credential) · **Browser client:** `client.licenses.assign()`

* **Permission:** `iam:licenses:assign` on `iam/licenses/assignments`.
* **Audited as:** `iam:licenses:assign`, `license:assign` on the person or group (metadata: `assignmentId`,
  `productId`, `productKey`, `subjectType`), and the seat changes.
* **Errors:** `INVALID_INPUT` for a `subjectType` other than `identity` or `group`; `NOT_FOUND` when the product is
  not visible to the tenant or the person or group is not in it (deleted people included); `CONFLICT` (409) when the
  product is already assigned to the subject; `INVALID_TRANSITION` (409) for a retired product; `LIMIT_EXCEEDED` (409)
  past 10,000 assignments of the product in the tenant; `INVARIANT_VIOLATION` when the seats it activates would break
  an enforced invariant.

Claimants take an active seat while capacity lasts and join the waiting list otherwise; the result carries the
`assignment` and those `seats`. A person already holding the product another way keeps their one seat, which gains a
source. Disabled people may be assigned and claim a seat when they are active again. To license a team, assign its
backing group: the team's members and those of the teams below it claim seats.

```ts
const { assignment, seats } = await iam.api.licenses.assign(credential, {
  tenantId,
  productId,
  subjectType: 'group',
  subjectId: designGroupId,
});
```

```ts title="Signature"
iam.api.licenses.assign(
  credential: CredentialInput,
  input: {
    tenantId: string;
    productId: string;
    subjectType: LicenseSubjectType;
    subjectId: string;
  },
): Promise<LicenseAssignResult>
```

## assignMany [#assignmany]

Gives a product to up to 100 people in one transaction.

**HTTP:** `POST /api/iam/licenses/assignMany` (requires a credential) · **Browser client:** `client.licenses.assignMany()`

* **Permission:** `iam:licenses:assign` on `iam/licenses/assignments`.
* **Audited as:** `iam:licenses:assign`, `license:assign` for each person assigned, and the seat changes.
* **Errors:** `INVALID_INPUT` for an empty list or more than 100 `identityIds`; `NOT_FOUND` when any person is not in
  the tenant, which assigns nobody; `INVALID_TRANSITION` (409) for a retired product; `LIMIT_EXCEEDED` (409) past 10,000
  assignments; `INVARIANT_VIOLATION` as for `assign`.

Duplicates in the list are ignored, and people who already hold a direct assignment of the product are returned in
`skipped` instead of failing the call. The result is `{ assigned, skipped, seats }`; with nobody new, nothing is
reconciled and `seats` is empty.

```ts title="Signature"
iam.api.licenses.assignMany(
  credential: CredentialInput,
  input: { tenantId: string; productId: string; identityIds: string[] },
): Promise<LicenseAssignManyResult>
```

## configure [#configure]

Turns inactive-seat reclaim and waiting-list email on or off for the tenant.

**HTTP:** `POST /api/iam/licenses/configure` (requires a credential) · **Browser client:** `client.licenses.configure()`

* **Permission:** `iam:licenses:manage` on `iam/licenses/settings`.
* **Audited as:** `iam:licenses:manage` and `license:settings` (metadata: `reclaimAfterDays`, `notifyWaiting`,
  `previousReclaimAfterDays`, `previousNotifyWaiting`).
* **Errors:** `INVALID_INPUT` with nothing to change, for a `reclaimAfterDays` that is not a whole number from 7 to
  365 (or `null`), or a `notifyWaiting` that is not a boolean; `INVALID_TRANSITION` (409) for a deleted tenant.

`reclaimAfterDays` makes the daily job `iam.licenses.reclaim` remove the direct assignments of people with no sign-in
and no use of their own sessions or API keys for that many days (assignments made within the period are left alone,
and seats held through groups are never reclaimed); `null` turns it off, the default. `notifyWaiting` emails people
(active users with an address, through the outbox) when they join a waiting list (`license-waiting`) and when their
waiting seat becomes active (`license-activated`). Fields left out keep their value. The result is the settings as
[`getSettings`](#getsettings) returns them.

```ts title="Signature"
iam.api.licenses.configure(
  credential: CredentialInput,
  input: {
    tenantId: string;
    reclaimAfterDays?: number | null;
    notifyWaiting?: boolean;
  },
): Promise<LicenseSettingsView>
```

## createProduct [#createproduct]

Defines a product: a permanent key, a name, and the feature keys a seat unlocks.

**HTTP:** `POST /api/iam/licenses/createProduct` (requires a credential) · **Browser client:** `client.licenses.createProduct()`

* **Permission:** `iam:licenses:manage` on `iam/licenses/products`, and a recent sign-in; on the root tenant (a
  platform product), a root administrator.
* **Audited as:** `iam:licenses:manage` and `license:product-create` (metadata: `productId`, `key`, `name`,
  `description`, `featureKeys`, `status`).
* **Errors:** `INVALID_INPUT` for a key that is not 1 to 64 lowercase letters, digits, dots, underscores, or hyphens
  starting with a letter or digit, a missing name or one over 100 characters, a description over 512 characters, or
  `featureKeys` that is not a list of at most 50 valid feature keys; `CONFLICT` (409) when the tenant or an enclosing
  tenant (the platform included) already uses the key; `LIMIT_EXCEEDED` (409) past 200 products in the tenant;
  `ACCESS_DENIED` for a platform product without root; `INVALID_TRANSITION` (409) for a deleted tenant;
  `RECENT_AUTH_REQUIRED`.

Feature keys use the flag-key syntax (lowercase letters and digits joined by `-`, `_`, or `.`, starting with a letter)
and are deduplicated and sorted; they need not name existing feature flags. The product starts `active` with no seats:
add capacity with [`addPool`](#addpool) and hand it out with [`assign`](#assign). The result is the product as
[`getProduct`](#getproduct) returns it.

```ts
const pro = await iam.api.licenses.createProduct(credential, {
  tenantId,
  key: 'pro',
  name: 'Pro',
  description: 'Exports, SSO and the audit log',
  featureKeys: ['exports', 'sso', 'audit-log'],
});
```

```ts title="Signature"
iam.api.licenses.createProduct(
  credential: CredentialInput,
  input: {
    tenantId: string;
    key: string;
    name: string;
    description?: string;
    featureKeys?: string[];
  },
): Promise<LicenseProductView>
```

## getProduct [#getproduct]

Returns one product visible to the tenant, with where it is defined and whether it is shadowed.

**HTTP:** `POST /api/iam/licenses/getProduct` (requires a credential) · **Browser client:** `client.licenses.getProduct()`

* **Permission:** `iam:licenses:read` on `iam/licenses/products/{id}`.
* **Audited as:** `iam:licenses:read`.
* **Errors:** `NOT_FOUND` when the product is neither the tenant's nor an enclosing tenant's.

The view carries the product's fields and `scope` (`platform` for the root tenant's products, else `tenant`),
`definedBy` (the defining tenant), `definedHere` (whether this tenant defines it and so may change it), and `shadowed`
(an enclosing tenant defines the same key too, and its product wins: this one's seats grant nothing).

```ts title="Signature"
iam.api.licenses.getProduct(
  credential: CredentialInput,
  input: { tenantId: string; productId: string },
): Promise<LicenseProductView>
```

## getSettings [#getsettings]

Returns the tenant's license settings, with the defaults when they were never configured.

**HTTP:** `POST /api/iam/licenses/getSettings` (requires a credential) · **Browser client:** `client.licenses.getSettings()`

* **Permission:** `iam:licenses:read` on `iam/licenses/settings`.
* **Audited as:** `iam:licenses:read`.

The result is `tenantId`, `notifyWaiting` (false by default), `reclaimAfterDays` when reclaim is on, and
`updatedAt`/`updatedBy` once [`configure`](#configure) has run.

```ts title="Signature"
iam.api.licenses.getSettings(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<LicenseSettingsView>
```

## listAssignments [#listassignments]

Lists the tenant's assignments, newest first, optionally of one product or subject.

**HTTP:** `POST /api/iam/licenses/listAssignments` (requires a credential) · **Browser client:** `client.licenses.listAssignments()`

* **Permission:** `iam:licenses:read` on `iam/licenses/assignments`.
* **Audited as:** `iam:licenses:read`.
* **Errors:** `INVALID_INPUT` for an unknown `subjectType`, or a `limit` outside 1 to 1000 or a negative `offset`.

Each assignment carries the product's key and name and the subject's name (a person's email too); a subject that no
longer exists shows its ID. Filter with `productId`, `subjectType`, and `subjectId`, and page with `limit` (100 by
default) and `offset`. The result is `{ assignments, total }`.

```ts title="Signature"
iam.api.licenses.listAssignments(
  credential: CredentialInput,
  input: {
    tenantId: string;
    productId?: string;
    subjectType?: LicenseSubjectType;
    subjectId?: string;
    limit?: number;
    offset?: number;
  },
): Promise<{ assignments: LicenseAssignmentView[]; total: number }>
```

## listPools [#listpools]

Lists the tenant's pools and, for products it defines, the pools it granted to tenants below it, newest first.

**HTTP:** `POST /api/iam/licenses/listPools` (requires a credential) · **Browser client:** `client.licenses.listPools()`

* **Permission:** `iam:licenses:read` on `iam/licenses/pools`.
* **Audited as:** `iam:licenses:read`.
* **Errors:** `INVALID_INPUT` for a non-boolean `includeEnded`, or a `limit` outside 1 to 1000 or a negative `offset`.

Each pool carries its receiving tenant's name, the product's key and name, and `live` (started and not ended). Ended
pools are left out unless `includeEnded: true`; they are kept 400 days after their end as purchase history, then swept.
Filter with `productId`. The result is `{ pools, total }`.

```ts title="Signature"
iam.api.licenses.listPools(
  credential: CredentialInput,
  input: {
    tenantId: string;
    productId?: string;
    includeEnded?: boolean;
    limit?: number;
    offset?: number;
  },
): Promise<{ pools: LicensePoolView[]; total: number }>
```

## listProducts [#listproducts]

Lists the products the tenant can use, its own and its ancestors', sorted by key.

**HTTP:** `POST /api/iam/licenses/listProducts` (requires a credential) · **Browser client:** `client.licenses.listProducts()`

* **Permission:** `iam:licenses:read` on `iam/licenses/products`.
* **Audited as:** `iam:licenses:read`.
* **Errors:** `INVALID_INPUT` for a `status` other than `active` or `retired`.

Platform products (the root tenant's) appear in every tenant. When two products share a key, the enclosing tenant's
comes first and the other is marked `shadowed`; only the enclosing product's seats grant the key. The result is
`{ tenantId, products }`.

```ts title="Signature"
iam.api.licenses.listProducts(
  credential: CredentialInput,
  input: { tenantId: string; status?: LicenseProductStatus },
): Promise<{ tenantId: string; products: LicenseProductView[] }>
```

## listSeats [#listseats]

Lists the tenant's seats: who holds each product and who waits, in order.

**HTTP:** `POST /api/iam/licenses/listSeats` (requires a credential) · **Browser client:** `client.licenses.listSeats()`

* **Permission:** `iam:licenses:read` on `iam/licenses/seats`.
* **Audited as:** `iam:licenses:read`.
* **Errors:** `INVALID_INPUT` for a `status` other than `active` or `waiting`, or a `limit` outside 1 to 1000 or a
  negative `offset`.

Seats are sorted by product key, active before waiting, then by seniority. Each carries the holder's name, email, and
kind, `status`, `position` on the waiting list (from 1, waiting seats only), its sources (`direct`, and the assigned
`groupIds` the person is a member of), `assignedAt` (seniority), `activatedAt`, and `lastActivityAt` (as the last
reclaim pass saw it). Filter with `productId`, `status`, and `identityId`. The result is `{ seats, total }`.

```ts
const { seats } = await iam.api.licenses.listSeats(credential, { tenantId, productId, status: 'waiting' });
```

```ts title="Signature"
iam.api.licenses.listSeats(
  credential: CredentialInput,
  input: {
    tenantId: string;
    productId?: string;
    status?: LicenseSeatStatus;
    identityId?: string;
    limit?: number;
    offset?: number;
  },
): Promise<{ seats: LicenseSeatView[]; total: number }>
```

## mine [#mine]

Returns the caller's own licenses in the tenant, waiting-list places included, and the feature keys they unlock.

**HTTP:** `POST /api/iam/licenses/mine` (requires a credential) · **Browser client:** `client.licenses.mine()`

* **Permission:** None beyond a credential of the tenant (a session or an API key); root administrators too.
* **Audited as:** Not audited; it only reads.
* **Errors:** `ACCESS_DENIED` for a credential of another tenant; `UNAUTHENTICATED` without a valid credential.

Each license names the product (`productId`, `key`, `name`, `description`), its `status`, the `position` while waiting,
`featureKeys` (none while waiting), `platform` (whether the root tenant defines the product) and `definedBy` (the
defining tenant). `featureKeys` at the top level is the union over active seats of **platform** products, the paid
entitlements no tenant can mint for itself (what `iam.licenses.features` returns by default); `allFeatureKeys` also
includes products the caller's tenant or a tenant above it defines, so filter `licenses` by `definedBy` before treating
those as entitlements. Retired products and seats held only through a lapsed membership are left out. Assumed roles,
and root administrators reading another tenant, get empty lists. It backs a "your plan" page and client-side feature
toggles; guard the server side with `principal.licenses` too.

```ts
const { licenses, featureKeys, allFeatureKeys } = await client.licenses.mine({ tenantId });
```

```ts title="Signature"
iam.api.licenses.mine(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<MyLicenses>
```

## removePool [#removepool]

Removes a pool; the newest active seats beyond the remaining capacity move to the waiting list.

**HTTP:** `POST /api/iam/licenses/removePool` (requires a credential) · **Browser client:** `client.licenses.removePool()`

* **Permission:** `iam:licenses:manage` on `iam/licenses/pools/{id}` in the product's defining tenant; for a platform
  product, a root administrator.
* **Audited as:** `iam:licenses:manage`, `license:pool-remove` in the receiving tenant (the pool's metadata), and the
  seat changes.
* **Errors:** `NOT_FOUND` when the pool is not the tenant's; `ACCESS_DENIED` for anyone but the defining tenant's
  managers; `INVALID_TRANSITION` (409) for a retired product; `INVARIANT_VIOLATION` when the change would break an
  enforced invariant.

`tenantId` is the tenant the pool belongs to. To keep the purchase history, end the pool with
[`updatePool`](#updatepool) instead. The defining tenant can also remove a pool of a tenant that has since moved out
from under it (`tenants.reparent`), whose seats of the product no longer count. The result is
`{ removed: true, seats }`.

```ts title="Signature"
iam.api.licenses.removePool(
  credential: CredentialInput,
  input: { tenantId: string; poolId: string },
): Promise<{ removed: true; seats: LicenseSeatChange[] }>
```

## retireProduct [#retireproduct]

Retires a product for good, releasing its seats in every tenant.

**HTTP:** `POST /api/iam/licenses/retireProduct` (requires a credential) · **Browser client:** `client.licenses.retireProduct()`

* **Permission:** `iam:licenses:manage` on `iam/licenses/products/{id}` in the defining tenant, and a recent sign-in;
  for a platform product, a root administrator.
* **Audited as:** `iam:licenses:manage`, `license:product-retire` in the defining tenant (metadata: `productId`, `key`,
  `releasedSeats`, `tenants`), and `license:seat-release` (reason `product-retired`) in each seat's tenant.
* **Errors:** `INVALID_INPUT` when an enclosing tenant defines the product; `NOT_FOUND` when it is not visible;
  `INVALID_TRANSITION` (409) when it is already retired; `ACCESS_DENIED` for a platform product without root;
  `RECENT_AUTH_REQUIRED`.

The product stays listed as `retired` and its key stays taken. Its pools and assignments remain as read-only history:
assigning it or changing its pools answers `INVALID_TRANSITION`, and no job gives it seats again. Unassigning still
works for tidying up. Only the defining tenant's enforced invariants are checked. The result is `{ product, seats }`.

```ts title="Signature"
iam.api.licenses.retireProduct(
  credential: CredentialInput,
  input: { tenantId: string; productId: string },
): Promise<LicenseProductRetireResult>
```

## unassign [#unassign]

Removes an assignment; seats it carried are released unless another assignment still covers the person.

**HTTP:** `POST /api/iam/licenses/unassign` (requires a credential) · **Browser client:** `client.licenses.unassign()`

* **Permission:** `iam:licenses:assign` on `iam/licenses/assignments`.
* **Audited as:** `iam:licenses:assign`, `license:unassign` on the person or group, and the seat changes.
* **Errors:** `NOT_FOUND` when no such assignment exists in the tenant; `INVALID_INPUT` without an `assignmentId` or
  a complete `productId`, `subjectType`, and `subjectId`; `INVARIANT_VIOLATION` when the seats it moves would break an
  enforced invariant.

Name the assignment by `assignmentId`, or by product and subject. Waiting people move up into the freed seats. The
result is `{ removed: true, seats }`.

```ts
await iam.api.licenses.unassign(credential, {
  tenantId,
  productId,
  subjectType: 'identity',
  subjectId: aliceId,
});
```

```ts title="Signature"
iam.api.licenses.unassign(
  credential: CredentialInput,
  input: {
    tenantId: string;
    assignmentId?: string;
    productId?: string;
    subjectType?: LicenseSubjectType;
    subjectId?: string;
  },
): Promise<{ removed: true; seats: LicenseSeatChange[] }>
```

## updatePool [#updatepool]

Changes a pool's quantity, its end, or its note; seats follow the new capacity at once.

**HTTP:** `POST /api/iam/licenses/updatePool` (requires a credential) · **Browser client:** `client.licenses.updatePool()`

* **Permission:** `iam:licenses:manage` on `iam/licenses/pools/{id}` in the product's defining tenant; for a platform
  product, a root administrator.
* **Audited as:** `iam:licenses:manage`, `license:pool-update` in the receiving tenant (metadata: `before`, `after`),
  and the seat changes.
* **Errors:** `INVALID_INPUT` with nothing to change, for a `quantity` outside 1 to 1,000,000, or an `endsAt` not
  after `startsAt`; `NOT_FOUND` when the pool is not the tenant's; `ACCESS_DENIED` for anyone but the defining tenant's
  managers; `INVALID_TRANSITION` (409) for a retired product; `INVARIANT_VIOLATION` as for `addPool`.

`endsAt: null` makes the pool open-ended and a past time ends it now; `note: null` clears the note. A capacity cut
never fails: the newest active seats move to the head of the waiting list. The result carries the pool and the
`seats` that moved.

```ts title="Signature"
iam.api.licenses.updatePool(
  credential: CredentialInput,
  input: {
    tenantId: string;
    poolId: string;
    quantity?: number;
    endsAt?: number | null;
    note?: string | null;
  },
): Promise<LicensePoolResult>
```

## updateProduct [#updateproduct]

Renames a product, changes its description, or replaces its feature keys; the key never changes.

**HTTP:** `POST /api/iam/licenses/updateProduct` (requires a credential) · **Browser client:** `client.licenses.updateProduct()`

* **Permission:** `iam:licenses:manage` on `iam/licenses/products/{id}` in the defining tenant; for a platform
  product, a root administrator.
* **Audited as:** `iam:licenses:manage` and `license:product-update` (metadata: `before`, `after`).
* **Errors:** `INVALID_INPUT` with nothing to change, for an invalid name, description, or feature key, or when an
  enclosing tenant defines the product (change it there); `NOT_FOUND` when it is not visible; `ACCESS_DENIED` for a
  platform product without root.

`description: null` clears the description, and `featureKeys` replaces the whole list. New feature keys apply to every
active seat at once: in [`mine`](#mine) (`featureKeys` for a platform product, `allFeatureKeys` always) and in
`iam.licenses.features` for a platform product, or for a tenant's product where the caller trusts that tenant
(`trustedTenantId`). No recent sign-in is needed.

```ts title="Signature"
iam.api.licenses.updateProduct(
  credential: CredentialInput,
  input: {
    tenantId: string;
    productId: string;
    name?: string;
    description?: string | null;
    featureKeys?: string[];
  },
): Promise<LicenseProductView>
```

## usage [#usage]

Reports, per product the tenant uses, its capacity and how many seats are active, waiting, available, and reclaimable.

**HTTP:** `POST /api/iam/licenses/usage` (requires a credential) · **Browser client:** `client.licenses.usage()`

* **Permission:** `iam:licenses:read` on `iam/licenses/seats`.
* **Audited as:** `iam:licenses:read`.

A product is listed when the tenant has pools, seats, or assignments of it, or defines it. Each entry has `capacity`
(from live pools), `active`, `waiting`, `available`, live `pools`, `assignments`, and with reclaim on,
`reclaimable` (active seats older than `reclaimAfterDays` whose holders have been inactive that long) and
`reclaimableThroughGroups` (the part held only through groups, which the reclaim job leaves: remove those people from
the group). The result is `{ tenantId, reclaimAfterDays, products }`.

```ts title="Signature"
iam.api.licenses.usage(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<LicenseUsageReport>
```
