Push Alerts to Slack 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 Slack in real time. Two integration options are covered:
- Incoming Webhook (recommended / simple): one fixed URL per channel; can only post to that channel
- Bot Token +
chat.postMessage(flexible): one token can post to multiple channels and supports richer features
2. Prerequisites
2.1 Option A: Create an Incoming Webhook
- Visit https://api.slack.com/apps and click Create New App → From scratch
- Select the target workspace; the app name can be anything
- In the left menu under Incoming Webhooks, toggle
Activate Incoming Webhooks - Click Add New Webhook to Workspace and pick the destination channel
- Copy the generated Webhook URL, which looks like:
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
2.2 Option B: Create a Bot Token
- In the same app config page, go to OAuth & Permissions
- Under Scopes → Bot Token Scopes, add:
chat:write,chat:write.public(if posting to channels the bot has not joined) - Click Install to Workspace. After authorizing, you'll receive the Bot User OAuth Token, which looks like:
xoxb-1234567890-1234567890123-xxxxxxxxxxxxxxxxxxxxxxxx - Invite the bot into the target channel:
/invite @YourBotName
2.3 Protect the Webhook URL / Bot Token (Required Reading)
⚠️ Security notice:
- A leaked Incoming Webhook URL allows anyone to post to that channel
- A Bot Token grants broader access — depending on scopes, it may allow reading channel messages, posting to multiple channels, or modifying the bot's own configuration
Follow these rules:
- Do not hard-code Webhook URLs or Bot Tokens into scripts, screenshots, or public repositories (GitHub's secret scanner automatically detects leaked Slack tokens)
- Recommended: Store URLs/tokens in the device's (or asset's)
server_attrsand reference them viadevice.server_attrs.slack_webhookor.slack_token - If you discover a leak, immediately revoke the token / disable the webhook in the Slack App admin page, then regenerate
- Grant least privilege scopes — only request
chat:write; do not addchat:readorusers:readunless required - Periodically audit the app's installation scope and permissions
- Use different apps or webhooks for different business scenarios to limit blast radius
3. Calling Slack from an RPC (Option A: Webhook)
3.1 Store Credentials in server_attrs
json
{
"slack_webhook": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
}3.2 RPC Script: Block Kit Rich Message
javascript
function rpc_script({ device, params, alarms, logger }) {
let url = device.server_attrs?.slack_webhook;
if (!url) {
logger.error("slack_webhook not configured", { eui: device.eui });
return null;
}
let levelEmoji = {
urgent: ":rotating_light:",
high: ":warning:",
mid: ":large_yellow_circle:",
low: ":information_source:"
}[params.level] || ":bell:";
return [
{
sleepTimeMs: 0,
type: "axios",
dnMsg: {
method: "POST",
url: url,
headers: { "Content-Type": "application/json" },
data: {
text: `${levelEmoji} Device Alert - ${device.name}`, // fallback for notifications
blocks: [
{
type: "header",
text: { type: "plain_text", text: `${levelEmoji} Device Alert` }
},
{
type: "section",
fields: [
{ type: "mrkdwn", text: `*Device:*\n${device.name}` },
{ type: "mrkdwn", text: `*EUI:*\n\`${device.eui}\`` },
{ type: "mrkdwn", text: `*Level:*\n${(params.level || "").toUpperCase()}` },
{ type: "mrkdwn", text: `*Time:*\n${new Date().toISOString()}` }
]
},
{
type: "section",
text: { type: "mrkdwn", text: `*Description:*\n${params.desc || "n/a"}` }
}
]
},
timeout: 5000
}
}
];
}4. Calling Slack from an RPC (Option B: Bot Token)
4.1 Store Credentials in server_attrs
json
{
"slack_token": "xoxb-1234567890-xxxxxxxxxxxxxxxxxxxxxxxx",
"slack_channel": "C01234ABCDE"
}💡
slack_channelcan be a channel ID (recommended, more stable) or#channel-name. To find the channel ID, right-click the channel in the Slack client → "View channel details" → scroll to the bottom.
4.2 RPC Script
javascript
function rpc_script({ device, params, alarms, logger }) {
let token = device.server_attrs?.slack_token;
let channel = device.server_attrs?.slack_channel;
if (!token || !channel) {
logger.error("slack credentials not configured", { eui: device.eui });
return null;
}
return [
{
sleepTimeMs: 0,
type: "axios",
dnMsg: {
method: "POST",
url: "https://slack.com/api/chat.postMessage",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json; charset=utf-8"
},
data: {
channel: channel,
text: `:warning: *${device.name}* alert: ${params.desc || ""}`,
mrkdwn: true
},
timeout: 5000
}
}
];
}5. RPC Parameter Configuration
| Attr Name | Type | Alias | Description |
|---|---|---|---|
level | string | Alert level | low / mid / high / urgent |
desc | string | Alert description | Shown in the message body |
6. Wiring into the Alert Flow
See WeCom AN §5 for the trigger script example — just change method to the name of this RPC.
7. Troubleshooting
| Symptom | Likely Cause |
|---|---|
Webhook returns invalid_payload | Malformed JSON; blocks structure invalid |
Webhook returns no_service / 404 | Wrong webhook URL, or it has been revoked |
chat.postMessage returns ok: false, error: not_in_channel | Bot not invited to the target channel (use /invite @bot) |
chat.postMessage returns error: missing_scope | Bot Token lacks required scopes; add chat:write and reinstall the app |
| Request timeout | TKL server cannot reach hooks.slack.com / slack.com |
| Rate limit | Slack limits each channel to ~1 message per second on average |
8. Notes
- Prefer Block Kit (
blocks) over the legacyattachmentsfield — it renders consistently across all Slack clients - Do not put the Bot Token itself in messages (e.g. don't dump full request bodies or stack traces)
- Enable Token Auto-rotation (OAuth v2) in workspace settings to reduce the impact of long-term token leakage
- For critical alerts, configure multiple notification channels (e.g. Slack + email) so a single-channel outage does not silence alerts