BetterIAM
Server 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

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.licenses lists 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, and policies.test defaults the key to an empty list.
  • Birthright packages. Package rules test identity.licenses with 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.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 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, 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.

Methods18
Serveriam.api.licenses
Clientclient.licenses
HTTPPOST /api/iam/licenses/*
MethodWhat it doesAccess
addPoolGives a tenant seats of a product: a quantity, optionally from a start until an end.Credential
assignGives a product to a person or a group of the tenant; the person, or each live member of the group, claims a seat.Credential
assignManyGives a product to up to 100 people in one transaction.Credential
configureTurns inactive-seat reclaim and waiting-list email on or off for the tenant.Credential
createProductDefines a product: a permanent key, a name, and the feature keys a seat unlocks.Credential
getProductReturns one product visible to the tenant, with where it is defined and whether it is shadowed.Credential
getSettingsReturns the tenant's license settings, with the defaults when they were never configured.Credential
listAssignmentsLists the tenant's assignments, newest first, optionally of one product or subject.Credential
listPoolsLists the tenant's pools and, for products it defines, the pools it granted to tenants below it, newest first.Credential
listProductsLists the products the tenant can use, its own and its ancestors', sorted by key.Credential
listSeatsLists the tenant's seats: who holds each product and who waits, in order.Credential
mineReturns the caller's own licenses in the tenant, waiting-list places included, and the feature keys they unlock.Credential
removePoolRemoves a pool; the newest active seats beyond the remaining capacity move to the waiting list.Credential
retireProductRetires a product for good, releasing its seats in every tenant.Credential
unassignRemoves an assignment; seats it carried are released unless another assignment still covers the person.Credential
updatePoolChanges a pool's quantity, its end, or its note; seats follow the new capacity at once.Credential
updateProductRenames a product, changes its description, or replaces its feature keys; the key never changes.Credential
usageReports, 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.

POST/api/iam/licenses/addPool
client.licenses.addPool()Credential

Used inLicenses

  • 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.

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

Prop

Type

Returns

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
}'
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

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

POST/api/iam/licenses/assign
client.licenses.assign()Credential

Used inLicenses

  • 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.

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

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/assignMany
client.licenses.assignMany()Credential

Used inLicenses

  • 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.

Input

Prop

Type

Returns

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>"
  ]
}'
Signature
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.

POST/api/iam/licenses/configure
client.licenses.configure()Credential

Used inLicenses

  • 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 returns them.

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/createProduct
client.licenses.createProduct()Credential

Used inLicenses

  • 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 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'],
});
Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/getProduct
client.licenses.getProduct()Credential
  • 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).

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/getSettings
client.licenses.getSettings()Credential
  • 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 has run.

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/listAssignments
client.licenses.listAssignments()Credential
  • 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 }.

Input

Prop

Type

Returns

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>"
}'
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

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

POST/api/iam/licenses/listPools
client.licenses.listPools()Credential
  • 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 }.

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/listProducts
client.licenses.listProducts()Credential
  • 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 }.

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/listSeats
client.licenses.listSeats()Credential

Used inLicenses

  • 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 }.

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

Prop

Type

Returns

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>"
}'
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

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

POST/api/iam/licenses/mine
client.licenses.mine()Credential

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_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.

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

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/removePool
client.licenses.removePool()Credential
  • 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 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 }.

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/retireProduct
client.licenses.retireProduct()Credential
  • 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 }.

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/unassign
client.licenses.unassign()Credential
  • 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 }.

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

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/updatePool
client.licenses.updatePool()Credential
  • 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.

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/updateProduct
client.licenses.updateProduct()Credential
  • 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 (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.

Input

Prop

Type

Returns

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>"
}'
Signature
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.

POST/api/iam/licenses/usage
client.licenses.usage()Credential

Used inLicenses

  • 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 }.

Input

Prop

Type

Returns

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>"
}'
Signature
iam.api.licenses.usage(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<LicenseUsageReport>

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page