Skip to content

Push Alerts to DingTalk via RPC

1. Overview

ThinkLink (TKL) RPC can call external HTTP endpoints (see RPC Model §1.3.2 Type 4) and forward device alerts to a DingTalk custom group bot in real time. This note shows how to push messages securely using DingTalk's HMAC signing mode.

2. Prerequisites

2.1 Create a DingTalk Custom Bot

  1. In a DingTalk group, open Group Settings → Group Assistant → Add Bot → Custom
  2. Under security settings, strongly recommend choosing "Additional Signing" (加签) mode (do not rely solely on keyword or IP whitelist)
  3. Copy the generated Webhook URL and Signing Secret (starts with SEC)
Webhook: https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxxxx
Secret:  SECxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

2.2 Protect access_token and Secret (Required Reading)

⚠️ Security notice: access_token is the bot's send credential, Secret is the signing key. Leakage of either allows an attacker to forge messages from the bot.

Follow these rules:

  • Do not hard-code access_token or Secret into the RPC script, config files, or screenshots
  • Recommended: Store both in the device's (or asset's) server_attrs and reference them via device.server_attrs.dingtalk_token and .dingtalk_secret
  • Always enable signing mode: DingTalk's "keyword" and "IP whitelist" modes are easy to bypass; signing is the only mode that resists forgery
  • Rotate the credentials at least once a quarter (recreate the bot)
  • Limit group membership strictly; remove bots that are no longer needed
  • If you suspect a leak, delete the bot immediately and create a new one

3. Calling DingTalk from an RPC

3.1 Store Credentials in server_attrs

In Operations → Device Management → Device Detail → Attributes, add to server_attrs:

json
{
    "dingtalk_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "dingtalk_secret": "SECxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

3.2 RPC Script: Signed Markdown Message

javascript
function rpc_script({ device, params, alarms, logger }) {
    let token  = device.server_attrs?.dingtalk_token;
    let secret = device.server_attrs?.dingtalk_secret;
    if (!token || !secret) {
        logger.error("dingtalk credentials not configured", { eui: device.eui });
        return null;
    }

    // DingTalk signature: HmacSHA256(timestamp + "\n" + secret), then base64 + URL encode
    let timestamp = Date.now();
    let stringToSign = `${timestamp}\n${secret}`;

    let crypto = require("crypto");
    let sign = encodeURIComponent(
        crypto.createHmac("sha256", secret).update(stringToSign).digest("base64")
    );

    let url = `https://oapi.dingtalk.com/robot/send?access_token=${token}` +
              `&timestamp=${timestamp}&sign=${sign}`;

    let title = `Device Alert - ${device.name}`;
    let text = [
        `### ${title}`,
        ``,
        `- **EUI**: \`${device.eui}\``,
        `- **Level**: **${(params.level || "").toUpperCase()}**`,
        `- **Description**: ${params.desc || "n/a"}`,
        `- **Time**: ${new Date().toISOString()}`
    ].join("\n");

    return [
        {
            sleepTimeMs: 0,
            type: "axios",
            dnMsg: {
                method: "POST",
                url: url,
                headers: { "Content-Type": "application/json" },
                data: {
                    msgtype: "markdown",
                    markdown: { title: title, text: text },
                    at: {
                        atMobiles: params.at_mobiles || [],
                        isAtAll: !!params.at_all
                    }
                },
                timeout: 5000
            }
        }
    ];
}

💡 DingTalk requires timestamp to be within 1 hour of server time. The signature must be recomputed for the current timestamp — never reuse a precomputed one.

3.3 RPC Script: Plain Text Message

javascript
function rpc_script({ device, params, alarms, logger }) {
    let token  = device.server_attrs?.dingtalk_token;
    let secret = device.server_attrs?.dingtalk_secret;
    if (!token || !secret) return null;

    let timestamp = Date.now();
    let crypto = require("crypto");
    let sign = encodeURIComponent(
        crypto.createHmac("sha256", secret)
              .update(`${timestamp}\n${secret}`).digest("base64")
    );

    return [
        {
            sleepTimeMs: 0,
            type: "axios",
            dnMsg: {
                method: "POST",
                url: `https://oapi.dingtalk.com/robot/send?access_token=${token}&timestamp=${timestamp}&sign=${sign}`,
                headers: { "Content-Type": "application/json" },
                data: {
                    msgtype: "text",
                    text: { content: params.content || `${device.name} alert` }
                },
                timeout: 5000
            }
        }
    ];
}

4. RPC Parameter Configuration

Attr NameTypeAliasDescription
contentstringMessage contentUsed in plain-text mode
levelstringAlert levellow / mid / high / urgent
descstringAlert descriptionMarkdown-mode body text
at_mobilesobject@ mobile listArray, e.g. ["13800000000"]
at_allboolean@ everyoneUse sparingly

5. Wiring into the Alert Flow

See WeCom AN §5 for the trigger script example — just change method to the name of this RPC.

6. Troubleshooting

SymptomLikely Cause
errcode: 310000 sign not matchtimestamp or secret mismatch; wrong signature encoding (must be base64 then URL encode)
errcode: 130101Keyword mode active but message lacks the configured keyword
No message at allWrong access_token; bot disabled by group admin; TKL server cannot reach oapi.dingtalk.com
Rate limitA single bot allows max 20 messages per minute

7. Notes

  • Signing requires a new signature per message — never reuse an old one
  • Do not include customer PII, internal IPs, tokens, or other sensitive data in messages
  • @-mentioned mobile numbers must be the registered numbers of group members
  • For critical alerts, configure multiple notification channels (e.g. DingTalk + email) so a single-channel outage does not silence alerts