Skip to main content

Group Robot

A Group Robot is bound to a specific group chat. An external system can send robot messages to that group through a Webhook, making it suitable for monitoring alerts, CI/CD notifications, and business reminders. Group Robots and Application Robots are independent channels; their credentials, authentication methods, and message types must not be mixed.

Comparison with the Application Robot​

ComparisonGroup RobotApplication Robot
Message destinationSpecific group chatBot one-to-one chat with a member
AuthenticationWebhook access_token + HMAC-SHA256 signatureApp-level access_token + permission scope
Credential sourceGenerated after the bot is added to a group chatBot settings in the app configuration
Supported message typestext, image, voice, video, file, location, customtext, card
Target controlDetermined by the group chat bound to the botDetermined by the target member list and the app visibility scope
Requires a published appNoThe app must be published and enabled

Do not use a Group Robot's access_token for app-level APIs, and do not use an Application Robot's robotCode for a group robot Webhook.

API information​

ItemDescription
API nameSend a Group Robot message
HTTP methodPOST
Endpointhttps://{gateway_host}/webhook/robot/send
Request formatapplication/json
AuthenticationQuery parameters access_token, timestamp, and sign
Successful responseReturns the robot message msgId

This API does not use the app-level access_token and does not require a published app. The request usually returns HTTP 200; callers must still use the code in the response body to determine business success.

Integration steps​

1. Create a Group Robot​

Add the bot to the target group chat. After creation, save the following credentials:

CredentialPurposeStorage requirement
access_tokenIncluded in the Webhook URL query to identify the bot and target groupTreat it as a secret; never expose it or write it into frontend code
secret_keyUsed for HMAC-SHA256 signature verificationKeep it only on the caller's server; never commit it to a public repository

The basic Webhook URL format is:

https://{gateway_host}/webhook/robot/send?access_token={access_token}

2. Build request parameters​

Query parameters​

ParameterTypeRequiredDescription
access_tokenstringYesThe Group Robot Webhook token
timestampint64YesCurrent Unix timestamp in milliseconds
signstringYesBase64 encoding of the HMAC-SHA256 result over the signing input

JSON request body​

ParameterTypeRequiredDescription
mtypestringYesMessage type; see Message types
contentstringYesMessage content, up to 65,536 characters

The raw HTTP request body is limited to 1 MiB; Content-Length and chunked transfers are subject to the same limit.content is a JSON string field; do not send an Application Robot's card object directly as the Group Robot request body.

Text message example:

{
"mtype": "text",
"content": "Build #1024 completed"
}

3. Compute the signature​

The signing input and calculation are fixed as follows:

stringToSign = timestamp + "\n" + secret_key
sign = Base64(HmacSHA256(stringToSign, secret_key))

Follow these rules when computing the signature:

  1. timestamp Use a millisecond Unix timestamp as a decimal string.
  2. There must be exactly one newline between the timestamp and secret_key, represented by \n.
  3. Use secret_key as the HMAC-SHA256 key.
  4. Base64-encode the binary HMAC result, then URL-encode sign and put it in the query.
  5. The server accepts signatures only within five minutes of the current time.
  6. A signature can be used only once during its validity period; retries must generate a new timestamp and sign.

4. Send the request​

The complete request format is:

POST https://{gateway_host}/webhook/robot/send?access_token={access_token}&timestamp={timestamp}&sign={url_encoded_sign}
Content-Type: application/json

{"mtype":"text","content":"Build #1024 completed"}

5. Handle the response​

Successful response:

{
"code": 200,
"data": {
"msgId": "message-id-xxx"
},
"msg": ""
}

Failure response:

{
"code": 500,
"data": null,
"msg": "invalid signature"
}

The caller should check network errors, the HTTP status, and the response body code. For request too frequent, use a new timestamp and retry,do not reuse the original signature; recompute it with a new millisecond timestamp.

Message types​

Group Robots support the following message types:

mtypeDescription
textPlain text message
imageImage message
voiceVoice message
videoVideo message
fileFile message
locationLocation message
customCustom message

The structured card type is supported only by Application Robots, not by Group Robot Webhooks. The message service parses content according to its message type; ensure the content format matches mtype before sending.

Four-language call examples​

All examples use the same query authentication, millisecond timestamp, and text request body. Tokens, secrets, and gateway addresses are placeholders; read them from server-side secret configuration in production.

Shell​

#!/usr/bin/env bash
set -euo pipefail

GATEWAY_HOST="https://{gateway_host}"
WEBHOOK_TOKEN="webhook-token-xxx"
WEBHOOK_SECRET="webhook-secret-xxx"
TIMESTAMP=$(( $(date +%s) * 1000 ))

# The signature must be generated only on the server; never send secret_key to a browser or mobile client.
STRING_TO_SIGN="$TIMESTAMP\n$WEBHOOK_SECRET"
SIGNATURE=$(printf '%b' "$STRING_TO_SIGN" \
| openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -binary \
| openssl base64 -A)

# The Base64 signature in the query must be URL-encoded so `+`, `/`, and `=` are not parsed incorrectly.
TOKEN_URLENCODED=$(printf '%s' "$WEBHOOK_TOKEN" | sed 's/%/%25/g; s/+/%2B/g; s|/|%2F|g; s/=/%3D/g')
SIGNATURE_URLENCODED=$(printf '%s' "$SIGNATURE" | sed 's/%/%25/g; s/+/%2B/g; s|/|%2F|g; s/=/%3D/g')
curl -sS -X POST "$GATEWAY_HOST/webhook/robot/send?access_token=$TOKEN_URLENCODED&timestamp=$TIMESTAMP&sign=$SIGNATURE_URLENCODED" \
-H 'Content-Type: application/json' \
-d '{"mtype":"text","content":"Build #1024 completed"}'

PHP​

<?php

$gatewayHost = 'https://{gateway_host}';
$accessToken = 'webhook-token-xxx';
$secretKey = 'webhook-secret-xxx';
$timestamp = (int) floor(microtime(true) * 1000);

// There must be exactly one newline between the timestamp and secret, and the signing secret is server-only.
$stringToSign = $timestamp . "\n" . $secretKey;
$sign = base64_encode(hash_hmac('sha256', $stringToSign, $secretKey, true));
$query = http_build_query([
'access_token' => $accessToken,
'timestamp' => $timestamp,
'sign' => $sign,
]);

$body = json_encode([
'mtype' => 'text',
'content' => 'Build #1024 completed',
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);

$handle = curl_init($gatewayHost . '/webhook/robot/send?' . $query);
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);

$response = curl_exec($handle);
if ($response === false) {
throw new RuntimeException(curl_error($handle));
}
curl_close($handle);

// Production code should parse code and record msgId or the error.
echo $response;

Golang​

package main

import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)

func main() {
gatewayHost := "https://{gateway_host}"
accessToken := "webhook-token-xxx"
secretKey := "webhook-secret-xxx"
timestamp := time.Now().UnixMilli()

// The signing input must use a millisecond timestamp, one newline, and secret_key.
stringToSign := strconv.FormatInt(timestamp, 10) + "\n" + secretKey
mac := hmac.New(sha256.New, []byte(secretKey))
_, _ = mac.Write([]byte(stringToSign))
sign := base64.StdEncoding.EncodeToString(mac.Sum(nil))

query := url.Values{}
query.Set("access_token", accessToken)
query.Set("timestamp", strconv.FormatInt(timestamp, 10))
query.Set("sign", sign)
body := bytes.NewBufferString(`{"mtype":"text","content":"Build #1024 completed"}`)
request, err := http.NewRequest(http.MethodPost, gatewayHost+"/webhook/robot/send?"+query.Encode(), body)
if err != nil {
panic(err)
}
request.Header.Set("Content-Type", "application/json")

response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
data, _ := io.ReadAll(response.Body)
fmt.Println(string(data))
}

C++​

#include <curl/curl.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>

#include <chrono>
#include <sstream>
#include <string>

std::string base64Encode(const unsigned char* data, unsigned int length) {
std::string output(4 * ((length + 2) / 3), '\\0');
const int encodedLength = EVP_EncodeBlock(
reinterpret_cast<unsigned char*>(output.data()), data, length);
output.resize(encodedLength);
return output;
}

std::string signRequest(long long timestamp, const std::string& secretKey) {
// The HMAC input must be timestamp + newline + secret_key.
const std::string stringToSign = std::to_string(timestamp) + "\n" + secretKey;
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digestLength = 0;
HMAC(EVP_sha256(), secretKey.data(), static_cast<int>(secretKey.size()),
reinterpret_cast<const unsigned char*>(stringToSign.data()),
stringToSign.size(), digest, &digestLength);
return base64Encode(digest, digestLength);
}

int main() {
const std::string gatewayHost = "https://{gateway_host}";
const std::string accessToken = "webhook-token-xxx";
const std::string secretKey = "webhook-secret-xxx";
const auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
const std::string sign = signRequest(timestamp, secretKey);

CURL* handle = curl_easy_init();
if (handle == nullptr) {
return 1;
}

// URL-encode the token and signature in the query so special characters do not break the request.
char* escapedToken = curl_easy_escape(handle, accessToken.c_str(), 0);
char* escapedSign = curl_easy_escape(handle, sign.c_str(), 0);
std::ostringstream url;
url << gatewayHost << "/webhook/robot/send?access_token=" << escapedToken
<< "&timestamp=" << timestamp << "&sign=" << escapedSign;
curl_free(escapedToken);
curl_free(escapedSign);

const std::string body = R"({"mtype":"text","content":"Build #1024 completed"})";
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(handle, CURLOPT_URL, url.str().c_str());
curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(handle, CURLOPT_POST, 1L);
curl_easy_setopt(handle, CURLOPT_POSTFIELDS, body.c_str());
const CURLcode result = curl_easy_perform(handle);
curl_slist_free_all(headers);
curl_easy_cleanup(handle);
return result == CURLE_OK ? 0 : 1;
}

Error code descriptions​

API failures usually return HTTP 200 with a unified response body and code=500. The following msg values help locate the problem:

msgCauseSuggested handling
access_token is requiredMissing access_tokenAdd the robot token to the query
timestamp is requiredMissing timestampAdd a millisecond timestamp
invalid timestampThe timestamp is not a valid integerUse a decimal int64 millisecond timestamp
sign is requiredMissing signCompute the signature and put it in the query
robot not foundThe robot associated with the token does not existCheck the token and confirm that the bot is still in the group
robot secret key not configuredThe robot has no signing secret configuredConfigure or regenerate the secret in the robot settings
signature expiredThe timestamp differs from the server by more than five minutesRecompute the signature with the current time
invalid signatureThe signing input, secret, or encoding is incorrectCheck the newline, HMAC key, and Base64 encoding
request too frequent, use a new timestamp and retryThe same signature has already been consumedRecompute with a new millisecond timestamp and retry
robot push disabledRobot message delivery is disabledEnable message delivery in the robot settings
session not foundThe group chat bound to the robot does not existCheck the group bound to the robot
unsupported message typemtype is not in the allowed listUse a supported message type
invalid request bodyThe JSON format or a required field is invalidCheck mtype, content, and the JSON format
request body too largeThe raw request body exceeds 1 MiBReduce the request body and retry

Security and retry recommendations​

  1. Keep access_token and secret_key only on the caller's server; never put them in frontend code, a mobile client, a public repository, or logs.
  2. Production environments must use HTTPS to prevent tokens and message content from being intercepted or tampered with.
  3. Do not put the complete Webhook URL in screenshots, tickets, or monitoring logs; if it leaks, remove the robot or rotate its credentials.
  4. Before retrying, obtain a new millisecond timestamp and recompute sign. Do not reuse a signature that has already been sent.
  5. After a successful send, save data.msgId for later delivery troubleshooting.