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
| Comparison | Group Robot | Application Robot |
|---|---|---|
| Message destination | Specific group chat | Bot one-to-one chat with a member |
| Authentication | Webhook access_token + HMAC-SHA256 signature | App-level access_token + permission scope |
| Credential source | Generated after the bot is added to a group chat | Bot settings in the app configuration |
| Supported message types | text, image, voice, video, file, location, custom | text, card |
| Target control | Determined by the group chat bound to the bot | Determined by the target member list and the app visibility scope |
| Requires a published app | No | The 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
| Item | Description |
|---|---|
| API name | Send a Group Robot message |
| HTTP method | POST |
| Endpoint | https://{gateway_host}/webhook/robot/send |
| Request format | application/json |
| Authentication | Query parameters access_token, timestamp, and sign |
| Successful response | Returns 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:
| Credential | Purpose | Storage requirement |
|---|---|---|
access_token | Included in the Webhook URL query to identify the bot and target group | Treat it as a secret; never expose it or write it into frontend code |
secret_key | Used for HMAC-SHA256 signature verification | Keep 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
| Parameter | Type | Required | Description |
|---|---|---|---|
access_token | string | Yes | The Group Robot Webhook token |
timestamp | int64 | Yes | Current Unix timestamp in milliseconds |
sign | string | Yes | Base64 encoding of the HMAC-SHA256 result over the signing input |
JSON request body
| Parameter | Type | Required | Description |
|---|---|---|---|
mtype | string | Yes | Message type; see Message types |
content | string | Yes | Message 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:
timestampUse a millisecond Unix timestamp as a decimal string.- There must be exactly one newline between the timestamp and
secret_key, represented by\n. - Use
secret_keyas the HMAC-SHA256 key. - Base64-encode the binary HMAC result, then URL-encode
signand put it in the query. - The server accepts signatures only within five minutes of the current time.
- A signature can be used only once during its validity period; retries must generate a new
timestampandsign.
4. Send the request
The complete request format is:
POST https://{gateway_host}/webhook/robot/send?access_token={access_token}×tamp={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:
mtype | Description |
|---|---|
text | Plain text message |
image | Image message |
voice | Voice message |
video | Video message |
file | File message |
location | Location message |
custom | Custom 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×tamp=$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
<< "×tamp=" << 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:
msg | Cause | Suggested handling |
|---|---|---|
access_token is required | Missing access_token | Add the robot token to the query |
timestamp is required | Missing timestamp | Add a millisecond timestamp |
invalid timestamp | The timestamp is not a valid integer | Use a decimal int64 millisecond timestamp |
sign is required | Missing sign | Compute the signature and put it in the query |
robot not found | The robot associated with the token does not exist | Check the token and confirm that the bot is still in the group |
robot secret key not configured | The robot has no signing secret configured | Configure or regenerate the secret in the robot settings |
signature expired | The timestamp differs from the server by more than five minutes | Recompute the signature with the current time |
invalid signature | The signing input, secret, or encoding is incorrect | Check the newline, HMAC key, and Base64 encoding |
request too frequent, use a new timestamp and retry | The same signature has already been consumed | Recompute with a new millisecond timestamp and retry |
robot push disabled | Robot message delivery is disabled | Enable message delivery in the robot settings |
session not found | The group chat bound to the robot does not exist | Check the group bound to the robot |
unsupported message type | mtype is not in the allowed list | Use a supported message type |
invalid request body | The JSON format or a required field is invalid | Check mtype, content, and the JSON format |
request body too large | The raw request body exceeds 1 MiB | Reduce the request body and retry |
Security and retry recommendations
- Keep
access_tokenandsecret_keyonly on the caller's server; never put them in frontend code, a mobile client, a public repository, or logs. - Production environments must use HTTPS to prevent tokens and message content from being intercepted or tampered with.
- Do not put the complete Webhook URL in screenshots, tickets, or monitoring logs; if it leaks, remove the robot or rotate its credentials.
- Before retrying, obtain a new millisecond timestamp and recompute
sign. Do not reuse a signature that has already been sent. - After a successful send, save
data.msgIdfor later delivery troubleshooting.