| name | manage-licensing |
| description | Audit, assign, and reclaim Webex licenses across the organization.
Covers license inventory, usage analysis, unused license reports, capacity checks,
bulk assignment, and license reclamation from inactive users. Also covers resource
groups and resource group memberships, which partition license assignment across
hybrid services connector clusters.
NOT for: initial user provisioning with licenses (use provision-calling) or
resource deletion (use teardown).
|
| allowed-tools | Read, Grep, Glob, Bash |
| argument-hint | ["operation — e.g. \"audit usage\"","assign licenses","reclaim unused","check capacity","move resource group"] |
Manage Licensing Workflow
Checkpoint — do NOT proceed until you can answer these:
- What API method assigns licenses to users? (Answer: PATCH
/licenses/users — not PUT. The body uses add/remove operations with license IDs and user IDs.)
- What must you include when assigning a Webex Calling license? (Answer: Either an extension or phone number — the PATCH call requires one of these for calling licenses or it will fail.)
If you cannot answer both, you skipped reading this skill. Go back and read it.
Step 1: Load references
- Read
docs/reference/admin-licensing.md for license API patterns, PATCH body schema, and error codes
- Read
docs/reference/provisioning.md § "Licenses API" (and § "Provisioning Workflow" for how license assignment ties to user creation) — it has a Table of Contents with section anchors; load only those sections, not the whole doc
- For resource group operations (Operation J) only, read
docs/reference/admin-apps-data.md § "Resource Group Assignment Flow" — skip this unless the user asks about resource groups or hybrid clusters
Mandatory --help verification: Before constructing any wxcli command, run wxcli <group> --help to verify the subcommand exists, then wxcli <group> <subcommand> --help to verify the exact flags. Do NOT rely on examples in this skill or reference docs — the CLI is auto-generated and flag names may differ from what documentation suggests.
Step 2: Verify auth token is working
Before any licensing operation, confirm the token is valid and has the required scopes.
Required scopes by operation
| Operation | Scope(s) |
|---|
| List/view licenses | Admin token (full or read-only admin) |
| Assign/remove licenses (PATCH) | Admin token (full admin) |
| List people (for cross-reference) | spark-admin:people_read |
| List resource groups / memberships | spark-admin:resource_groups_read |
| Update a resource group membership | spark-admin:resource_group_memberships_write |
Verification sequence
2a. Check token identity:
wxcli whoami
Inspect the output:
- Confirm a valid user/admin identity is returned (display name, org ID present).
- If this fails with 401 — token is expired or missing. Run
wxcli configure or check ~/.wxcli/config.json.
- If this fails with 403 — token exists but lacks permissions. You may have a user-level token instead of an admin token.
- Personal access tokens last 12 hours. Service app tokens vary by grant type.
2b. Verify admin scope by exercising the license API:
wxcli licenses list
Inspect the output:
- Success (license table returned): Admin read scope confirmed. Proceed.
- 403 Forbidden: Token is not an admin token, or the admin role is insufficient. Stop and resolve.
- 401 Unauthorized: Token is invalid or expired. Re-run
wxcli configure.
- Empty result (no licenses): Unusual — confirm org ID is correct via
wxcli whoami output.
2c. For write operations (assign/remove), verify full admin:
If the user's operation requires assignment or removal, confirm the token is a full admin token (not read-only admin). Read-only admins can list licenses but cannot PATCH. The only reliable test is to note the admin role from wxcli whoami output, or to attempt the operation and handle a 403.
Gate
Do not proceed until token confirmed valid. Both wxcli whoami and wxcli licenses list must succeed before moving to Step 3. If either fails, stop and troubleshoot auth:
- Token expired — re-run
wxcli configure with a fresh token
- Token not configured — run
wxcli configure or check ~/.wxcli/config.json
- Token lacks admin access — read-only admins can list but cannot assign/remove licenses
- Wrong org — verify the org ID in
wxcli whoami matches the target organization
Step 3: Determine the operation
Ask the user which licensing operation they need. The seven supported operations are:
| Operation | What It Does | CLI Group |
|---|
| License inventory | See all licenses and usage counts | licenses list |
| License details | Get details for a specific license | licenses show |
| Assign licenses | Add licenses to users | licenses update (PATCH with --json-body) |
| Usage audit | Find unused/unassigned licenses | licenses + people (cross-reference) |
| Reclaim licenses | Remove licenses from inactive users | licenses update + people |
| Calling license check | Quick calling-focused license list | licenses list -o json + client-side filter (no --calling-only flag) |
| Resource group assignment | See or change which hybrid cluster serves a user's licensed service | resource-groups + resource-group-memberships |
Group naming: licenses is the canonical group (generated). licenses-api still works as a deprecated alias for one release (renamed 2026-07-02).
Confirm with the user before proceeding:
- Which operation from the table above
- Target details (license type, user emails, usage threshold, etc.)
- For reclamation: criteria for "inactive" (days since last login, specific users, etc.)
Step 4: Check prerequisites
Run these checks based on the operation. Stop and report if any prerequisite fails.
For license inventory or audit:
wxcli licenses list
For license assignment:
wxcli licenses list -o json
wxcli people list --email user@example.com -o json
wxcli locations list
For license reclamation:
wxcli licenses list -o json
wxcli licenses show "LICENSE_ID" --include-assigned-to user -o json
wxcli people list -o json
Step 5: Build plan — [SHOW BEFORE EXECUTING]
Present the plan to the user and wait for approval. Never execute write operations without confirmation.
Format the plan as:
=== Licensing Plan ===
Operation: [Assign License / Reclaim License / Audit Usage / etc.]
Target:
- [specific details — license name, user emails, criteria]
Steps:
1. [First CLI command — what it does]
2. [Second CLI command — what it does]
3. Verify: [what we'll check after]
Prerequisites verified:
✓ Auth token valid (authenticated as [name])
✓ License available: [name] ([consumed]/[total])
✓ Target user exists: [email] ([person_id])
✓ [any other checks]
Proceed? [wait for user confirmation]
Step 6: Execute the licensing operation
Operation A: License Inventory
wxcli licenses list
wxcli licenses list -o json | python3.11 -c "
import json, sys
for lic in json.load(sys.stdin):
if 'calling' in lic['name'].lower():
print(f\"{lic['name']}: {lic.get('consumedUnits', 0)}/{lic.get('totalUnits', 0)}\")
"
wxcli licenses list -o json | python3.11 -c "
import json, sys
data = json.load(sys.stdin)
for lic in data:
avail = lic.get('totalUnits', 0) - lic.get('consumedUnits', 0)
print(f\"{lic['name']}: {lic.get('consumedUnits', 0)}/{lic.get('totalUnits', 0)} (available: {avail})\")
"
Operation B: License Details
wxcli licenses show "LICENSE_ID"
wxcli licenses show "LICENSE_ID" --include-assigned-to user -o json
wxcli licenses show "LICENSE_ID" --include-assigned-to user --limit 100
Operation C: Assign a Non-Calling License
For Meetings, Messaging, or other non-calling licenses. No properties block needed.
wxcli licenses update --email "user@example.com" --json-body '{
"email": "user@example.com",
"licenses": [
{"id": "LICENSE_ID", "operation": "add"}
]
}'
Operation D: Assign a Calling License
Calling licenses require properties with at least phoneNumber or extension. If phoneNumber is omitted, locationId is mandatory.
wxcli licenses update --email "user@example.com" --json-body '{
"email": "user@example.com",
"licenses": [
{
"id": "CALLING_PRO_LICENSE_ID",
"operation": "add",
"properties": {
"locationId": "LOCATION_ID",
"extension": "1001"
}
}
]
}'
wxcli licenses update --email "user@example.com" --json-body '{
"email": "user@example.com",
"licenses": [
{
"id": "CALLING_PRO_LICENSE_ID",
"operation": "add",
"properties": {
"locationId": "LOCATION_ID",
"phoneNumber": "+14085551234",
"extension": "1001"
}
}
]
}'
Key constraints:
locationId is write-once — can only be set on initial calling license assignment, cannot be changed after
extension value must NOT include the location routing prefix
- Either
phoneNumber or extension is mandatory for calling licenses
- To move a user to a new location: remove calling license, then re-add with new location (this is destructive)
Operation E: Remove a License
wxcli licenses update --email "user@example.com" --json-body '{
"email": "user@example.com",
"licenses": [
{"id": "LICENSE_ID", "operation": "remove"}
]
}'
WARNING: Removing a Calling license is destructive. ALL calling configuration is deleted: extension, phone number, call forwarding rules, voicemail settings, monitoring lists, and device associations. There is no undo. Always document the user's settings before removing a calling license.
Operation F: Add and Remove in One Call
Swap licenses (e.g., upgrade from Standard to Professional) in a single PATCH:
wxcli licenses update --email "user@example.com" --json-body '{
"email": "user@example.com",
"licenses": [
{
"id": "NEW_CALLING_PRO_LICENSE_ID",
"operation": "add",
"properties": {
"locationId": "LOCATION_ID",
"extension": "1001"
}
},
{"id": "OLD_CALLING_STD_LICENSE_ID", "operation": "remove"}
]
}'
Operation G: Usage Audit
Cross-reference license inventory with user data to find gaps.
wxcli licenses list -o json
wxcli licenses show "CALLING_LICENSE_ID" --include-assigned-to user -o json
wxcli people list -o json
Operation H: Reclaim Licenses from Inactive Users
wxcli licenses show "CALLING_LICENSE_ID" --include-assigned-to user -o json
wxcli people list -o json
wxcli users show PERSON_ID -o json
wxcli licenses update --email "inactive.user@example.com" --json-body '{
"email": "inactive.user@example.com",
"licenses": [
{"id": "CALLING_LICENSE_ID", "operation": "remove"}
]
}'
wxcli licenses show "CALLING_LICENSE_ID"
Operation I: Bulk License Assignment
For assigning the same license to multiple users, loop the PATCH command:
LICENSE_ID="<license_id>"
LOCATION_ID="<location_id>"
EXT=200
for email in user1@example.com user2@example.com user3@example.com; do
wxcli licenses update --json-body "{
\"email\": \"$email\",
\"licenses\": [
{
\"id\": \"$LICENSE_ID\",
\"operation\": \"add\",
\"properties\": {
\"locationId\": \"$LOCATION_ID\",
\"extension\": \"$EXT\"
}
}
]
}"
echo "Assigned to: $email (ext $EXT)"
EXT=$((EXT + 1))
done
Note: For large batches (50+ users), use the migration engine's async pattern for better performance with concurrent requests and automatic 429 retry handling. For smaller batches, shell loops with sleep 1 suffice.
Operation J: Resource Groups and Memberships
Resource groups partition license assignment in hybrid services deployments. A resource group represents an on-premises connector cluster (calendar, calling, or messaging). A membership binds one (person + license) pair to one resource group — that is what decides which cluster serves that user's licensed service. Orgs with no hybrid services typically have a single "Default resource group" and never need to touch this.
Use this when the user asks which cluster serves a user, or wants to move a user's licensed service to a different cluster.
Read the groups (both commands are read-only — resource-groups has no create/update/delete):
wxcli resource-groups list
wxcli resource-groups show "RESOURCE_GROUP_ID"
Read the memberships:
wxcli resource-group-memberships list -o json
wxcli resource-group-memberships list --license-id "LICENSE_ID" -o json
wxcli resource-group-memberships list --person-id "PERSON_ID" -o json
wxcli resource-group-memberships list-v2 --type User -o json
wxcli resource-group-memberships list-v2 --type Workspace -o json
wxcli resource-group-memberships show "RESOURCE_GROUP_MEMBERSHIP_ID" -o json
Each membership returns exactly id, resourceGroupId, licenseId, personId, and personOrgId — the licenseId + personId pair is what ties this group to licensing.
--status is a silent-empty trap (verified live). Both list and list-v2 accept --status pending|activated|error, but on an org with 17 memberships all three values returned 0 results, and no membership response carried a status field at all. If you filter by status and get nothing, that is not evidence the memberships are missing — re-run without --status before concluding anything. --type on list-v2 does work (User returned all 17, Workspace returned 0).
Move a user's licensed service to a different cluster (write — show the plan first):
wxcli resource-group-memberships update "RESOURCE_GROUP_MEMBERSHIP_ID" \
--resource-group-id "NEW_RESOURCE_GROUP_ID"
Verify the move by reading the membership back and confirming resourceGroupId now holds the new group ID:
wxcli resource-group-memberships show "RESOURCE_GROUP_MEMBERSHIP_ID" -o json
Do not verify via --status — see the trap above.
Step 7: Verify results
Always read back the modified resources to confirm.
Verify a license assignment:
wxcli licenses show "LICENSE_ID"
wxcli licenses show "LICENSE_ID" --include-assigned-to user -o json
wxcli users show PERSON_ID -o json
Verify a license removal:
wxcli licenses show "LICENSE_ID"
wxcli licenses show "LICENSE_ID" --include-assigned-to user -o json
Verify bulk operations:
wxcli licenses list -o json | python3.11 -c "
import json, sys
data = json.load(sys.stdin)
for lic in data:
if 'calling' in lic.get('name', '').lower():
avail = lic.get('totalUnits', 0) - lic.get('consumedUnits', 0)
print(f\"{lic['name']}: {lic.get('consumedUnits', 0)}/{lic.get('totalUnits', 0)} (available: {avail})\")
"
Step 8: Report results
Summarize what was done:
=== Licensing Operation Complete ===
Operation: [what was done]
Results:
- [license]: [action] for [user/count] ([status])
- Capacity: [consumed]/[total] (was [old_consumed]/[total])
Verification:
✓ [what was confirmed]
Next steps:
- [any follow-up actions needed]
Critical Rules
-
ALWAYS test auth first -- Run wxcli whoami before any licensing call. Do not proceed on auth failure.
-
ALWAYS show plan before executing -- Present the licensing plan and wait for user confirmation. Never assign or remove licenses without approval.
-
Removing a Calling license is destructive -- ALL calling configuration is deleted: extension, phone number, call forwarding rules, voicemail settings, monitoring lists, and device associations. There is no undo. Always document settings before removal.
-
License IDs are org-specific base64 strings -- Never hardcode them. Always retrieve via wxcli licenses list first.
-
Calling licenses require properties -- Unlike Meetings or Messaging licenses, Calling license assignment must include properties with at least phoneNumber or extension. If you omit phoneNumber, you must provide locationId. Error code 400411 indicates missing properties.
-
locationId is write-once on calling licenses -- Can only be set when first assigning a calling license. Cannot be changed after. To move a user, remove calling license and re-add with new location (destructive).
-
licenses update confirms "Updated" even on no-op -- If the user already has the license, the CLI prints "Updated." without error. Always verify the actual state with licenses show after assignment.
-
License conflicts are strictly enforced -- A user cannot hold both Calling Professional and Calling Standard. The API returns specific error codes for each conflict (400404, 400406, 400407, 400410).
-
Attendant Console has a prerequisite -- Assign Calling Professional before assigning Attendant Console. Error code 400408 if the prerequisite is missing.
-
206 Partial Content is not an error -- The PATCH endpoint can return HTTP 206 when some licenses succeeded but others failed. Always compare the returned licenses array against what was requested.
-
licenses-api is a deprecated alias for licenses -- Both names resolve to the same module with the same commands (list, show, update). Prefer licenses; licenses-api will be removed.
-
Log all operations -- Print what you are about to do before each CLI command, and print the result after. This creates an audit trail for troubleshooting.
-
resource-group-memberships --status returns 0 results for every value -- Verified live: pending, activated, and error each returned nothing on an org with 17 memberships, and no membership response contains a status field. Filter with --license-id, --person-id, or list-v2 --type instead. An empty --status result is a filter artifact, not a finding — never report "no memberships" based on it.
-
Resource groups only matter for hybrid services -- A membership binds a (person + license) pair to an on-premises connector cluster. Changing it re-homes that user's licensed service to a different cluster; it does NOT add, remove, or free a license. Orgs without hybrid services have a single default group and should not be touched. resource-groups is read-only (list/show only) -- groups themselves are created in Control Hub, not via the CLI.
Error Handling
When a wxcli command fails:
A. Fix and retry -- Missing required field, wrong ID, format issue:
- Read the full error message
- Run
wxcli licenses update --help to check required flags
- Fix the command and retry
B. Skip and continue -- License already assigned or already removed:
- Verify current state:
wxcli licenses show LICENSE_ID --include-assigned-to user -o json
- If state is correct, skip to next operation
C. Escalate -- Unclear or persistent error:
- Run with
--debug for raw HTTP details
- Invoke
the wxc-calling-debug skill for systematic diagnosis
Licensing-specific errors:
- 400 with code 400000: License ID not recognized -- re-run
licenses list to get correct IDs
- 400 with code 400411: Missing
properties on calling license -- add locationId, phoneNumber, or extension
- 400 with code 400404: Cannot hold both Calling Professional and Standard simultaneously -- remove one first
- 400 with code 400408: Attendant Console requires Calling Professional prerequisite
- 400 with code 400112: Cannot downgrade Calling Professional to Standard
- 206: Partial success -- check which licenses in the array failed
- 403: Token lacks admin write access -- verify admin role
- Empty result from
resource-group-memberships list --status ...: expected -- the status filter matches nothing for any value. Drop --status and re-run (Critical Rule 13)
Required Scopes Reference
| Operation | Scope(s) |
|---|
| List/view licenses | Admin token (full or read-only admin) |
| Assign/remove licenses (PATCH) | Admin token (full admin) |
| List people (for cross-reference) | spark-admin:people_read |
| List resource groups / memberships | spark-admin:resource_groups_read |
| Update a resource group membership | spark-admin:resource_group_memberships_write |
License Type Quick Reference
| License Name Pattern | Type | Properties Required |
|---|
| Webex Calling - Professional | Calling | locationId + (phoneNumber or extension) |
| Webex Calling - Standard | Calling | locationId + (phoneNumber or extension) |
| Webex Calling - Common Area | Calling | locationId + (phoneNumber or extension) |
| CX Essentials | Calling add-on | Requires Calling Professional first |
| Webex Attendant Console | Calling add-on | Requires Calling Professional first |
| Webex Meetings | Non-calling | None (optionally siteUrl) |
| Webex Messaging | Non-calling | None |
Match by substring (e.g., "calling" in name), not exact string. License names vary by subscription.
Cross-Reference
For calling license assignment during user provisioning, see the provision-calling skill. The licenses update command now provides CLI-native license assignment.
Context Compaction Recovery
If context compacts mid-execution, recover by:
- Read
docs/reference/admin-licensing.md to recover PATCH body format and error codes
- Check current license state:
wxcli licenses list -o json
- Resume from the first incomplete step