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,1444 @@
1
+ <!-- Updated by playbook session 2026-05-01 -->
2
+ # Webhooks & Telephony Events Reference
3
+
4
+ ## Sources
5
+
6
+ - wxc_sdk v1.30.0
7
+ - OpenAPI spec: specs/webex-cloud-calling.json
8
+ - developer.webex.com Webhooks APIs
9
+
10
+ Webex webhooks deliver real-time notifications to your application when resources change. This document covers webhook CRUD operations and the `telephony_calls` resource events specific to Webex Calling.
11
+
12
+ ## Table of Contents
13
+
14
+ 1. [Webhook CRUD Operations](#1-webhook-crud-operations)
15
+ 2. [Webhook Data Model](#2-webhook-data-model)
16
+ 3. [Webhook Resources (All Available)](#3-webhook-resources-all-available)
17
+ 4. [Telephony Call Events](#4-telephony-call-events)
18
+ 5. [Filtering Telephony Webhooks](#5-filtering-telephony-webhooks)
19
+ 6. [Webhook Setup: Step-by-Step](#6-webhook-setup-step-by-step)
20
+ 7. [Webhook Event Data Class Hierarchy](#7-webhook-event-data-class-hierarchy)
21
+ 8. [Webhook Security: HMAC Signature Verification](#8-webhook-security-hmac-signature-verification)
22
+ 9. [Key Gotchas](#9-key-gotchas)
23
+ 10. [Org-Level Webhooks](#10-org-level-webhooks)
24
+ 11. [Webhook Delivery Mechanics](#11-webhook-delivery-mechanics)
25
+ 12. [Service App Webhooks](#12-service-app-webhooks)
26
+ 13. [callSessionId Correlation Pattern](#13-callsessionid-correlation-pattern)
27
+ 14. [Scale Patterns](#14-scale-patterns)
28
+ 15. [wxcli Command Reference](#15-wxcli-command-reference)
29
+
30
+ ---
31
+
32
+ ## Required Scopes
33
+
34
+ | Scope | Purpose |
35
+ |-------|---------|
36
+ | `spark:calls_read` | Required to create a webhook for `telephony_calls` resource (user-level) |
37
+ | `spark:all` | Firehose webhook (resource=`all`, event=`all`) |
38
+
39
+ Creating a webhook requires **read** scope on the resource the webhook is for. For telephony call events, that means `spark:calls_read`.
40
+
41
+ ---
42
+
43
+ ## Raw HTTP Reference (All Webhook Endpoints)
44
+
45
+ Webhook endpoints use standard REST verbs on `https://webexapis.com/v1/webhooks`. No special scoping beyond read access for the target resource.
46
+
47
+ ```python
48
+ from wxc_sdk import WebexSimpleApi
49
+ api = WebexSimpleApi()
50
+ BASE = "https://webexapis.com/v1"
51
+ ```
52
+
53
+ ### Webhook CRUD Endpoints
54
+
55
+ | Action | Method | URL | Body / Params |
56
+ |--------|--------|-----|---------------|
57
+ | List Webhooks | GET | `{BASE}/webhooks` | Query: `ownedBy` (optional) |
58
+ | Create Webhook | POST | `{BASE}/webhooks` | `name`, `targetUrl`, `resource`, `event`, `filter`, `secret`, `ownedBy` |
59
+ | Get Webhook | GET | `{BASE}/webhooks/{webhookId}` | (none) |
60
+ | Update Webhook | PUT | `{BASE}/webhooks/{webhookId}` | `name`, `targetUrl`, `secret`, `status` |
61
+ | Delete Webhook | DELETE | `{BASE}/webhooks/{webhookId}` | (none) |
62
+
63
+ ### Raw HTTP Examples
64
+
65
+ #### Create a telephony call webhook
66
+
67
+ ```python
68
+ body = {
69
+ "name": "Call Events",
70
+ "targetUrl": "https://example.com/webhooks/calls",
71
+ "resource": "telephony_calls",
72
+ "event": "all",
73
+ "secret": "my-hmac-secret"
74
+ }
75
+ result = api.session.rest_post(f"{BASE}/webhooks", json=body)
76
+ # Returns: {id, name, targetUrl, resource, event, status, created, orgId, ...}
77
+ webhook_id = result["id"]
78
+ ```
79
+
80
+ #### List all webhooks
81
+
82
+ ```python
83
+ result = api.session.rest_get(f"{BASE}/webhooks")
84
+ webhooks = result.get("items", [])
85
+ for wh in webhooks:
86
+ print(f"{wh['id']} | {wh['resource']} | {wh['event']} | {wh['status']}")
87
+ ```
88
+
89
+ #### Get webhook details
90
+
91
+ ```python
92
+ result = api.session.rest_get(f"{BASE}/webhooks/{webhook_id}")
93
+ # Returns full webhook object
94
+ ```
95
+
96
+ #### Update a webhook (reactivate after auto-deactivation)
97
+
98
+ ```python
99
+ body = {
100
+ "name": "Call Events (reactivated)",
101
+ "targetUrl": "https://example.com/webhooks/calls",
102
+ "status": "active"
103
+ }
104
+ result = api.session.rest_put(f"{BASE}/webhooks/{webhook_id}", json=body)
105
+ ```
106
+
107
+ #### Delete a webhook
108
+
109
+ ```python
110
+ api.session.rest_delete(f"{BASE}/webhooks/{webhook_id}")
111
+ # Returns 204 (no content)
112
+ ```
113
+
114
+ #### Create filtered webhook (incoming calls only)
115
+
116
+ ```python
117
+ body = {
118
+ "name": "Incoming Calls",
119
+ "targetUrl": "https://example.com/webhooks/incoming",
120
+ "resource": "telephony_calls",
121
+ "event": "all",
122
+ "filter": "personality=terminator"
123
+ }
124
+ result = api.session.rest_post(f"{BASE}/webhooks", json=body)
125
+ ```
126
+
127
+ ### Raw HTTP Gotchas
128
+
129
+ 1. **List response key is `items`** -- `GET /webhooks` returns `{"items": [...]}`, not a bare array.
130
+ 2. **Update uses PUT, not PATCH** -- You must supply all updatable fields (`name`, `targetUrl`, `secret`, `status`), not just the changed ones.
131
+ 3. **Cannot change resource/event/filter** -- These are immutable after creation. Delete and recreate instead.
132
+ 4. **Delete returns 204** -- No response body on successful deletion.
133
+ 5. **`ownedBy` on create vs list** -- On create, set `"ownedBy": "org"` for org-level webhooks. On list, pass as query param to filter.
134
+ 6. **For telephony events** -- Set `resource` to `"telephony_calls"` (underscore, not hyphen). The `event` field is the webhook trigger type (`created`, `updated`, `deleted`, `all`), distinct from `data.eventType` in the delivered payload.
135
+
136
+ ---
137
+
138
+ ## 1. Webhook CRUD Operations
139
+
140
+ All webhook management is under `POST/GET/PUT/DELETE /v1/webhooks`.
141
+
142
+ ### Create Webhook
143
+
144
+ ```
145
+ POST /v1/webhooks
146
+ ```
147
+
148
+ **SDK Signature:**
149
+ ```python
150
+ WebhookApi.create(
151
+ name: str, # user-friendly name
152
+ target_url: str, # URL that receives POST requests
153
+ resource: WebhookResource, # e.g. 'telephony_calls'
154
+ event: WebhookEventType, # e.g. 'all', 'created', 'updated', 'deleted'
155
+ filter: str = None, # scope filter (see Filtering section)
156
+ secret: str = None, # HMAC secret for payload signature
157
+ owned_by: str = None # 'org' for org-level webhooks
158
+ ) -> Webhook
159
+ ```
160
+
161
+ **Request body:**
162
+ ```json
163
+ {
164
+ "name": "My Telephony Webhook",
165
+ "targetUrl": "https://example.com/webhooks/calls",
166
+ "resource": "telephony_calls",
167
+ "event": "all",
168
+ "secret": "my-secret-for-hmac"
169
+ }
170
+ ```
171
+
172
+ **Response (201):**
173
+ ```json
174
+ {
175
+ "id": "Y2lzY29zcGFyay...",
176
+ "name": "My Telephony Webhook",
177
+ "targetUrl": "https://example.com/webhooks/calls",
178
+ "resource": "telephony_calls",
179
+ "event": "all",
180
+ "status": "active",
181
+ "created": "2024-01-15T10:30:00.000Z",
182
+ "orgId": "OWM5LTRINWQt...",
183
+ "createdBy": "YzOC1jODQw...",
184
+ "appId": "Y2lzY29zcGFyay..."
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ### List Webhooks
191
+
192
+ ```
193
+ GET /v1/webhooks
194
+ ```
195
+
196
+ **SDK Signature:**
197
+ ```python
198
+ WebhookApi.list(owned_by: str = None) -> Generator[Webhook, None, None]
199
+ ```
200
+
201
+ Returns a paginated generator of all webhooks. Use `owned_by='org'` to filter to org-level webhooks only.
202
+
203
+ ---
204
+
205
+ ### Get Webhook Details
206
+
207
+ ```
208
+ GET /v1/webhooks/{webhookId}
209
+ ```
210
+
211
+ **SDK Signature:**
212
+ ```python
213
+ WebhookApi.details(webhook_id: str) -> Webhook
214
+ ```
215
+
216
+ ---
217
+
218
+ ### Update Webhook
219
+
220
+ ```
221
+ PUT /v1/webhooks/{webhookId}
222
+ ```
223
+
224
+ **SDK Signature:**
225
+ ```python
226
+ WebhookApi.update(webhook_id: str, update: Webhook) -> Webhook
227
+ ```
228
+
229
+ **Updatable fields:** `name`, `targetUrl`, `secret`, `status`
230
+
231
+ All other fields are ignored if supplied. You can reactivate an auto-deactivated webhook by setting `status` to `active`, but you **cannot** use this call to deactivate a webhook.
232
+
233
+ ---
234
+
235
+ ### Delete Webhook
236
+
237
+ ```
238
+ DELETE /v1/webhooks/{webhookId}
239
+ ```
240
+
241
+ **SDK Signature:**
242
+ ```python
243
+ WebhookApi.webhook_delete(webhook_id: str) -> None
244
+ ```
245
+
246
+ ---
247
+
248
+ ## 2. Webhook Data Model
249
+
250
+ ### Webhook Object
251
+
252
+ | Field | Type | Description |
253
+ |-------|------|-------------|
254
+ | `id` | str | Unique webhook identifier |
255
+ | `name` | str | User-friendly name |
256
+ | `targetUrl` | str | URL that receives POST requests |
257
+ | `resource` | WebhookResource | Resource type (e.g., `telephony_calls`) |
258
+ | `event` | WebhookEventType | Event type (e.g., `created`, `updated`, `deleted`, `all`) |
259
+ | `filter` | str (optional) | Filter defining webhook scope |
260
+ | `secret` | str (optional) | Secret for HMAC payload signature |
261
+ | `status` | WebhookStatus | `active` or `inactive` |
262
+ | `created` | datetime | When the webhook was created |
263
+ | `orgId` | str (optional) | Organization ID |
264
+ | `createdBy` | str (optional) | Person ID of creator |
265
+ | `appId` | str (optional) | Application ID |
266
+ | `ownedBy` | str (optional) | `creator` or `org` |
267
+
268
+ ### WebhookStatus
269
+
270
+ | Value | Description |
271
+ |-------|-------------|
272
+ | `active` | Webhook is active and receiving events |
273
+ | `inactive` | Webhook is inactive (auto-deactivated after delivery failures) |
274
+
275
+ ---
276
+
277
+ ## 3. Webhook Resources (All Available)
278
+
279
+ | Resource Value | Description | Supports `ownedBy: org`? |
280
+ |----------------|-------------|:------------------------:|
281
+ | `telephony_calls` | Webex Calling call events | Yes (verified 2026-05-01, not in OpenAPI spec — see §10) |
282
+ | `telephony_conference` | Webex Calling conference controls | No |
283
+ | `telephony_mwi` | Voicemail message waiting indicator | No |
284
+ | `messages` | Messaging (chat) | Yes |
285
+ | `memberships` | Room/space memberships | No |
286
+ | `rooms` | Rooms/spaces | Yes |
287
+ | `meetings` | Meetings | Yes |
288
+ | `recordings` | Meeting recordings | Yes |
289
+ | `convergedRecordings` | Converged (call) recordings | Yes |
290
+ | `meetingParticipants` | Meeting participants | Yes |
291
+ | `meetingTranscripts` | Meeting transcripts | Yes |
292
+ | `uc_counters` | Unified Communications counters | No |
293
+ | `attachmentActions` | Attachment/card actions | No |
294
+ | `dataSources` | Data sources | No |
295
+ | `serviceApp` | Service app authorization events | No |
296
+ | `adminBatchJobs` | Admin batch jobs (note: org-level support restricted to **Compliance Officers** only — a standard full admin cannot create org-level `adminBatchJobs` webhooks) | Yes |
297
+ | `videoMeshAlerts` | Video Mesh alert events | Yes |
298
+ | `controlHubAlerts` | Control Hub alert events | Yes |
299
+ | `all` | Firehose: all resources | No |
300
+
301
+ ### Event Types (All Available)
302
+
303
+ The `event` field on webhook creation determines which lifecycle events trigger delivery. Different resources use different event types.
304
+
305
+ | Event Value | Description | Used By |
306
+ |-------------|-------------|---------|
307
+ | `created` | Resource was created | telephony_calls, messages, memberships, rooms, attachmentActions |
308
+ | `updated` | Resource was modified | telephony_calls, messages, memberships, rooms |
309
+ | `deleted` | Resource was removed | telephony_calls, messages, memberships |
310
+ | `started` | Resource started | meetings |
311
+ | `ended` | Resource ended | meetings |
312
+ | `joined` | Participant joined | meetingParticipants |
313
+ | `left` | Participant left | meetingParticipants |
314
+ | `migrated` | Resource was migrated | meetings |
315
+ | `authorized` | App was authorized | serviceApp |
316
+ | `deauthorized` | App was deauthorized | serviceApp |
317
+ | `statusChanged` | Status changed | adminBatchJobs |
318
+ | `all` | Subscribe to all event types for the resource | All resources |
319
+
320
+ ---
321
+
322
+ ## 4. Telephony Call Events
323
+
324
+ The `telephony_calls` resource uses the standard webhook event types (`created`, `updated`, `deleted`) which map to specific telephony event types in the `data.eventType` field.
325
+
326
+ ### Event Type Mapping
327
+
328
+ | Webhook `event` | `data.eventType` values | When it fires |
329
+ |-----------------|------------------------|---------------|
330
+ | `created` | `answered` | A call was answered |
331
+ | `created` | `alerting` | A call is ringing on the user's devices |
332
+ | `updated` | `connected` | A call transitioned to connected state |
333
+ | `updated` | `held` | The user placed the call on hold |
334
+ | `updated` | `remoteHeld` | The remote party placed the call on hold |
335
+ | `updated` | `resumed` | A held call was resumed |
336
+ | `updated` | `recording` | Recording state changed on the call |
337
+ | `deleted` | `disconnected` | A call ended (hung up) |
338
+ | `deleted` | `forwarded` | A call was forwarded away from the user |
339
+
340
+ <!-- Partial verification 2026-03-19: event_type is a free-form str (not an enum), so additional values beyond those listed may appear. The listed values (alerting, answered, connected, held, remoteHeld, resumed, recording, disconnected, forwarded) match the wxc_sdk TelephonyEventData class and Webex developer docs. Full confirmation requires receiving live call events. -->
341
+
342
+ ### Event Payload Structure
343
+
344
+ When a webhook fires, Webex sends a POST to your `targetUrl` with the full webhook object plus event-specific `data`.
345
+
346
+ **Full event payload (disconnected call example):**
347
+ ```json
348
+ {
349
+ "id": "Y2lzY29zcGFyazovL3VzL1...",
350
+ "name": "My Telephony Webhook",
351
+ "targetUrl": "https://example.com/webhooks/calls",
352
+ "resource": "telephony_calls",
353
+ "event": "deleted",
354
+ "orgId": "OWM5LTRINWQtYjZiOCO5NDZ3MGI...",
355
+ "createdBy": "YzOC1jODQwLTMmU...",
356
+ "appId": "Y2lzY29zcGFyazovLY...",
357
+ "ownedBy": "creator",
358
+ "status": "active",
359
+ "created": "2022-09-14T18:03:25.829Z",
360
+ "actorId": "Y2lzY29zcGFyazovL3VzL1...",
361
+ "data": {
362
+ "eventType": "disconnected",
363
+ "actorPersonId": "RS84MWNhZjUzOC1j...",
364
+ "orgId": "OWM5LTRINWQtYjZiOCO5NDZ3MGI...",
365
+ "eventTimestamp": "2022-10-15T18:06:20.781Z",
366
+ "callId": "Y2lzY29zcGFyazovL3Vz...",
367
+ "callSessionId": "OGQ3YzhkNzgtZjIxZib...",
368
+ "personality": "terminator",
369
+ "state": "disconnected",
370
+ "remoteParty": {
371
+ "name": "agent x",
372
+ "number": "1012",
373
+ "personId": "Y2lzY29zcGFyazovL3V...",
374
+ "privacyEnabled": false,
375
+ "callType": "location"
376
+ },
377
+ "created": "2022-09-15T18:06:10.269Z",
378
+ "answered": "2022-09-15T18:06:17.211Z",
379
+ "disconnected": "2022-09-15T18:06:20.781Z"
380
+ }
381
+ }
382
+ ```
383
+
384
+ ### Event Data Fields (`data` object)
385
+
386
+ The `data` object in telephony_calls events corresponds to the SDK's `TelephonyEventData` class, which extends both `WebhookEventData` and `TelephonyCall`.
387
+
388
+ | Field | Type | Description |
389
+ |-------|------|-------------|
390
+ | `eventType` | str | Telephony event type: `alerting`, `answered`, `connected`, `held`, `remoteHeld`, `resumed`, `recording`, `disconnected`, `forwarded` |
391
+ | `actorPersonId` | str | Person ID of the actor who triggered the event |
392
+ | `orgId` | str | Organization ID |
393
+ | `eventTimestamp` | datetime | When the event occurred |
394
+ | `callId` | str | Unique call identifier |
395
+ | `callSessionId` | str | Session ID to correlate multiple calls in the same session |
396
+ | `personality` | str | `originator`, `terminator`, or `clickToDial` |
397
+ | `state` | str | Current call state: `connecting`, `alerting`, `connected`, `held`, `remoteHeld`, `disconnected` |
398
+ | `remoteParty` | object | Details of the other party (see below) |
399
+ | `appearance` | int (optional) | Appearance value for call ordering |
400
+ | `created` | datetime | When the call was created |
401
+ | `answered` | datetime (optional) | When the call was answered |
402
+ | `disconnected` | datetime (optional) | When the call was disconnected |
403
+ | `redirections` | array (optional) | Previous redirections (most recent first) |
404
+ | `recall` | object (optional) | Recall details (e.g., park recall) |
405
+ | `recordingState` | str (optional) | `pending`, `started`, `paused`, `stopped`, `failed` |
406
+
407
+ ### remoteParty Object
408
+
409
+ | Field | Type | Description |
410
+ |-------|------|-------------|
411
+ | `name` | str (optional) | Party name |
412
+ | `number` | str | Party number (digits or URI) |
413
+ | `personId` | str (optional) | Person ID |
414
+ | `placeId` | str (optional) | Place ID |
415
+ | `privacyEnabled` | bool | Whether privacy is enabled |
416
+ | `callType` | str | `location`, `organization`, `external`, `emergency`, `repair`, `other` |
417
+
418
+ ---
419
+
420
+ ### 4b. Messaging Resource Events
421
+
422
+ The messaging webhook resources (`messages`, `memberships`, `rooms`, `attachmentActions`) follow the same delivery pattern as telephony: Webex POSTs to your `targetUrl` with the webhook envelope plus a `data` object containing resource metadata.
423
+
424
+ **Critical gotcha for `messages` webhooks:** The webhook payload does NOT include the message text when the receiving token is a bot. The bot must call `GET /messages/{messageId}` (or `wxcli messages show MESSAGE_ID`) to retrieve the actual message content. This is the standard bot pattern: webhook fires → bot fetches message → bot processes text.
425
+
426
+ #### messages Resource
427
+
428
+ | Webhook `event` | When It Fires |
429
+ |-----------------|---------------|
430
+ | `created` | A message was posted to a space |
431
+ | `updated` | A message was edited |
432
+ | `deleted` | A message was deleted from a space |
433
+
434
+ **Event payload `data` fields:**
435
+
436
+ | Field | Type | Description |
437
+ |-------|------|-------------|
438
+ | `id` | str | Message ID |
439
+ | `roomId` | str | Space the message was posted in |
440
+ | `roomType` | str | `direct` or `group` |
441
+ | `personId` | str | Person who sent the message |
442
+ | `personEmail` | str | Email of the sender |
443
+ | `created` | datetime | When the message was created |
444
+
445
+ **Note:** `text` is NOT included in the webhook payload for bot tokens (security measure). Always call `wxcli messages show MESSAGE_ID` to retrieve the text.
446
+
447
+ **CLI to fetch full message:**
448
+ ```bash
449
+ wxcli messages show MESSAGE_ID
450
+ ```
451
+
452
+ #### memberships Resource
453
+
454
+ | Webhook `event` | When It Fires |
455
+ |-----------------|---------------|
456
+ | `created` | Someone was added to a space |
457
+ | `updated` | A membership was changed (e.g., moderator status) |
458
+ | `deleted` | Someone was removed from a space |
459
+
460
+ **Event payload `data` fields:**
461
+
462
+ | Field | Type | Description |
463
+ |-------|------|-------------|
464
+ | `id` | str | Membership ID |
465
+ | `roomId` | str | Space the membership is in |
466
+ | `personId` | str | Person whose membership changed |
467
+ | `personEmail` | str | Email of the person |
468
+ | `personDisplayName` | str (optional) | Display name of the person |
469
+ | `personOrgId` | str (optional) | Organization ID of the person |
470
+ | `isModerator` | bool | Whether the person is a moderator |
471
+ | `isMonitor` | bool | Whether the person is a monitor (deprecated) |
472
+ | `isRoomHidden` | bool (optional) | Whether the direct type room is hidden |
473
+ | `roomType` | str (optional) | Type of room (`direct` or `group`) |
474
+ | `created` | datetime | When the membership was created |
475
+
476
+ #### rooms Resource
477
+
478
+ | Webhook `event` | When It Fires |
479
+ |-----------------|---------------|
480
+ | `created` | A new space was created |
481
+ | `updated` | A space was modified (title, lock status, etc.) |
482
+
483
+ **Event payload `data` fields:**
484
+
485
+ <!-- Partial verification 2026-03-19: Rooms webhook creation confirmed via live API. The Webex Filtering Webhooks guide confirms `type` and `isLocked` as valid rooms filter fields, implying these are present in the data payload. Full data field confirmation requires receiving a live rooms event. -->
486
+
487
+ | Field | Type | Description |
488
+ |-------|------|-------------|
489
+ | `id` | str | Space (room) ID |
490
+ | `title` | str | Space title |
491
+ | `type` | str | `direct` or `group` |
492
+ | `isLocked` | bool | Whether the space is locked |
493
+ | `creatorId` | str | Person ID of the space creator |
494
+ | `created` | datetime | When the space was created |
495
+ | `lastActivity` | datetime | Timestamp of last activity |
496
+
497
+ #### attachmentActions Resource
498
+
499
+ | Webhook `event` | When It Fires |
500
+ |-----------------|---------------|
501
+ | `created` | A user submitted an adaptive card (`Action.Submit`) |
502
+
503
+ **Event payload `data` fields:**
504
+
505
+ | Field | Type | Description |
506
+ |-------|------|-------------|
507
+ | `id` | str | Attachment action ID |
508
+ | `type` | str | Always `submit` |
509
+ | `messageId` | str | ID of the message containing the card |
510
+ | `personId` | str | Person who submitted the card action |
511
+ | `roomId` | str | Space where the card was submitted |
512
+ | `created` | datetime | When the action was submitted |
513
+
514
+ **Note:** `inputs` (the user's form values) are NOT included in the webhook payload. Call `wxcli attachment-actions show ACTION_ID` to retrieve the submitted values.
515
+
516
+ **CLI to fetch card response inputs:**
517
+ ```bash
518
+ wxcli attachment-actions show ACTION_ID
519
+ ```
520
+
521
+ ---
522
+
523
+ ## 5. Filtering Telephony Webhooks
524
+
525
+ Use the `filter` parameter when creating a webhook to narrow which events are delivered.
526
+
527
+ ### Filter Syntax
528
+
529
+ Filters use the format: `fieldName=value` or `fieldName=value1,value2` for multiple values.
530
+
531
+ **Available filters for `telephony_calls`:**
532
+
533
+ | Filter | Example | Description |
534
+ |--------|---------|-------------|
535
+ | `personality` | `personality=terminator` | Only incoming calls |
536
+ | `personality` | `personality=originator` | Only outgoing calls |
537
+ | `state` | `state=connected` | Only connected state events |
538
+ | `callType` | `callType=external` | Only external calls |
539
+ | `personId` | `personId=Y2lzY29z...` | Only events for a specific person |
540
+ | `address` | `address=+15551234567` | Only events for a specific phone number or SIP address |
541
+
542
+ **Available filters for messaging resources:**
543
+
544
+ | Resource | Filter | Example | Description |
545
+ |----------|--------|---------|-------------|
546
+ | `messages` | `roomId` | `roomId=Y2lz...` | Only messages in a specific space |
547
+ | `messages` | `roomType` | `roomType=group` | Only messages in `direct` or `group` spaces |
548
+ | `messages` | `personId` | `personId=Y2lz...` | Only messages from a specific person |
549
+ | `messages` | `personEmail` | `personEmail=user@example.com` | Only messages from a specific email |
550
+ | `messages` | `mentionedPeople` | `mentionedPeople=me` | Only messages that @mention someone (use `me` for the authenticated user) |
551
+ | `messages` | `mentionedGroups` | `mentionedGroups=all` | Only messages that @mention a group (e.g., `all`) |
552
+ | `messages` | `hasFiles` | `hasFiles=true` | Only messages with file attachments |
553
+ | `messages` | `hasAttachments` | `hasAttachments=true` | Only messages with adaptive card attachments |
554
+ | `memberships` | `roomId` | `roomId=Y2lz...` | Only membership changes in a specific space |
555
+ | `memberships` | `personId` | `personId=Y2lz...` | Only membership changes for a specific person |
556
+ | `memberships` | `personEmail` | `personEmail=user@example.com` | Only membership changes for a specific email |
557
+ | `memberships` | `isModerator` | `isModerator=true` | Only moderator status changes |
558
+
559
+ **Example: Create a webhook for incoming calls only:**
560
+ ```python
561
+ api.webhook.create(
562
+ name="Incoming Calls Only",
563
+ target_url="https://example.com/incoming",
564
+ resource=WebhookResource.telephony_calls,
565
+ event=WebhookEventType.all,
566
+ filter="personality=terminator"
567
+ )
568
+ ```
569
+
570
+ ---
571
+
572
+ ## 6. Webhook Setup: Step-by-Step
573
+
574
+ ### Register for Telephony Call Events
575
+
576
+ ```python
577
+ from wxc_sdk import WebexSimpleApi
578
+ from wxc_sdk.webhook import WebhookResource, WebhookEventType
579
+
580
+ api = WebexSimpleApi(tokens=tokens)
581
+
582
+ # 1. Clean up old webhooks (optional)
583
+ existing = list(api.webhook.list())
584
+ for wh in existing:
585
+ if wh.name == "My Call Monitor":
586
+ api.webhook.webhook_delete(webhook_id=wh.webhook_id)
587
+
588
+ # 2. Create webhook for all telephony call events
589
+ webhook = api.webhook.create(
590
+ name="My Call Monitor",
591
+ target_url="https://your-server.com/webhooks/calls",
592
+ resource=WebhookResource.telephony_calls,
593
+ event=WebhookEventType.all,
594
+ secret="your-hmac-secret"
595
+ )
596
+
597
+ print(f"Webhook created: {webhook.webhook_id}")
598
+ print(f"Status: {webhook.status}")
599
+ ```
600
+
601
+ ### Parse Incoming Events
602
+
603
+ ```python
604
+ from wxc_sdk.webhook import WebhookEvent
605
+ from wxc_sdk.telephony.calls import TelephonyEventData
606
+
607
+ # In your webhook handler (e.g., Flask route)
608
+ def handle_webhook(request_json: dict):
609
+ event = WebhookEvent.model_validate(request_json)
610
+
611
+ # The data field is auto-parsed to TelephonyEventData
612
+ # for telephony_calls resource
613
+ data: TelephonyEventData = event.data
614
+
615
+ print(f"Event type: {data.event_type}")
616
+ print(f"Call ID: {data.call_id}")
617
+ print(f"State: {data.state}")
618
+ print(f"Personality: {data.personality}")
619
+ print(f"Remote party: {data.remote_party.name} ({data.remote_party.number})")
620
+
621
+ if data.event_type == "disconnected":
622
+ print(f"Call ended at: {data.disconnected}")
623
+ elif data.event_type == "answered":
624
+ print(f"Call answered at: {data.answered}")
625
+ ```
626
+
627
+ ### Firehose Webhook (All Events)
628
+
629
+ The firehose pattern creates a webhook for all resources and all events. Useful for logging or debugging.
630
+
631
+ ```python
632
+ # Create firehose
633
+ api.webhook.create(
634
+ name="Firehose",
635
+ resource=WebhookResource.all, # resource='all'
636
+ event=WebhookEventType.all, # event='all'
637
+ target_url="https://your-server.com/firehose"
638
+ )
639
+ ```
640
+
641
+ The SDK's `firehose.py` example demonstrates a complete Flask-based firehose listener that:
642
+ 1. Uses ngrok for a public URL
643
+ 2. Cleans up old webhooks matching the class name
644
+ 3. Creates a firehose webhook (`resource='all'`, `event='all'`)
645
+ 4. Routes events to resource-specific handlers
646
+ 5. Auto-parses `WebhookEvent` from the POST body, which dispatches to the correct `WebhookEventData` subclass based on the resource (e.g., `TelephonyEventData` for `telephony_calls`)
647
+
648
+ ---
649
+
650
+ ### 6b. Bot Webhook Pattern
651
+
652
+ This section covers the standard webhook interaction loop for bots. For webhook CRUD commands, see Sections 1 and 6 above.
653
+
654
+ #### Message Handling Loop
655
+
656
+ How a bot "hears" and responds to messages:
657
+
658
+ **Step 1 — Create a webhook for incoming messages:**
659
+ ```bash
660
+ wxcli webhooks create \
661
+ --name "Bot Messages" \
662
+ --target-url https://your-bot.example.com/webhook \
663
+ --resource messages \
664
+ --event created
665
+ ```
666
+
667
+ **Step 2-5 — The interaction loop (at runtime):**
668
+
669
+ 1. User sends a message to a space where the bot is a member (or @mentions the bot)
670
+ 2. Webex fires a POST to your `targetUrl` with message metadata — **no message text** (bot security restriction)
671
+ 3. Bot extracts `data.id` from the webhook payload and fetches the message:
672
+ ```bash
673
+ wxcli messages show MESSAGE_ID
674
+ ```
675
+ 4. Bot processes the message text and sends a response:
676
+ ```bash
677
+ wxcli messages create --room-id ROOM_ID --text "Response text here"
678
+ ```
679
+
680
+ **Key rule:** Never assume the webhook payload contains the message text. Always fetch via `wxcli messages show MESSAGE_ID`. See Section 4b for the complete list of fields available in the payload.
681
+
682
+ #### Card Interaction Loop
683
+
684
+ How a bot sends adaptive cards and captures user responses:
685
+
686
+ **Step 1 — Create a webhook for card submissions:**
687
+ ```bash
688
+ wxcli webhooks create \
689
+ --name "Bot Card Responses" \
690
+ --target-url https://your-bot.example.com/webhook \
691
+ --resource attachmentActions \
692
+ --event created
693
+ ```
694
+
695
+ **Step 2-5 — The card interaction loop (at runtime):**
696
+
697
+ 1. Bot sends a message containing an adaptive card:
698
+ ```bash
699
+ wxcli messages create --json-body '{
700
+ "roomId": "ROOM_ID",
701
+ "text": "Approval request (fallback)",
702
+ "attachments": [{
703
+ "contentType": "application/vnd.microsoft.card.adaptive",
704
+ "content": { ... card payload ... }
705
+ }]
706
+ }'
707
+ ```
708
+ 2. User clicks `Action.Submit` in the rendered card
709
+ 3. Webex fires a POST to your `targetUrl` with action metadata — `inputs` (the user's form values) are NOT in the payload
710
+ 4. Bot extracts `data.id` from the payload and fetches the action:
711
+ ```bash
712
+ wxcli attachment-actions show ACTION_ID
713
+ ```
714
+ 5. Bot reads the `inputs` object from the response and sends a follow-up:
715
+ ```bash
716
+ wxcli messages create --room-id ROOM_ID --text "Got your response: Approved"
717
+ ```
718
+
719
+ #### Typical Bot Webhook Setup (both loops)
720
+
721
+ Most interactive bots need both webhooks:
722
+
723
+ ```bash
724
+ # 1. Subscribe to incoming messages
725
+ wxcli webhooks create \
726
+ --name "Bot Messages" \
727
+ --target-url https://your-bot.example.com/webhook \
728
+ --resource messages \
729
+ --event created
730
+
731
+ # 2. Subscribe to card responses
732
+ wxcli webhooks create \
733
+ --name "Bot Card Responses" \
734
+ --target-url https://your-bot.example.com/webhook \
735
+ --resource attachmentActions \
736
+ --event created
737
+
738
+ # 3. Verify both are active
739
+ wxcli webhooks list --output json
740
+ ```
741
+
742
+ Add `--filter "roomId=ROOM_ID"` to either webhook to scope it to a specific space.
743
+
744
+ ---
745
+
746
+ ## 7. Webhook Event Data Class Hierarchy
747
+
748
+ The SDK uses a registration pattern to auto-parse event data:
749
+
750
+ ```
751
+ WebhookEventDataForbid (base, with registry)
752
+ └── WebhookEventData (allows extra fields)
753
+ └── TelephonyEventData (resource = 'telephony_calls')
754
+ ├── inherits: WebhookEventData (metadata fields)
755
+ └── inherits: TelephonyCall (call detail fields)
756
+ ```
757
+
758
+ **SDK class definition:**
759
+ ```python
760
+ class TelephonyEventData(WebhookEventData, TelephonyCall):
761
+ """data in a webhook 'telephony_calls' event"""
762
+ resource = 'telephony_calls'
763
+ event_type: str
764
+ event_timestamp: datetime.datetime
765
+ ```
766
+
767
+ When `WebhookEvent.model_validate(payload)` processes the JSON, it checks `data.resource` against the registry and automatically instantiates the correct subclass. For `telephony_calls`, the `data` field becomes a `TelephonyEventData` instance with all `TelephonyCall` fields plus `event_type` and `event_timestamp`.
768
+
769
+ ---
770
+
771
+ ## 8. Webhook Security: HMAC Signature Verification
772
+
773
+ When you provide a `secret` during webhook creation, Webex signs each event payload. The signature is sent in the `X-Spark-Signature` HTTP header as an HMAC-SHA1 hex digest.
774
+
775
+ **Verification example:**
776
+ ```python
777
+ import hmac
778
+ import hashlib
779
+
780
+ def verify_webhook_signature(request_body: bytes, signature: str, secret: str) -> bool:
781
+ """Verify the X-Spark-Signature header."""
782
+ expected = hmac.new(
783
+ secret.encode('utf-8'),
784
+ request_body,
785
+ hashlib.sha1
786
+ ).hexdigest()
787
+ return hmac.compare_digest(expected, signature)
788
+ ```
789
+
790
+ <!-- Confirmed 2026-03-19: Webhook creation with secret confirmed via live API (secret is echoed back in the response). X-Spark-Signature with HMAC-SHA1 is confirmed by official Webex developer documentation (Webex developer blog post "Using a Webhook Secret"). -->
791
+
792
+ ---
793
+
794
+ ## 9. Key Gotchas
795
+
796
+ 1. **Auto-deactivation** -- Webhooks that fail to receive events (target URL returns errors repeatedly) are automatically set to `inactive`. Use the `update` API to reactivate by setting `status: active`.
797
+
798
+ 2. **Cannot deactivate via API** -- You can only reactivate an auto-deactivated webhook. To stop receiving events, you must `delete` the webhook.
799
+
800
+ 3. **Read scope required for creation** -- Creating a `telephony_calls` webhook requires `spark:calls_read` scope, not write scope.
801
+
802
+ 4. **`event` field vs `data.eventType`** -- The top-level `event` field is the generic webhook event type (`created`, `updated`, `deleted`). The telephony-specific event type is in `data.eventType` (`alerting`, `answered`, `connected`, `held`, `remoteHeld`, `resumed`, `recording`, `disconnected`, `forwarded`).
803
+
804
+ 5. **`callId` aliasing** -- In webhook event data, the call identifier field is `callId`. In direct API responses, it is `id`. The SDK's `TelephonyCall` model handles both via the `call_id` property.
805
+
806
+ 6. **Firehose scope** -- A firehose webhook (`resource=all`, `event=all`) requires broad scopes. You will only receive events for resources your token has read access to.
807
+
808
+ 7. **Updatable fields** -- Only `name`, `targetUrl`, `secret`, and `status` can be updated on an existing webhook. You cannot change the `resource`, `event`, or `filter` after creation. Delete and recreate instead.
809
+
810
+ 8. **Webhook URL must be HTTPS** -- The `targetUrl` must be publicly accessible over HTTPS. For local development, use ngrok or a similar tunnel (as shown in the firehose example).
811
+
812
+ 9. **Event delivery is not guaranteed to be ordered** -- Events may arrive out of order. Use `data.eventTimestamp` and `data.callSessionId` to correlate and order events for the same call session.
813
+
814
+ 10. **One webhook per resource/event combo** -- Creating a duplicate webhook (same resource + event + filter) will create a second webhook, resulting in duplicate event delivery. Always clean up old webhooks before creating new ones.
815
+
816
+ 11. **Org-level `telephony_calls` works but is missing from OpenAPI spec** -- The `ownedBy: org` parameter's description in the OpenAPI spec lists supported resources but does NOT include `telephony_calls`. Live testing (2026-05-01) confirmed it works — the API returns 201 with `status: active`. The spec has a documentation gap, not a feature gap.
817
+
818
+ 12. **Service App `application:webhooks_*` scopes not supported** -- Service Apps cannot use `application:webhooks_write` or `application:webhooks_read` scopes. They create webhooks using standard resource read scopes (e.g., `spark:calls_read`). See §12.
819
+
820
+ 13. **Event storm at scale** -- An org-level webhook for 35,000+ users can generate 70,000+ events/hour at peak. If your receiver slows down under load, the auto-deactivation threshold (gotcha #1) may trigger, silently stopping all event delivery. Monitor webhook status and build receiver capacity well above peak estimates.
821
+
822
+ 14. **Org-level + creator-level overlap** -- If you have both an org-level webhook and a creator-level webhook for the same resource, the creator's events are delivered to BOTH webhooks. This is usually a mistake — use `wxcli webhooks list` and `wxcli webhooks list --owned-by org` to audit for overlaps.
823
+
824
+ 15. **`ownedBy` is immutable after creation** -- The `update` endpoint accepts `ownedBy` in the CLI schema, but the OpenAPI spec does not list it as an updatable field. Like `resource`, `event`, and `filter`, ownership is fixed at creation time. Delete and recreate to change.
825
+
826
+ 16. **Missing event types for non-telephony resources** -- The `event` enum includes `started`, `ended`, `joined`, `left`, `migrated`, `authorized`, `deauthorized`, and `statusChanged` in addition to the common `created`/`updated`/`deleted`. These apply to meetings, meetingParticipants, serviceApp, and adminBatchJobs respectively. See the Event Types table in §3.
827
+
828
+ ---
829
+
830
+ ## 10. Org-Level Webhooks
831
+
832
+ ### Creator-Level vs Org-Level
833
+
834
+ By default, webhooks are **creator-level** (`ownedBy: creator`): they receive events only for the authenticated user who created them. An **org-level** webhook (`ownedBy: org`) receives events for all users in the organization.
835
+
836
+ | Aspect | Creator-Level (default) | Org-Level (`ownedBy: org`) |
837
+ |--------|------------------------|---------------------------|
838
+ | Event scope | Events for the creating user only | Events for ALL users in the org |
839
+ | Who can create | Any user with read scope on the resource | Admin or Service App with admin-level scopes |
840
+ | Typical token | User OAuth (PAT or integration) | Admin OAuth or Service App token |
841
+ | Use case | Per-user notifications, bot messages | Org-wide monitoring, compliance, middleware |
842
+
843
+ ### Scope Requirements for Org-Level Webhooks
844
+
845
+ Creating an org-level webhook requires **admin-level scopes** on the target resource. For example:
846
+ - `spark-admin:telephony_config_read` or `spark:calls_read` with an admin token for `telephony_calls`
847
+ - `spark-admin:rooms_read` for `rooms`
848
+ - `spark-admin:messages_read` for `messages`
849
+
850
+ The scope requirements are the same read scopes, but the token must belong to an admin (full admin or read-only admin) or a Service App with admin authorization.
851
+
852
+ ### Supported Resources
853
+
854
+ The OpenAPI spec explicitly lists org-level support for: `meetings`, `recordings`, `convergedRecordings`, `meetingParticipants`, `meetingTranscripts`, `videoMeshAlerts`, `controlHubAlerts`, `rooms`, `messaging`, and `adminBatchJobs`. **Note on `messaging` vs `messages`:** The OpenAPI spec uses `messaging` as the org-level resource identifier in the `ownedBy: org` support list, while the §3 resource table (and webhook creation) uses `messages`. These refer to the same resource family — `messaging` is the identifier used in the API guides for org-level context, while `messages` is the value you supply in the `resource` field when creating a webhook.
855
+
856
+ > **`telephony_calls` is NOT in this list, but it works.** The OpenAPI spec does not list `telephony_calls` as supporting `ownedBy: org`. However, live API testing (2026-05-01) confirmed that creating an org-level `telephony_calls` webhook succeeds with HTTP 201 and `status: active`. The spec has a documentation gap — the API supports it.
857
+ >
858
+ > **Verified with:**
859
+ > ```bash
860
+ > wxcli webhooks create \
861
+ > --name "Org Telephony Test" \
862
+ > --target-url https://example.com/test \
863
+ > --resource telephony_calls \
864
+ > --event all \
865
+ > --owned-by org -o json
866
+ > # Result: 201, ownedBy: "org", status: "active"
867
+ > ```
868
+
869
+ ### Behavior Details
870
+
871
+ **Multiple org-level webhooks:** You can create multiple org-level webhooks for the same resource/event. Each receives a copy of every event, resulting in duplicate delivery. This is usually a mistake — clean up duplicates via `wxcli webhooks list --owned-by org`.
872
+
873
+ **Interaction with `filter`:** Org-level and `filter` combine as AND logic. An org-level webhook with `filter=personality=terminator` receives events for all users in the org, but only for incoming calls. This is the recommended pattern for large-scale deployments — subscribe to all org events, filter server-side.
874
+
875
+ **Deprovisioning the creator:** When the user or Service App that created an org-level webhook is deprovisioned or deauthorized, the webhook's behavior is undocumented. **Requires live verification:**
876
+
877
+ ```bash
878
+ # 1. Create org-level webhook with Service App token
879
+ wxcli webhooks create --name "Test" --target-url https://example.com/test \
880
+ --resource messages --event created --owned-by org
881
+
882
+ # 2. Revoke the Service App authorization in Control Hub
883
+ # 3. Check if the webhook still exists and delivers events:
884
+ wxcli webhooks list --owned-by org
885
+ ```
886
+
887
+ ### Raw HTTP: Create Org-Level Webhook
888
+
889
+ ```python
890
+ body = {
891
+ "name": "Org Call Events",
892
+ "targetUrl": "https://middleware.example.com/webhooks",
893
+ "resource": "telephony_calls",
894
+ "event": "all",
895
+ "ownedBy": "org",
896
+ "filter": "personality=terminator",
897
+ "secret": "hmac-secret-here"
898
+ }
899
+ result = api.session.rest_post(f"{BASE}/webhooks", json=body)
900
+ ```
901
+
902
+ ---
903
+
904
+ ## 11. Webhook Delivery Mechanics
905
+
906
+ Webex delivers events as HTTP POST requests to your `targetUrl`. Understanding the delivery contract is critical for building reliable receivers.
907
+
908
+ ### Delivery Contract
909
+
910
+ | Aspect | Behavior | Source |
911
+ |--------|----------|--------|
912
+ | Transport | HTTPS POST to `targetUrl` | Spec: `targetUrl` must be HTTPS |
913
+ | Payload format | JSON (`Content-Type: application/json`) | Observed |
914
+ | Signature header | `X-Spark-Signature` (HMAC-SHA1 hex digest) if `secret` is set | Spec + SDK |
915
+ | Batching | Events are delivered **individually** (one POST per event) | Observed — no batch envelope in spec |
916
+
917
+ ### Timeout and Response Codes
918
+
919
+ The Webex platform expects your server to respond quickly. The exact timeout is not documented in the OpenAPI spec.
920
+
921
+ | Aspect | Documented Behavior |
922
+ |--------|-------------------|
923
+ | Expected response | 2xx status code (any 200-299) |
924
+ | Response body | Ignored — Webex only checks the status code |
925
+ | Timeout | **Requires live verification.** Developer community reports suggest ~15 seconds, but this is not in the spec. |
926
+
927
+ **Test command to measure timeout:**
928
+ ```python
929
+ # Deploy a webhook handler that sleeps increasing durations
930
+ # and log which deliveries Webex considers failed.
931
+ import time
932
+ from flask import Flask, request
933
+
934
+ app = Flask(__name__)
935
+
936
+ @app.route('/webhook', methods=['POST'])
937
+ def webhook():
938
+ time.sleep(20) # Adjust to find the cutoff
939
+ return '', 200
940
+ ```
941
+
942
+ ### Retry Policy
943
+
944
+ The retry behavior on delivery failure is **not documented** in the OpenAPI spec. Observable behavior:
945
+
946
+ | Aspect | Observed Behavior |
947
+ |--------|-------------------|
948
+ | Retry on 5xx | **Requires live verification.** No retry policy is documented. |
949
+ | Retry on timeout | **Requires live verification.** |
950
+ | Retry on connection refused | **Requires live verification.** |
951
+ | Backoff strategy | **Requires live verification.** |
952
+
953
+ **What IS documented:** After sustained delivery failures, the webhook is auto-deactivated (see below). The absence of documented retries suggests events may be dropped on first failure — design receivers accordingly.
954
+
955
+ ### Auto-Deactivation
956
+
957
+ Webhooks that fail to receive events are automatically set to `inactive` status.
958
+
959
+ | Aspect | Behavior |
960
+ |--------|----------|
961
+ | Trigger | Repeated delivery failures to `targetUrl` |
962
+ | Failure threshold | **100 failed delivery attempts** within a five-minute period (officially documented). |
963
+ | Time window | Five minutes (officially documented). |
964
+ | Notification | No notification — poll `wxcli webhooks list` or check `status` field |
965
+ | Reactivation | `wxcli webhooks update WEBHOOK_ID --status active` |
966
+ | Cannot deactivate via API | You cannot set `status` to `inactive` — only delete the webhook to stop delivery |
967
+
968
+ **Test command to find the threshold:**
969
+ ```bash
970
+ # 1. Create a webhook pointing at a non-existent URL
971
+ wxcli webhooks create --name "Deactivation Test" \
972
+ --target-url https://nonexistent.example.com/webhook \
973
+ --resource messages --event created
974
+
975
+ # 2. Generate events (send messages)
976
+ # 3. Poll until status changes:
977
+ wxcli webhooks show WEBHOOK_ID
978
+ ```
979
+
980
+ ### Event Ordering
981
+
982
+ **Events are NOT guaranteed to arrive in order.** This is critical for telephony event processing.
983
+
984
+ For a single call, you might receive events in this order:
985
+ ```
986
+ answered (timestamp: T+0s)
987
+ connected (timestamp: T+1s) ← may arrive before or after "answered"
988
+ held (timestamp: T+30s)
989
+ resumed (timestamp: T+35s)
990
+ disconnected (timestamp: T+60s)
991
+ ```
992
+
993
+ **Ordering rules:**
994
+ - Events from different calls have no ordering relationship
995
+ - Events from the same call (`callSessionId`) may arrive out of order
996
+ - Use `data.eventTimestamp` as the authoritative ordering field
997
+ - Use `data.callSessionId` to group events belonging to the same call session
998
+ - See §13 for a worked correlation example
999
+
1000
+ ### Duplicate Delivery
1001
+
1002
+ Events may be delivered more than once under these conditions:
1003
+ 1. **Multiple webhooks** for the same resource/event/filter → each receives the event (gotcha #10)
1004
+ 2. **Org-level + creator-level overlap** → user events delivered to both
1005
+ 3. **Infrastructure retries** (if any) → possible duplicate on timeout edge cases
1006
+
1007
+ Design receivers to be **idempotent**. Key on `data.callId` + `data.eventType` (or message `data.id` + `event`) for deduplication.
1008
+
1009
+ ---
1010
+
1011
+ ## 12. Service App Webhooks
1012
+
1013
+ ### Can a Service App Create Webhooks?
1014
+
1015
+ **Yes, with caveats.** A Service App uses a machine-to-machine OAuth token with scopes authorized by an org admin. If the authorized scopes include read access to the target resource, the Service App can create webhooks.
1016
+
1017
+ However, the OpenAPI spec explicitly states that **`application:webhooks_write` and `application:webhooks_read` scopes are NOT supported** for Service Apps (see [authentication.md](authentication.md)). Service Apps create webhooks using the standard resource read scopes, not webhook-specific scopes.
1018
+
1019
+ ### Required Scopes
1020
+
1021
+ | Resource | Scope Needed | Notes |
1022
+ |----------|-------------|-------|
1023
+ | `telephony_calls` | `spark:calls_read` or `spark-admin:telephony_config_read` | Admin must authorize these on the Service App |
1024
+ | `messages` | `spark:messages_read` or `spark-admin:messages_read` | |
1025
+ | `meetings` | `spark:meetings_read` or `spark-admin:meetings_read` | |
1026
+ | Any resource | Read scope for that resource | Service App must be authorized for it |
1027
+
1028
+ ### Org-Level Behavior
1029
+
1030
+ A webhook created by a Service App does **not** automatically become org-level. You must explicitly set `ownedBy: org` on creation, same as with user tokens. A Service App token with admin-level scopes can create org-level webhooks for supported resources (see §10 for the supported resource list).
1031
+
1032
+ ### Token Refresh and Webhook Lifecycle
1033
+
1034
+ Service App access tokens expire after **14 days**. Refresh tokens expire after **90 days** of non-use.
1035
+
1036
+ | Scenario | Webhook Impact |
1037
+ |----------|---------------|
1038
+ | Access token expires, refresh succeeds | **No impact** — webhook continues to deliver events. The webhook is not tied to the token; it's tied to the authorization. |
1039
+ | Refresh token expires (90 days unused) | **Requires live verification.** The webhook may continue delivering (it's server-side), but you lose the ability to manage it until the Service App is re-authorized. |
1040
+ | Service App deauthorized by admin | **Requires live verification.** Webhooks created by the app may be auto-deleted or orphaned. Test before relying on this in production. |
1041
+
1042
+ **Test commands:**
1043
+ ```bash
1044
+ # Verify Service App can create webhooks
1045
+ wxcli webhooks create --name "SA Test" \
1046
+ --target-url https://example.com/webhook \
1047
+ --resource telephony_calls --event all
1048
+
1049
+ # Check the webhook's createdBy and appId fields
1050
+ wxcli webhooks show WEBHOOK_ID -o json
1051
+ ```
1052
+
1053
+ ### Cross-Reference
1054
+
1055
+ See [admin-apps-data.md](admin-apps-data.md) §1 for Service App registration, authorization, and token management.
1056
+
1057
+ ---
1058
+
1059
+ ## 13. callSessionId Correlation Pattern
1060
+
1061
+ When processing telephony webhook events, you need to correlate multiple events that belong to the same call. The key fields are:
1062
+
1063
+ | Field | Scope | Purpose |
1064
+ |-------|-------|---------|
1065
+ | `callSessionId` | Spans the entire call session (including transfers) | Group all events for one end-to-end call |
1066
+ | `callId` | Unique per call leg | Identify a specific leg (e.g., before vs after transfer) |
1067
+ | `actorPersonId` | The Webex user whose perspective this event represents | Route events to the correct handler |
1068
+ | `personality` | `originator` or `terminator` | Distinguish outbound vs inbound perspective |
1069
+
1070
+ ### Worked Example: Inbound Call with Hold
1071
+
1072
+ A customer calls a store associate. The associate answers, places the call on hold to check inventory, resumes, then hangs up.
1073
+
1074
+ **Event sequence (6 events, one `callSessionId`):**
1075
+
1076
+ ```
1077
+ Event 1: created/alerting
1078
+ callSessionId: "session-ABC"
1079
+ callId: "call-123"
1080
+ actorPersonId: "associate-001"
1081
+ personality: "terminator"
1082
+ state: "alerting"
1083
+ eventTimestamp: "2026-05-01T14:00:01.000Z"
1084
+ remoteParty: { number: "+15551234567", callType: "external" }
1085
+
1086
+ Event 2: created/answered
1087
+ callSessionId: "session-ABC"
1088
+ callId: "call-123"
1089
+ actorPersonId: "associate-001"
1090
+ personality: "terminator"
1091
+ state: "connected"
1092
+ eventTimestamp: "2026-05-01T14:00:05.000Z"
1093
+
1094
+ Event 3: updated/connected
1095
+ callSessionId: "session-ABC"
1096
+ callId: "call-123"
1097
+ actorPersonId: "associate-001"
1098
+ personality: "terminator"
1099
+ state: "connected"
1100
+ eventTimestamp: "2026-05-01T14:00:05.500Z"
1101
+
1102
+ Event 4: updated/held
1103
+ callSessionId: "session-ABC"
1104
+ callId: "call-123"
1105
+ actorPersonId: "associate-001"
1106
+ personality: "terminator"
1107
+ state: "held"
1108
+ eventTimestamp: "2026-05-01T14:00:35.000Z"
1109
+
1110
+ Event 5: updated/resumed
1111
+ callSessionId: "session-ABC"
1112
+ callId: "call-123"
1113
+ actorPersonId: "associate-001"
1114
+ personality: "terminator"
1115
+ state: "connected"
1116
+ eventTimestamp: "2026-05-01T14:01:05.000Z"
1117
+
1118
+ Event 6: deleted/disconnected
1119
+ callSessionId: "session-ABC"
1120
+ callId: "call-123"
1121
+ actorPersonId: "associate-001"
1122
+ personality: "terminator"
1123
+ state: "disconnected"
1124
+ eventTimestamp: "2026-05-01T14:02:30.000Z"
1125
+ disconnected: "2026-05-01T14:02:30.000Z"
1126
+ ```
1127
+
1128
+ ### Correlation Rules
1129
+
1130
+ 1. **Group by `callSessionId`** to track one call end-to-end. A transfer creates new `callId` values but keeps the same `callSessionId`.
1131
+ 2. **Key timers and state on `callId`**, not `callSessionId`. A transferred call has multiple legs — each leg has its own hold/resume cycle.
1132
+ 3. **Use `actorPersonId`** to route events to the correct user context (e.g., map to a store register).
1133
+ 4. **Use `personality`** to distinguish the user's role: `terminator` = they received the call, `originator` = they placed it.
1134
+ 5. **Use `eventTimestamp`** for ordering, not arrival order. Events can arrive out of sequence (see §11).
1135
+
1136
+ ### Middleware State Machine
1137
+
1138
+ For a hold-timeout middleware (e.g., the AAP retail pattern), the state machine keyed on `callId`:
1139
+
1140
+ ```
1141
+ answered
1142
+ ┌──────────────────────────────┐
1143
+ │ ▼
1144
+ [IDLE] ──alerting──▶ [RINGING] ──▶ [ACTIVE]
1145
+ │ ▲
1146
+ held │ │ resumed
1147
+ ▼ │
1148
+ [ON_HOLD]
1149
+
1150
+ timer fires (60s)
1151
+
1152
+
1153
+ [HANGUP_PENDING]
1154
+
1155
+ disconnected (any state)
1156
+
1157
+
1158
+ [IDLE]
1159
+ ```
1160
+
1161
+ **Key design decisions:**
1162
+ - Key the state map on `callId` (not `callSessionId`) — each leg is independent
1163
+ - Store state in Redis with TTL (e.g., 120s) — auto-cleanup if `disconnected` is lost
1164
+ - Handle out-of-order: if `resumed` arrives while hangup API call is in flight, the hangup fails safely (call is no longer held)
1165
+ - Handle duplicate: check current state before acting — if already in `[IDLE]`, ignore the event
1166
+
1167
+ ### Race Condition Handling
1168
+
1169
+ | Race Condition | Resolution |
1170
+ |----------------|------------|
1171
+ | `resumed` arrives after timer fires but before hangup completes | Hangup API returns error (call not held) — safe to ignore |
1172
+ | `disconnected` arrives before `held` | State transitions to IDLE; late `held` ignored (call already gone) |
1173
+ | Duplicate `held` events | Timer already running — ignore duplicate |
1174
+ | `connected` arrives before `answered` | Both transition to ACTIVE — idempotent |
1175
+
1176
+ ---
1177
+
1178
+ ## 14. Scale Patterns
1179
+
1180
+ ### One Org-Level Webhook vs Many Filtered Webhooks
1181
+
1182
+ | Approach | Pros | Cons |
1183
+ |----------|------|------|
1184
+ | **One org-level webhook** (`ownedBy: org`, `event: all`) | Single subscription, simple management, no per-user provisioning | High event volume, receiver must filter/route, org-level may not be supported for all resources (see §10) |
1185
+ | **Per-user filtered webhooks** (`filter: personId=X`) | Low noise per endpoint, precise routing | O(n) webhook management, provisioning/deprovisioning overhead, webhook count limits |
1186
+ | **Org-level + server-side filter** | Best of both — one subscription, filter in middleware | Receiver must handle full org volume |
1187
+
1188
+ **Recommendation for 1,000+ users:** One org-level webhook with server-side filtering. Per-user webhooks don't scale — you'd need to create/delete webhooks as users are provisioned/deprovisioned, and there may be undocumented limits on total webhook count per org.
1189
+
1190
+ ### Volume Estimation
1191
+
1192
+ Estimate event volume for capacity planning:
1193
+
1194
+ ```
1195
+ Events/hour = concurrent_calls × events_per_call × calls_per_hour_per_line
1196
+
1197
+ Example (4,500-store retail chain, 35,000 phones):
1198
+ Peak concurrent utilization: 10% = 3,500 active calls
1199
+ Events per call: ~6 (alerting, answered, connected, [held, resumed], disconnected)
1200
+ Call duration: ~3 minutes average
1201
+ Calls per hour per active line: ~20
1202
+
1203
+ Peak burst: 3,500 × 6 = 21,000 events in a burst window
1204
+ Sustained: ~70,000 events/hour at peak
1205
+ ```
1206
+
1207
+ ### Stateless Receiver Architecture
1208
+
1209
+ For high-volume webhook processing, use stateless HTTP receivers behind a load balancer:
1210
+
1211
+ ```
1212
+ Webex Platform
1213
+
1214
+ │ HTTPS POST (one per event)
1215
+
1216
+ ┌─────────────┐
1217
+ │ Load Balancer│
1218
+ │ (ALB / CLB) │
1219
+ └──────┬──────┘
1220
+
1221
+ ┌────┼────┐
1222
+ ▼ ▼ ▼
1223
+ ┌───┐┌───┐┌───┐
1224
+ │ W ││ W ││ W │ Stateless workers
1225
+ │ 1 ││ 2 ││ 3 │ (Lambda / Cloud Run / K8s pods)
1226
+ └─┬─┘└─┬─┘└─┬─┘
1227
+ │ │ │
1228
+ └────┼────┘
1229
+
1230
+ ┌─────────────┐
1231
+ │ Redis Cluster│ Timer state, dedup, call-to-register mapping
1232
+ └─────────────┘
1233
+ ```
1234
+
1235
+ **Design principles:**
1236
+ - Workers are stateless — any worker can handle any event
1237
+ - Shared state (timers, call tracking) lives in Redis with TTLs
1238
+ - HMAC verification at the worker level (each worker has the secret)
1239
+ - Return 200 immediately, process asynchronously if heavy work is needed
1240
+ - No persistent connections to Webex — webhooks are push-only
1241
+
1242
+ ### Timer State Management (Redis Pattern)
1243
+
1244
+ For hold-timeout tracking at scale:
1245
+
1246
+ ```python
1247
+ import redis
1248
+ import json
1249
+
1250
+ r = redis.Redis()
1251
+
1252
+ def on_held(call_id: str, actor_person_id: str):
1253
+ """Start a 60-second hold timer."""
1254
+ key = f"hold:{call_id}"
1255
+ r.setex(key, 120, json.dumps({ # TTL 120s (2× timer for safety)
1256
+ "actor": actor_person_id,
1257
+ "held_at": time.time()
1258
+ }))
1259
+ # Enqueue a delayed job to check at T+60s
1260
+ # (use Redis Streams, SQS delay, or scheduled task)
1261
+
1262
+ def on_resumed(call_id: str):
1263
+ """Cancel the hold timer."""
1264
+ r.delete(f"hold:{call_id}")
1265
+
1266
+ def on_timer_fire(call_id: str):
1267
+ """Timer expired — check if still held, then act."""
1268
+ key = f"hold:{call_id}"
1269
+ data = r.get(key)
1270
+ if not data:
1271
+ return # Already resumed or disconnected
1272
+ r.delete(key)
1273
+ # Execute hangup via Service App Members API
1274
+ ```
1275
+
1276
+ ### Why Not XSI for Large-Scale Event Routing
1277
+
1278
+ XSI (Extended Services Interface) provides real-time call event streaming but has architectural constraints that make webhooks the better choice at scale:
1279
+
1280
+ | Factor | XSI | Webhooks |
1281
+ |--------|-----|----------|
1282
+ | Connection model | Persistent streaming (one channel per XSP) | Stateless HTTP POST (push) |
1283
+ | Scaling model | Single-process Python (GIL-bound) | Horizontally scaled workers |
1284
+ | Org-wide subscription | Single connection for entire org | Single webhook registration |
1285
+ | Failure recovery | 60-second gap on channel failure, events lost | Individual event delivery, auto-deactivation on sustained failure |
1286
+ | Duplicate subscriptions | Duplicate org-wide subscriptions cause duplicate events | Same (multiple webhooks = duplicate delivery) |
1287
+ | Queue bottleneck | `queue.Queue(maxsize=500)` in-process | External (Redis, SQS, etc.) |
1288
+ | Deployment | Requires long-running process with DNS SRV discovery | Serverless-compatible (Lambda, Cloud Run) |
1289
+
1290
+ **Bottom line:** XSI is designed for single-site monitoring dashboards (wallboards, supervisor consoles). Webhooks are designed for distributed, horizontally-scaled event processing. For 1,000+ users, use webhooks.
1291
+
1292
+ See [wxcadm-xsi-realtime.md](wxcadm-xsi-realtime.md) for XSI architecture details and the event streaming API.
1293
+
1294
+ ---
1295
+
1296
+ ## 15. wxcli Command Reference
1297
+
1298
+ All webhook management is via the `wxcli webhooks` command group.
1299
+
1300
+ ### webhooks list
1301
+
1302
+ ```bash
1303
+ wxcli webhooks list [OPTIONS]
1304
+ ```
1305
+
1306
+ | Flag | Type | Default | Description |
1307
+ |------|------|---------|-------------|
1308
+ | `--owned-by` | str | (none) | Filter to org-wide webhooks only (value: `org`) |
1309
+ | `--output`, `-o` | str | `table` | Output format: `table` or `json` |
1310
+ | `--limit` | int | `0` | Max results (0 = all) |
1311
+ | `--offset` | int | `0` | Pagination offset |
1312
+ | `--debug` | flag | | Enable debug output |
1313
+
1314
+ ```bash
1315
+ # List all webhooks in table format
1316
+ wxcli webhooks list
1317
+
1318
+ # List org-level webhooks as JSON
1319
+ wxcli webhooks list --owned-by org -o json
1320
+
1321
+ # List first 10 webhooks
1322
+ wxcli webhooks list --limit 10
1323
+ ```
1324
+
1325
+ ### webhooks create
1326
+
1327
+ ```bash
1328
+ wxcli webhooks create [OPTIONS]
1329
+ ```
1330
+
1331
+ | Flag | Type | Required | Description |
1332
+ |------|------|----------|-------------|
1333
+ | `--name` | str | Yes | User-friendly name |
1334
+ | `--target-url` | str | Yes | HTTPS URL that receives POST requests |
1335
+ | `--resource` | str | Yes | Resource type (e.g., `telephony_calls`, `messages`) |
1336
+ | `--event` | str | Yes | Event type (e.g., `all`, `created`, `updated`, `deleted`) |
1337
+ | `--filter` | str | No | Filter expression (e.g., `personality=terminator`) |
1338
+ | `--secret` | str | No | HMAC secret for payload signature verification |
1339
+ | `--owned-by` | str | No | Set to `org` for org-level webhook |
1340
+ | `--json-body` | str | No | Full JSON request body (overrides all other options) |
1341
+ | `--output`, `-o` | str | `id` | Output format: `id` (just the webhook ID) or `json` (full response) |
1342
+ | `--debug` | flag | | Enable debug output |
1343
+
1344
+ ```bash
1345
+ # Create a telephony webhook with HMAC secret
1346
+ wxcli webhooks create \
1347
+ --name "Call Events" \
1348
+ --target-url https://middleware.example.com/webhooks \
1349
+ --resource telephony_calls \
1350
+ --event all \
1351
+ --secret "my-hmac-secret"
1352
+
1353
+ # Create an org-level filtered webhook, output full JSON
1354
+ wxcli webhooks create \
1355
+ --name "Org Incoming Calls" \
1356
+ --target-url https://middleware.example.com/webhooks \
1357
+ --resource telephony_calls \
1358
+ --event all \
1359
+ --owned-by org \
1360
+ --filter "personality=terminator" \
1361
+ -o json
1362
+
1363
+ # Create via JSON body
1364
+ wxcli webhooks create --json-body '{
1365
+ "name": "Custom Webhook",
1366
+ "targetUrl": "https://example.com/hook",
1367
+ "resource": "messages",
1368
+ "event": "created",
1369
+ "filter": "roomId=Y2lz..."
1370
+ }'
1371
+ ```
1372
+
1373
+ ### webhooks show
1374
+
1375
+ ```bash
1376
+ wxcli webhooks show WEBHOOK_ID [OPTIONS]
1377
+ ```
1378
+
1379
+ | Flag | Type | Default | Description |
1380
+ |------|------|---------|-------------|
1381
+ | `--output`, `-o` | str | `json` | Output format: `table` or `json` |
1382
+ | `--debug` | flag | | Enable debug output |
1383
+
1384
+ ```bash
1385
+ # Show webhook details as JSON
1386
+ wxcli webhooks show "Y2lzY29zcGFyazov..."
1387
+
1388
+ # Show as table
1389
+ wxcli webhooks show "Y2lzY29zcGFyazov..." -o table
1390
+ ```
1391
+
1392
+ ### webhooks update
1393
+
1394
+ ```bash
1395
+ wxcli webhooks update WEBHOOK_ID [OPTIONS]
1396
+ ```
1397
+
1398
+ | Flag | Type | Description |
1399
+ |------|------|-------------|
1400
+ | `--name` | str | New name |
1401
+ | `--target-url` | str | New target URL |
1402
+ | `--secret` | str | New HMAC secret |
1403
+ | `--owned-by` | str | Ownership (present in CLI but immutable after creation — see gotcha #7) |
1404
+ | `--status` | str | Set to `active` to reactivate an auto-deactivated webhook |
1405
+ | `--json-body` | str | Full JSON request body (overrides all other options) |
1406
+ | `--debug` | flag | Enable debug output |
1407
+
1408
+ ```bash
1409
+ # Reactivate an auto-deactivated webhook
1410
+ wxcli webhooks update "Y2lzY29zcGFyazov..." --status active
1411
+
1412
+ # Change target URL and secret
1413
+ wxcli webhooks update "Y2lzY29zcGFyazov..." \
1414
+ --target-url https://new-server.example.com/webhooks \
1415
+ --secret "new-hmac-secret"
1416
+ ```
1417
+
1418
+ ### webhooks delete
1419
+
1420
+ ```bash
1421
+ wxcli webhooks delete WEBHOOK_ID [OPTIONS]
1422
+ ```
1423
+
1424
+ | Flag | Type | Description |
1425
+ |------|------|-------------|
1426
+ | `--force` | flag | Skip confirmation prompt |
1427
+ | `--debug` | flag | Enable debug output |
1428
+
1429
+ ```bash
1430
+ # Delete with confirmation prompt
1431
+ wxcli webhooks delete "Y2lzY29zcGFyazov..."
1432
+
1433
+ # Delete without confirmation
1434
+ wxcli webhooks delete "Y2lzY29zcGFyazov..." --force
1435
+ ```
1436
+
1437
+ ---
1438
+
1439
+ ## See Also
1440
+
1441
+ - **[call-control.md](call-control.md)** — Call control actions and the `TelephonyCall` data model. `TelephonyEventData` inherits from `TelephonyCall`, so the `remoteParty`, `state`, `personality`, and other call fields documented there apply directly to webhook event data.
1442
+ - **[authentication.md](authentication.md)** — Scope definitions and OAuth token management. Creating a `telephony_calls` webhook requires `spark:calls_read` scope; a firehose requires `spark:all`.
1443
+ - **[admin-apps-data.md](admin-apps-data.md)** — Service App registration, authorization, and token lifecycle. Cross-reference §12 for Service App webhook creation.
1444
+ - **[wxcadm-xsi-realtime.md](wxcadm-xsi-realtime.md)** — XSI event streaming architecture. Cross-reference §14 for why webhooks scale better than XSI for large deployments.