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
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
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. 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
- Policies.
principal.licenseslists the keys of the products the principal holds an active seat for in the decision's tenant (see 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, andpolicies.testdefaults the key to an empty list. - Birthright packages. Package rules test
identity.licenseswith product keys (see 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.featuresreturns the feature keys a person's active seats unlock for server code, counting only platform products (defined by the root tenant) unless you passtrustedTenantId, so no tenant can mint a paid feature for itself by defining a product that lists its key.minereturns them to the person, with the platform's keys infeatureKeysand every product's inallFeatureKeys.
Assigning, unassigning, and pool changes run the receiving tenant's enforced
access invariants, so a seat change that newly breaks one
fails with INVARIANT_VIOLATION.
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 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 | Gives a tenant seats of a product: a quantity, optionally from a start until an end. | Credential |
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 | Gives a product to up to 100 people in one transaction. | Credential |
configure | Turns inactive-seat reclaim and waiting-list email on or off for the tenant. | Credential |
createProduct | Defines a product: a permanent key, a name, and the feature keys a seat unlocks. | Credential |
getProduct | Returns one product visible to the tenant, with where it is defined and whether it is shadowed. | Credential |
getSettings | Returns the tenant's license settings, with the defaults when they were never configured. | Credential |
listAssignments | Lists the tenant's assignments, newest first, optionally of one product or subject. | Credential |
listPools | Lists the tenant's pools and, for products it defines, the pools it granted to tenants below it, newest first. | Credential |
listProducts | Lists the products the tenant can use, its own and its ancestors', sorted by key. | Credential |
listSeats | Lists the tenant's seats: who holds each product and who waits, in order. | Credential |
mine | Returns the caller's own licenses in the tenant, waiting-list places included, and the feature keys they unlock. | Credential |
removePool | Removes a pool; the newest active seats beyond the remaining capacity move to the waiting list. | Credential |
retireProduct | Retires a product for good, releasing its seats in every tenant. | Credential |
unassign | Removes an assignment; seats it carried are released unless another assignment still covers the person. | Credential |
updatePool | Changes a pool's quantity, its end, or its note; seats follow the new capacity at once. | Credential |
updateProduct | Renames a product, changes its description, or replaces its feature keys; the key never changes. | Credential |
usage | Reports, per product the tenant uses, its capacity and how many seats are active, waiting, available, and reclaimable. | Credential |
addPool
Gives a tenant seats of a product: a quantity, optionally from a start until an end.
Used inLicenses
- Permission:
iam:licenses:manageoniam/licenses/poolsin the product's defining tenant; for a platform product, a root administrator. - Audited as:
iam:licenses:manage(where the permission was checked),license:pool-addin the receiving tenant (metadata:poolId,productId,productKey,quantity,startsAt,endsAt,source,subscriptionId), and the seat changes. - Errors:
ACCESS_DENIEDfor anyone but the defining tenant's managers (the receiving tenant's own administrators included);INVALID_INPUTfor aquantityoutside 1 to 1,000,000, anendsAtthat is not in the future and afterstartsAt, a note over 512 characters, or atenantIdthat is neither the defining tenant nor below it;NOT_FOUNDfor an unknown product or asubscriptionIdthat 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_VIOLATIONwhen 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.
await iam.api.licenses.addPool(credential, {
tenantId,
productId,
quantity: 25,
endsAt: Date.parse('2027-01-01T00:00:00Z'),
note: 'PO 2026-114',
});Prop
Type
A LicensePoolResult object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/addPool" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"productId": "<productId>",
"quantity": 1
}'iam.api.licenses.addPool(
credential: CredentialInput,
input: {
tenantId: string;
productId: string;
quantity: number;
startsAt?: number;
endsAt?: number;
note?: string;
subscriptionId?: string;
},
): Promise<LicensePoolResult>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.
Used inLicenses
- Permission:
iam:licenses:assignoniam/licenses/assignments. - Audited as:
iam:licenses:assign,license:assignon the person or group (metadata:assignmentId,productId,productKey,subjectType), and the seat changes. - Errors:
INVALID_INPUTfor asubjectTypeother thanidentityorgroup;NOT_FOUNDwhen 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_VIOLATIONwhen 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.
const { assignment, seats } = await iam.api.licenses.assign(credential, {
tenantId,
productId,
subjectType: 'group',
subjectId: designGroupId,
});Prop
Type
A LicenseAssignResult object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/assign" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"productId": "<productId>",
"subjectType": "identity",
"subjectId": "<subjectId>"
}'iam.api.licenses.assign(
credential: CredentialInput,
input: {
tenantId: string;
productId: string;
subjectType: LicenseSubjectType;
subjectId: string;
},
): Promise<LicenseAssignResult>assignMany
Gives a product to up to 100 people in one transaction.
Used inLicenses
- Permission:
iam:licenses:assignoniam/licenses/assignments. - Audited as:
iam:licenses:assign,license:assignfor each person assigned, and the seat changes. - Errors:
INVALID_INPUTfor an empty list or more than 100identityIds;NOT_FOUNDwhen 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_VIOLATIONas forassign.
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.
Prop
Type
A LicenseAssignManyResult object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/assignMany" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"productId": "<productId>",
"identityIds": [
"<identityId>"
]
}'iam.api.licenses.assignMany(
credential: CredentialInput,
input: { tenantId: string; productId: string; identityIds: string[] },
): Promise<LicenseAssignManyResult>configure
Turns inactive-seat reclaim and waiting-list email on or off for the tenant.
Used inLicenses
- Permission:
iam:licenses:manageoniam/licenses/settings. - Audited as:
iam:licenses:manageandlicense:settings(metadata:reclaimAfterDays,notifyWaiting,previousReclaimAfterDays,previousNotifyWaiting). - Errors:
INVALID_INPUTwith nothing to change, for areclaimAfterDaysthat is not a whole number from 7 to 365 (ornull), or anotifyWaitingthat 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 returns them.
Prop
Type
A LicenseSettingsView object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/configure" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.licenses.configure(
credential: CredentialInput,
input: {
tenantId: string;
reclaimAfterDays?: number | null;
notifyWaiting?: boolean;
},
): Promise<LicenseSettingsView>createProduct
Defines a product: a permanent key, a name, and the feature keys a seat unlocks.
Used inLicenses
- Permission:
iam:licenses:manageoniam/licenses/products, and a recent sign-in; on the root tenant (a platform product), a root administrator. - Audited as:
iam:licenses:manageandlicense:product-create(metadata:productId,key,name,description,featureKeys,status). - Errors:
INVALID_INPUTfor 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, orfeatureKeysthat 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_DENIEDfor 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 and hand it out with assign. The result is the product as
getProduct returns it.
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'],
});Prop
Type
A LicenseProductView object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/createProduct" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"key": "<key>",
"name": "<name>"
}'iam.api.licenses.createProduct(
credential: CredentialInput,
input: {
tenantId: string;
key: string;
name: string;
description?: string;
featureKeys?: string[];
},
): Promise<LicenseProductView>getProduct
Returns one product visible to the tenant, with where it is defined and whether it is shadowed.
- Permission:
iam:licenses:readoniam/licenses/products/{id}. - Audited as:
iam:licenses:read. - Errors:
NOT_FOUNDwhen 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).
Prop
Type
A LicenseProductView object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/getProduct" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"productId": "<productId>"
}'iam.api.licenses.getProduct(
credential: CredentialInput,
input: { tenantId: string; productId: string },
): Promise<LicenseProductView>getSettings
Returns the tenant's license settings, with the defaults when they were never configured.
- Permission:
iam:licenses:readoniam/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 has run.
Prop
Type
A LicenseSettingsView object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/getSettings" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.licenses.getSettings(
credential: CredentialInput,
input: { tenantId: string },
): Promise<LicenseSettingsView>listAssignments
Lists the tenant's assignments, newest first, optionally of one product or subject.
- Permission:
iam:licenses:readoniam/licenses/assignments. - Audited as:
iam:licenses:read. - Errors:
INVALID_INPUTfor an unknownsubjectType, or alimitoutside 1 to 1000 or a negativeoffset.
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 }.
Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/listAssignments" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'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
Lists the tenant's pools and, for products it defines, the pools it granted to tenants below it, newest first.
- Permission:
iam:licenses:readoniam/licenses/pools. - Audited as:
iam:licenses:read. - Errors:
INVALID_INPUTfor a non-booleanincludeEnded, or alimitoutside 1 to 1000 or a negativeoffset.
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 }.
Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/listPools" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.licenses.listPools(
credential: CredentialInput,
input: {
tenantId: string;
productId?: string;
includeEnded?: boolean;
limit?: number;
offset?: number;
},
): Promise<{ pools: LicensePoolView[]; total: number }>listProducts
Lists the products the tenant can use, its own and its ancestors', sorted by key.
- Permission:
iam:licenses:readoniam/licenses/products. - Audited as:
iam:licenses:read. - Errors:
INVALID_INPUTfor astatusother thanactiveorretired.
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 }.
Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/listProducts" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.licenses.listProducts(
credential: CredentialInput,
input: { tenantId: string; status?: LicenseProductStatus },
): Promise<{ tenantId: string; products: LicenseProductView[] }>listSeats
Lists the tenant's seats: who holds each product and who waits, in order.
Used inLicenses
- Permission:
iam:licenses:readoniam/licenses/seats. - Audited as:
iam:licenses:read. - Errors:
INVALID_INPUTfor astatusother thanactiveorwaiting, or alimitoutside 1 to 1000 or a negativeoffset.
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 }.
const { seats } = await iam.api.licenses.listSeats(credential, { tenantId, productId, status: 'waiting' });Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/listSeats" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'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
Returns the caller's own licenses in the tenant, waiting-list places included, and the feature keys they unlock.
Used inLicenses
- 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_DENIEDfor a credential of another tenant;UNAUTHENTICATEDwithout 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.
const { licenses, featureKeys, allFeatureKeys } = await client.licenses.mine({ tenantId });Prop
Type
A MyLicenses object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/mine" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.licenses.mine(
credential: CredentialInput,
input: { tenantId: string },
): Promise<MyLicenses>removePool
Removes a pool; the newest active seats beyond the remaining capacity move to the waiting list.
- Permission:
iam:licenses:manageoniam/licenses/pools/{id}in the product's defining tenant; for a platform product, a root administrator. - Audited as:
iam:licenses:manage,license:pool-removein the receiving tenant (the pool's metadata), and the seat changes. - Errors:
NOT_FOUNDwhen the pool is not the tenant's;ACCESS_DENIEDfor anyone but the defining tenant's managers;INVALID_TRANSITION(409) for a retired product;INVARIANT_VIOLATIONwhen 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 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 }.
Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/removePool" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"poolId": "<poolId>"
}'iam.api.licenses.removePool(
credential: CredentialInput,
input: { tenantId: string; poolId: string },
): Promise<{ removed: true; seats: LicenseSeatChange[] }>retireProduct
Retires a product for good, releasing its seats in every tenant.
- Permission:
iam:licenses:manageoniam/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-retirein the defining tenant (metadata:productId,key,releasedSeats,tenants), andlicense:seat-release(reasonproduct-retired) in each seat's tenant. - Errors:
INVALID_INPUTwhen an enclosing tenant defines the product;NOT_FOUNDwhen it is not visible;INVALID_TRANSITION(409) when it is already retired;ACCESS_DENIEDfor 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 }.
Prop
Type
A LicenseProductRetireResult object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/retireProduct" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"productId": "<productId>"
}'iam.api.licenses.retireProduct(
credential: CredentialInput,
input: { tenantId: string; productId: string },
): Promise<LicenseProductRetireResult>unassign
Removes an assignment; seats it carried are released unless another assignment still covers the person.
- Permission:
iam:licenses:assignoniam/licenses/assignments. - Audited as:
iam:licenses:assign,license:unassignon the person or group, and the seat changes. - Errors:
NOT_FOUNDwhen no such assignment exists in the tenant;INVALID_INPUTwithout anassignmentIdor a completeproductId,subjectType, andsubjectId;INVARIANT_VIOLATIONwhen 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 }.
await iam.api.licenses.unassign(credential, {
tenantId,
productId,
subjectType: 'identity',
subjectId: aliceId,
});Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/unassign" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.licenses.unassign(
credential: CredentialInput,
input: {
tenantId: string;
assignmentId?: string;
productId?: string;
subjectType?: LicenseSubjectType;
subjectId?: string;
},
): Promise<{ removed: true; seats: LicenseSeatChange[] }>updatePool
Changes a pool's quantity, its end, or its note; seats follow the new capacity at once.
- Permission:
iam:licenses:manageoniam/licenses/pools/{id}in the product's defining tenant; for a platform product, a root administrator. - Audited as:
iam:licenses:manage,license:pool-updatein the receiving tenant (metadata:before,after), and the seat changes. - Errors:
INVALID_INPUTwith nothing to change, for aquantityoutside 1 to 1,000,000, or anendsAtnot afterstartsAt;NOT_FOUNDwhen the pool is not the tenant's;ACCESS_DENIEDfor anyone but the defining tenant's managers;INVALID_TRANSITION(409) for a retired product;INVARIANT_VIOLATIONas foraddPool.
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.
Prop
Type
A LicensePoolResult object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/updatePool" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"poolId": "<poolId>"
}'iam.api.licenses.updatePool(
credential: CredentialInput,
input: {
tenantId: string;
poolId: string;
quantity?: number;
endsAt?: number | null;
note?: string | null;
},
): Promise<LicensePoolResult>updateProduct
Renames a product, changes its description, or replaces its feature keys; the key never changes.
- Permission:
iam:licenses:manageoniam/licenses/products/{id}in the defining tenant; for a platform product, a root administrator. - Audited as:
iam:licenses:manageandlicense:product-update(metadata:before,after). - Errors:
INVALID_INPUTwith nothing to change, for an invalid name, description, or feature key, or when an enclosing tenant defines the product (change it there);NOT_FOUNDwhen it is not visible;ACCESS_DENIEDfor 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 (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.
Prop
Type
A LicenseProductView object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/updateProduct" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"productId": "<productId>"
}'iam.api.licenses.updateProduct(
credential: CredentialInput,
input: {
tenantId: string;
productId: string;
name?: string;
description?: string | null;
featureKeys?: string[];
},
): Promise<LicenseProductView>usage
Reports, per product the tenant uses, its capacity and how many seats are active, waiting, available, and reclaimable.
Used inLicenses
- Permission:
iam:licenses:readoniam/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 }.
Prop
Type
A LicenseUsageReport object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/licenses/usage" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.licenses.usage(
credential: CredentialInput,
input: { tenantId: string },
): Promise<LicenseUsageReport>Better IAM is created by Sean Filimon
Last updated