wxcli 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. wxcli/__init__.py +4 -0
  2. wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
  3. wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
  4. wxcli/_playbook/.claude/rules/cleanup.md +29 -0
  5. wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
  6. wxcli/_playbook/.claude/rules/org-health.md +14 -0
  7. wxcli/_playbook/.claude/settings.json +8 -0
  8. wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
  9. wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
  10. wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
  11. wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
  12. wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
  13. wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
  14. wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
  15. wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
  16. wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
  17. wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
  18. wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
  19. wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
  20. wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
  21. wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
  22. wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
  23. wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
  24. wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
  25. wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
  26. wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
  27. wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
  28. wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
  29. wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
  30. wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
  31. wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
  32. wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
  33. wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
  34. wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
  35. wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
  36. wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
  37. wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
  38. wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
  39. wxcli/_playbook/CLAUDE.md +373 -0
  40. wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
  41. wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
  42. wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
  43. wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
  44. wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
  45. wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
  46. wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
  47. wxcli/_playbook/docs/reference/admin-partner.md +357 -0
  48. wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
  49. wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
  50. wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
  51. wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
  52. wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
  53. wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
  54. wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
  55. wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
  56. wxcli/_playbook/docs/reference/authentication.md +965 -0
  57. wxcli/_playbook/docs/reference/call-control.md +1205 -0
  58. wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
  59. wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
  60. wxcli/_playbook/docs/reference/call-routing.md +2304 -0
  61. wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
  62. wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
  63. wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
  64. wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
  65. wxcli/_playbook/docs/reference/devices-core.md +1869 -0
  66. wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
  67. wxcli/_playbook/docs/reference/devices-platform.md +467 -0
  68. wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
  69. wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
  70. wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
  71. wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
  72. wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
  73. wxcli/_playbook/docs/reference/meetings-content.md +473 -0
  74. wxcli/_playbook/docs/reference/meetings-core.md +903 -0
  75. wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
  76. wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
  77. wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
  78. wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
  79. wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
  80. wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
  81. wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
  82. wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
  83. wxcli/_playbook/docs/reference/provisioning.md +1366 -0
  84. wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
  85. wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
  86. wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
  87. wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
  88. wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
  89. wxcli/_version.py +24 -0
  90. wxcli/auth.py +137 -0
  91. wxcli/commands/__init__.py +0 -0
  92. wxcli/commands/_registry.py +181 -0
  93. wxcli/commands/activation_email.py +103 -0
  94. wxcli/commands/admin_recordings.py +585 -0
  95. wxcli/commands/analytics.py +105 -0
  96. wxcli/commands/announcement_playlists.py +219 -0
  97. wxcli/commands/announcements.py +519 -0
  98. wxcli/commands/archive_users.py +36 -0
  99. wxcli/commands/attachment_actions.py +60 -0
  100. wxcli/commands/audit_events.py +83 -0
  101. wxcli/commands/authorizations.py +113 -0
  102. wxcli/commands/auto_attendant.py +642 -0
  103. wxcli/commands/broadworks_billing_reports.py +126 -0
  104. wxcli/commands/broadworks_enterprises.py +160 -0
  105. wxcli/commands/broadworks_subscribers.py +319 -0
  106. wxcli/commands/broadworks_workspaces.py +108 -0
  107. wxcli/commands/call_controls.py +1304 -0
  108. wxcli/commands/call_park.py +494 -0
  109. wxcli/commands/call_pickup.py +229 -0
  110. wxcli/commands/call_queue.py +1878 -0
  111. wxcli/commands/call_recording.py +707 -0
  112. wxcli/commands/call_routing.py +1610 -0
  113. wxcli/commands/call_settings_for_me_phase_5.py +212 -0
  114. wxcli/commands/caller_reputation.py +169 -0
  115. wxcli/commands/calling_service.py +305 -0
  116. wxcli/commands/cc_address_book.py +770 -0
  117. wxcli/commands/cc_agent_greetings.py +242 -0
  118. wxcli/commands/cc_agent_summaries.py +39 -0
  119. wxcli/commands/cc_agent_wellbeing.py +216 -0
  120. wxcli/commands/cc_agents.py +425 -0
  121. wxcli/commands/cc_ai_assistant.py +55 -0
  122. wxcli/commands/cc_ai_feature.py +294 -0
  123. wxcli/commands/cc_audio_files.py +149 -0
  124. wxcli/commands/cc_auto_csat.py +311 -0
  125. wxcli/commands/cc_aux_code.py +458 -0
  126. wxcli/commands/cc_business_hour.py +353 -0
  127. wxcli/commands/cc_call_monitoring.py +228 -0
  128. wxcli/commands/cc_callbacks.py +194 -0
  129. wxcli/commands/cc_campaign.py +212 -0
  130. wxcli/commands/cc_captures.py +39 -0
  131. wxcli/commands/cc_contact_list.py +169 -0
  132. wxcli/commands/cc_contact_number.py +295 -0
  133. wxcli/commands/cc_data_sources.py +235 -0
  134. wxcli/commands/cc_desktop_layout.py +380 -0
  135. wxcli/commands/cc_desktop_profile.py +322 -0
  136. wxcli/commands/cc_dial_number.py +511 -0
  137. wxcli/commands/cc_dial_plan.py +347 -0
  138. wxcli/commands/cc_dnc.py +101 -0
  139. wxcli/commands/cc_entry_point.py +545 -0
  140. wxcli/commands/cc_ewt.py +51 -0
  141. wxcli/commands/cc_flow.py +477 -0
  142. wxcli/commands/cc_global_vars.py +430 -0
  143. wxcli/commands/cc_holiday_list.py +335 -0
  144. wxcli/commands/cc_journey.py +1261 -0
  145. wxcli/commands/cc_legacy_flows.py +49 -0
  146. wxcli/commands/cc_multimedia_profile.py +436 -0
  147. wxcli/commands/cc_notification.py +51 -0
  148. wxcli/commands/cc_outdial_ani.py +604 -0
  149. wxcli/commands/cc_overrides.py +341 -0
  150. wxcli/commands/cc_queue.py +1083 -0
  151. wxcli/commands/cc_queue_stats.py +55 -0
  152. wxcli/commands/cc_realtime.py +51 -0
  153. wxcli/commands/cc_resource_collection.py +312 -0
  154. wxcli/commands/cc_search.py +46 -0
  155. wxcli/commands/cc_site.py +400 -0
  156. wxcli/commands/cc_skill.py +350 -0
  157. wxcli/commands/cc_skill_profile.py +289 -0
  158. wxcli/commands/cc_subscriptions.py +426 -0
  159. wxcli/commands/cc_summaries.py +130 -0
  160. wxcli/commands/cc_tasks.py +844 -0
  161. wxcli/commands/cc_team.py +325 -0
  162. wxcli/commands/cc_user_profiles.py +349 -0
  163. wxcli/commands/cc_users.py +701 -0
  164. wxcli/commands/cc_work_types.py +363 -0
  165. wxcli/commands/cdr.py +89 -0
  166. wxcli/commands/classifications.py +38 -0
  167. wxcli/commands/cleanup.py +1427 -0
  168. wxcli/commands/client_settings.py +72 -0
  169. wxcli/commands/conference.py +328 -0
  170. wxcli/commands/configure.py +72 -0
  171. wxcli/commands/converged_recordings.py +347 -0
  172. wxcli/commands/converged_recordings_export.py +310 -0
  173. wxcli/commands/cq_playlists.py +42 -0
  174. wxcli/commands/cucm.py +2794 -0
  175. wxcli/commands/cucm_config.py +129 -0
  176. wxcli/commands/customer_assist.py +373 -0
  177. wxcli/commands/data_sources.py +227 -0
  178. wxcli/commands/dect_devices.py +787 -0
  179. wxcli/commands/device_configurations.py +77 -0
  180. wxcli/commands/device_dynamic_settings.py +320 -0
  181. wxcli/commands/device_settings.py +1489 -0
  182. wxcli/commands/devices.py +280 -0
  183. wxcli/commands/domains.py +142 -0
  184. wxcli/commands/ecm.py +178 -0
  185. wxcli/commands/emergency_services.py +827 -0
  186. wxcli/commands/events.py +83 -0
  187. wxcli/commands/external_voicemail.py +52 -0
  188. wxcli/commands/groups.py +206 -0
  189. wxcli/commands/guest_management.py +76 -0
  190. wxcli/commands/hds.py +340 -0
  191. wxcli/commands/hot_desk.py +67 -0
  192. wxcli/commands/hot_desking_members.py +118 -0
  193. wxcli/commands/hot_desking_portal.py +124 -0
  194. wxcli/commands/hunt_group.py +577 -0
  195. wxcli/commands/hybrid_clusters.py +71 -0
  196. wxcli/commands/hybrid_connectors.py +67 -0
  197. wxcli/commands/identity_org.py +159 -0
  198. wxcli/commands/init_playbook.py +155 -0
  199. wxcli/commands/licenses.py +106 -0
  200. wxcli/commands/live_monitoring.py +40 -0
  201. wxcli/commands/location_call_handling.py +577 -0
  202. wxcli/commands/location_schedules.py +341 -0
  203. wxcli/commands/location_settings.py +1297 -0
  204. wxcli/commands/location_voicemail.py +494 -0
  205. wxcli/commands/locations.py +336 -0
  206. wxcli/commands/meeting_captions.py +110 -0
  207. wxcli/commands/meeting_chats.py +63 -0
  208. wxcli/commands/meeting_invitees.py +234 -0
  209. wxcli/commands/meeting_messages.py +28 -0
  210. wxcli/commands/meeting_participants.py +278 -0
  211. wxcli/commands/meeting_polls.py +111 -0
  212. wxcli/commands/meeting_preferences.py +515 -0
  213. wxcli/commands/meeting_qa.py +78 -0
  214. wxcli/commands/meeting_qualities.py +43 -0
  215. wxcli/commands/meeting_reports.py +101 -0
  216. wxcli/commands/meeting_session_types.py +105 -0
  217. wxcli/commands/meeting_site.py +57 -0
  218. wxcli/commands/meeting_slido.py +44 -0
  219. wxcli/commands/meeting_summaries.py +92 -0
  220. wxcli/commands/meeting_tracking_codes.py +233 -0
  221. wxcli/commands/meeting_transcripts.py +230 -0
  222. wxcli/commands/meetings.py +1997 -0
  223. wxcli/commands/memberships.py +164 -0
  224. wxcli/commands/messages.py +213 -0
  225. wxcli/commands/mode_management.py +244 -0
  226. wxcli/commands/my_call_settings.py +3577 -0
  227. wxcli/commands/numbers.py +405 -0
  228. wxcli/commands/operating_modes.py +410 -0
  229. wxcli/commands/org_contacts.py +284 -0
  230. wxcli/commands/org_health_cli.py +55 -0
  231. wxcli/commands/org_settings.py +71 -0
  232. wxcli/commands/organizations.py +83 -0
  233. wxcli/commands/paging_group.py +257 -0
  234. wxcli/commands/partner_admins.py +141 -0
  235. wxcli/commands/partner_reports.py +188 -0
  236. wxcli/commands/partner_tags.py +216 -0
  237. wxcli/commands/people.py +279 -0
  238. wxcli/commands/person_call_settings.py +71 -0
  239. wxcli/commands/pstn.py +280 -0
  240. wxcli/commands/recording_report.py +154 -0
  241. wxcli/commands/report_templates.py +38 -0
  242. wxcli/commands/reports.py +144 -0
  243. wxcli/commands/resource_group_memberships.py +160 -0
  244. wxcli/commands/resource_groups.py +67 -0
  245. wxcli/commands/roles.py +63 -0
  246. wxcli/commands/room_tabs.py +160 -0
  247. wxcli/commands/rooms.py +225 -0
  248. wxcli/commands/scim_bulk.py +46 -0
  249. wxcli/commands/scim_groups.py +246 -0
  250. wxcli/commands/scim_schemas.py +82 -0
  251. wxcli/commands/scim_users.py +283 -0
  252. wxcli/commands/security_audit.py +54 -0
  253. wxcli/commands/service_apps.py +51 -0
  254. wxcli/commands/single_number_reach.py +209 -0
  255. wxcli/commands/team_memberships.py +155 -0
  256. wxcli/commands/teams.py +153 -0
  257. wxcli/commands/ucm_profile.py +42 -0
  258. wxcli/commands/update.py +219 -0
  259. wxcli/commands/user_settings.py +5116 -0
  260. wxcli/commands/video_mesh.py +1090 -0
  261. wxcli/commands/virtual_extensions.py +502 -0
  262. wxcli/commands/virtual_line_settings.py +2072 -0
  263. wxcli/commands/webhooks.py +176 -0
  264. wxcli/commands/wholesale_billing_reports.py +147 -0
  265. wxcli/commands/wholesale_provisioning.py +523 -0
  266. wxcli/commands/workspace_locations.py +339 -0
  267. wxcli/commands/workspace_metrics.py +103 -0
  268. wxcli/commands/workspace_personalization.py +70 -0
  269. wxcli/commands/workspace_settings.py +3347 -0
  270. wxcli/commands/workspaces.py +279 -0
  271. wxcli/commands/xapi.py +114 -0
  272. wxcli/config.py +139 -0
  273. wxcli/errors.py +77 -0
  274. wxcli/main.py +210 -0
  275. wxcli/migration/CLAUDE.md +106 -0
  276. wxcli/migration/__init__.py +1 -0
  277. wxcli/migration/advisory/CLAUDE.md +196 -0
  278. wxcli/migration/advisory/__init__.py +46 -0
  279. wxcli/migration/advisory/advisor.py +107 -0
  280. wxcli/migration/advisory/advisory_patterns.py +2552 -0
  281. wxcli/migration/advisory/recommendation_rules.py +835 -0
  282. wxcli/migration/cucm/CLAUDE.md +74 -0
  283. wxcli/migration/cucm/__init__.py +5 -0
  284. wxcli/migration/cucm/connection.py +265 -0
  285. wxcli/migration/cucm/discovery.py +225 -0
  286. wxcli/migration/cucm/extractors/__init__.py +1 -0
  287. wxcli/migration/cucm/extractors/announcements.py +92 -0
  288. wxcli/migration/cucm/extractors/base.py +90 -0
  289. wxcli/migration/cucm/extractors/device_profiles.py +98 -0
  290. wxcli/migration/cucm/extractors/devices.py +193 -0
  291. wxcli/migration/cucm/extractors/e911.py +103 -0
  292. wxcli/migration/cucm/extractors/features.py +309 -0
  293. wxcli/migration/cucm/extractors/helpers.py +73 -0
  294. wxcli/migration/cucm/extractors/informational.py +295 -0
  295. wxcli/migration/cucm/extractors/locations.py +221 -0
  296. wxcli/migration/cucm/extractors/moh.py +100 -0
  297. wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
  298. wxcli/migration/cucm/extractors/routing.py +493 -0
  299. wxcli/migration/cucm/extractors/shared_lines.py +146 -0
  300. wxcli/migration/cucm/extractors/templates.py +229 -0
  301. wxcli/migration/cucm/extractors/tier4.py +173 -0
  302. wxcli/migration/cucm/extractors/users.py +240 -0
  303. wxcli/migration/cucm/extractors/voicemail.py +243 -0
  304. wxcli/migration/cucm/extractors/workspaces.py +88 -0
  305. wxcli/migration/cucm/unity_connection.py +304 -0
  306. wxcli/migration/execute/CLAUDE.md +372 -0
  307. wxcli/migration/execute/__init__.py +303 -0
  308. wxcli/migration/execute/batch.py +277 -0
  309. wxcli/migration/execute/dependency.py +483 -0
  310. wxcli/migration/execute/engine.py +964 -0
  311. wxcli/migration/execute/handlers.py +2232 -0
  312. wxcli/migration/execute/planner.py +1939 -0
  313. wxcli/migration/execute/runtime.py +571 -0
  314. wxcli/migration/export/CLAUDE.md +49 -0
  315. wxcli/migration/export/__init__.py +8 -0
  316. wxcli/migration/export/csv_export.py +126 -0
  317. wxcli/migration/export/deployment_plan.py +479 -0
  318. wxcli/migration/export/json_export.py +60 -0
  319. wxcli/migration/models.py +941 -0
  320. wxcli/migration/phone_models.py +50 -0
  321. wxcli/migration/preflight/CLAUDE.md +52 -0
  322. wxcli/migration/preflight/__init__.py +89 -0
  323. wxcli/migration/preflight/checks.py +911 -0
  324. wxcli/migration/preflight/runner.py +293 -0
  325. wxcli/migration/rate_limiter.py +144 -0
  326. wxcli/migration/report/CLAUDE.md +245 -0
  327. wxcli/migration/report/__init__.py +7 -0
  328. wxcli/migration/report/appendix.py +2437 -0
  329. wxcli/migration/report/assembler.py +183 -0
  330. wxcli/migration/report/charts.py +263 -0
  331. wxcli/migration/report/executive.py +776 -0
  332. wxcli/migration/report/explainer.py +704 -0
  333. wxcli/migration/report/helpers.py +74 -0
  334. wxcli/migration/report/ingest.py +152 -0
  335. wxcli/migration/report/notice_templates.py +192 -0
  336. wxcli/migration/report/score.py +375 -0
  337. wxcli/migration/report/styles.py +1052 -0
  338. wxcli/migration/report/user_diff.py +1052 -0
  339. wxcli/migration/report/user_notice.py +425 -0
  340. wxcli/migration/state.py +139 -0
  341. wxcli/migration/store.py +860 -0
  342. wxcli/migration/transform/CLAUDE.md +107 -0
  343. wxcli/migration/transform/__init__.py +12 -0
  344. wxcli/migration/transform/analysis_pipeline.py +453 -0
  345. wxcli/migration/transform/analyzers/__init__.py +142 -0
  346. wxcli/migration/transform/analyzers/css_permission.py +189 -0
  347. wxcli/migration/transform/analyzers/css_routing.py +335 -0
  348. wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
  349. wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
  350. wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
  351. wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
  352. wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
  353. wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
  354. wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
  355. wxcli/migration/transform/analyzers/missing_data.py +300 -0
  356. wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
  357. wxcli/migration/transform/analyzers/shared_line.py +170 -0
  358. wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
  359. wxcli/migration/transform/analyzers/workspace_license.py +212 -0
  360. wxcli/migration/transform/cross_reference.py +1136 -0
  361. wxcli/migration/transform/cucm_pattern.py +246 -0
  362. wxcli/migration/transform/decisions.py +301 -0
  363. wxcli/migration/transform/e164.py +145 -0
  364. wxcli/migration/transform/engine.py +256 -0
  365. wxcli/migration/transform/mappers/CLAUDE.md +123 -0
  366. wxcli/migration/transform/mappers/__init__.py +76 -0
  367. wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
  368. wxcli/migration/transform/mappers/base.py +190 -0
  369. wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
  370. wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
  371. wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
  372. wxcli/migration/transform/mappers/css_mapper.py +868 -0
  373. wxcli/migration/transform/mappers/dect_mapper.py +367 -0
  374. wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
  375. wxcli/migration/transform/mappers/device_mapper.py +227 -0
  376. wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
  377. wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
  378. wxcli/migration/transform/mappers/e911_mapper.py +129 -0
  379. wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
  380. wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
  381. wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
  382. wxcli/migration/transform/mappers/line_mapper.py +310 -0
  383. wxcli/migration/transform/mappers/location_mapper.py +235 -0
  384. wxcli/migration/transform/mappers/moh_mapper.py +114 -0
  385. wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
  386. wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
  387. wxcli/migration/transform/mappers/routing_mapper.py +821 -0
  388. wxcli/migration/transform/mappers/snr_mapper.py +201 -0
  389. wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
  390. wxcli/migration/transform/mappers/user_mapper.py +264 -0
  391. wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
  392. wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
  393. wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
  394. wxcli/migration/transform/normalizers.py +2008 -0
  395. wxcli/migration/transform/pattern_converter.py +106 -0
  396. wxcli/migration/transform/pipeline.py +271 -0
  397. wxcli/migration/transform/rules.py +255 -0
  398. wxcli/org_health/CLAUDE.md +63 -0
  399. wxcli/org_health/__init__.py +7 -0
  400. wxcli/org_health/analyze.py +93 -0
  401. wxcli/org_health/checks.py +458 -0
  402. wxcli/org_health/collector.py +62 -0
  403. wxcli/org_health/models.py +78 -0
  404. wxcli/org_health/report.py +300 -0
  405. wxcli/output.py +101 -0
  406. wxcli-1.2.0.dist-info/METADATA +505 -0
  407. wxcli-1.2.0.dist-info/RECORD +413 -0
  408. wxcli-1.2.0.dist-info/WHEEL +5 -0
  409. wxcli-1.2.0.dist-info/entry_points.txt +2 -0
  410. wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
  411. wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
  412. wxcli-1.2.0.dist-info/scm_version.json +8 -0
  413. wxcli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1366 @@
1
+ # Provisioning Reference
2
+
3
+ User, license, and location provisioning for Webex Calling via the `wxc_sdk` Python SDK. Each section includes both typed SDK methods and **Raw HTTP** examples using `api.session.rest_*()`.
4
+
5
+ > **Prerequisite:** All examples assume a configured `WebexSimpleApi` instance. See `authentication.md` for token setup. For the raw HTTP pattern, see `archive/wxc-sdk-patterns.md` section 1.5.
6
+
7
+ ## Sources
8
+
9
+ - wxc_sdk v1.30.0 (github.com/jeokrohn/wxc_sdk)
10
+ - OpenAPI spec: `specs/webex-admin.json` (People, Licenses, Locations, Organizations APIs)
11
+ - developer.webex.com People, Licenses, Locations, and Organizations APIs
12
+
13
+ ---
14
+
15
+ ## Table of Contents
16
+
17
+ 1. [Required Scopes](#required-scopes)
18
+ 2. [People API](#people-api)
19
+ 3. [Licenses API](#licenses-api)
20
+ 4. [Locations API](#locations)
21
+ 5. [Organization API](#organization-api)
22
+ 6. [Numbers API](#numbers)
23
+ 7. [Provisioning Workflow](#provisioning-workflow)
24
+ 8. [Data Models](#data-models)
25
+ 9. [Gotchas](#gotchas) (cross-cutting)
26
+ 10. [Bulk Cleanup / Teardown](#bulk-cleanup--teardown)
27
+ 11. [See Also](#see-also)
28
+
29
+ ---
30
+
31
+ ## Required Scopes
32
+
33
+ | Operation | Scope(s) Required |
34
+ |-----------|-------------------|
35
+ | List/view people | `spark:people_read` (own info) or `spark-admin:people_read` (all org users) |
36
+ | Create/update/delete people | `spark-admin:people_write` **and** `spark-admin:people_read` |
37
+ | List licenses | `spark-admin:licenses_read` |
38
+ | Assign licenses (PATCH) | `spark-admin:people_write` |
39
+ | List/view locations | `spark-admin:locations_read`, `spark-admin:people_read`, or `spark-admin:device_read` |
40
+ | Create/update/delete locations | `spark-admin:locations_write` |
41
+ | Delete organization | `spark-admin:organizations_write` |
42
+ | Enable location for calling | `spark-admin:telephony_config_write` (and likely `spark-admin:locations_write`) |
43
+
44
+ All provisioning operations require an **administrator auth token**. Non-admin tokens can only read people via `spark:people_read` with email or displayName filters.
45
+
46
+ ---
47
+
48
+ ## People API
49
+
50
+ Base path: `/v1/people`
51
+
52
+ ### Listing People
53
+
54
+ ```python
55
+ from wxc_sdk import WebexSimpleApi
56
+
57
+ api = WebexSimpleApi()
58
+
59
+ # List ALL people (admin only, no filter required)
60
+ all_users = list(api.people.list())
61
+
62
+ # Filter by email
63
+ user = next(api.people.list(email='jsmith@example.com'), None)
64
+
65
+ # Filter by display name (prefix match)
66
+ users = list(api.people.list(display_name='John'))
67
+
68
+ # Filter by location
69
+ users_at_hq = list(api.people.list(location_id='<location_id>'))
70
+
71
+ # List by IDs (up to 85)
72
+ users = list(api.people.list(id_list=['id1', 'id2', 'id3']))
73
+ ```
74
+
75
+ ### The `calling_data=True` Parameter (Critical)
76
+
77
+ To get calling-specific fields (`location_id`, `phone_numbers` with `work_extension` type, `extension`), you **must** pass `calling_data=True`. Without it, these fields are absent from the response.
78
+
79
+ ```python
80
+ # WITHOUT calling_data -- location_id and calling fields are missing
81
+ user = api.people.details(person_id='<id>')
82
+ print(user.location_id) # None
83
+
84
+ # WITH calling_data -- calling fields populated
85
+ user = api.people.details(person_id='<id>', calling_data=True)
86
+ print(user.location_id) # 'Y2lzY29zcGF...'
87
+ print(user.extension) # '1001'
88
+ ```
89
+
90
+ This applies to `list()`, `details()`, `create()`, and `update()`.
91
+
92
+ ### Identifying Calling Users
93
+
94
+ A calling user is one with a `location_id` set. The SDK examples demonstrate two approaches:
95
+
96
+ **Approach 1 -- Filter on `location_id` (synchronous)**
97
+
98
+ ```python
99
+ # From examples/calling_users.py
100
+ calling_users = [user for user in api.people.list(calling_data=True)
101
+ if user.location_id]
102
+ ```
103
+
104
+ **Approach 2 -- Filter on calling license IDs (async)**
105
+
106
+ ```python
107
+ # From examples/calling_users_async.py
108
+ calling_license_ids = set(
109
+ lic.license_id for lic in await api.licenses.list()
110
+ if lic.webex_calling
111
+ )
112
+ calling_users = [
113
+ user async for user in api.people.list_gen()
114
+ if any(lic_id in calling_license_ids for lic_id in user.licenses)
115
+ ]
116
+ ```
117
+
118
+ ### Creating a Person
119
+
120
+ At minimum, one of `displayName`, `firstName`, or `lastName` is required. For a **Webex Calling** user, you must also provide `phoneNumbers` or `extension`, `locationId`, and `licenses` in the same request.
121
+
122
+ ```python
123
+ from wxc_sdk.people import Person, PhoneNumber, PhoneNumberType
124
+
125
+ new_user = api.people.create(
126
+ settings=Person(
127
+ emails=['jsmith@example.com'],
128
+ display_name='John Smith',
129
+ first_name='John',
130
+ last_name='Smith',
131
+ licenses=['<calling_license_id>'],
132
+ location_id='<location_id>',
133
+ extension='1001',
134
+ phone_numbers=[PhoneNumber(type=PhoneNumberType.work, value='5551234567')]
135
+ ),
136
+ calling_data=True
137
+ )
138
+ ```
139
+
140
+ **Important notes on create:**
141
+ - A POST that returns 400 may **still have created the person**. Check with a GET before retrying.
142
+ - SIP addresses are assigned asynchronously -- they may not appear in the POST response. Verify with a subsequent GET.
143
+ - When assigning multiple licenses, the system assigns all valid ones and silently skips invalid ones.
144
+
145
+ ### Updating a Person
146
+
147
+ The update is a **full PUT** -- you must include all fields, not just the changed ones. Standard pattern: GET details first, modify, then PUT.
148
+
149
+ ```python
150
+ # GET current state
151
+ user = api.people.details(person_id='<id>', calling_data=True)
152
+
153
+ # Modify
154
+ user.extension = '2002'
155
+
156
+ # PUT back
157
+ updated = api.people.update(person=user, calling_data=True)
158
+ ```
159
+
160
+ **Key constraints on update:**
161
+ - `location_id` can only be set when **first assigning** a calling license. It cannot be changed for an existing calling user.
162
+ - The `extension` field value should **not** include the location routing prefix. The `work_extension` phone number in the response *will* include it, but when setting `extension` on update, omit the prefix.
163
+ - When updating a user with multiple email addresses, the **primary email must be listed first** in the array.
164
+ - Some licenses are implicitly assigned by the system and cannot be removed. If you get an error about implicit licenses, make sure they remain in the `licenses` array.
165
+ - A PUT that returns an error **may still have partially modified** the person. Always do a GET afterward to verify current state.
166
+
167
+ ### Deleting a Person
168
+
169
+ ```python
170
+ api.people.delete_person(person_id='<id>')
171
+ ```
172
+
173
+ Required roles: Full Administrator, User Administrator, or External Full Administrator.
174
+
175
+ ### Get Current User
176
+
177
+ ```python
178
+ me = api.people.me(calling_data=True)
179
+ ```
180
+
181
+ ### Raw HTTP
182
+ <!-- Updated by playbook session 2026-03-18 -->
183
+
184
+ All People API operations can be performed via raw HTTP using `api.session.rest_*()`. This is the preferred execution pattern -- wxc_sdk handles auth and session management, while you control the exact request.
185
+
186
+ ```python
187
+ from wxc_sdk import WebexSimpleApi
188
+ from wxc_sdk.rest import RestError
189
+
190
+ api = WebexSimpleApi()
191
+ BASE = "https://webexapis.com/v1"
192
+
193
+ # ── List people ───────────────────────────────────────────────────
194
+ result = api.session.rest_get(f"{BASE}/people", params={
195
+ "max": 1000,
196
+ "callingData": "true", # string "true", not bool
197
+ })
198
+ people = result.get("items", [])
199
+
200
+ # ── List with filters ────────────────────────────────────────────
201
+ result = api.session.rest_get(f"{BASE}/people", params={
202
+ "email": "jsmith@example.com",
203
+ "callingData": "true",
204
+ })
205
+
206
+ # ── Get person details ───────────────────────────────────────────
207
+ person = api.session.rest_get(f"{BASE}/people/{person_id}", params={
208
+ "callingData": "true",
209
+ })
210
+
211
+ # ── Create a person ──────────────────────────────────────────────
212
+ body = {
213
+ "emails": ["jsmith@example.com"],
214
+ "displayName": "John Smith",
215
+ "firstName": "John",
216
+ "lastName": "Smith",
217
+ "licenses": [calling_license_id],
218
+ "locationId": location_id,
219
+ "extension": "1001",
220
+ "phoneNumbers": [{"type": "work", "value": "+15551234567"}],
221
+ }
222
+ result = api.session.rest_post(f"{BASE}/people", json=body)
223
+ new_person_id = result["id"]
224
+
225
+ # ── Update a person (full PUT -- include all fields) ─────────────
226
+ person = api.session.rest_get(f"{BASE}/people/{person_id}", params={
227
+ "callingData": "true",
228
+ })
229
+ person["extension"] = "2002"
230
+ api.session.rest_put(f"{BASE}/people/{person_id}", json=person)
231
+
232
+ # ── Delete a person ──────────────────────────────────────────────
233
+ api.session.rest_delete(f"{BASE}/people/{person_id}")
234
+
235
+ # ── Get current user ─────────────────────────────────────────────
236
+ me = api.session.rest_get(f"{BASE}/people/me", params={
237
+ "callingData": "true",
238
+ })
239
+ ```
240
+
241
+ **Key differences from typed SDK:**
242
+ - `callingData` is a string `"true"`, not a Python bool
243
+ - Response key for list is `items`, not `people`
244
+ - No auto-pagination -- use `max=1000` for large orgs
245
+ - Update is a raw PUT -- you must include all fields (GET first, modify, PUT back)
246
+
247
+ ### CLI Examples
248
+
249
+ ```bash
250
+ # ── List all people (table output) ────────────────────────────────
251
+ wxcli people list
252
+
253
+ # ── List with calling data (shows locationId, extension) ─────────
254
+ wxcli people list --calling-data true
255
+
256
+ # ── Search by email ───────────────────────────────────────────────
257
+ wxcli people list --email jsmith@example.com
258
+
259
+ # ── Search by display name (prefix match) ─────────────────────────
260
+ wxcli people list --display-name "John"
261
+
262
+ # ── List people at a specific location ────────────────────────────
263
+ wxcli people list --location-id <location_id> --calling-data true
264
+
265
+ # ── Get person details (JSON output) ──────────────────────────────
266
+ wxcli people show <person_id>
267
+
268
+ # ── Get person details as table ────────────────────────────────────
269
+ wxcli people show <person_id> -o table
270
+
271
+ # ── Get current user details ──────────────────────────────────────
272
+ wxcli people list-me --calling-data true
273
+
274
+ # ── Create a person ───────────────────────────────────────────────
275
+ wxcli people create --first-name "John" --last-name "Smith" \
276
+ --display-name "John Smith" --location-id <location_id> \
277
+ --extension "1001"
278
+
279
+ # ── Create with full JSON body (for emails, licenses, phoneNumbers)
280
+ wxcli people create --json-body '{
281
+ "emails": ["jsmith@example.com"],
282
+ "displayName": "John Smith",
283
+ "firstName": "John",
284
+ "lastName": "Smith",
285
+ "licenses": ["<calling_license_id>"],
286
+ "locationId": "<location_id>",
287
+ "extension": "1001",
288
+ "phoneNumbers": [{"type": "work", "value": "+15551234567"}]
289
+ }'
290
+
291
+ # ── Update a person ───────────────────────────────────────────────
292
+ wxcli people update <person_id> --extension "2002"
293
+ wxcli people update <person_id> --display-name "John A. Smith"
294
+ wxcli people update <person_id> --department "Engineering" --title "Senior Engineer"
295
+
296
+ # ── Delete a person ───────────────────────────────────────────────
297
+ wxcli people delete <person_id>
298
+
299
+ # ── Delete without confirmation prompt ────────────────────────────
300
+ wxcli people delete <person_id> --force
301
+ ```
302
+
303
+ **CLI notes:**
304
+ - `wxcli people list` defaults to table output (`-o table`); use `-o json` for full JSON.
305
+ - `wxcli people show` defaults to JSON output; use `-o table` for a summary view.
306
+ - The `--calling-data true` flag is the CLI equivalent of `calling_data=True` in the SDK. Pass it when you need calling fields (locationId, extension, phoneNumbers).
307
+ - For create/update operations with complex nested fields (emails, licenses, phoneNumbers), use `--json-body` with the full JSON payload.
308
+ - The `--limit` and `--offset` flags control client-side pagination of results.
309
+
310
+ ### Gotchas
311
+
312
+ **`calling_data=True` is not optional -- it is mandatory for calling fields.**
313
+ The single most common mistake. Without `calling_data=True` on `list()`, `details()`, `create()`, and `update()`, the response will **not include** `location_id`, `extension`, or calling-related `phone_numbers`. Your code will see `None` for these fields and may incorrectly conclude the user is not calling-enabled.
314
+
315
+ **`location_id` is write-once for calling users.**
316
+ You can set `location_id` when you first assign a calling license to a user. After that, `location_id` **cannot be changed** via the People API update. The preferred path for moving a calling user to a different location is the dedicated **Move Users Job API**: `POST /v1/people/{personId}/actions/moveLocation`. This moves the user without removing and re-adding the calling license. See `User Call Settings - Validate or Initiate Move Users Job` in the API docs. As a fallback, you can remove the calling license and re-add it with the new `location_id`, but the Move Users Job API is preferred.
317
+
318
+ **The People API is a composite of multiple microservices.**
319
+ A create or update call can **partially succeed**. For example, the user may be created but the phone number assignment may fail (especially with invalid numbers). Always verify with a subsequent GET after errors.
320
+
321
+ **Performance limits with `calling_data=True`.**
322
+ The SDK enforces a soft limit of 10 users per page when fetching with `calling_data=True` (constant `MAX_USERS_WITH_CALLING_DATA = 10`). This is an **SDK internal constant**, not an API enforcement — the live API allows up to **100 results per page** with `callingData=true` (or 50 when `locationId` is also specified). Users of raw HTTP or wxcli are not subject to the SDK's 10-item constant. For large orgs using the SDK, consider the async API with `concurrent_requests` tuning.
323
+
324
+ **Extension vs. work_extension.**
325
+ - When **writing**: set `person.extension = '1001'` (no routing prefix).
326
+ - When **reading**: `phone_numbers` of type `work_extension` will have the value `<routing_prefix><extension>` (e.g., `'8001001'` where `800` is the prefix and `1001` is the extension).
327
+
328
+ **Deleting a calling user may have delayed side effects.**
329
+ After deleting a user, their phone numbers may not be immediately available for reassignment. The SDK test suite retries number removal with delays of up to 10 seconds between attempts when encountering 502 errors.
330
+
331
+ **`PeopleApi.create()` takes a `Person` model, not kwargs.**
332
+
333
+ ```python
334
+ from wxc_sdk.people import Person
335
+ person = Person(emails=[email], first_name=first, last_name=last)
336
+ result = api.people.create(settings=person)
337
+ ```
338
+
339
+ Do NOT call `api.people.create(emails=[...], first_name=...)` — this raises `TypeError`.
340
+
341
+ ---
342
+
343
+ ## Licenses API
344
+
345
+ > For full org-wide license auditing, assignment, and reclamation, see [admin-licensing.md](admin-licensing.md).
346
+
347
+ Base path: `/v1/licenses`
348
+
349
+ ### Listing Licenses
350
+
351
+ ```python
352
+ all_licenses = api.licenses.list()
353
+
354
+ for lic in all_licenses:
355
+ print(f'{lic.name}: {lic.consumed_units}/{lic.total_units}')
356
+ ```
357
+
358
+ ### Finding Specific Licenses
359
+
360
+ The `License` model has convenience properties for common license types:
361
+
362
+ | Property | License Name Matched |
363
+ |----------|---------------------|
364
+ | `lic.webex_calling_professional` | `"Webex Calling - Professional"` |
365
+ | `lic.webex_calling_basic` | `"Webex Calling - Basic"` |
366
+ | `lic.webex_calling_workspaces` | `"Webex Calling - Workspaces"` |
367
+ | `lic.webex_calling` | Any of the above three |
368
+ | `lic.cx_essentials` | `"Customer Experience - Essential"` |
369
+
370
+ **Find the Webex Calling Professional license:**
371
+
372
+ ```python
373
+ wxc_pro_license = next(
374
+ (lic for lic in api.licenses.list()
375
+ if lic.webex_calling_professional),
376
+ None
377
+ )
378
+ ```
379
+
380
+ **Find an available calling license (with capacity):**
381
+
382
+ ```python
383
+ def get_calling_license(api):
384
+ """Get ID of an available calling license with remaining capacity."""
385
+ licenses = [
386
+ lic for lic in api.licenses.list()
387
+ if lic.webex_calling and not lic.webex_calling_workspaces
388
+ ]
389
+ available = next(
390
+ (lic for lic in licenses
391
+ if lic.consumed_units < lic.total_units),
392
+ None
393
+ )
394
+ return available.license_id if available else None
395
+ ```
396
+
397
+ **Find and store both Calling and UCM licenses (for migration):**
398
+
399
+ ```python
400
+ # From Cisco Live lab
401
+ wxc_pro_license = None
402
+ ucm_license = None
403
+
404
+ for lic in api.licenses.list():
405
+ if lic.name == 'Webex Calling - Professional':
406
+ wxc_pro_license = lic
407
+ if lic.name == 'Unified Communication Manager (UCM)':
408
+ ucm_license = lic
409
+ ```
410
+
411
+ ### License Details
412
+
413
+ ```python
414
+ license_detail = api.licenses.details(license_id='<id>')
415
+ print(f'Name: {license_detail.name}')
416
+ print(f'Used: {license_detail.consumed_units}/{license_detail.total_units}')
417
+ print(f'By users: {license_detail.consumed_by_users}')
418
+ print(f'By workspaces: {license_detail.consumed_by_workspaces}')
419
+ ```
420
+
421
+ ### Listing Users Assigned to a License
422
+
423
+ ```python
424
+ users = list(api.licenses.assigned_users(license_id='<id>'))
425
+ for user in users:
426
+ print(f'{user.display_name} ({user.email}) - {user.type}')
427
+ ```
428
+
429
+ ### Assigning Licenses to Users (PATCH Method)
430
+
431
+ The `assign_licenses_to_users` method is the **recommended approach** for license assignment, especially for Webex Calling. It supports adding and removing licenses in a single call, and it handles calling-specific properties (location, phone number, extension).
432
+
433
+ ```python
434
+ from wxc_sdk.licenses import LicenseRequest, LicenseProperties, LicenseRequestOperation
435
+
436
+ # Assign a Webex Calling license with location and extension
437
+ api.licenses.assign_licenses_to_users(
438
+ person_id='<person_id>',
439
+ licenses=[
440
+ LicenseRequest(
441
+ id='<calling_license_id>',
442
+ operation=LicenseRequestOperation.add, # default is 'add'
443
+ properties=LicenseProperties(
444
+ location_id='<location_id>',
445
+ extension='1001'
446
+ )
447
+ )
448
+ ]
449
+ )
450
+ ```
451
+
452
+ **With phone number instead of extension:**
453
+
454
+ ```python
455
+ api.licenses.assign_licenses_to_users(
456
+ person_id='<person_id>',
457
+ licenses=[
458
+ LicenseRequest(
459
+ id='<calling_license_id>',
460
+ properties=LicenseProperties(
461
+ location_id='<location_id>',
462
+ phone_number='+15551234567'
463
+ )
464
+ )
465
+ ]
466
+ )
467
+ ```
468
+
469
+ **Removing a license:**
470
+
471
+ ```python
472
+ api.licenses.assign_licenses_to_users(
473
+ person_id='<person_id>',
474
+ licenses=[
475
+ LicenseRequest(
476
+ id='<license_id_to_remove>',
477
+ operation=LicenseRequestOperation.remove
478
+ )
479
+ ]
480
+ )
481
+ ```
482
+
483
+ **Combined: remove UCM + add Calling in one call:**
484
+
485
+ ```python
486
+ api.licenses.assign_licenses_to_users(
487
+ person_id='<person_id>',
488
+ licenses=[
489
+ LicenseRequest(
490
+ id=ucm_license.license_id,
491
+ operation=LicenseRequestOperation.remove
492
+ ),
493
+ LicenseRequest(
494
+ id=wxc_pro_license.license_id,
495
+ operation=LicenseRequestOperation.add,
496
+ properties=LicenseProperties(
497
+ location_id='<location_id>',
498
+ extension='1001'
499
+ )
500
+ )
501
+ ]
502
+ )
503
+ ```
504
+
505
+ **LicenseProperties requirements for Calling licenses:**
506
+ - Either `phone_number` or `extension` is mandatory.
507
+ - If `phone_number` is not provided, then `location_id` is mandatory.
508
+
509
+ You can identify the user by either `email` or `person_id` (at least one required).
510
+
511
+ ### Raw HTTP
512
+ <!-- Updated by playbook session 2026-03-18 -->
513
+
514
+ ```python
515
+ from wxc_sdk import WebexSimpleApi
516
+ api = WebexSimpleApi()
517
+ BASE = "https://webexapis.com/v1"
518
+
519
+ # ── List all licenses ────────────────────────────────────────────
520
+ result = api.session.rest_get(f"{BASE}/licenses", params={"max": 1000})
521
+ licenses = result.get("items", [])
522
+
523
+ # ── Get license details ──────────────────────────────────────────
524
+ license_detail = api.session.rest_get(f"{BASE}/licenses/{license_id}")
525
+
526
+ # ── Find calling licenses ────────────────────────────────────────
527
+ calling_licenses = [
528
+ lic for lic in licenses
529
+ if "Webex Calling" in lic.get("name", "")
530
+ ]
531
+
532
+ # ── Assign license to user (PATCH) ──────────────────────────────
533
+ # The PATCH endpoint for license assignment:
534
+ body = {
535
+ "email": "jsmith@example.com",
536
+ "licenses": [
537
+ {
538
+ "id": calling_license_id,
539
+ "operation": "add",
540
+ "properties": {
541
+ "locationId": location_id,
542
+ "extension": "1001",
543
+ }
544
+ }
545
+ ]
546
+ }
547
+ api.session.rest_post(f"{BASE}/licenses/users", json=body)
548
+ ```
549
+
550
+ **Note:** The Licenses API is read-only for license inventory (list/details). License assignment to users is done via the `/licenses/users` PATCH-style endpoint (implemented as POST). There is no create/update/delete for license definitions themselves.
551
+
552
+ ### Gotchas
553
+
554
+ **License ID assignment requires the full base64-encoded ID.**
555
+ License IDs in Webex are long base64-encoded strings (e.g., `Y2lzY29zcGFyazov...`). Always retrieve them programmatically from `api.licenses.list()` -- never hardcode them, as they vary by organization and subscription.
556
+
557
+ **SCIM 2.0 is now the recommended path for user creation.**
558
+ As of January 2024, Webex recommends SCIM 2.0 (`wxc_sdk.scim.ScimV2Api`) over the People API for user creation and management due to higher performance and standard connectors. Users created via SCIM can then be licensed using the `assign_licenses_to_users` PATCH method.
559
+
560
+ ---
561
+
562
+ ## Locations API
563
+
564
+ Base path: `/v1/locations`
565
+
566
+ ### Listing Locations
567
+
568
+ ```python
569
+ # All locations
570
+ locations = list(api.locations.list())
571
+
572
+ # Filter by name (case-insensitive contains match)
573
+ locations = list(api.locations.list(name='headquarters'))
574
+
575
+ # Filter by ID
576
+ locations = list(api.locations.list(location_id='<id>'))
577
+ ```
578
+
579
+ ### Find Location by Exact Name
580
+
581
+ The SDK provides a convenience method not available in the raw API:
582
+
583
+ ```python
584
+ location = api.locations.by_name('Pod18')
585
+ if location:
586
+ print(f'Found: {location.name} (ID: {location.location_id})')
587
+ ```
588
+
589
+ This iterates the list internally and matches on exact name equality.
590
+
591
+ ### Location Details
592
+
593
+ ```python
594
+ location = api.locations.details(location_id='<id>')
595
+ print(f'Name: {location.name}')
596
+ print(f'Address: {location.address.address1}, {location.address.city}, {location.address.state}')
597
+ print(f'Timezone: {location.time_zone}')
598
+ ```
599
+
600
+ ### Creating a Location
601
+
602
+ All of the following parameters are **required**:
603
+ - `name`
604
+ - `time_zone`
605
+ - `preferred_language`
606
+ - `announcement_language`
607
+ - `address1`, `city`, `state`, `postal_code`, `country`
608
+
609
+ ```python
610
+ location_id = api.locations.create(
611
+ name='San Jose Office',
612
+ time_zone='America/Los_Angeles',
613
+ preferred_language='en_us',
614
+ announcement_language='en_us',
615
+ address1='123 Main St',
616
+ city='San Jose',
617
+ state='CA',
618
+ postal_code='95113',
619
+ country='US'
620
+ )
621
+ print(f'Created location: {location_id}')
622
+ ```
623
+
624
+ Optional parameters: `address2`, `latitude`, `longitude`, `notes`.
625
+
626
+ **Name length constraint:** While the API supports up to 256 characters, locations that will be **enabled for Webex Calling** must have names with a maximum of **80 characters**.
627
+
628
+ The return value is the new location's ID string.
629
+
630
+ ### Updating a Location
631
+
632
+ ```python
633
+ location = api.locations.details(location_id='<id>')
634
+ location.name = 'San Jose HQ'
635
+ api.locations.update(location_id=location.location_id, settings=location)
636
+ ```
637
+
638
+ The same 80-character name limit applies if the location is calling-enabled.
639
+
640
+ ### Deleting a Location
641
+
642
+ ```python
643
+ api.locations.delete(location_id='<id>')
644
+ ```
645
+
646
+ **Prerequisite:** Webex Calling must be **disabled** for the location before it can be deleted.
647
+
648
+ ### Enabling a Location for Webex Calling
649
+
650
+ Creating a location via the Locations API does **not** automatically enable it for Webex Calling. You must use the separate Location Call Settings API:
651
+
652
+ ```python
653
+ # SDK method:
654
+ api.telephony.location.enable_for_calling(location_id='<id>', ...)
655
+
656
+ # Raw HTTP equivalent:
657
+ api.session.rest_post( # POST — not PUT; URL path should be verified against live API reference
658
+ f"{BASE}/telephony/config/locations/{loc_id}",
659
+ json={"announcementLanguage": "en_us"}, # lowercase required
660
+ )
661
+ ```
662
+
663
+ The `announcement_language` field is **required** when enabling a location for Webex Calling. It must be **lowercase** (`en_us`, not `en_US`) -- see gotchas #13 and #14.
664
+
665
+ ### Raw HTTP
666
+ <!-- Updated by playbook session 2026-03-18 -->
667
+
668
+ ```python
669
+ from wxc_sdk import WebexSimpleApi
670
+ from wxc_sdk.rest import RestError
671
+
672
+ api = WebexSimpleApi()
673
+ BASE = "https://webexapis.com/v1"
674
+
675
+ # ── List locations ───────────────────────────────────────────────
676
+ result = api.session.rest_get(f"{BASE}/locations", params={"max": 1000})
677
+ locations = result.get("items", [])
678
+
679
+ # ── List with name filter ────────────────────────────────────────
680
+ result = api.session.rest_get(f"{BASE}/locations", params={
681
+ "name": "headquarters",
682
+ "max": 1000,
683
+ })
684
+
685
+ # ── Get location details ─────────────────────────────────────────
686
+ location = api.session.rest_get(f"{BASE}/locations/{loc_id}")
687
+
688
+ # ── Create a location ────────────────────────────────────────────
689
+ body = {
690
+ "name": "San Jose Office",
691
+ "timeZone": "America/Los_Angeles",
692
+ "preferredLanguage": "en_us",
693
+ "announcementLanguage": "en_us",
694
+ "address": {
695
+ "address1": "123 Main St",
696
+ "city": "San Jose",
697
+ "state": "CA",
698
+ "postalCode": "95113",
699
+ "country": "US",
700
+ },
701
+ }
702
+ result = api.session.rest_post(f"{BASE}/locations", json=body)
703
+ new_loc_id = result["id"]
704
+
705
+ # ── Update a location ────────────────────────────────────────────
706
+ location = api.session.rest_get(f"{BASE}/locations/{loc_id}")
707
+ location["name"] = "San Jose HQ"
708
+ api.session.rest_put(f"{BASE}/locations/{loc_id}", json=location)
709
+
710
+ # ── Delete a location ────────────────────────────────────────────
711
+ # WARNING: Calling-enabled locations return 409 -- see gotcha #15
712
+ api.session.rest_delete(f"{BASE}/locations/{loc_id}")
713
+
714
+ # ── Enable location for Webex Calling ────────────────────────────
715
+ # This is a SEPARATE telephony endpoint, not the Locations API
716
+ body = {
717
+ "announcementLanguage": "en_us", # MUST be lowercase -- see gotcha #13
718
+ }
719
+ api.session.rest_post( # POST — not PUT; URL path should be verified against live API reference
720
+ f"{BASE}/telephony/config/locations/{loc_id}",
721
+ json=body,
722
+ )
723
+ ```
724
+
725
+ **Raw HTTP gotchas for locations:**
726
+ - `announcementLanguage` must be **lowercase** (`en_us` not `en_US`) when calling `enable_for_calling` -- the telephony backend rejects mixed case with "Invalid Language Code" (gotcha #13)
727
+ - `announcementLanguage` returns `None` from the locations details endpoint even when set -- always set it explicitly before enabling calling (gotcha #14)
728
+ - Calling-enabled locations **cannot be deleted via API** -- returns `409 Conflict: Location is being referenced, cannot be deleted`. Must use Control Hub (gotcha #15)
729
+ - The `safe_delete_check` response uses field `locationDeleteStatus` (not `status`), with value `"UNBLOCKED"` or `"BLOCKED"` (gotcha #17)
730
+ - The `address` field in raw HTTP is a nested object with `address1`, `city`, `state`, `postalCode`, `country`. The SDK flattens these to top-level kwargs in `locations.create()`.
731
+
732
+ ### CLI Examples
733
+
734
+ ```bash
735
+ # ── List all locations ────────────────────────────────────────────
736
+ wxcli locations list
737
+
738
+ # ── Filter locations by name (case-insensitive contains match) ────
739
+ wxcli locations list --name "headquarters"
740
+
741
+ # ── List locations as JSON ────────────────────────────────────────
742
+ wxcli locations list -o json
743
+
744
+ # ── Get location details ──────────────────────────────────────────
745
+ wxcli locations show <location_id>
746
+
747
+ # ── Get location details as table ─────────────────────────────────
748
+ wxcli locations show <location_id> -o table
749
+
750
+ # ── Create a location (all required fields) ───────────────────────
751
+ wxcli locations create \
752
+ --name "San Jose Office" \
753
+ --time-zone "America/Los_Angeles" \
754
+ --preferred-language "en_us" \
755
+ --announcement-language "en_us" \
756
+ --json-body '{
757
+ "address": {
758
+ "address1": "123 Main St",
759
+ "city": "San Jose",
760
+ "state": "CA",
761
+ "postalCode": "95113",
762
+ "country": "US"
763
+ }
764
+ }'
765
+
766
+ # ── Update a location ────────────────────────────────────────────
767
+ wxcli locations update <location_id> --name "San Jose HQ"
768
+ wxcli locations update <location_id> --time-zone "America/New_York"
769
+ ```
770
+
771
+ **CLI notes:**
772
+ - The `locations` group covers both REST CRUD (list, create, show, update, delete) and floors management.
773
+ - `wxcli locations create` requires `--name`, `--time-zone`, `--preferred-language`, and `--announcement-language`. The address must be passed via `--json-body` since it is a nested object.
774
+ - Use lowercase language codes (e.g., `en_us` not `en_US`) for `--announcement-language` to avoid "Invalid Language Code" errors when later enabling calling.
775
+ - Location names must be 80 characters or fewer if the location will be enabled for Webex Calling.
776
+
777
+ ### Floors
778
+
779
+ Locations support floor management for workspace organization:
780
+
781
+ ```python
782
+ # List floors
783
+ floors = api.locations.list_floors(location_id='<id>')
784
+
785
+ # Create a floor
786
+ floor = api.locations.create_floor(location_id='<id>', floor_number=3, display_name='3rd Floor')
787
+
788
+ # Get floor details
789
+ floor = api.locations.floor_details(location_id='<id>', floor_id='<floor_id>')
790
+
791
+ # Update a floor
792
+ floor.display_name = 'Third Floor - Engineering'
793
+ api.locations.update_floor(floor=floor)
794
+
795
+ # Delete a floor
796
+ api.locations.delete_floor(location_id='<id>', floor_id='<floor_id>')
797
+ ```
798
+
799
+ ### Gotchas
800
+
801
+ **Location must exist before user assignment.**
802
+ You cannot assign a user to a location that does not exist. Create the location first, optionally enable it for calling, then assign users.
803
+
804
+ **Location name length for calling.**
805
+ Locations enabled for Webex Calling must have names of **80 characters or fewer**. The general Locations API allows 256, but calling features and Control Hub enforce the shorter limit.
806
+
807
+ **`enable_for_calling` requires lowercase language codes.**
808
+ The telephony `enable_for_calling` API rejects `en_US` (mixed case) for `announcement_language` with error `Invalid Language Code`. Use `en_us` (all lowercase). The general Locations API stores `preferredLanguage` as `en_US` but the telephony backend expects lowercase.
809
+
810
+ ```python
811
+ location = api.locations.details(location_id=loc_id)
812
+ if not location.announcement_language:
813
+ location.announcement_language = (location.preferred_language or "en_US").lower()
814
+ api.telephony.location.enable_for_calling(location=location)
815
+ ```
816
+
817
+ **`announcement_language` returns None from details endpoint.**
818
+ `LocationsApi.details()` returns `announcement_language = None` even on locations that have it set. This is a Webex API inconsistency. Always set it explicitly before calling `enable_for_calling`.
819
+
820
+ **Cannot delete calling-enabled locations via API.**
821
+ `LocationsApi.delete()` returns `409 Conflict: Location is being referenced, cannot be deleted` for any location with Webex Calling enabled. There is **no API to disable calling on a location** — `wxcadm` confirms: "There is currently no way to delete a Location outside of Control Hub." The `safe_delete_check_before_disabling_calling_location` precheck may return `UNBLOCKED` but the delete still fails due to the telephony reference.
822
+
823
+ Calling-enabled locations can only be deleted from Control Hub.
824
+
825
+ **`SafeDeleteCheckResponse` uses `location_delete_status`, not `status`.**
826
+ The response model field is `location_delete_status` (value: `"UNBLOCKED"` or `"BLOCKED"`), not `status`. The `blocking` field contains a model with `users_in_use_count`, `trunks_in_use_count`, etc.
827
+
828
+ ---
829
+
830
+ ## Organization API
831
+
832
+ Base path: `/v1/organizations`
833
+
834
+ ### Listing Organizations
835
+
836
+ ```python
837
+ orgs = api.organizations.list()
838
+
839
+ # With XSI (BroadSoft) endpoint data
840
+ orgs = api.organizations.list(calling_data=True)
841
+ ```
842
+
843
+ ### Organization Details
844
+
845
+ ```python
846
+ org = api.organizations.details(org_id='<id>', calling_data=True)
847
+ print(f'Name: {org.display_name}')
848
+ print(f'XSI Actions: {org.xsi_actions_endpoint}')
849
+ print(f'XSI Events: {org.xsi_events_endpoint}')
850
+ print(f'XSI Domain: {org.xsi_domain}')
851
+ ```
852
+
853
+ The `calling_data=True` parameter returns XSI (BroadSoft) endpoint values:
854
+ - `xsi_actions_endpoint` -- base path to xsi-actions
855
+ - `xsi_events_endpoint` -- base path to xsi-events
856
+ - `xsi_events_channel_endpoint` -- base path to xsi-events-channel
857
+ - `xsi_domain` -- api-prepended bcBaseDomain for the org
858
+
859
+ ### Deleting an Organization
860
+
861
+ ```python
862
+ api.organizations.delete(org_id='<id>')
863
+ ```
864
+
865
+ Requires authorization from a user with the **Full Administrator Role**. Deletion may take up to 10 minutes to complete after the response returns.
866
+
867
+ ### Raw HTTP
868
+
869
+ ```python
870
+ from wxc_sdk import WebexSimpleApi
871
+ api = WebexSimpleApi()
872
+ BASE = "https://webexapis.com/v1"
873
+
874
+ # ── List organizations ──────────────────────────────────────────
875
+ result = api.session.rest_get(f"{BASE}/organizations", params={"max": 1000})
876
+ orgs = result.get("items", [])
877
+
878
+ # ── List with calling data (XSI endpoints) ─────────────────────
879
+ result = api.session.rest_get(f"{BASE}/organizations", params={
880
+ "callingData": "true",
881
+ "max": 1000,
882
+ })
883
+
884
+ # ── Get organization details ────────────────────────────────────
885
+ org = api.session.rest_get(f"{BASE}/organizations/{org_id}", params={
886
+ "callingData": "true",
887
+ })
888
+ # XSI fields: xsiActionsEndpoint, xsiEventsEndpoint,
889
+ # xsiEventsChannelEndpoint, xsiDomain
890
+
891
+ # ── Delete an organization ──────────────────────────────────────
892
+ # WARNING: Requires Full Administrator Role. Deletion takes up to 10 min.
893
+ api.session.rest_delete(f"{BASE}/organizations/{org_id}")
894
+ ```
895
+
896
+ **Key differences from typed SDK:**
897
+ - `callingData` is a string `"true"`, not a Python bool
898
+ - Response key for list is `items`, not `organizations`
899
+ - XSI field names are camelCase in raw JSON: `xsiActionsEndpoint`, `xsiEventsEndpoint`, `xsiEventsChannelEndpoint`, `xsiDomain`
900
+
901
+ ---
902
+
903
+ ## Numbers API
904
+
905
+ Base path: `/v1/telephony/config/numbers` (list) and `/v1/telephony/config/locations/{locationId}/numbers` (add/activate/remove)
906
+
907
+ Numbers must be added to a location's inventory before they can be assigned to users or workspaces. The Numbers API manages the phone number lifecycle: adding numbers to locations, activating/deactivating them, and removing them.
908
+
909
+ ### CLI Examples
910
+
911
+ ```bash
912
+ # ── List all phone numbers in the org ─────────────────────────────
913
+ wxcli numbers list
914
+
915
+ # ── List numbers for a specific location ──────────────────────────
916
+ wxcli numbers list --location-id <location_id>
917
+
918
+ # ── Search for a specific phone number ────────────────────────────
919
+ wxcli numbers list --phone-number "+15551234567"
920
+
921
+ # ── List only available (unassigned) numbers ──────────────────────
922
+ wxcli numbers list --available true
923
+
924
+ # ── Filter by number state ────────────────────────────────────────
925
+ wxcli numbers list --state ACTIVE
926
+ wxcli numbers list --state INACTIVE
927
+
928
+ # ── Filter by owner type ──────────────────────────────────────────
929
+ wxcli numbers list --owner-type PEOPLE
930
+ wxcli numbers list --owner-type PLACE
931
+
932
+ # ── Filter by number type ────────────────────────────────────────
933
+ wxcli numbers list --number-type NUMBER
934
+ wxcli numbers list --number-type EXTENSION
935
+
936
+ # ── List numbers as JSON ──────────────────────────────────────────
937
+ wxcli numbers list -o json
938
+
939
+ # ── Add phone numbers to a location ───────────────────────────────
940
+ wxcli numbers create <location_id> --json-body '{
941
+ "phoneNumbers": ["+15551234567", "+15551234568"]
942
+ }'
943
+
944
+ # ── Activate numbers in a location ────────────────────────────────
945
+ wxcli numbers update <location_id> --action ACTIVATE --json-body '{
946
+ "phoneNumbers": ["+15551234567"]
947
+ }'
948
+
949
+ # ── Deactivate numbers in a location ──────────────────────────────
950
+ wxcli numbers update <location_id> --action DEACTIVATE --json-body '{
951
+ "phoneNumbers": ["+15551234567"]
952
+ }'
953
+
954
+ # ── Remove phone numbers from a location ──────────────────────────
955
+ wxcli numbers delete <location_id>
956
+
957
+ # ── Remove without confirmation prompt ────────────────────────────
958
+ wxcli numbers delete <location_id> --force
959
+
960
+ # ── Validate phone numbers before adding ──────────────────────────
961
+ wxcli numbers validate-phone-numbers --json-body '{
962
+ "phoneNumbers": ["+15551234567", "+15551234568"]
963
+ }'
964
+ ```
965
+
966
+ **CLI notes:**
967
+ - The `numbers` group covers number CRUD, validation, and manage-numbers jobs.
968
+ - `wxcli numbers list` returns org-wide numbers by default. Use `--location-id` to scope to a specific location.
969
+ - Adding and activating numbers requires `--json-body` with a `phoneNumbers` array since the CLI cannot flatten nested array body fields into flags.
970
+ - Numbers must be added to a location (`create`) and activated (`update --action ACTIVATE`) before they can be assigned to users.
971
+ - Use `validate-phone-numbers` to check number validity before adding them to avoid errors.
972
+
973
+ ---
974
+
975
+ ## Provisioning Workflow
976
+
977
+ Step-by-step process to enable a user for Webex Calling. Based on the Cisco Live provisioning lab and SDK test patterns.
978
+
979
+ ### Method A: Using People API (update licenses on Person object)
980
+
981
+ This approach manipulates the `licenses` array directly on the Person object.
982
+
983
+ ```python
984
+ from wxc_sdk import WebexSimpleApi
985
+
986
+ api = WebexSimpleApi()
987
+
988
+ # ── Step 1: Find the required licenses ──────────────────────────────
989
+ wxc_pro_license = None
990
+ ucm_license = None
991
+
992
+ for lic in api.licenses.list():
993
+ if lic.name == 'Webex Calling - Professional':
994
+ wxc_pro_license = lic
995
+ if lic.name == 'Unified Communication Manager (UCM)':
996
+ ucm_license = lic
997
+
998
+ print(f'WxC Pro: {wxc_pro_license.license_id}')
999
+ print(f'UCM: {ucm_license.license_id if ucm_license else "not found"}')
1000
+
1001
+ # ── Step 2: Find the target location ────────────────────────────────
1002
+ location = api.locations.by_name('San Jose Office')
1003
+ print(f'Location: {location.name} (ID: {location.location_id})')
1004
+
1005
+ # ── Step 3: Get the user's full details ─────────────────────────────
1006
+ user = next(api.people.list(email='jsmith@example.com'), None)
1007
+ user_details = api.people.details(person_id=user.person_id, calling_data=True)
1008
+
1009
+ # ── Step 4: Update licenses and location ────────────────────────────
1010
+ # Add Webex Calling Professional license
1011
+ if wxc_pro_license.license_id not in user_details.licenses:
1012
+ user_details.licenses.append(wxc_pro_license.license_id)
1013
+
1014
+ # Remove UCM license (if migrating from on-prem)
1015
+ if ucm_license and ucm_license.license_id in user_details.licenses:
1016
+ user_details.licenses.remove(ucm_license.license_id)
1017
+
1018
+ # Set location and extension
1019
+ user_details.location_id = location.location_id
1020
+ user_details.extension = '1001'
1021
+
1022
+ # ── Step 5: Push the update ─────────────────────────────────────────
1023
+ updated_user = api.people.update(person=user_details, calling_data=True)
1024
+
1025
+ # ── Step 6: Verify ──────────────────────────────────────────────────
1026
+ verified = api.people.details(person_id=updated_user.person_id, calling_data=True)
1027
+ assert verified.location_id is not None
1028
+ assert wxc_pro_license.license_id in verified.licenses
1029
+ print(f'User {verified.display_name} enabled for calling at {location.name}')
1030
+ ```
1031
+
1032
+ ### Method B: Using Licenses PATCH API (recommended for new provisioning)
1033
+
1034
+ This approach uses the dedicated `assign_licenses_to_users` PATCH endpoint, which is cleaner for new license assignments and supports calling-specific properties natively.
1035
+
1036
+ ```python
1037
+ from wxc_sdk import WebexSimpleApi
1038
+ from wxc_sdk.licenses import LicenseRequest, LicenseProperties
1039
+ from wxc_sdk.people import Person
1040
+
1041
+ api = WebexSimpleApi()
1042
+
1043
+ # ── Step 1: Find the calling license ────────────────────────────────
1044
+ calling_license_id = next(
1045
+ (lic.license_id for lic in api.licenses.list()
1046
+ if lic.webex_calling_professional
1047
+ and lic.consumed_units < lic.total_units),
1048
+ None
1049
+ )
1050
+
1051
+ # ── Step 2: Find the target location ────────────────────────────────
1052
+ location = api.locations.by_name('San Jose Office')
1053
+
1054
+ # ── Step 3: Create the user (if new) ────────────────────────────────
1055
+ new_user = api.people.create(
1056
+ settings=Person(
1057
+ emails=['jsmith@example.com'],
1058
+ display_name='John Smith',
1059
+ first_name='John',
1060
+ last_name='Smith'
1061
+ )
1062
+ )
1063
+
1064
+ # ── Step 4: Assign calling license with location and extension ──────
1065
+ response = api.licenses.assign_licenses_to_users(
1066
+ person_id=new_user.person_id,
1067
+ licenses=[
1068
+ LicenseRequest(
1069
+ id=calling_license_id,
1070
+ properties=LicenseProperties(
1071
+ location_id=location.location_id,
1072
+ extension='1001'
1073
+ )
1074
+ )
1075
+ ]
1076
+ )
1077
+
1078
+ # ── Step 5: Verify ──────────────────────────────────────────────────
1079
+ verified = api.people.details(person_id=new_user.person_id, calling_data=True)
1080
+ assert calling_license_id in verified.licenses
1081
+ assert verified.location_id is not None
1082
+ print(f'License response: {response}')
1083
+ ```
1084
+
1085
+ ### Method B is preferred when:
1086
+ - You are provisioning net-new users (create user, then assign license separately).
1087
+ - You need to set calling-specific properties (`phone_number`, `extension`, `location_id`) as part of the license assignment in one atomic call.
1088
+ - You are bulk-provisioning users created via SCIM 2.0.
1089
+
1090
+ ### Method A is preferred when:
1091
+ - You are migrating existing users (e.g., removing UCM, adding WxC).
1092
+ - You need to modify other Person fields at the same time as license changes.
1093
+
1094
+ ---
1095
+
1096
+ ## Data Models
1097
+
1098
+ ### Person
1099
+
1100
+ | Field | Type | Notes |
1101
+ |-------|------|-------|
1102
+ | `person_id` | `str` | Unique ID (aliased from `id` in JSON) |
1103
+ | `emails` | `list[str]` | Currently only one email supported |
1104
+ | `phone_numbers` | `list[PhoneNumber]` | Requires `calling_data=True` to populate |
1105
+ | `extension` | `str` | Webex Calling extension (no routing prefix). Requires `calling_data=True` |
1106
+ | `location_id` | `str` | Calling location. Requires `calling_data=True`. Only settable on initial calling license assignment |
1107
+ | `display_name` | `str` | Full name |
1108
+ | `first_name` | `str` | |
1109
+ | `last_name` | `str` | |
1110
+ | `org_id` | `str` | Organization |
1111
+ | `licenses` | `list[str]` | License IDs assigned to user |
1112
+ | `roles` | `list[str]` | Role IDs |
1113
+ | `department` | `str` | Business department |
1114
+ | `manager` | `str` | Manager identifier |
1115
+ | `manager_id` | `str` | Manager's Person ID |
1116
+ | `title` | `str` | Job title |
1117
+ | `addresses` | `list[PersonAddress]` | Postal addresses |
1118
+ | `site_urls` | `list[str]` | Webex Meetings site URLs |
1119
+ | `timezone` | `str` | User's timezone |
1120
+ | `status` | `PeopleStatus` | Presence status (org members only) |
1121
+ | `invite_pending` | `bool` | Awaiting activation (admin-visible only) |
1122
+ | `login_enabled` | `bool` | Can use Webex (admin-visible only) |
1123
+ | `person_type` | `PersonType` | `person`, `bot`, or `appuser` |
1124
+ | `sip_addresses` | `list[SipAddress]` | Read-only SIP addresses |
1125
+ | `created` | `datetime` | Read-only |
1126
+ | `last_modified` | `datetime` | Read-only |
1127
+
1128
+ **Helper properties on Person:**
1129
+ - `person_id_uuid` -- Person ID decoded to UUID format
1130
+ - `plus_e164` -- List of +E.164 phone numbers
1131
+ - `tn` -- First +E.164 phone number (primary TN)
1132
+
1133
+ ### PhoneNumber
1134
+
1135
+ | Field | Type | Values |
1136
+ |-------|------|--------|
1137
+ | `number_type` | `PhoneNumberType` | `work`, `work_extension`, `mobile`, `fax`, `enterprise`, `alternate1`, `alternate2` |
1138
+ | `value` | `str` | The phone number or extension value |
1139
+ | `primary` | `bool` | Whether this is the primary number |
1140
+
1141
+ Note: `work_extension` values include the location **routing prefix** prepended to the extension. When setting `extension` on a Person, do **not** include the routing prefix.
1142
+
1143
+ ### License
1144
+
1145
+ | Field | Type | Notes |
1146
+ |-------|------|-------|
1147
+ | `license_id` | `str` | Unique ID (aliased from `id`) |
1148
+ | `name` | `str` | Human-readable license name |
1149
+ | `total_units` | `int` | Total allocated |
1150
+ | `consumed_units` | `int` | Total consumed |
1151
+ | `consumed_by_users` | `int` | Consumed by users |
1152
+ | `consumed_by_workspaces` | `int` | Consumed by workspaces |
1153
+ | `subscription_id` | `str` | Associated subscription |
1154
+ | `site_url` | `str` | Webex Meetings site (if applicable) |
1155
+ | `site_type` | `SiteType` | `Control Hub managed site`, `Linked site`, or `Site Admin managed site` |
1156
+
1157
+ **Helper properties on License:**
1158
+ - `webex_calling` -- True if any calling license type
1159
+ - `webex_calling_professional` -- True if name is `"Webex Calling - Professional"`
1160
+ - `webex_calling_basic` -- True if name is `"Webex Calling - Basic"`
1161
+ - `webex_calling_workspaces` -- True if name is `"Webex Calling - Workspaces"`
1162
+ - `cx_essentials` -- True if name is `"Customer Experience - Essential"`
1163
+
1164
+ > **Note — "Basic" vs. "Standard" tier naming:** The `"Webex Calling - Basic"` name matched by `lic.webex_calling_basic` may be stale for newer orgs. Cisco's current tier naming uses **Standard** (not Basic). Any code doing exact string matching on `"Webex Calling - Basic"` may miss Standard-tier licenses. The SDK property `lic.webex_calling_basic` is an internal convenience name and may still resolve correctly depending on SDK version, but verify the actual license name string returned by `GET /v1/licenses` in your org before relying on exact string matches.
1165
+
1166
+ ### LicenseRequest (for PATCH assignment)
1167
+
1168
+ | Field | Type | Notes |
1169
+ |-------|------|-------|
1170
+ | `id` | `str` | License ID to add/remove |
1171
+ | `operation` | `LicenseRequestOperation` | `add` (default) or `remove` |
1172
+ | `properties` | `LicenseProperties` | Location, phone, extension (for calling licenses) |
1173
+
1174
+ ### LicenseProperties
1175
+
1176
+ | Field | Type | Notes |
1177
+ |-------|------|-------|
1178
+ | `location_id` | `str` | Required if `phone_number` not provided |
1179
+ | `phone_number` | `str` | Work phone number (E.164) |
1180
+ | `extension` | `str` | Webex Calling extension |
1181
+
1182
+ Either `phone_number` or `extension` is mandatory for Calling license assignment. If `phone_number` is omitted, `location_id` is mandatory.
1183
+
1184
+ ### Location
1185
+
1186
+ | Field | Type | Notes |
1187
+ |-------|------|-------|
1188
+ | `location_id` | `str` | Unique ID (aliased from `id`) |
1189
+ | `name` | `str` | Max 80 chars if calling-enabled |
1190
+ | `org_id` | `str` | Organization |
1191
+ | `address` | `LocationAddress` | Physical address |
1192
+ | `time_zone` | `str` | IANA timezone |
1193
+ | `preferred_language` | `str` | Default email language |
1194
+ | `announcement_language` | `str` | Phone announcement language. Required for calling enablement |
1195
+ | `latitude` | `float` | |
1196
+ | `longitude` | `float` | |
1197
+ | `notes` | `str` | |
1198
+
1199
+ ### LocationAddress
1200
+
1201
+ | Field | Type |
1202
+ |-------|------|
1203
+ | `address1` | `str` |
1204
+ | `address2` | `str` |
1205
+ | `city` | `str` |
1206
+ | `state` | `str` |
1207
+ | `postal_code` | `str` |
1208
+ | `country` | `str` (ISO-3166 2-letter code) |
1209
+
1210
+ ### Organization
1211
+
1212
+ | Field | Type | Notes |
1213
+ |-------|------|-------|
1214
+ | `org_id` | `str` | Unique ID (aliased from `id`) |
1215
+ | `display_name` | `str` | Organization name |
1216
+ | `created` | `datetime` | Creation timestamp |
1217
+ | `xsi_actions_endpoint` | `str` | BroadSoft XSI actions path. Requires `calling_data=True` |
1218
+ | `xsi_events_endpoint` | `str` | BroadSoft XSI events path. Requires `calling_data=True` |
1219
+ | `xsi_events_channel_endpoint` | `str` | XSI events channel path. Requires `calling_data=True` |
1220
+ | `xsi_domain` | `str` | api-prepended bcBaseDomain. Requires `calling_data=True` |
1221
+
1222
+ ---
1223
+
1224
+ ## Gotchas
1225
+
1226
+ Cross-cutting gotchas that span multiple API surfaces. Section-specific gotchas are inline within their respective sections above.
1227
+
1228
+ ### 403 errors on license or people calls
1229
+
1230
+ Usually means the access token does not have admin scopes. Verify the token has `spark-admin:people_read`, `spark-admin:people_write`, and `spark-admin:licenses_read`.
1231
+
1232
+ ### Phone numbers must be provisioned to the location first
1233
+
1234
+ Before assigning a DID/TN to a user, the number must already be added to the location's number inventory via the telephony location numbers API:
1235
+
1236
+ ```python
1237
+ api.telephony.location.number.add(
1238
+ location_id='<location_id>',
1239
+ phone_numbers=['+15551234567']
1240
+ )
1241
+ ```
1242
+
1243
+ ### Numbers list API returns key `phoneNumbers`, not `numbers`
1244
+
1245
+ `GET /telephony/config/numbers` returns a response body with the key `phoneNumbers`, not `numbers`. Code that looks for `response['numbers']` will get a `KeyError`.
1246
+
1247
+ ### Workspaces list API returns key `items`, not `workspaces`
1248
+
1249
+ `GET /workspaces` returns a response body with the key `items`, not `workspaces`. Parse using `response['items']` when working with raw API responses.
1250
+
1251
+ ### Manage numbers jobs list API returns key `items`, not `manageNumbers`
1252
+
1253
+ The manage numbers jobs list endpoint returns its results under the key `items`, not `manageNumbers`. This is inconsistent with other telephony job endpoints.
1254
+
1255
+ ### Manage numbers job body uses `numberList` array, not `phoneNumbers`
1256
+
1257
+ The manage numbers job creation body expects a `numberList` array where each element contains `locationId` and `numbers`. Do not use `phoneNumbers` as the key -- the API will reject or ignore it.
1258
+
1259
+ ### Location creation is two steps: create + enable calling
1260
+
1261
+ `POST /v1/locations` creates the location but does NOT enable Webex Calling. You must separately call `POST /v1/telephony/config/locations` with the location's `id`, `name`, `timeZone`, `preferredLanguage`, `announcementLanguage`, and `address`. Without this second call, assigning calling-licensed users to the location fails with "Calling flag not set".
1262
+
1263
+ ### Calling user creation requires extension or phone number
1264
+
1265
+ `POST /v1/people?callingData=true` with a calling license and location requires either `extension` or `phoneNumbers` in the body. The API rejects with "Create Calling user either Phone number or Extension is required" if neither is provided. You cannot create a calling user first and assign an extension separately — it must be done atomically.
1266
+
1267
+ ### User create with callingData=false may silently create the user
1268
+
1269
+ If `POST /v1/people?callingData=false` fails with 400 (e.g., "Calling flag not set"), the user may have already been created without calling configuration. A subsequent retry returns 409 Conflict. Always check `GET /v1/people?email=...` before retrying user creation. If the user exists, use `PUT` to update with calling data instead.
1270
+
1271
+ ### Number porting has no public API
1272
+
1273
+ Number port-in requests, LOA submission, porting status tracking, and new number ordering from Cisco Calling Plan are all done through the Control Hub UI or via Cisco's PTS (PSTN Technical Support) team. The Numbers API (`wxcli numbers`) only manages numbers *after* they are ported in or provisioned — it cannot initiate a port.
1274
+
1275
+ ### Location deletion via API is unreliable for calling-enabled locations
1276
+
1277
+ Locations with Webex Calling enabled cannot be deleted directly via the public
1278
+ API (`409 Conflict: Location is being referenced`). The older guidance to
1279
+ "disable calling first" via a `wxcli location-call-settings update-location-calling`
1280
+ command is stale for this repo:
1281
+
1282
+ - that command does **not** exist in the current CLI
1283
+ - the public API does **not** expose a reliable "disable calling on location"
1284
+ operation for general teardown workflows
1285
+
1286
+ What you *can* do with CLI/API:
1287
+ 1. Delete all location-scoped resources first (virtual lines, call parks, hunt groups, call queues, schedules, trunks, devices, workspaces, users)
1288
+ 2. Run `wxcli location-settings safe-delete-check LOCATION_ID` to inspect visible blockers
1289
+ 3. Retry `wxcli locations delete --force LOCATION_ID`
1290
+
1291
+ What may still require Control Hub:
1292
+ - final disable/delete of a calling-enabled location, even after all visible
1293
+ dependencies have been removed via API
1294
+
1295
+ Practical operator rule: use CLI/API to clear dependencies, but warn the user
1296
+ that the final location removal may still need to be completed in Control Hub.
1297
+
1298
+ ---
1299
+
1300
+ ## Bulk Cleanup / Teardown
1301
+
1302
+ When tearing down resources programmatically (e.g., cleaning up after a stress test or migration dry run), resources must be deleted in **reverse dependency order**. Deleting in the wrong order produces 409 (Conflict) or 400 (reference still exists) errors.
1303
+
1304
+ ### Deletion Order (top-to-bottom)
1305
+
1306
+ | Step | Resource Type | wxcli Delete Command | Notes |
1307
+ |------|--------------|---------------------|-------|
1308
+ | 1 | Dial Plans | `wxcli call-routing delete --force {id}` | Just `delete`, not `delete-dial-plan` |
1309
+ | 2 | Route Lists | `wxcli call-routing delete-route-lists --force {id}` | Plural suffix |
1310
+ | 3 | Route Groups | `wxcli call-routing delete-route-groups --force {id}` | Plural suffix |
1311
+ | 4 | Translation Patterns | `wxcli call-routing delete-translation-patterns-call-routing --force {id}` | |
1312
+ | 5 | Trunks | `wxcli call-routing delete-trunks --force {id}` | Check for dial plan refs first (Error 27349) |
1313
+ | 6 | Call Queues | `wxcli call-queue delete --force {locationId} {id}` | Needs locationId |
1314
+ | 7 | Hunt Groups | `wxcli hunt-group delete --force {locationId} {id}` | Needs locationId |
1315
+ | 8 | Auto Attendants | `wxcli auto-attendant delete --force {locationId} {id}` | Needs locationId |
1316
+ | 9 | Paging Groups | `wxcli paging-group delete --force {locationId} {id}` | Needs locationId |
1317
+ | 10 | Call Parks | `wxcli call-park delete --force {locationId} {id}` | **Must list per-location** (see below) |
1318
+ | 11 | Call Pickups | `wxcli call-pickup delete --force {locationId} {id}` | **Must list per-location** (see below) |
1319
+ | 12 | Virtual Lines | Raw `DELETE /v1/telephony/config/virtualLines/{id}` | See VL bug below |
1320
+ | 13 | Workspaces | `wxcli workspaces delete --force {id}` | |
1321
+ | 14 | Users | `wxcli users delete --force {id}` | |
1322
+ | 15 | Schedules | `wxcli location-schedules delete --force {locationId} {type} {scheduleId}` | |
1323
+ | 16 | Locations | `wxcli locations delete --force {id}` | Clear blockers first; final delete of a calling-enabled location may still require Control Hub |
1324
+
1325
+ ### Key Behaviors
1326
+
1327
+ **Always use `--force` on delete commands** to skip the `[y/N]` confirmation prompt that blocks non-interactive execution.
1328
+
1329
+ **Call Parks and Call Pickups must be listed per-location.** `wxcli call-park list` and `wxcli call-pickup list` without a location argument return empty even when resources exist. You must iterate over each location:
1330
+ ```bash
1331
+ for LOC_ID in $(wxcli locations list -o json | jq -r '.[].id // empty'); do
1332
+ wxcli call-park list "$LOC_ID" -o json
1333
+ wxcli call-pickup list "$LOC_ID" -o json
1334
+ done
1335
+ ```
1336
+
1337
+ **Virtual Line ID type mismatch bug:** The Numbers API returns virtual line owners with `VIRTUAL_LINE`-encoded IDs, but `wxcli virtual-extensions delete` sends them to the `/virtualExtensions/` endpoint which expects `VIRTUAL_EXTENSION`-encoded IDs. This always fails with 400 "Wrong type of Webex ID provided". Workaround: use raw HTTP `DELETE /v1/telephony/config/virtualLines/{id}` with a bearer token. Discover VL IDs from `wxcli numbers list -o json` (owner field) — `wxcli virtual-extensions list` may return empty.
1338
+
1339
+ **Location deletion cooldown:** Even after disabling calling and deleting all sub-resources, location DELETE may return 409 for minutes to hours. Preferred recovery: **re-invoke `wxcli cleanup run` (it is idempotent and resumes from where it left off)** — do NOT write an inline Python/bash `time.sleep` polling loop inside a single Bash tool call. The Bash tool has a ~10-minute hard timeout and long loops die silently mid-wait, leaving partial state. If bespoke retry is required, split into **discrete Bash tool calls** (one per location per attempt) with the sleep **between** tool calls, cap total wall time at ≤3 minutes, and if 409s persist, report them and stop rather than looping. See `.claude/skills/teardown/SKILL.md` → "Rule: never hand-roll polling loops".
1340
+
1341
+ **CCP-integrated PSTN gotcha (dCloud / Cisco Calling Plan orgs):** On orgs using Cisco Calling Plan, number deletion returns `ERR.V.TRM.TMN60004` ("DELETE number is supported only for non-integrated CCP") because phone-number lifecycle is owned by the PSTN portal, not the API. After trunk/route-group teardown, location DELETE then 409s with "being referenced" indefinitely — not because of locally visible resources but because Webex's internal PSTN backend is async-releasing trunk references. This typically clears in **1-4 hours** and no API action can unblock it. `wxcli cleanup run` detects this signature in two places:
1342
+ 1. **Numbers:** a `ERR.V.TRM.TMN60004` response is logged as `[number=<ext/e164>] skipped — CCP-integrated, managed via PSTN portal` and treated as a clean skip, not a failure.
1343
+ 2. **Locations:** if the 409 body contains `ERR.V.TRM.TMN60004`, OR says "being referenced" while a pre-check finds no local dependencies (users, workspaces, devices, features, trunks, route groups), the retry loop short-circuits and the cleanup exits 0 with a dedicated footer: `N locations blocked by CCP backend — retry in a few hours`. Re-invoke `wxcli cleanup run` later — the command is idempotent and picks up where it left off.
1344
+
1345
+ **Trunk deletion requires no remaining references:** Error 27349 names the referencing dial plan in the message.
1346
+
1347
+ ### API Format Differences for Members
1348
+
1349
+ Different features use different agent/member formats in POST bodies:
1350
+
1351
+ | Feature | Field | Format | Example |
1352
+ |---------|-------|--------|---------|
1353
+ | Hunt Group | `agents` | Array of objects | `[{"id": "person_id", "weight": 50}]` |
1354
+ | Call Queue | `agents` | Array of objects | `[{"id": "person_id"}]` |
1355
+ | Call Pickup | `agents` | Array of strings | `["person_id_1", "person_id_2"]` |
1356
+ | Paging Group | `targets`, `originators` | Array of strings | `["person_id_1"]` |
1357
+
1358
+ Using the wrong format (e.g., `[{"id": ...}]` for pickup) produces 400 "Invalid field value: agents".
1359
+
1360
+ ---
1361
+
1362
+ ## See Also
1363
+
1364
+ - **`authentication.md`** — Token setup, OAuth flows, and scope reference.
1365
+ - **`archive/wxc-sdk-patterns.md`** — Async bulk provisioning patterns (recipes 5.3, 5.4), workspace provisioning (recipe 5.12). Archived: historical SDK doc.
1366
+ - **`location-calling-core.md`** — Location calling enablement and location-level telephony configuration.