Subscriptions

SmartApps may create subscriptions to events for authorized devices. When the subscribed-to event occurs, your SmartApp will receive a POST request with information about the triggering event.

For example, an app may wish to be notified when a configured motion sensor detects motion, when a light is turned on, or when a refrigerator door is opened.

A SmartApp creates subscriptions using the Subscriptions API.

Subscriptions API basics

Subscriptions belong to a specific installed instance of a SmartApp, and the API follows the path pattern of https://api.smartthings.com/installedapps/{installedAppId}/subscriptions.

Subscriptions are typically created or updated during the INSTALL and UPDATE lifecycles, when the user has selected devices and granted the requested access to your SmartApp.

When a subscribed-to event occurs, your SmartApp will receive a POST request containing information about the event, so your app can react accordingly. See Subscription event handling for more information.

Subscription types

There are two types of subscriptions that may be created:

  1. Subscriptions to specific devices authorized by the user.
  2. Subscriptions to all devices in a user’s Location for a specified Capability, if the user authorizes access to all devices.

Device subscriptions

SmartApps may create subscriptions for specific devices selected and authorized by the user during Configuration. Device subscriptions are created by specifying "sourceType:": "DEVICE" and including a "device" object in the JSON request body:

{
  "sourceType": "DEVICE",
  "device": {
    "deviceId": "736e3903-001c-4d40-b408-ff40d162a06b",
    "componentId": "main",
    "capability": "switch",
    "attribute": "switch",
    "stateChangeOnly": true,
    "value": "on"
  }
}

Components, Capabilities, and Attributes

Device subscriptions may be created at the Component, Capability, and Attribute (and attribute value) granularity, or some combination thereof.

IMPORTANT

When subscribing to a specific attribute value, the value can be any valid JSON object type, and it should correspond to the attribute type defined by the Capability. For example, if an attribute is of type NUMBER, then a JSON number would be used.

Moreover, when receiving device events, the type of the value will also correspond to the attribute type defined in the Capability, as noted above.

At its most granular, subscriptions can be created for a specific value of an attribute of a specified capability, on a specific component. The following shows a subscription request body for when a “switch” capability device, of the “main” component, has its “switch” attribute change to “on”:

{
  "sourceType": "DEVICE",
  "device": {
    "deviceId": "736e3903-001c-4d40-b408-ff40d162a06b",
    "componentId": "main",
    "capability": "switch",
    "attribute": "switch",
    "stateChangeOnly": true,
    "value": "on"
  }
}

Use the wildcard operator, *, to specify any value. The following example subscription create request body shows how to create a subscription to a device for any capability and attribute event:

{
  "sourceType": "DEVICE",
  "device": {
    "deviceId": "736e3903-001c-4d40-b408-ff40d162a06b",
    "componentId": "main",
    "capability": "*",
    "attribute": "*",
    "value": "*"
  }
}

Omitting the value is equivalent to specifying all values.

Required permissions

Creating a device subscription requires that the SmartApp has requested the permission to read specific authorized devices. This is set during app creation or update, and the permission format is r:devices:*.

The SmartApp does not need to request any additional permissions during the INITIALIZE phase of the CONFIGURE lifecycle.

Upon selection of devices and authorization by the user to read these devices, all incoming requests to your SmartApp will contain permission in the form of "r:devices:". This permission allows your app to get information (including device attribute values) about the authorized device.

Capability subscriptions

SmartApps may also create subscriptions for all devices of a specified Capability in a user’s Location, if the user authorized the app to read all devices in their Location.

Capability subscriptions are created by specifying "sourceType:": "CAPABILITY" and including a "capability" object in the JSON request body:

{
    "sourceType": "CAPABILITY",
    "capability": {
      "locationId": "76fa4215-f9f5-4532-897e-5207db0da124",
      "capability": "contactSensor",
      "attribute": "contact",
      "value": "*",
      "stateChangeOnly": true,
      "subscriptionName": "all_contacts_sub"
    }
  }

In the example above, a subscription is created for all contact sensors in the user’s Location. Any removal or addition of devices in the user’s Location will also be accounted for in the subscription without your app needing to do anything.

Required permissions

Creating a Capability subscription requires that the SmartApp has requested the read all devices permission. This is set during app creation or update, and the permission format is r:devices.

The SmartApp also needs to request the r:devices permission during the INITIALIZE phase of the CONFIGURE lifeycle:

return {
    "name": "Your app name",
    "description": "Your app description",
    "id": "app",
    "permissions":["r:devices"],
    "firstPageId": "1"
  }

The user will be prompted to grant your app permission to read all devices in their Location.

Subscription event handling

When a subscribed-to event occurs, your SmartApp will be called with an EVENT lifecycle phase, and an Event data object containing information about the triggering event:

{
  "lifecycle": "EVENT",
  "executionId": "b328f242-c602-4204-8d73-33c48ae180af",
  "locale": "en",
  "version": "1.0.0",
  "eventData": {
    "authToken": "f01894ce-013a-434a-b51e-f82126fd72e4",
    "installedApp": {
      "installedAppId": "d692699d-e7a6-400d-a0b7-d5be96e7a564",
      "locationId": "e675a3d9-2499-406c-86dc-8a492a886494",
      "config": {
        "contactSensor": [
          {
            "valueType": "DEVICE",
            "deviceConfig": {
              "deviceId": "e457978e-5e37-43e6-979d-18112e12c961",
              "componentId": "main"
            }
          }
        ],
        "lightSwitch":[
          {
            "valueType": "DEVICE",
            "deviceConfig": {
              "deviceId": "74aac3bb-91f2-4a88-8c49-ae5e0a234d76",
              "componentId": "main"
            }
          }
        ],
        "minutes":[
          {
            "valueType": "STRING",
            "stringConfig": {
              "value": "5"
            }
          }
        ],
        "permissions": [
          "r:devices:e457978e-5e37-43e6-979d-18112e12c961",
          "r:devices:74aac3bb-91f2-4a88-8c49-ae5e0a234d76",
          "x:devices:74aac3bb-91f2-4a88-8c49-ae5e0a234d76"
        ]
      }
    },
    "events": [
      {
        "eventType": "DEVICE_EVENT",
        "deviceEvent": {
          "subscriptionName": "motion_sensors",
          "eventId": "736e3903-001c-4d40-b408-ff40d162a06b",
          "locationId": "499e28ba-b33b-49c9-a5a1-cce40e41f8a6",
          "deviceId": "6f5ea629-4c05-4a90-a244-cc129b0a80c3",
          "componentId": "main",
          "capability": "motionSensor",
          "attribute": "motion",
          "value" :"active",
          "stateChange": true
        }
      }
    ]
  },
  "settings": {
    "property1": "string",
    "property2": "string"
  }
}

Deleting subscriptions

Individual subscriptions can be deleted by issuing a DELETE request to the /installedapps/{installedAppId}/subscriptions{subscriptionId} endpoint:

const request = require('request');

function deleteSubscription(installedAppId, subscriptionId, authToken) {
  let path = `installedapps/${installedAppId/subscriptions/${subscriptionId}}`;
  request.delete({
    url: `https://api.smartthings.com/installedapps/${path}`,
    json: true,
    headers: `Authorization: Bearer: ${authToken}`
  },
  function(err, resp) {
    if (!err && 200 == resp.statusCode) {
      console.log(`subscription deleted: ${JSON.stringify(resp)}`);
    } else {
      console.error(`Error deleting subscription: ${JSON.stringify(err)}`);
    }
  });
}

All subscriptions for an installed app can be deleted by issuing a DELETE request to the /installedapps/{installedAppId}/subscriptions endpoint:

const request = require('request');

function deleteSubscription(installedAppId, subscriptionId, authToken) {
  let path = `installedapps/${installedAppId}/subscriptions`;
  request.delete({
    url: `https://api.smartthings.com/installedapps/${path}`,
    json: true,
    headers: `Authorization: Bearer: ${authToken}`
  },
  function(err, resp) {
    if (!err && 200 == resp.statusCode) {
      console.log(`subscription deleted: ${JSON.stringify(resp)}`);
    } else {
      console.error(`Error deleting subscription: ${JSON.stringify(err)}`);
    }
  });
}

Handling updated user configuration

Once a user installs your SmartApp, they may later reconfigure it to select different devices. If any of these devices have been previously subscribed-to, your app needs to account for this by deleting subscriptions to devices no longer configured, and creating new subscriptions for newly configured devices.

Examples

Device subscriptions

The following example shows creating a subscription to the “open” attribute of a contact sensor, including the necessary permissions requested during configuration. It also shows the handling the UPDATE lifecycle, when a user may have changed the configured devices. In that case, our app needs to delete subscriptions for devices that are no longer authorized, and create new subscriptions for newly configured devices. For simplicity, our app will simply delete all existing subscriptions, and create a new subscription, regardless of if the device configuration has changed.

const express = require('express');
const bodyParser = require('body-parser');
const request = require('request');
const baseUrl = 'https://api.smartthing.com';

app.use(bodyParser.json());

// handle all incoming requests to our app
app.post('/', function(req, resp, err) {
  let evt = req.body;
  let lifecycle = evt.lifecycle;
  let res = null;

  switch(lifecycle) {
    case 'CONFIGURE':
      res = handleConfig(evt.configurationData);
      resp.json({statusCode: 200, configurationData: res});
      break;
    case 'INSTALL':
      handleInstall(evt.installData.installedApp, evt.installData.authToken);
      resp.json({statusCode: 200, installData: {}});
      break;
    case 'UPDATE':
      handleUpdate(evt.updateData.installedApp, evt.authToken);
      resp.json({statusCode: 200, updateData: {}});
      break;

    // handle other lifecycles...

    default:
      console.log(`lifecycle ${lifecycle} not supported`);
  }
});

function handleConfig(configData) {
  if (!configData.config) {
    throw new Error('No config section set in request.');
  }
  let config = {};
  const phase = configData.phase;
  const pageId = configData.pageId;
  const settings = configData.config;
  switch (phase) {
    case 'INITIALIZE':
      config.initialize = createConfigInitializeSetting();
      break;
    case 'PAGE':
      config.page = createConfigPage(pageId, settings);
      break;
    default:
      throw new Error(`Unsupported config phase: ${phase}`);
      break;
  }
  return config;
}

function createConfigInitializeSetting() {
  return {
    name: 'Your app name',
    description: 'Some app description',
    id: 'app',
    permissions:['l:devices'], // Need list devices permission for app to allow user to select devices
    firstPageId: '1'
  }
}

/**
 * Creates a simple one page configuration screen where the user can
 * select a contact sensor device, and we will request read access to this
 * device.
 */
function createConfigPage(pageId, currentConfig) {
  if (pageId !== '1') {
    throw new Error(`Unsupported page name: ${pageId}`);
  }

  return {
    pageId: '1',
    name: 'Some page name',
    nextPageId: null,
    previousPageId: null,
    complete: true, // last page
    sections: [
      {
        name: 'When this opens...',
        settings: [
          {
            id: 'contactSensor',
            name: 'Which contact sensor?',
            description: 'Tap to set',
            type: 'DEVICE',
            required: true,
            multiple: false,
            capabilities: ['contactSensor'],
            permissions: ['r'] // need read permission to create subscriptions!
          }
        ]
      }
    ]
  };
}

/**
 * Once the user has selected the device and agreed to the requested
 * permissions, our app will create a subscription for the "open" value
 * of the "contact" attribute for the contact sensor.
 */
function handleInstall(installedApp, authToken) {
    let deviceConfig = installedApp.config.contactSensor[0].deviceConfig;
    createSubscription(deviceConfig);
}

function createSubscription(deviceConfig, authToken) {
  const path = `/installedapps/${installedApp.installedAppId}/subscriptions`;

  let subRequest = {
    sourceType: 'DEVICE',
    device: {
      componentId: deviceConfig.componentId,
      deviceId: deviceConfig.deviceId,
      capability: 'contactSensor',
      attribute: 'contact',
      stateChangeOnly: true,
      subscriptionName: "contact_subscription",
      value: "open"
    }
  };
  request.post({
    url: `${ baseUrl }${ path }`,
    json: true,
    body: subRequest,
    headers: {
      'Authorization': 'Bearer ' + authToken
    }
  },
  function (error, response, body) {
    if (!error && response.statusCode == 200) {
      console.log('subscription created')
    } else {
      console.log('failed to created subscriptions');
      console.log(error);
    }
  });  
}

/**
 * If the user has updated their configuration, for example they may
 * have selected a different contact sensor, we need to delete the
 * old subscriptions and create a new subscription.
 */
function handleUpdate(installedApp, authToken) {
  const path = `/installedapps/${installedApp.installedAppId}/subscriptions`;

  // delete all subscriptions, since some may have changed.
  request.delete({
    url: `${ baseUrl }${ path }`,
    json: true,
    headers: {
      'Authorization': 'Bearer ' + authToken
    }},
    function (error, response, body) {
      if (!error && response.statusCode == 200) {
        console.log('subscriptions deleted');
        // now create new subscription for (possibly) changed device configuration
        createSubscription(installedApp.config.contactSensor[0].deviceConfig, authToken);  
      } else {
        console.log('failed to delete subscriptions');
        console.log(error);
      }
    });  
}

Capability subscription

The following example shows creating a subscription to the “open” attribute of all contact sensors for the user’s Location, including the necessary permissions requested during configuration.

In this case, there is no need to handle the UPDATE lifecycle, since the user has agreed to authorize your app to read information about all devices in their Location:

const express = require('express');
const bodyParser = require('body-parser');
const request = require('request');
const baseUrl = 'https://api.smartthing.com';

app.use(bodyParser.json());

// handle all incoming requests to our app
app.post('/', function(req, resp, err) {
  let evt = req.body;
  let lifecycle = evt.lifecycle;
  let res = null;

  switch(lifecycle) {
    case 'CONFIGURE':
      res = handleConfig(evt.configurationData);
      resp.json({statusCode: 200, configurationData: res});
      break;
    case 'INSTALL':
      handleInstall(evt.installData.installedApp, evt.installData.authToken);
      resp.json({statusCode: 200, installData: {}});
      break;

    // handle other lifecycles...

    default:
      console.log(`lifecycle ${lifecycle} not supported`);
  }
});

function handleConfig(configData) {
  if (!configData.config) {
    throw new Error('No config section set in request.');
  }
  let config = {};
  const phase = configData.phase;
  const pageId = configData.pageId;
  const settings = configData.config;
  switch (phase) {
    case 'INITIALIZE':
      config.initialize = createConfigInitializeSetting();
      break;
    case 'PAGE':
      config.page = createConfigPage(pageId, settings);
      break;
    default:
      throw new Error(`Unsupported config phase: ${phase}`);
      break;
  }
  return config;
}

function createConfigInitializeSetting() {
  return {
    name: 'Your app name',
    description: 'Some app description',
    id: 'app',
    permissions:['r:devices'], // Need permission to read all devices
    firstPageId: '1'
  }
}

/**
 * Creates a simple one page configuration screen where the user can
 * select a contact sensor device, and we will request read access to this
 * device.
 */
function createConfigPage(pageId, currentConfig) {
  if (pageId !== '1') {
    throw new Error(`Unsupported page name: ${pageId}`);
  }

  return {
    // some page info for other configuration needed
  };
}

/**
 * Once the user has selected the device and agreed to the requested
 * permissions, our app will create a subscription for the "open" value
 * of the "contact" attribute for the contact sensor.
 */
function handleInstall(installedApp, authToken) {
    let deviceConfig = installedApp.config.contactSensor[0].deviceConfig;
    createSubscription(deviceConfig);
}

function createSubscription(deviceConfig, authToken) {
  const path = `/installedapps/${installedApp.installedAppId}/subscriptions`;

  let subRequest = {
    sourceType: 'CAPABILITY',
    capability: {
      locationId: "76fa4215-f9f5-4532-897e-5207db0da124",
      capability: "contactSensor",
      attribute: "contact",
      value: "*",
      stateChangeOnly: true,
      subscriptionName: "all_contacts_sub"
    }
  };

  request.post({
    url: `${ baseUrl }${ path }`,
    json: true,
    body: subRequest,
    headers: {
      'Authorization': 'Bearer ' + authToken
    }
  },
  function (error, response, body) {
    if (!error && response.statusCode == 200) {
      console.log('subscription created')
    } else {
      console.log('failed to created subscriptions');
      console.log(error);
    }
  });
}

/**
 * If the user has updated their configuration, for example they may
 * have selected a different contact sensor, we need to delete the
 * old subscriptions and create a new subscription.
 */
function handleUpdate(installedApp, authToken) {
  // no need to handle capability subscriptions in update.
}