APIs
XML API DeprecationGetting StartedREST API BasicsComplianceWebhooksWebex APIs
Admin
Calling
Contact Center
Devices
Meetings
Messaging
Webex Assistant Skills
FedRAMP
Full API Reference
API Changelog

Workspace Integrations Guide

Workspace Integrations provide a framework for services to access Webex developer APIs, with a focus on extending the capabilities of Workspaces and Webex devices. These integrations can be public (built by 3rd party vendors that engage with Cisco to provide all customers access to the integration), or they can be private (built by and available only to the customers themselves).

For the customer Control Hub administrator the integrations framework provides a way to enable a 3rd party vendor to interact with the Webex APIs, and especially the Webex device xAPI, in a transparent and controlled manner. It's easy to activate and deactivate integrations, understand what permissions they have been granted, and be in control of changes to these permissions as the integrations evolve.

This article describes how to develop and deploy these integrations, for both 3rd party vendors and customers that want to build private integrations.

How to Setup a Workspace Integration

Follow the steps below to set up a new org private Workspace Integration. Every step is detailed further below in this guide.

  1. Describe the application in a manifest file and upload it in Control Hub. This gives you an OAuth client ID and a client secret. For further details, see The Integration Manifest for information regarding the manifest, and Deployment to make the integration available for customers to activate.

  2. Receive an activation JWT. If the manifest specified the manual provisioning flow, this happens by an admin activating the integration in Control Hub and copy-pasting the JWT to your integration, as described in Activation.

  3. Use the OAuth credentials from step 1 and the credentials from step 2 to update the integration status, as described in Update the integration activation status.

    At this point, you have everything you need to start interacting with the Webex APIs.

  4. Implement the rest of the Workspace Integrations protocol. See Management.

  5. Use your credentials to interact with the Webex APIs in order to implement your functionality.

To create a public integration there are some additional steps, such as that the manifest needs to be approved and deployed by Cisco, as detailed in the sections below.

Comparison with REST API Integrations

Workspace Integrations are not the same as the integrations described in Integrations. They have things in common, specifically using the same underlying OAuth 2.0 framework, but differ in a few ways:

  1. Workspace integrations are exclusively managed from Control Hub under Workspaces > Integrations.
  2. The workspace integration framework was designed to support service to service interaction. The integration runs with the identity of a machine account created in the customer organization, rather than on behalf of a Webex user.
  3. Workspace integrations support fine grained access to the Webex devices xAPI and webhook change notifications for select xAPI statuses.
anchorThe Integration Manifest
anchor

The integration manifest provides the meta data needed to understand the capabilities of the integration, what permissions it needs and how it should be provisioned. Let's have a look at an example and break down the fields:

Example Manifest
{
  "id": "ac6b6972-538e-11ec-bf63-0242ac130002",
  "manifestVersion": 1,
  "displayName": "ACME Device Integration",
  "vendor": "ACME INC.",
  "email": "example-app@example.com",
  "description": "The ACME integration will do magic with your Webex device sensor data",
  "descriptionUrl": "https://example.com/webexintegration/details",
  "tocUrl": "https://example.com/webexintegration/toc",
  "availability": "global",
  "apiAccess": [
    {
      "scope": "spark-admin:devices_read",
      "access": "required",
      "role": "id_readonly_admin"
    },
    {
      "scope": "spark-admin:workspaces_read",
      "access": "required",
      "role": "id_readonly_admin"
    },
    {
      "scope": "spark:xapi_statuses",
      "access": "required"
    },
    {
      "scope": "spark:xapi_commands",
      "access": "required"
    }
  ],
  "xapiAccess": {
    "status": [
      {
        "path": "RoomAnalytics.*",
        "access": "required"
      },
      {
        "path": "Peripherals.ConnectedDevice[*].RoomAnalytics.*",
        "access": "required"
      },
      {
        "path": "Standby.State",
        "access": "required"
      },
      {
        "path": "SystemUnit.State.NumberOfActiveCalls",
        "access": "required"
      }
    ],
    "commands": [
      {
        "path": "Message.Send",
        "access": "required"
      }
    ],
    "events": [
      {
        "path": "BootEvent",
        "access": "required"
      },
      {
        "path": "UserInterface.Message.Prompt.Response",
        "access": "required"
      }
    ]
  },
  "provisioning": {
    "type": "manual"
  }
}

This example describes an integration from ACME, INC. It's a global integration, which means it will have to be approved and deployed by Cisco, and thus made available to any customer that wants to activate it. For customers building their own integrations, the availability field would be set to org_private, allowing the manifest to be uploaded in Control Hub.

The apiAccess shows four required scopes, that would give access to reading device and workspace details, reading statuses and executing commands for the Webex Devices over the xAPI. The status, command and event details are specified in the xapiAccess. The example is requesting access to:

  • All status under RoomAnalytics for both the device itself and the attached peripherals (like the Room Navigator), the device standby state and how many active calls the device has.
  • The UserInterface.Message.Prompt.Display command that displays a prompt on the device screen with a title, text and up to five options for the user.
  • Two events: the boot event that is sent when a device reboots and the UserInterface.Message.Prompt.Response following a selection by the user for the message prompt.

The provisioning section shows that the integration is deploy type manual, which means the Control Hub admin will need to copy an activation code and provide this to the integration.

This is how the manifest will render in Control Hub:

acme
Manifest Details
FieldRequired / OptionalValue spaceDescription
idRequiredUUIDThe Id of the integration. Generated by the integration developer, needs to be globally unique.
manifestVersionRequiredIntegerThe manifest version. When the manifest changes, the version must be incremented.
displayNameRequiredStringA display name for the integration
vendorRequiredStringThe name of the vendor / company that created the integration
emailRequiredStringAn email address from the integration vendor
descriptionRequiredStringA description of what the integration does and what value it will provide to the customer
descriptionUrlOptionalStringA URL to a more detailed description of what the integration does and what value it provides to the customer
tocUrlRequired (when global)URLA URL to a web page listing the terms and conditions for the integration. The admin will have to accept these when enabling the integration from Control Hub. Note that this URL is not required for private integrations.
availabilityRequiredglobal or org_privateDescribes if the integration is global and available to all customers, or org_private, which means it only applies to a specific customer organization. Note that only org_private integrations can be manually uploaded by a Control Hub admin. global integrations are deployed by Cisco on behalf of the vendor.
apiAccessRequiredArrayA list of Webex API scopes the integration is requesting. These scopes allow access to specific public Webex developer APIs.
apiAccess.scopeRequiredStringThe scope requested. See Webex API scopes for examples.
apiAccess.accessRequiredrequired or optionalIs the scope required or optional. Optional scopes can be opted out by the Control Hub admin.
apiAccess.roleOptionalid_full_admin, id_readonly_admin or id_device_adminSome APIs require a specific use role to be assigned to the account created for the integration. For most read APIs, id_readonly_admin should be sufficient. Check the API docs for the specific APIs to see what roles, if any, are required.
xapiAccessRequiredArrayA list of device xAPI status, commands and events that the integration is requesting access to. See https://roomos.cisco.com/xapi for more details of what is supported by the Webex Devices
xapiAccess.status.pathRequiredStringThe status path requested. Wildcards indicate all statuses recursively under the specific path, say RoomAnalytics.* means all statuses under the RoomAnalytics node in the status tree.
xapiAccess.status.accessRequiredrequired or optionalIs the status required or optional? Optional statuses can be opted out by the Control Hub admin.
xapiAccess.commands.pathRequiredStringThe command(s) requested. Wildcards indicate all commands recursively under the specific path, say Bookings.* means all commands under the Bookings node in the command tree.
xapiAccess.commands.accessRequiredrequired or optionalIs the command required or optional. Optional commands can be opted out by the Control Hub admin.
xapiAccess.events.pathRequiredStringThe event requested.
xapiAccess.events.accessRequiredrequired or optionalIs the event required or optional? Optional events can be opted out by the Control Hub admin.
provisioningRequiredProvisioningDescribes how the integration is provisioned when enabled from Control Hub
provisioning.typeRequiredmanual or httpsmanual provisioning means the Control Hub admin will be presented with an activation code that is copied and provided to the integration. https provisioning removes the need for the activation code but requires a global activation URL for the 3rd party service. More details on this later.
provisioning.urlRequired (when https)URLIf the type is manual, the URL is optional. If set, it provides a link to the page where the admin needs to provide the activation code. If the type is https, the URL is required and is where the activation code is posted to start the activation flow. Note that the URL must be an HTTPS URL.
anchorDeployment
anchor

Deploying an integration will make it available for activation in Control Hub. Private integrations will only be accessible to the integration developer organization, and can be uploaded in Control Hub by the administrator:

Upload integration

Global integrations are approved and deployed by Cisco. They are available for all customers to activate through Control Hub. In both cases, the output of the process is an OAuth clientId and secret that the integration needs to get an access token to use in the Webex APIs.

OAuth clientID and secret

anchorActivation
anchor

Activating an integration ultimately means to provide the integration with the details needed to integrate with the Webex APIs. Administrators do this in Control Hub via the "Activate" button.

These details are in both modes of provisioning encoded in a Cisco signed JSON Web Token (JWT), in Control Hub named an activation code:

  1. Manual: The JWT is copied from Control Hub UI and must be manually provided to the integration.
  2. HTTPS: The JWT is transported in an HTTPS POST request to the URL specified in the provisioning part of the manifest.
activation

The JWT uses the ES256 algorithm and an ECDSA signature. To verify the signature, the JSON Web Key Set (JWKS) can be found at the following regional URLs:

RegionDescriptionURL
us-west-2_rUS West (Oregon)https://xapi-r.wbx2.com/jwks
us-east-2_aUS East (Ohio)https://xapi-a.wbx2.com/jwks
eu-central-1_kEurope (Frankfurt)https://xapi-k.wbx2.com/jwks

The region key is embedded in the JWT and can be used to lookup the correct JWKS URL. If a region is found in the JWT that does not match one of the three in this list, default to the us-east-2_a URL: https://xapi-a.wbx2.com/jwks.

Example JSON Web Key Set (JWKS)
{
  "keys": [
    {
      "kty": "EC",
      "use": "sig",
      "crv": "P-256",
      "kid": "VkOd36xy3JeGFka5uW5gW525",
      "key_ops": [
        "verify"
      ],
      "x": "ilEKY13J464rnzK4CvdIh1_ow3q4e1eoqnjXES_PnC8",
      "y": "T-Kp8qcf4FBvvtNIMSkQmAWnbd3uiz2U_NqfTavc1x8",
      "alg": "ES256"
    },
    {
      "kty": "EC",
      "use": "sig",
      "crv": "P-256",
      "kid": "GzaEArXcMo24RDst1kP5r1q7",
      "key_ops": [
        "verify"
      ],
      "x": "vflTChGJ6bjxt2e4M3nIVb90IZb9Ms7fg0wcQ9FrFV0",
      "y": "07HeNRvKQW4b-JiAuvNoXc957flcH6PD538jhWTFvHI",
      "alg": "ES256"
    },
    {
      "kty": "EC",
      "use": "sig",
      "crv": "P-256",
      "kid": "m9vlvDThppHKT0LlQbeURKwd",
      "key_ops": [
        "verify"
      ],
      "x": "h0rF6Q0EZXQsAGmNG-S7Dnw29LEpDE3lGj_FNOYoRJc",
      "y": "td1qLYMGXK9m4PqKF4cdrHMPoeU-g9pmPQGsGTnhSFU",
      "alg": "ES256"
    }
  ]
}
Example Activation JWT

Let's have a look at an example JWT activation code and break down the fields. You can find more details on the JSON Web Token standard in RFC7519. The JWT is a string with three dot separated base64url encoded strings:

header.payload/claims.signature

Encoded: 

eyJraWQiOiJnQjFzbnd6bGJwb3RuMFdZTm1EMFNIZVUiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9
.
eyJzdWIiOiJZMmx6WTI5emNHRnlhem92TDNWeWJqcFVSVUZOT25WekxXVmhjM1F0TVY5cGJuUXhNeTlQVWtkQlRrbGFRVlJKVDA0dk0yRTJabVl6TnpNdE5qaGhOeTAwTkdVMExUa3haRFl0WVRJM05EWXdaVEJoWXpWaiIsIm9hdXRoVXJsIjoiaHR0cHM6Ly9pbnRlZ3JhdGlvbi53ZWJleGFwaXMuY29tL3YxL2FjY2Vzc190b2tlbiIsIm9yZ05hbWUiOiJBdGxhcyBEZXZpY2UgTWFuYWdlbWVudCB0ZXN0IG9yZyIsImFwcFVybCI6Imh0dHBzOi8veGFwaS1pbnRiLndieDIuY29tL3hhcGkvYXBpL29yZ2FuaXphdGlvbnMvM2E2ZmYzNzMtNjhhNy00NGU0LTkxZDYtYTI3NDYwZTBhYzVjL2FwcHMvYWM2YjY5NzItNTM4ZS0xMWVjLWJmNjMtMDI0MmFjMTMwMDAyIiwiYXBwSWQiOiJhYzZiNjk3Mi01MzhlLTExZWMtYmY2My0wMjQyYWMxMzAwMDIiLCJleHBpcnlUaW1lIjoiMjAyMS0xMi0wNFQwOTozMzowNS44NjcyMTFaIiwiYWN0aW9uIjoicHJvdmlzaW9uIiwid2ViZXhhcGlzQmFzZVVybCI6Imh0dHBzOi8vaW50ZWdyYXRpb24ud2ViZXhhcGlzLmNvbS92MSIsInNjb3BlcyI6InNwYXJrLWFkbWluOmRldmljZXNfcmVhZCxzcGFyay1hZG1pbjp3b3Jrc3BhY2VzX3JlYWQsc3Bhcms6eGFwaV9zdGF0dXNlcyxzcGFyazp4YXBpX2NvbW1hbmRzIiwiaWF0IjoxNjM4NTIzOTg1LCJqdGkiOiJueTBnMFBfZ1RBZV9PbFF2cGtWY1VRPT0iLCJyZWZyZXNoVG9rZW4iOiJaakJqWm1Jd016RXROakV4WWkwME1qTXdMVGszWXpBdE1EYzNPV0V4WkdZNE9EVTJZelptWTJFd1l6Z3RORE0yX0E1MkRfM2E2ZmYzNzMtNjhhNy00NGU0LTkxZDYtYTI3NDYwZTBhYzVjIiwieGFwaUFjY2VzcyI6IntcImNvbW1hbmRzXCI6W1wiTWVzc2FnZS5TZW5kXCJdLFwic3RhdHVzZXNcIjpbXCJSb29tQW5hbHl0aWNzLipcIixcIlBlcmlwaGVyYWxzLkNvbm5lY3RlZERldmljZVsqXS5Sb29tQW5hbHl0aWNzLipcIixcIlN5c3RlbVVuaXQuU3RhdGUuTnVtYmVyT2ZBY3RpdmVDYWxsc1wiLFwiU3RhbmRieS5TdGF0ZVwiXSxcImV2ZW50c1wiOltdfSJ9
.
33XZfYzMEZBvfE_APBxC4uVe6eSKajTlrEZPTeGPBOBapNQhKUzamISzONnHaCfZdt1iMh8Ev3PRE5tHztCmNA

Decoded:

Header:
{
  "kid": "gB1snwzlbpotn0WYNmD0SHeU",
  "typ": "JWT",
  "alg": "ES256"
}
Payload:
{
  "sub": "Y2lzY29zcGFyazovL3VybjpURUFNOnVzLWVhc3QtMV9pbnQxMy9PUkdBTklaQVRJT04vM2E2ZmYzNzMtNjhhNy00NGU0LTkxZDYtYTI3NDYwZTBhYzVj",
  "oauthUrl": "https://webexapis.com/v1/access_token",
  "orgName": "Atlas Device Management test org",
  "region": "us-east-2_a",
  "appUrl": "https://xapi-a.wbx2.com/xapi/api/organizations/3a6ff373-68a7-44e4-91d6-a27460e0ac5c/apps/ac6b6972-538e-11ec-bf63-0242ac130002",
  "appId": "ac6b6972-538e-11ec-bf63-0242ac130002",
  "expiryTime": "2021-12-04T09:33:05.867211Z",
  "action": "provision",
  "webexapisBaseUrl": "https://webexapis.com/v1",
  "iat": 1638523985,
  "jti": "ny0g0P_gTAe_OlQvpkVcUQ==",
  "refreshToken": "ZjBjZmIwMzEtNjExYi00MjMwLTk3YzAtMDc3OWExZGY4ODU2YzZmY2EwYzgtNDM2_A52D_3a6ff373-68a7-44e4-91d6-a27460e0ac5c",
  "scopes": "spark-admin:devices_read,spark-admin:workspaces_read,spark:xapi_statuses,spark:xapi_commands",
  "xapiAccess": "{\"commands\":[\"Message.Send\"],\"statuses\":[\"RoomAnalytics.*\",\"Peripherals.ConnectedDevice[*].RoomAnalytics.*\",\"SystemUnit.State.NumberOfActiveCalls\",\"Standby.State\"],\"events\":[]}"
}
Signature: 
33XZfYzMEZBvfE_APBxC4uVe6eSKajTlrEZPTeGPBOBapNQhKUzamISzONnHaCfZdt1iMh8Ev3PRE5tHztCmNA
FieldRequired / OptionalValue spaceDescription
kidRequiredStringThe Id of the key from the JSON Web Key Set (JWKS) used to sign this JWT
typRequiredStringWill be JWT for our use case here
algRequiredStringSpecifies the signature algorithm used to sign the key: ES256 which means ECDSA using P-256 and SHA-256.
subRequiredStringThe Webex customer / organization ID.
oauthUrlRequiredURLThe URL to fetch an OAuth access token from the provided refresh token
orgNameRequiredStringThe name of the Webex customer / organization
regionRequiredus-west-2_r, us-east-2_a or eu-central-1_kThe region for the Webex customer. Should be used to determine which JWKS URL to use.
appUrlRequiredURLThe URL to use to patch details about the integration, like actionUrl, webhookUrl and more.
appIdRequiredUUIDThe Id of the integration, same as in the manifest
expiryTimeRequiredTimestampISO8601 UTC date time when this request will expire. If an integration receives a JWT after this date, it should be refused. The expiry is 24 hours.
actionRequiredStringThe type of action this JWT embeds, which in the activation case is provision
webexapisBaseUrlRequiredURLThe base URL for the Webex public APIs. Append the specific API urls (e.g. v1/workspaces, v1/xapi/status) to this base URL.
iatRequiredNumericDateThe issued at field identifies the time at which the JWT was issued.
jtiRequiredStringThe JWT ID field provides a unique identifier for the JWT. The jti is used to prevent replays of the same message. Integrators should disallow a JWT with a jti that has been seen within the last 24 hours (which is the expiry time of the message). After 24h, a replay will be invalidated by the expiry time.
refreshTokenRequiredStringThe OAuth refresh token. This token can be used to get an access token from the oauthUrl.
scopesRequiredStringA comma separated list of scopes granted. If the manifest has optional scopes, this shows what scopes got approved.
xapiAccessRequiredObjectThe xAPI commands and statuses granted. If the manifest has optional status or commands, this shows which got approved.
Activation Flow
1. a) Provide Activation Code JWT (Manual)

The activation code JWT is copied from Control Hub and provided to the integration.

1. b) Provide Activation Code JWT (HTTPS)

The JWT is sent in an HTTPS POST (the payload is the same as for the manual case) to the URL provided in the provisioning part of the manifest. At this point, there is no association with the tenant / customer on the integrators side, which means the activation data will have to be stored temporarily with an activation session id embedded in the redirect URL returned. The response must contain said redirect URL in the success case or a detailed (human friendly) error if something goes wrong.

The redirect URL should be a landing page in the 3rd party integration that can authenticate the admin (in case they have an existing account) or allow the creation of a new user. When the admin is authenticated in the 3rd party system, the association between the Cisco activation JWT posted earlier and the 3rd party account can be made (including any additional 3rd party setup needed). The Cisco customer details from the JWT (orgName) can be used to render what customer from Webex the activation applies to.

POST https://example.com/webexintegration
{
  "jwt": "eyJhbGciOiJSUzI1NiJ9...."
}

Response 200 OK:
{
  "redirectUrl": "https://example.com/webexintegration/setup/81dc908d-39a1-4a11-9dd1-43ab22d1d571"
}

Response 5XX/4XX:
{
  "description": "A human friendly description of the failure",
  "trackingId": "an-error-reference-to-provide-to-support"
}
2. Validate the activation JWT

When the integration has received the activation JWT, it needs to be validated:

  1. Read the region field from the JWT (prior to validation) and use it to select from the list of regional JSON Web Key Set (JWKS) URLs described earlier. If no match, default to the us-east-2_a URL. Fetch the JWKS from the URL.
  2. Verify the ES256 signature using the the JWKS. The kid in the JWT header indicates what key to use from the key set. If there is no matching key in the key set, the JWT should be considered invalid.
  3. If the signature is valid, the integration should look at the expiryTime. If the current time is after this timestamp, the JWT is invalid and cannot be used for activation.
  4. The integration must look at the appId and verify that it matches the id provided in the app manifest.
  5. Store the jti for up to 24 hours and reject any JWT that contains the same ID at a later point in time.
3. Verify and store the activation payload

The most important part of the activation payload is the refreshToken. The integration should verify that the token can be exchanged for a valid access token using the oauthUrl from the JWT and the client ID and client secret from the deployment step:

POST {oauthUrl}
{
  "grant_type": "refresh_token",
  "client_id": "... from deployment ...",
  "client_secret": "... from deployment ...",
  "refresh_token": "... from the JWT ..."
}

Response 200 OK:
{
  "expires_in": 7199,
  "token_type": "Bearer",
  "refresh_token": "ZDI3MGEyYzQtNmFlNS00NDNhLWFlNzAtZGVjNjE0MGU1OGZmZWNmZDEwN2ItYTU3",
  "refresh_token_expires_in": 5090490,
  "access_token": "eyJhbGciOiJSUzI1NiJ9..."
}

The access_token is the token to use in all requests to the Webex APIs added to the Authorization HTTP header, but do note that it has an expiry and when expired, the integration must fetch a new access token using the refresh token:

Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...

Also note that if the POST returns a new refresh_token (not the same as the one in the JWT), update to use the one returned from the oauthUrl post.

When the refresh token has been validated, the details from the payload needs to be securely stored and associated with the 3rd party tenant account (if applicable).

4. Update the integration activation status

If the setup is completed on the 3rd party integrations side, this fact must be passed on by a PATCH to the appUrl from the manifest (using the access token from the previous step), together with the customer specific actionsUrl. The actionsUrl is where Webex will send JWT encoded actions to the integration. Note that the actionsUrl is optional, but not having one will significantly impair the management capabilities (no Webex initiated reprovisioning, updates, health checks etc.).

The integration can also pass in a webhook definition that will allow receiving xAPI status changes and the integration customer details.

PATCH {appUrl}
Authorization: Bearer {access_token}
{
  "provisioningState": "completed",
  "actionsUrl": "https://eu.example.com/customer/0beaca8b-3342-4eb4-973f-de70c14f2c91/webexintegration/actions",
  "webhook": {
    "targetUrl": "https://eu.example.com/customer/0beaca8b-3342-4eb4-973f-de70c14f2c91/webexintegration/sendmedata",
    "secret": "my-secret-key-for-signing"
  },
  "customer": {
    "id": "tenant / customer ID",
    "name": "display name of the tenant / customer"
  }
}
FieldRequired / OptionalValue spaceDescription
provisioningStateRequiredcompleted or errorThe state of the provisioning / activation
actionsUrlOptionalURLThe URL where Webex will send JWT encoded actions. Must be an HTTPS URL with a valid certificate and publicly available.
webhookOptionalObjectSpecifies a webhook to receive xAPI change notifications
webhook.targetUrlRequiredURLThe URL where the status change notifications will be posted. Must be an HTTPS URL with a valid certificate and publicly available.
webhook.secretRequiredStringThe secret used to produce the HMAC-SHA1 signature of the JSON payload. Must be 20 characters or longer.
customerOptionalObjectThe tenant / customer details from the 3rd party integration side
customer.idRequiredStringThe Id of the customer from the integration
customer.nameOptionalStringThe display name of the customer from the integration

At this point we should have a functioning integration and Control Hub will show the integration as active.

Webhooks

If a webhook was specified during activation, it will receive notifications for supported status and event keys that the integration has been granted access to (access is granted through the xapiAccess part of the manifest). The following keys are supported:

Supported Events
FrequencyEvent keys
Sent immediately when raisedBootEvent, UserInterface.Message.Prompt.Response
Supported Status
FrequencyStatus keys
Sent immediately when the value changesStandby.State, SystemUnit.State.NumberOfActiveCalls, Conference.Presentation.LocalInstance[*].SendingMode
Sent at most every minuteRoomAnalytics.PeoplePresence, RoomAnalytics.PeopleCount.Current
Sent at most every 5 minutesRoomAnalytics.AmbientNoise.Level.A, RoomAnalytics.AmbientTemperature, Peripherals.ConnectedDevice[*].RoomAnalytics.AmbientTemperature, RoomAnalytics.RelativeHumidity, Peripherals.ConnectedDevice[*].RoomAnalytics.RelativeHumidity, RoomAnalytics.Sound.Level.A, Peripherals.ConnectedDevice[*].RoomAnalytics.AirQuality.Index, RoomAnalytics.ReverberationTime.Middle.RT60

A notification is only sent if the value has changed since the last notification. If the value has been unchanged for longer than the defined max frequency, a notification is sent immediately once it changes. If the value changes multiple times during the notification period, the notification will only contain the last value.

Note that the RoomAnalytics data is only sent if the customer has opted into Workspace Utilization data and/or Workspace Environmental data in Control Hub (these settings are configured under "Workspaces > Settings").

Example Webhook

Let's have a look at an example webhook and the fields.

POST {webkook.targetUrl}
X-Spark-Signature: [... HMAC of the body using the webhook.secret ...]
{
  "deviceId": "Y2lzY29[...]MjZiYzZm",
  "workspaceId": "Y2lzY29zcGF[...]NjM=",
  "orgId": "Y2lzY29[...]YzVj",
  "timestamp": "1970-01-01T00:00:10Z",
  "type": "status",
  "changes": {
    "updated": {
      "Standby.State": "Halfwake",
      "RoomAnalytics.PeopleCount.Current": 2
    },
    "removed": [
      "Audio.Microphones.Mute"
    ]
  },
  "isFullSync": false
}
Fields that are present in all webhook messages
FieldRequired / OptionalValue spaceDescription
deviceIdRequiredStringThe device ID
workspaceIdRequiredStringThe workspace ID
orgIdRequiredStringThe customer ID. Corresponds to the sub in the activation JWT.
timestampRequiredTimestampISO8601 UTC date time when this message was sent.
typeRequiredstatus, event or healthCheckThe type of webhook message. In this case a status change notification.
Fields that are only present when type is status
FieldRequired / OptionalValue spaceDescription
changesRequiredObjectObject containing the updated or removed status keys
changes.updatedOptionalObjectObject containing the updated status keys. In the example above, the standby state and the people count values changed.
changes.removedOptionalArrayList of status keys that are no longer present. In the example above, the microphones are no longer muted.
isFullSyncRequiredBooleanIf true, this is an update containing the current state for the subscribed status values. This means that the update was not necessarily triggered by a status change; approximately every hour the devices will send out a "full sync" containing the relevant status keys.
Fields that are only present when type is event
FieldRequired / OptionalValue spaceDescription
eventsRequiredArrayList of events that have been triggered.
events.keyRequiredStringName of the event.
events.valueRequiredObjectObject containing any additional event data.
events.timestampRequiredTimestampISO8601 UTC date time the event occurred at.
HMAC verification

An HMAC of the entire webhook payload is computed and sent in the X-Spark-Signature header. The recipient must also compute this value and verify that the message is signed with the shared webhook secret. If the timestamp is older than 5 minutes, the message must be discarded to avoid replays of old, but valid messages.

Note that it is recommended to change the Webhook secret periodically, which the integration can do in a new patch to the appUrl. The new secret will apply after approx. 5 minutes after it is updated.

Webhook Health Check

To verify that the webhook is working as expected, a payload of type healthCheck may also be sent. This payload allows Cisco to verify that webhooks are being correctly received on the integrator side. The HMAC should be computed and verified. If verification succeeds, the integration should respond with a 200 OK.

POST {webkook.targetUrl}
X-Spark-Signature: [... HMAC of the body using the webhook.secret ...]
{
  "appId": "ac6b6972-538e-11ec-bf63-0242ac130002",
  "timestamp": "1970-01-01T00:00:10Z",
  "type": "healthCheck" // standard payloads have type = 'status'
}
anchorManagement
anchor

The integrations framework defines APIs and messages for the management of an integration beyond the initial activation and setup. This is important to manage the life cycle of the integration, from verifying status / health, updating the authorization tokens and to deactivating the integration.

The management APIs can be divided in two:

  1. Webex to integration: Actions posted to the provided actionsUrl. These are signed JWTs similar to the one user in the activation flow.
  2. Integration to Webex: HTTP GET or PATCH of the appUrl.

Let's have a look at these in more detail:

Actions: Webex to Integration

The actionsUrl provided by the integration is used to send signed JWT "actions". All the JWT payloads are signed the same way as the activation request (ES256 and ECDSA):

POST {actionsUrl}
{
  "jwt": "eyJhbGciOiJSUzI1NiJ9...."
}

The actions supported are as follows:

Health Check

Run a health / connectivity check for the integration. This allows Webex to check the current state and detect if the integration has been removed on the integrator side without a successfully completed removal flow (indicated by the integration returning 404 Not Found or 410 Gone). Note that the most important part of this check is for the integration to verify that it is able to talk to the Webex APIs and report a tokensState. The token state behavior should be as follows:

  • valid: The integration can successfully do a GET on the appUrl with a 200 OK
  • invalid: A GET on the appUrl returns a 401 / 403 and the app is unable to get a new access token from the refresh token. The fix will be to provision a new refresh token via the "update" action.
  • unknown: A GET on the appUrl fails with a non 200/401/403 status or a transient network issue (timeout etc.)

The operationalState can indicate other problems with the integration and the Control Hub admin will be guided to log in to check the state.

Request (JWT)
{
  "sub": "Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi8zYTZmZjM3My02OGE3LTQ0ZTQtOTFkNi1hMjc0NjBlMGFjNWM",
  "iat": 1516239022,
  "jti": "WTJselkyOXpjR0Z5YXpvdkw=",
  "appId": "6d93fe09-7130-4507-b261-3908b63428a4",
  "action": "healthCheck"
}

Response:
{
  "operationalState": "operational",
  "tokensState": "valid"
}
FieldRequired / OptionalValue spaceDescription
operationalStateRequiredoperational, impaired or outageThe operational state of the integration. If the integration is not operational, it can report either impaired to indicate that some features might not work or outage to indicate a full outage for all integration features.
tokensStateRequiredvalid, invalid or unknownThe state of the auth tokens. Should be verified by doing an HTTP GET of the appUrl.

If the integration uses webhooks, a healthCheck type payload is also sent to the webhook URL to verify that it is working correctly.

Update
  1. The appUrl and the region can change, for example when the Webex customer is moved to a different region.
  2. A new refresh token can be provided, if needed.
Request (JWT)
{
  "sub": "Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi8zYTZmZjM3My02OGE3LTQ0ZTQtOTFkNi1hMjc0NjBlMGFjNWM",
  "iat": 1516239022,
  "jti": "WTJselkyOXpjR0Z5YXpvdkw=",
  "appId": "6d93fe09-7130-4507-b261-3908b63428a4",
  "action": "update",
  "appUrl": "https://xapi-k.wbx2.com/xapi/api/organizations/3a6ff373-68a7-44e4-91d6-a27460e0ac5c/apps/6d93fe09-7130-4507-b261-3908b63428a4",
  "region": "eu-central-1_k",
  "refreshToken": "eyJhbGciOiJSUzI1NiJ9..."
}

Response 204 No content
Update Approved

The updateApproved action is sent when the admin has approved an update to a new manifest version. For convenience, the message contains the scopes and xApi access approved from the new manifest (including any optional ones).

When this message is received, the refresh token will already support the new API scopes and the integration can fetch a new access token to start using the new Webex APIs.

Note that in the case this message is lost, the integration can still check the current state by doing an HTTP GET request on the appUrl.

Request (JWT)
{
  "sub": "Y2lzY29zcGFyazovL3VybjpURUFNOnVzLWVhc3QtMV9pbnQxMy9PUkdBTklaQVRJT04vM2E2ZmYzNzMtNjhhNy00NGU0LTkxZDYtYTI3NDYwZTBhYzVj",
  "iat": 1629278630,
  "jti": "v0wyEuKHTxygQB53-h8_qw=="
  "action": "updateApproved",
  "manifestVersion": "3",
  "appId": "a9aecf7e-af2c-48e7-ae61-3f49773922b1",
  "scopes": "spark-admin:workspaces_read,spark:xapi_statuses"
  "xapiAccess": {"commands": [], "statuses": ["RoomAnalytics.*"], "events": []}
}

Response 204 No content
Deactivate

Deactivate or deprovision the integration. When this action is received, the integration should be removed and terminated. If the delete is interactive, an optional redirectUrl can be provided to render a landing page for the customer to perform additional cleanup or capture other information.

Request (JWT)
{
  "sub": "Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi8zYTZmZjM3My02OGE3LTQ0ZTQtOTFkNi1hMjc0NjBlMGFjNWM",
  "iat": 1516239022,
  "jti": "WTJselkyOXpjR0Z5YXpvdkw=",
  "appId": "6d93fe09-7130-4507-b261-3908b63428a4"
  "action": "deprovision",
  "interactive": true
}

Response 200 (with redirectUrl) / 204 No content (if not):
{
  "redirectUrl": "https://eu.example.com/customer/0beaca8b-3342-4eb4-973f-de70c14f2c91/apps/6d93fe09-7130-4507-b261-3908b63428a4/removed"
}
Integration to Webex (appUrl)
Update the integration
  1. The actionsUrl can be updated in case there is a need to do so from the integrator.
  2. Request a manifest update by adding a new manifest.updateRequest. An update request will trigger a flow where the admin can approve the update to a new version of the manifest including any changes to the permissions needed.

Note that an update request patched as described below will only apply to this customer. For global integrations, this can be used to roll out changes to Beta / Trial customers first, and when ready, Cisco will deploy the new manifest for all remaining customers.

PATCH {appUrl}
Authorization: Bearer {access_token}
{
  "actionsUrl": "https://us.example.com/customer/0beaca8b-3342-4eb4-973f-de70c14f2c91/apps/6d93fe09-7130-4507-b261-3908b63428a4",
  "updateRequest": { 
    "newManifest": { ... desired manifest for approval (as described in the manifest section) ... },
  }
}
Read the current integration state

Reading the current state allows the integration to:

  1. Check the current integration manifest (including the version of the integration the customer has running) and trigger an update request if needed. An ongoing update request is indicated by the updateRequest object with the new manifest and the update state.
  2. Detect if the integration has been removed on the Webex side without a successfully completed removal flow (indicated by a 404 Not Found or 410 Gone response code). This should trigger cleanup and removal of the integration setup on the integrators side.
  3. Check what scopes and xAPI access the integration was granted, in case the manifest had optional values.
  4. Verify that the actionsUrl is correct.
GET {appUrl}
Authorization: Bearer {access_token}
{
  {
  "id": "ac6b6972-538e-11ec-bf63-0242ac130002",
  "manifestVersion": 2,
  "scopes": [
    "spark:xapi_statuses",
    "spark:xapi_commands"
  ],
  "xapiAccessKeys": {
    "commands": [
      "Message.Send"
    ],
    "statuses": [
      "RoomAnalytics.*",
      "Standby.State"
    ]
  },
  "createdAt": "2021-10-27T08:48:26.232928Z",
  "updatedAt": "2021-11-19T11:26:50.209904Z",
  "provisioningState": "completed",
  "actionsUrl": "https://eu.example.com/customer/0beaca8b-3342-4eb4-973f-de70c14f2c91/webexintegration/actions",
  "webhook": {
    "targetUrl": "https://eu.example.com/customer/0beaca8b-3342-4eb4-973f-de70c14f2c91/webexintegration/sendmedata"
  },
  "customer": {
    "id": "tenant / customer ID",
    "name": "display name of the tenant / customer"
  }
}
anchorSecurity
anchor

Security is very important to Cisco and our customers and it's crucial that integrators focus on security when writing the integration. Please have a look at the following security guidelines as you plan and review the implementation of your integration:

ItemDescription
Validate all JSON Web Tokens (JWT)Make sure to verify the signature of the JWT and reject any JWTs where the jti (ID) has been seen before or where the appId does not match the appId provided by the integration in the manifest. In the case of the activation JWT (action: provision), reject any JWT where the expiryTime has passed. For any other JWT action type, an iat older than 5 minutes should be rejected.
Validate the webhook signatureAlways compute the HMAC for the webhook payload and assert that it is the same as the provided X-Spark-Signature HTTP header
Protect the webhook secretUse webhook secrets that are unique per customer. Also consider rotating / changing the secret periodically. If you do implement a secret rotation, do note that you need to accept payloads signed with both the old and the new secret for up to 5 minutes after the change.
Secure the webhook and JWT action transportBoth the webhook and action URLs must be HTTPS with a valid certificate signed by a trusted Certificate Authority. Self-signed certificates are not supported.
Minimize required xAPI accessTry to minimize the xAPI access required in your manifest. It's tempting to use wildcards to avoid having to specify individual statuses or commands, but this makes it a lot harder for the admin to know and understand what the integration is actually allowed to do.
Securely store dataAll data received by the integration should be securely stored and encrypted at rest. This holds especially true for the clientId, clientSecret and refreshToken.