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.
- wxcli/__init__.py +4 -0
- wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
- wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
- wxcli/_playbook/.claude/rules/cleanup.md +29 -0
- wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
- wxcli/_playbook/.claude/rules/org-health.md +14 -0
- wxcli/_playbook/.claude/settings.json +8 -0
- wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
- wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
- wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
- wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
- wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
- wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
- wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
- wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
- wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
- wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
- wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
- wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
- wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
- wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
- wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
- wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
- wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
- wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
- wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
- wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
- wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
- wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
- wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
- wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
- wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
- wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
- wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
- wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
- wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
- wxcli/_playbook/CLAUDE.md +373 -0
- wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
- wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
- wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
- wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
- wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
- wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
- wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
- wxcli/_playbook/docs/reference/admin-partner.md +357 -0
- wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
- wxcli/_playbook/docs/reference/authentication.md +965 -0
- wxcli/_playbook/docs/reference/call-control.md +1205 -0
- wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
- wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
- wxcli/_playbook/docs/reference/call-routing.md +2304 -0
- wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
- wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
- wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
- wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
- wxcli/_playbook/docs/reference/devices-core.md +1869 -0
- wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
- wxcli/_playbook/docs/reference/devices-platform.md +467 -0
- wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
- wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
- wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
- wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
- wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
- wxcli/_playbook/docs/reference/meetings-content.md +473 -0
- wxcli/_playbook/docs/reference/meetings-core.md +903 -0
- wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
- wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
- wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
- wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
- wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
- wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
- wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
- wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
- wxcli/_playbook/docs/reference/provisioning.md +1366 -0
- wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
- wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
- wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
- wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
- wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
- wxcli/_version.py +24 -0
- wxcli/auth.py +137 -0
- wxcli/commands/__init__.py +0 -0
- wxcli/commands/_registry.py +181 -0
- wxcli/commands/activation_email.py +103 -0
- wxcli/commands/admin_recordings.py +585 -0
- wxcli/commands/analytics.py +105 -0
- wxcli/commands/announcement_playlists.py +219 -0
- wxcli/commands/announcements.py +519 -0
- wxcli/commands/archive_users.py +36 -0
- wxcli/commands/attachment_actions.py +60 -0
- wxcli/commands/audit_events.py +83 -0
- wxcli/commands/authorizations.py +113 -0
- wxcli/commands/auto_attendant.py +642 -0
- wxcli/commands/broadworks_billing_reports.py +126 -0
- wxcli/commands/broadworks_enterprises.py +160 -0
- wxcli/commands/broadworks_subscribers.py +319 -0
- wxcli/commands/broadworks_workspaces.py +108 -0
- wxcli/commands/call_controls.py +1304 -0
- wxcli/commands/call_park.py +494 -0
- wxcli/commands/call_pickup.py +229 -0
- wxcli/commands/call_queue.py +1878 -0
- wxcli/commands/call_recording.py +707 -0
- wxcli/commands/call_routing.py +1610 -0
- wxcli/commands/call_settings_for_me_phase_5.py +212 -0
- wxcli/commands/caller_reputation.py +169 -0
- wxcli/commands/calling_service.py +305 -0
- wxcli/commands/cc_address_book.py +770 -0
- wxcli/commands/cc_agent_greetings.py +242 -0
- wxcli/commands/cc_agent_summaries.py +39 -0
- wxcli/commands/cc_agent_wellbeing.py +216 -0
- wxcli/commands/cc_agents.py +425 -0
- wxcli/commands/cc_ai_assistant.py +55 -0
- wxcli/commands/cc_ai_feature.py +294 -0
- wxcli/commands/cc_audio_files.py +149 -0
- wxcli/commands/cc_auto_csat.py +311 -0
- wxcli/commands/cc_aux_code.py +458 -0
- wxcli/commands/cc_business_hour.py +353 -0
- wxcli/commands/cc_call_monitoring.py +228 -0
- wxcli/commands/cc_callbacks.py +194 -0
- wxcli/commands/cc_campaign.py +212 -0
- wxcli/commands/cc_captures.py +39 -0
- wxcli/commands/cc_contact_list.py +169 -0
- wxcli/commands/cc_contact_number.py +295 -0
- wxcli/commands/cc_data_sources.py +235 -0
- wxcli/commands/cc_desktop_layout.py +380 -0
- wxcli/commands/cc_desktop_profile.py +322 -0
- wxcli/commands/cc_dial_number.py +511 -0
- wxcli/commands/cc_dial_plan.py +347 -0
- wxcli/commands/cc_dnc.py +101 -0
- wxcli/commands/cc_entry_point.py +545 -0
- wxcli/commands/cc_ewt.py +51 -0
- wxcli/commands/cc_flow.py +477 -0
- wxcli/commands/cc_global_vars.py +430 -0
- wxcli/commands/cc_holiday_list.py +335 -0
- wxcli/commands/cc_journey.py +1261 -0
- wxcli/commands/cc_legacy_flows.py +49 -0
- wxcli/commands/cc_multimedia_profile.py +436 -0
- wxcli/commands/cc_notification.py +51 -0
- wxcli/commands/cc_outdial_ani.py +604 -0
- wxcli/commands/cc_overrides.py +341 -0
- wxcli/commands/cc_queue.py +1083 -0
- wxcli/commands/cc_queue_stats.py +55 -0
- wxcli/commands/cc_realtime.py +51 -0
- wxcli/commands/cc_resource_collection.py +312 -0
- wxcli/commands/cc_search.py +46 -0
- wxcli/commands/cc_site.py +400 -0
- wxcli/commands/cc_skill.py +350 -0
- wxcli/commands/cc_skill_profile.py +289 -0
- wxcli/commands/cc_subscriptions.py +426 -0
- wxcli/commands/cc_summaries.py +130 -0
- wxcli/commands/cc_tasks.py +844 -0
- wxcli/commands/cc_team.py +325 -0
- wxcli/commands/cc_user_profiles.py +349 -0
- wxcli/commands/cc_users.py +701 -0
- wxcli/commands/cc_work_types.py +363 -0
- wxcli/commands/cdr.py +89 -0
- wxcli/commands/classifications.py +38 -0
- wxcli/commands/cleanup.py +1427 -0
- wxcli/commands/client_settings.py +72 -0
- wxcli/commands/conference.py +328 -0
- wxcli/commands/configure.py +72 -0
- wxcli/commands/converged_recordings.py +347 -0
- wxcli/commands/converged_recordings_export.py +310 -0
- wxcli/commands/cq_playlists.py +42 -0
- wxcli/commands/cucm.py +2794 -0
- wxcli/commands/cucm_config.py +129 -0
- wxcli/commands/customer_assist.py +373 -0
- wxcli/commands/data_sources.py +227 -0
- wxcli/commands/dect_devices.py +787 -0
- wxcli/commands/device_configurations.py +77 -0
- wxcli/commands/device_dynamic_settings.py +320 -0
- wxcli/commands/device_settings.py +1489 -0
- wxcli/commands/devices.py +280 -0
- wxcli/commands/domains.py +142 -0
- wxcli/commands/ecm.py +178 -0
- wxcli/commands/emergency_services.py +827 -0
- wxcli/commands/events.py +83 -0
- wxcli/commands/external_voicemail.py +52 -0
- wxcli/commands/groups.py +206 -0
- wxcli/commands/guest_management.py +76 -0
- wxcli/commands/hds.py +340 -0
- wxcli/commands/hot_desk.py +67 -0
- wxcli/commands/hot_desking_members.py +118 -0
- wxcli/commands/hot_desking_portal.py +124 -0
- wxcli/commands/hunt_group.py +577 -0
- wxcli/commands/hybrid_clusters.py +71 -0
- wxcli/commands/hybrid_connectors.py +67 -0
- wxcli/commands/identity_org.py +159 -0
- wxcli/commands/init_playbook.py +155 -0
- wxcli/commands/licenses.py +106 -0
- wxcli/commands/live_monitoring.py +40 -0
- wxcli/commands/location_call_handling.py +577 -0
- wxcli/commands/location_schedules.py +341 -0
- wxcli/commands/location_settings.py +1297 -0
- wxcli/commands/location_voicemail.py +494 -0
- wxcli/commands/locations.py +336 -0
- wxcli/commands/meeting_captions.py +110 -0
- wxcli/commands/meeting_chats.py +63 -0
- wxcli/commands/meeting_invitees.py +234 -0
- wxcli/commands/meeting_messages.py +28 -0
- wxcli/commands/meeting_participants.py +278 -0
- wxcli/commands/meeting_polls.py +111 -0
- wxcli/commands/meeting_preferences.py +515 -0
- wxcli/commands/meeting_qa.py +78 -0
- wxcli/commands/meeting_qualities.py +43 -0
- wxcli/commands/meeting_reports.py +101 -0
- wxcli/commands/meeting_session_types.py +105 -0
- wxcli/commands/meeting_site.py +57 -0
- wxcli/commands/meeting_slido.py +44 -0
- wxcli/commands/meeting_summaries.py +92 -0
- wxcli/commands/meeting_tracking_codes.py +233 -0
- wxcli/commands/meeting_transcripts.py +230 -0
- wxcli/commands/meetings.py +1997 -0
- wxcli/commands/memberships.py +164 -0
- wxcli/commands/messages.py +213 -0
- wxcli/commands/mode_management.py +244 -0
- wxcli/commands/my_call_settings.py +3577 -0
- wxcli/commands/numbers.py +405 -0
- wxcli/commands/operating_modes.py +410 -0
- wxcli/commands/org_contacts.py +284 -0
- wxcli/commands/org_health_cli.py +55 -0
- wxcli/commands/org_settings.py +71 -0
- wxcli/commands/organizations.py +83 -0
- wxcli/commands/paging_group.py +257 -0
- wxcli/commands/partner_admins.py +141 -0
- wxcli/commands/partner_reports.py +188 -0
- wxcli/commands/partner_tags.py +216 -0
- wxcli/commands/people.py +279 -0
- wxcli/commands/person_call_settings.py +71 -0
- wxcli/commands/pstn.py +280 -0
- wxcli/commands/recording_report.py +154 -0
- wxcli/commands/report_templates.py +38 -0
- wxcli/commands/reports.py +144 -0
- wxcli/commands/resource_group_memberships.py +160 -0
- wxcli/commands/resource_groups.py +67 -0
- wxcli/commands/roles.py +63 -0
- wxcli/commands/room_tabs.py +160 -0
- wxcli/commands/rooms.py +225 -0
- wxcli/commands/scim_bulk.py +46 -0
- wxcli/commands/scim_groups.py +246 -0
- wxcli/commands/scim_schemas.py +82 -0
- wxcli/commands/scim_users.py +283 -0
- wxcli/commands/security_audit.py +54 -0
- wxcli/commands/service_apps.py +51 -0
- wxcli/commands/single_number_reach.py +209 -0
- wxcli/commands/team_memberships.py +155 -0
- wxcli/commands/teams.py +153 -0
- wxcli/commands/ucm_profile.py +42 -0
- wxcli/commands/update.py +219 -0
- wxcli/commands/user_settings.py +5116 -0
- wxcli/commands/video_mesh.py +1090 -0
- wxcli/commands/virtual_extensions.py +502 -0
- wxcli/commands/virtual_line_settings.py +2072 -0
- wxcli/commands/webhooks.py +176 -0
- wxcli/commands/wholesale_billing_reports.py +147 -0
- wxcli/commands/wholesale_provisioning.py +523 -0
- wxcli/commands/workspace_locations.py +339 -0
- wxcli/commands/workspace_metrics.py +103 -0
- wxcli/commands/workspace_personalization.py +70 -0
- wxcli/commands/workspace_settings.py +3347 -0
- wxcli/commands/workspaces.py +279 -0
- wxcli/commands/xapi.py +114 -0
- wxcli/config.py +139 -0
- wxcli/errors.py +77 -0
- wxcli/main.py +210 -0
- wxcli/migration/CLAUDE.md +106 -0
- wxcli/migration/__init__.py +1 -0
- wxcli/migration/advisory/CLAUDE.md +196 -0
- wxcli/migration/advisory/__init__.py +46 -0
- wxcli/migration/advisory/advisor.py +107 -0
- wxcli/migration/advisory/advisory_patterns.py +2552 -0
- wxcli/migration/advisory/recommendation_rules.py +835 -0
- wxcli/migration/cucm/CLAUDE.md +74 -0
- wxcli/migration/cucm/__init__.py +5 -0
- wxcli/migration/cucm/connection.py +265 -0
- wxcli/migration/cucm/discovery.py +225 -0
- wxcli/migration/cucm/extractors/__init__.py +1 -0
- wxcli/migration/cucm/extractors/announcements.py +92 -0
- wxcli/migration/cucm/extractors/base.py +90 -0
- wxcli/migration/cucm/extractors/device_profiles.py +98 -0
- wxcli/migration/cucm/extractors/devices.py +193 -0
- wxcli/migration/cucm/extractors/e911.py +103 -0
- wxcli/migration/cucm/extractors/features.py +309 -0
- wxcli/migration/cucm/extractors/helpers.py +73 -0
- wxcli/migration/cucm/extractors/informational.py +295 -0
- wxcli/migration/cucm/extractors/locations.py +221 -0
- wxcli/migration/cucm/extractors/moh.py +100 -0
- wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
- wxcli/migration/cucm/extractors/routing.py +493 -0
- wxcli/migration/cucm/extractors/shared_lines.py +146 -0
- wxcli/migration/cucm/extractors/templates.py +229 -0
- wxcli/migration/cucm/extractors/tier4.py +173 -0
- wxcli/migration/cucm/extractors/users.py +240 -0
- wxcli/migration/cucm/extractors/voicemail.py +243 -0
- wxcli/migration/cucm/extractors/workspaces.py +88 -0
- wxcli/migration/cucm/unity_connection.py +304 -0
- wxcli/migration/execute/CLAUDE.md +372 -0
- wxcli/migration/execute/__init__.py +303 -0
- wxcli/migration/execute/batch.py +277 -0
- wxcli/migration/execute/dependency.py +483 -0
- wxcli/migration/execute/engine.py +964 -0
- wxcli/migration/execute/handlers.py +2232 -0
- wxcli/migration/execute/planner.py +1939 -0
- wxcli/migration/execute/runtime.py +571 -0
- wxcli/migration/export/CLAUDE.md +49 -0
- wxcli/migration/export/__init__.py +8 -0
- wxcli/migration/export/csv_export.py +126 -0
- wxcli/migration/export/deployment_plan.py +479 -0
- wxcli/migration/export/json_export.py +60 -0
- wxcli/migration/models.py +941 -0
- wxcli/migration/phone_models.py +50 -0
- wxcli/migration/preflight/CLAUDE.md +52 -0
- wxcli/migration/preflight/__init__.py +89 -0
- wxcli/migration/preflight/checks.py +911 -0
- wxcli/migration/preflight/runner.py +293 -0
- wxcli/migration/rate_limiter.py +144 -0
- wxcli/migration/report/CLAUDE.md +245 -0
- wxcli/migration/report/__init__.py +7 -0
- wxcli/migration/report/appendix.py +2437 -0
- wxcli/migration/report/assembler.py +183 -0
- wxcli/migration/report/charts.py +263 -0
- wxcli/migration/report/executive.py +776 -0
- wxcli/migration/report/explainer.py +704 -0
- wxcli/migration/report/helpers.py +74 -0
- wxcli/migration/report/ingest.py +152 -0
- wxcli/migration/report/notice_templates.py +192 -0
- wxcli/migration/report/score.py +375 -0
- wxcli/migration/report/styles.py +1052 -0
- wxcli/migration/report/user_diff.py +1052 -0
- wxcli/migration/report/user_notice.py +425 -0
- wxcli/migration/state.py +139 -0
- wxcli/migration/store.py +860 -0
- wxcli/migration/transform/CLAUDE.md +107 -0
- wxcli/migration/transform/__init__.py +12 -0
- wxcli/migration/transform/analysis_pipeline.py +453 -0
- wxcli/migration/transform/analyzers/__init__.py +142 -0
- wxcli/migration/transform/analyzers/css_permission.py +189 -0
- wxcli/migration/transform/analyzers/css_routing.py +335 -0
- wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
- wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
- wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
- wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
- wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
- wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
- wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
- wxcli/migration/transform/analyzers/missing_data.py +300 -0
- wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
- wxcli/migration/transform/analyzers/shared_line.py +170 -0
- wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
- wxcli/migration/transform/analyzers/workspace_license.py +212 -0
- wxcli/migration/transform/cross_reference.py +1136 -0
- wxcli/migration/transform/cucm_pattern.py +246 -0
- wxcli/migration/transform/decisions.py +301 -0
- wxcli/migration/transform/e164.py +145 -0
- wxcli/migration/transform/engine.py +256 -0
- wxcli/migration/transform/mappers/CLAUDE.md +123 -0
- wxcli/migration/transform/mappers/__init__.py +76 -0
- wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
- wxcli/migration/transform/mappers/base.py +190 -0
- wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
- wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
- wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
- wxcli/migration/transform/mappers/css_mapper.py +868 -0
- wxcli/migration/transform/mappers/dect_mapper.py +367 -0
- wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
- wxcli/migration/transform/mappers/device_mapper.py +227 -0
- wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
- wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
- wxcli/migration/transform/mappers/e911_mapper.py +129 -0
- wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
- wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
- wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
- wxcli/migration/transform/mappers/line_mapper.py +310 -0
- wxcli/migration/transform/mappers/location_mapper.py +235 -0
- wxcli/migration/transform/mappers/moh_mapper.py +114 -0
- wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
- wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
- wxcli/migration/transform/mappers/routing_mapper.py +821 -0
- wxcli/migration/transform/mappers/snr_mapper.py +201 -0
- wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
- wxcli/migration/transform/mappers/user_mapper.py +264 -0
- wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
- wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
- wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
- wxcli/migration/transform/normalizers.py +2008 -0
- wxcli/migration/transform/pattern_converter.py +106 -0
- wxcli/migration/transform/pipeline.py +271 -0
- wxcli/migration/transform/rules.py +255 -0
- wxcli/org_health/CLAUDE.md +63 -0
- wxcli/org_health/__init__.py +7 -0
- wxcli/org_health/analyze.py +93 -0
- wxcli/org_health/checks.py +458 -0
- wxcli/org_health/collector.py +62 -0
- wxcli/org_health/models.py +78 -0
- wxcli/org_health/report.py +300 -0
- wxcli/output.py +101 -0
- wxcli-1.2.0.dist-info/METADATA +505 -0
- wxcli-1.2.0.dist-info/RECORD +413 -0
- wxcli-1.2.0.dist-info/WHEEL +5 -0
- wxcli-1.2.0.dist-info/entry_points.txt +2 -0
- wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
- wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
- wxcli-1.2.0.dist-info/scm_version.json +8 -0
- 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.
|