This post is meant to document another project I’ve been working on recently:CMCC Notify。
Project Address:
github.com/thekfjie/cmcc-notify
Thank you to OpenClaw for open-sourcing this project, allowing majorInflexible service providersThey’ve also released quite a few cool APIs (🙏). This time, I’m using China Mobile’s New Message Channel API, which was originally designed to allow tools like OpenClaw and MClaw to integrate with conversations, but I’m using it to receive server notifications.
Both email and Gotify work, but it’s nice to have an additional way to view messages directly on your phone. Server alerts, script results, and NAS notifications can all be sent here.
We’ve now implemented two usage options: Users already using Gotify can install a plugin to forward messages; those who want to call the script directly can deploy a standalone service via the HTTP API. We support text, images, and files, and we also provide Docker images and builds for AMD64 and ARM64.
Setup and Configuration
The access credentials were obtained after activation and authorization. Channel API Key. The program uses this key to connect to the gateway, and messages are then sent to the corresponding bound users; there is no need to enter a phone number when sending them.
If you’re handling the protocol yourself, you need to include the following during the WebSocket handshake: X-API-Key, and after connecting, send another authentication message:
{"type":"auth","apiKey":"ak_...","version":"2.0"}
Received auth_ok Only then is authentication considered successful. The SDK also includes a timer. ping, Check pong This handles reconnection after a disconnection, so you don’t have to write a separate set of code when handling send/receive notifications.
The actual frame for displaying text is very simple:
{
"type": "send",
"apiKey": "ak_...",
"content": "备份完成",
"messageId": "msg_..."
}
Leave this blank to You can then send it to the user associated with the key. If you’re using only the standalone version or the Gotify plugin, the SDK handles this for you—all you need to do is configure the key.
In the project, each key is stored as a channel, and you can name it and add notes. You can paste it directly during configuration. ak_......You can also paste the entire authorization text message; the program will extract the key from it, and the rest of the message will not be saved. This saves you the trouble of manually selecting and copying the text.
If a notification needs to be sent to multiple people, add their respective channels to the same notification group. The server will send the notification one by one and record the results separately; if some succeed and others fail, the API will return 207 Multi-Status...so you can see exactly which channel is having the problem.
These configurations can be completed in the standalone version of the WebUI, where you can also view the connection status and send test messages.
Standalone Service and Gotify Plugin
The standalone version is primarily intended for use with monitoring tools, scripts, and NAS systems.
I don’t want to have to enter the underlying Channel API Key in every script, and to ensure that ourNot reallySecurity is a top priority, so we created a separate “Notification App.” For example, the monitoring system uses one app, while the backup script uses another; each uses its own token to send notifications to pre-configured channels or notification groups.
This message appears once when you create an app cn_app_... For tokens, the server stores only their SHA-256 hash. Applications can individually disable or rotate tokens, or set a per-minute request limit.
A call is simply a standard HTTP request:
curl -X POST https://notify.example.com/v1/notify \
-H "Authorization: Bearer <APPLICATION_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"title": "服务告警",
"message": "磁盘使用率超过 90%"
}'
The destination is configured on the management page; the script is only responsible for submitting content. If you need to change the destination channel later, simply modify the application configuration.
The WebUI also includes copyable API examples, as well as tests for sending text, images, and files. Once you’ve finished configuring it, you can try it out on the page first before integrating it into your own service.
For the Gotify plugin, simply use the built-in configuration and details pages provided by Gotify. It reads the user’s message stream, filters it based on the Application ID and Priority, and then forwards it to the configured CMCC channel.
For example, you might choose to forward only messages from a specific monitoring app, or only alerts that meet a specified priority. The “Priority” setting here is used solely to filter messages; it does not result in any additional high-priority styling on the mobile app.
The plugins page displays connection status, the anonymized key, and transmission statistics, which is generally sufficient for routine checks.
Pictures, Files, and Long Messages
The images and files have already been successfully sent. The process consists of two steps: first, upload the files using HTTP multipart, and then obtain mediaUrl, and then send the media data via WebSocket.
本地文件 → HTTP 上传 → mediaUrl → WebSocket 媒体消息 → 手机
The upload interface is /upload, forms with files and apiKey, and after it succeeds, from the returned data Retrieve the media URL, then include the filename, MIME type, and size in the transmission frame.
URLs for externally hosted media may also be accepted by the gateway, but performance is not as stable as when files are uploaded first and then sent. The current implementation in the project uses the local file upload method, and we recommend using this approach first to ensure everything works properly when you integrate it yourself.
The upload size is currently limited to 200 MiB on the client side. This is a project-level restriction; it does not mean that all file types within this range have been tested on mobile devices. When integrating the service, you can start by testing with a small image or file.
There’s one somewhat annoying issue during testing: although media frames can include captions, they may not always appear on mobile devices.
So now, if you’re sending both text and an attachment, first send a plain-text message, and then send the image or file without any body text. Under normal circumstances, you’ll receive two messages on your phone. However, these two transmissions are separate; even after the text is sent successfully, the attachment may still fail to upload or send, so you’ll need to check the results for each one separately when troubleshooting.
You’ll also need to be mindful of how long messages are displayed. Based on our testing so far, line breaks are preserved and URLs are recognized, but Markdown and HTML won’t render correctly. If you add a bunch of headings or bold tags to a notification, they’ll still appear as symbols on a mobile device—it’s best to just use plain text for line breaks.
If the main text is too long, it may be collapsed into a preview with an ellipsis; to view the full content, click through to the platform-generated webpage. This collapse is handled by the platform; the SDK does not delete content, create summaries, or alter the formatting on your behalf.
Based on current observations, to ensure notifications are displayed as directly as possible, keep the text within 220 Unicode characters—including spaces and line breaks—and limit the total number of lines to no more than 20, with no more than 15 non-empty lines.
These numbers are suggested guidelines that allow for some leeway; they should not be treated as the platform’s fixed folding thresholds, nor are they the character limits for the API. Posts exceeding these limits can still be published; the SDK will preserve the original text, though mobile users may need to tap to open the webpage to view the full content.
Alerts can list the service name, status, and key metrics at the beginning; for example, this is sufficient:
NAS 磁盘告警
数据盘使用率:92%
剩余空间:80 GB
检查时间:09:30
If necessary, add a link to the full details at the end. Daily reports or long logs are fine, but they’re not as easy to find when scrolling through messages on a phone, so try not to bury important results at the very end.
We’ve also documented the protocol structures for audio and video, but we still need to continue testing how they perform on end devices. For now, we’ll stick to text, images, and files.
Code Splitting and Plugin Building
Both access methods share a single Go SDK, which includes WebSocket authentication, heartbeats, reconnection after disconnection, and media uploads. The repository is divided into three modules:
cmcc-notify/
├── cmcc/ CMCC Go SDK
├── gotify-plugin/ Gotify 插件
├── server/ 独立服务与 WebUI
├── docs/ 架构与协议说明
└── go.work 本地联合开发
Each of the three directories contains go.mod, locally via go.work Develop together, and you can also GOWORK=off Build separately. The dependencies for the standalone HTTP service, configuration, and WebUI remain in server/... the plugin only includes the necessary parts.
The link looks something like this:
flowchart TB GOTIFY["Gotify 消息"] --> PLUGIN["Gotify 插件"] APP["监控 / 脚本 / NAS"] --> API["独立服务 REST API"] WEBUI["WebUI 配置与测试"] --> API PLUGIN --> SDK["CMCC Go SDK"] API --> SDK SDK --> CMCC["中国移动新消息网关"] CMCC --> PHONE["手机"]
Building the Gotify Go plugin is a bit of a hassle,.so This depends on the Gotify version, the Go toolchain, the operating system, and the CPU architecture; be sure to select the appropriate build when downloading.
We have now successfully built and loaded the Gotify 3.1.1 ARM64 plugin. We have also added dependency checks for the plugin in CI to prevent unnecessary dependencies from being included in the plugin when we modify the standalone version in the future.
The standalone version of WebUI is directly embedded in the Go binary, so there’s no need to include a separate front-end file during deployment. Test each of the three modules individually,go test -race、go vet It’s now connected to the Actions check as well.
Please note when using this product
API Response 202 Accepted, or the SDK returns accepted: true, indicating that the write operation for this submission was successful. Currently, there is no reliable confirmation of delivery or read receipt, so while you can check the connection and submission results on the page, you’ll have to actually check your phone to see if you’ve received the message.
There’s one more media report media_processed This status indicates that the gateway has processed the media, but it cannot be used to determine whether the phone has actually received it. When implementing the protocol yourself, it is recommended that you first send a short text message to confirm that the phone can receive it, and then try images, files, and long body text. This way, it will be easier to identify which step is causing the problem.
Messages in the group are sent independently through each channel; a failure in one channel will not cause other messages that have already been submitted to be withdrawn. Attachments must also be handled on a per-channel basis: each channel uploads its own file using its own key, then sends the URL it receives. The project does not reuse the link from a single channel’s upload for the entire group. If partial failures occur during integration, you can troubleshoot by reviewing the results for each channel returned by the API.
I’ll keep using it for now; I mainly want to add examples for message history, application auditing, and integration with additional monitoring systems. I also need to continue observing how the gateway and mobile app behave, and make adjustments if any issues arise.
The code and instructions are all hereWarehouseInside. Gotify users can view gotify-plugin/, check by calling the API directly server/; If you only need to send the protocol, you can also use it on its own cmcc/ the SDK included in it.

