App Webhook Guide

The UltronSMART Cloud API - Personal supports webhook notifications for device state changes. When a device's state changes, the system automatically sends HTTP POST requests to your configured webhook endpoint.


Last Updated: 2025-11-07


Prerequisites

1. Valid App Subscription

  • Must have an active UltronSMART application subscription
  • Subscription status must be active
  • You can check the available app subscriptions in: https://account.ultroncloud.com/account/app
  • Ensure subscription includes webhook functionality permissions

2. Device Assignment to App

  • Devices must be added to appDevices to receive state change notifications
  • Only devices assigned to your app will trigger webhook events
  • Use /usr/v5/AddAppDevices API to add devices to your app

3. API Authentication

X-Api-Key: YOUR_API_KEY
Ultron-Cloud-Appid: YOUR_APP_ID

Setup Process

Step 1: Add Devices to App

First, ensure the devices you want to monitor are added to your app:

Example: Adding devices to your app

curl -X POST "https://api.ultroncloud.com/usr/v5/AddAppDevices" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Ultron-Cloud-Appid: YOUR_APP_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "appId": "YOUR_APP_ID",
    "deviceNameMap": {
      "UT3702-ES12345": "Living Room Light",
      "UTU301-2K26DVZ": "Temperature Sensor"
    }
  }'

Step 2: Create App Settings for Webhook

Example: Creating a webhook subscription

curl --request POST \
     --url https://api.ultroncloud.com/usr/v5/CreateAppWebhook \
     --header 'Ultron-Cloud-Appid: YOUR_APP_ID' \
     --header 'X-Api-Key: YOUR_API_KEY' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "eventTypes": [
    "stateChanged",
    " onlineStateChanged",
    " deviceListChanged"
  ],
  "appId": "AppID",
  "name": "Dev test webhook",
  "webhookURL": "https://abc.com/webhook",
  "webhookSecret": "WEBHOOK_SECRET"
}
'

Step 3: Test Your Webhook URL

Before or after creating your app settings, you can send a test event to your endpoint to verify that it is reachable and can correctly handle signature verification. This API allows you to test any URL without saving it as a setting.

Example: Sending a test event

curl -X POST "https://api.ultroncloud.com/usr/v5/TestWebhookURL" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Ultron-Cloud-Appid: YOUR_APP_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "appId": "YOUR_APP_ID",
    "webhookURL": "https://your-domain.com/webhook/test-endpoint",
    "webhookSecret": "your-secret-key-for-signature-verification"
  }'

The system will send a sample test event to the webhookURL provided in the request body. This allows you to validate your endpoint logic before committing to a setting.

Step 4: Configure Webhook Settings (Optional)

Update existing webhook settings if needed:

Example: Updating webhook settings

curl --request POST \
     --url https://api.ultroncloud.com/usr/v5/UpdateAppWebhook \
     --header 'Ultron-Cloud-Appid: YOUR_APP_ID' \
     --header 'X-Api-Key: YOUR_API_KEY' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "eventTypes": [
    "stateChanged",
    "deviceListChanged",
    "onlineStateChanged"
  ],
  "appId": "YOUR_APP_ID",
  "name": "NEW Webhook name",
  "webhookId": "WEBHOOK_ID",
  "webhookSecret": "NEW_WEBHOOK_SECRET",
  "webhookURL": "https://abc.com/NEW_WEBHOOK_ENDPOINT"
}
'

Step 5: Verify Configuration

Check your current app settings:

Example: Retrieving app settings

curl --request POST \
     --url https://api.ultroncloud.com/usr/v5/ListAppWebhooks \
     --header 'Ultron-Cloud-Appid: YOUR_APP_ID' \
     --header 'X-Api-Key: YOUR_API_KEY' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "appId": "YOUR_APP_ID"
}
'

Verify devices are added to your app:

Example: Retrieving app devices

curl -X POST "https://api.ultroncloud.com/usr/v5/GetUserAppDevices" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Ultron-Cloud-Appid: YOUR_APP_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "appId": "YOUR_APP_ID"
  }'

Webhook Event Format

Event Types

  • stateChanged: A device's state has changed.
  • onlineStateChanged: A device's online/offline status has changed.
  • deviceListChanged: A device has been added to or removed from a group.

Note: This list may be expanded in the future. Refer to the latest API documentation for the most up-to-date list of event types.

Understanding Event Scopes

It's important to understand that not all event types are triggered under the same conditions. They are designed for different purposes: discovery and monitoring.

Discovery Event: deviceListChanged

The deviceListChanged event operates at a higher level. Its primary purpose is to notify your application of potential devices it might be interested in.

  • Trigger: This event fires whenever a device is added to or removed from a user's group.
  • Action: Upon receiving this event, your application can inspect the newDevices list and decide if it wants to start monitoring any of them. To begin receiving detailed state updates for a new device, you must explicitly add it to your app's scope using the /usr/v5/AddAppDevices endpoint.

Monitoring Events: stateChanged & onlineStateChanged

These events are more precise and are designed for monitoring devices that your application is actively managing.

  • Trigger: These events will only fire for devices that you have explicitly added to your app using the /usr/v5/AddAppDevices endpoint.
  • Action: Use these webhooks to keep your application's state in sync with the device's real-time status and data. If a device has not been added to your app, you will not receive its stateChanged or onlineStateChanged notifications.

Request Body

stateChanged

{
  "eventType": "stateChanged",
  "data": {
    "groupId": "EoiXXXXXXXXEZpfYVC9q4fC",
    "uid": "6KeZXXXXXXeNfbXFmhXXXXXX2",
    "sn": "UTU309-F8AEXXXX643",
    "publishTime": "2025-04-08T07:52:27.556299908Z",
    "statesData": {
      "states": {
        "pir_sensor": {
          "serialNo": 83,
          "detected": ["motionDetected"]
        }
      },
      "reqId": null,
      "devices": ["pir_sensor"]
    }
  },
  "timestamp": 1744098747
}

onlineStateChanged

{
  "eventType": "onlineStateChanged",
  "data": {
    "groupId": "EoiXXXXXXXXEZpfYVC9q4fC",
    "uid": "6KeZXXXXXXeNfbXFmhXXXXXX2",
    "sn": "UTU309-F8AEXXXX643",
    "publishTime": "2025-04-08T08:00:00Z",
    "onlineState": {
      "online": true,
      "time": "2025-04-08T08:00:00Z"
    }
  },
  "timestamp": 1744099200
}

deviceListChanged

{
  "eventType": "deviceListChanged",
  "data": {
    "groupId": "EoiXXXXXXXXEZpfYVC9q4fC",
    "newDevices": ["UT3702-ES12345"],
    "removedDevices": [],
    "publishTime": "2025-04-08T09:00:00Z"
  },
  "timestamp": 1744102800
}

Field Descriptions

  • eventType: (string) The type of event. Can be stateChanged, onlineStateChanged, or deviceListChanged.
  • data: (object) An object containing the details of the event.
    • groupId: (string) The ID of the group the device belongs to. For organization-level webhooks, this field will be omitted.
    • collectionId: (string) (For organization webhooks only) The ID of the collection the device belongs to.
    • uid: (string) The unique ID of the user associated with the device. (Not present in deviceListChanged)
    • sn: (string) The product serial number (ProductSN) of the device. (Not present in deviceListChanged)
    • publishTime: (string) The timestamp when the event was published, in RFC3339 format.
    • statesData: (object) (For stateChanged only) An object containing the state data.
    • onlineState: (object) (For onlineStateChanged only) An object containing the online state.
    • newDevices: (array of string) (For deviceListChanged only) A list of ProductSNs for devices added to the group.
    • removedDevices: (array of string) (For deviceListChanged only) A list of ProductSNs for devices removed from the group.
  • timestamp: (integer) The Unix timestamp when the event occurred.

Request Headers

Content-Type: application/json
X-Ultron-Signature: <base64-encoded-hmac-sha256-signature>

Request Body

{
  "eventType": "stateChanged",
  "data": {
    "groupId": "EoiXXXXXXXXEZpfYVC9q4fC",
    "uid": "6KeZXXXXXXeNfbXFmhXXXXXX2",
    "sn": "UTU309-F8AEXXXX643",
    "publishTime": "2025-04-08T07:52:27.556299908Z",
    "statesData": {
      "states": {
        "pir_sensor": {
          "serialNo": 83,
          "detected": ["motionDetected"]
        }
      },
      "reqId": null,
      "devices": ["pir_sensor"]
    }
  },
  "timestamp": 1744098747
}

Field Descriptions

  • eventType: (string) The type of event. In this case, it's always "stateChanged".
  • data: (object) An object containing the details of the state change.
    • groupId: (string) The ID of the group the device belongs to.
    • uid: (string) The unique ID of the user associated with the device.
    • sn: (string) The product serial number (ProductSN) of the device.
    • publishTime: (string) The timestamp when the state change was published, in RFC3339 format.
    • statesData: (object) An object containing the state data.
      • states: (object) A map where the key is the device name, and the value is an object containing the device's state.
      • reqId: (null) The request ID.
      • devices: (array of string) An array of device names.
  • timestamp: (integer) The Unix timestamp when the event occurred.

Webhook Event Security: Signature Verification

It is strongly recommended to verify the signature of each incoming webhook event. This ensures that the event originated from the Ultron Cloud platform, and not from a malicious source.

Signature Details

  • Header: The signature is provided in the X-Ultron-Signature header of the HTTP request.
  • Algorithm: The signature is generated using the HMAC-SHA256 algorithm.
  • Secret: The secret key used for signature generation is obtained from the GetAppSettings API. The secret is returned in the webhookSecret field when you create the app settings.

Hash Calculation Algorithm

  1. secretBytes = []byte(secret): Convert the secret string to a byte array.
  2. hash = HMAC-SHA256(requestBody, secretBytes): Calculate the HMAC-SHA256 hash of the request body using the secret key.
    • requestBody: The raw byte array of the HTTP request body.
    • secretBytes: The secret key as a byte array.
  3. X-Ultron-Signature = base64StdEncode(hash): Base64 encode the resulting hash. The result is the value of the X-Ultron-Signature header.

Signature Verification Example

Given:

  • webhookSecret: "JpLvyZUcvFaXXXXXXXsqniG"
  • requestBodyStr: {"eventType":"stateChanged","data":{"groupId":"EoiXXXXXXXXEZpfYVC9q4fC","uid":"6KeZXXXXXXeNfbXFmhXXXXXX2","sn":"UTU309-F8AEXXXX643","publishTime":"2025-04-08T07:52:27.556299908Z","statesData":{"states":{"pir_sensor":{"serialNo":83,"detected":["motionDetected"]}},"reqId":null,"devices":["pir_sensor"]}},"timestamp":1744098747}

Calculation Steps:

  1. Convert Secret to Byte Array (secretBytes): [74 112 76 118 121 90 85 99 118 70 97 88 88 88 88 88 88 88 115 113 110 105 71]

  2. Calculate HMAC-SHA256 Hash (hash): [34 237 114 244 228 117 28 190 87 82 133 179 140 30 8 27 90 159 252 164 6 199 198 145 16 181 13 27 151 31 89 135]

  3. Base64 Encode the Hash (signature): Iu1y9OR1HL5XUoWzjB4IG1qf/KQGx8aRELUNG5cfWYc=

Implementation Examples

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(payload, secret, signature) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('base64');
  
  return signature === expectedSignature;
}

// Usage
const receivedSignature = req.headers['x-ultron-signature'];
const payload = JSON.stringify(req.body);
const isValid = verifyWebhookSignature(
  payload, 
  'your-webhook-secret', 
  receivedSignature
);

Python

import hmac
import hashlib
import base64
import json

def verify_webhook_signature(payload, secret, signature):
    expected_signature = base64.b64encode(
        hmac.new(
            secret.encode('utf-8'),
            payload.encode('utf-8'),
            hashlib.sha256
        ).digest()
    ).decode('utf-8')
    
    return signature == expected_signature

# Usage
received_signature = request.headers.get('X-Ultron-Signature')
payload = json.dumps(request.json, separators=(',', ':'))
is_valid = verify_webhook_signature(
    payload,
    'your-webhook-secret',
    received_signature
)

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
)

func CalculateWebhookSignature(message []byte, secret string) string {
    key := []byte(secret)
    h := hmac.New(sha256.New, key)
    h.Write(message)
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

func VerifyWebhookSignature(payload []byte, secret, signature string) bool {
    expectedSignature := CalculateWebhookSignature(payload, secret)
    return signature == expectedSignature
}

Error Handling

Common Error Codes

  • 311: SkuOutOfSubscriptionItem - Subscription doesn't include webhook functionality
  • 323: SkuSubscriptionInactive - Subscription is inactive
  • 105: CmdInvalidParams - Invalid webhook URL format
  • 100: CmdDeviceNotFound - Device not found in app devices

Webhook Delivery

  • Timeout: 30 seconds
  • Retry Policy: 3 attempts with exponential backoff
  • Expected Response: HTTP 200-299 status codes
  • Response Time: Should respond within 5 seconds

Best Practices

Security

  1. Always verify signatures using the webhook secret
  2. Use HTTPS for webhook endpoints
  3. Validate payload structure before processing
  4. Rate limiting to prevent abuse

Reliability

  1. Idempotency: Handle duplicate events gracefully
  2. Quick Response: Return HTTP 200 within 5 seconds
  3. Async Processing: Process webhook data asynchronously
  4. Error Logging: Log failed webhook deliveries for debugging

Example Webhook Handler

app.post('/webhook/device-state', (req, res) => {
  try {
    // 1. Verify signature
    const signature = req.headers['x-ultron-signature'];
    const payload = JSON.stringify(req.body);
    
    if (!verifyWebhookSignature(payload, webhookSecret, signature)) {
      return res.status(401).send('Invalid signature');
    }
    
    // 2. Quick response
    res.status(200).send('OK');
    
    // 3. Process asynchronously
    processDeviceStateChange(req.body);
    
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).send('Internal server error');
  }
});

Next Steps

After completing these prerequisites, you can proceed with the following:

  1. Create a Webhook Endpoint: Set up a publicly accessible URL to receive webhook events.
  2. Configure Webhooks: Use the CreateAppSettings API to configure your webhook URL and the events you want to receive.
  3. Handle Webhook Events: Implement logic in your application to receive, verify, and process incoming webhook events.

Troubleshooting

Webhook Not Receiving Events

  1. Send a test event using the /usr/v5/TestWebhookURL API to your endpoint. This is the best way to confirm basic connectivity and signature handling without involving actual device events.
  2. Check if devices are added to app using /usr/v5/GetUserAppDevices
  3. Verify webhook URL is accessible and returns 200
  4. Ensure subscription includes webhook permissions
  5. Check webhook settings are configured using /usr/v5/GetAppSettings

Signature Verification Fails

  1. Ensure webhook secret matches the configured secret
  2. Verify payload is not modified before verification
  3. Check signature calculation algorithm implementation
  4. Ensure proper encoding (UTF-8 for payload, Base64 for signature)
  5. Make sure JSON payload format matches exactly (no extra spaces)