Business Server Integration
WeChat VoIP calling involves four parties: Mini Program, business server, Tange Cloud platform, and device. The business server is the custom core: it handles WeChat callbacks, calls Tange Cloud APIs, pushes messages to devices over a long-connection channel, and exposes APIs for both the Mini Program and devices.
Interface ownership:
- WeChat provides:
cgi-bin/token,sns/jscode2session,wxa/getsnticket,wxa/business/iot/voip/callserver APIs, andiot_voip_notifycallback push- Tange Cloud provides:
POST /v1/token/wxvoip(TGV1-HMAC-SHA256 authentication)- Business server implements: all
/v1/voip/*HTTP endpoints, the WeChat callback endpoint, and the device downlink channel
The API paths and field names in this document are reference examples. Use your own naming conventions in production — only the responsibilities need to match.
Read Call flows first.
I. What to Implement
In development order:
| # | Module | Purpose |
|---|---|---|
| 1 | WeChat API wrapper | Encapsulate access_token retrieval and caching — all other WeChat APIs depend on it |
| 2 | WeChat callback handling | Receive join_voip_room → call Tange Cloud Token API → push downlink message to device |
| 3 | Device downlink channel | Server-to-device push channel for call notifications, cancel, and authorization updates |
| 4 | Mini Program APIs | WeChat login, VoIP authorization bind/unbind, SN Ticket issuance |
| 5 | Device APIs | Device media profile upload, authorized caller list, active call initiation |
| 6 | Call cancellation | Mini Program API + downlink call_cancel |
II. WeChat API Wrapper
All WeChat APIs require access_token. Wrap this first.
2.1 access_token
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}
- Valid for 7200s. Cache in memory with a 5-minute refresh margin.
- Use per-appID mutex to avoid duplicate requests.
2.2 jscode2session
GET https://api.weixin.qq.com/sns/jscode2session?appid={appid}&secret={secret}&js_code={code}&grant_type=authorization_code
Mini Program calls wx.login() → code → server exchanges for openid.
2.3 getsnticket
POST https://api.weixin.qq.com/wxa/getsnticket?access_token={access_token}
{ "sn": "TIRZ00000001", "model_id": "HRHY_xxx" }Returns sn_ticket for the Mini Program's wx.requestDeviceVoIP call.
2.4 iot/voip/call
POST https://api.weixin.qq.com/wxa/business/iot/voip/call?access_token={access_token}
{
"model_id": "HRHY_xxx",
"sn": "TIRZ00000001",
"openid": "o4DLd5...",
"room_type": "voice",
"version_type": 2,
"payload": ""
}Calling this triggers an incoming call ring on the target user's Mini Program.
III. Handling WeChat Server Callbacks
This is the core processing chain. Prioritize this after obtaining access_token.
3.1 Endpoint
GET|POST /v1/voip/notification/:wx_app_id
Configure this URL in the WeChat Official Accounts admin panel:
https://your-domain.com/v1/voip/notification/{wx_app_id}3.2 Processing Flow
POST /v1/voip/notification/:wx_app_id
│
├─ ① Verify signature
│ sha1(sort(token, timestamp, nonce)) == query param signature
│ In secure mode (encrypt_type=aes), also verify msg_signature
│
├─ ② AES decrypt (if encrypt_type=aes)
│ EncodingAESKey (43-char Base64) → 32-byte key
│ Decrypted format: random(16) + msg_len(4) + XML + app_id
│ Verify trailing app_id matches config; reject otherwise
│
├─ ③ Parse XML, only process:
│ MsgType = "event"
│ Event = "iot_voip_notify"
│ Action = "join_voip_room"
│ Return non-zero errcode for everything else
│
├─ ④ Look up device media profile by Sn (uploaded via /v1/voip/device/profile)
│ No profile → return errcode=10, cannot deliver call
│
├─ ⑤ Call Tange Cloud POST /v1/token/wxvoip (see section IV)
│ Input = WeChat callback fields (passthrough) + profile fields + server config
│ Output = peer_id + token
│
├─ ⑥ Push call_incoming to device via downlink channel (see section V)
│
└─ ⑦ Return {"errcode":0, "errmsg":"ok"} to WeChat3.3 Field Flow Summary
"→ Token" means the field goes into the Tange Cloud Token request. "→ Downlink" means it goes into the call_incoming message.
| Source | Field | → Token | → Downlink | Notes |
|---|---|---|---|---|
| WeChat XML | Sn | ✓ | — | Device ID, used for profile lookup |
| WeChat XML | RoomId | ✓ | ✓ | As wx_room_id |
| WeChat XML | SessionKey | ✓ | ✓ | As wx_session_key |
| WeChat XML | ServerToken | ✓ | ✓ | As wx_server_token |
| WeChat XML | Payload | ✓ | ✓ | Raw Base64 passthrough |
| WeChat URL | openid | — | ✓ | Caller identity |
| Device profile | All fields | ✓ | — | A/V parameters |
| Server config | wx_app_id | ✓ | ✓ | |
| Server config | wx_model_id | ✓ | ✓ | |
| Tange Cloud response | peer_id | — | ✓ | Device WHIP connect address |
| Tange Cloud response | token | — | ✓ | Device WHIP auth credential |
Core principle: nearly all WeChat callback fields are passed through. The server does not interpret them — it only transports and assembles.
IV. Calling the Tange Cloud Token API
POST /v1/token/wxvoip — TGV1-HMAC-SHA256 authentication. See the API reference for complete field details and signing.
Called once per join_voip_room callback. Merges WeChat session fields with device capabilities to obtain the WHIP connection credentials the device needs.
Request body:
{
"wx_session_key": "<WeChat callback passthrough>",
"wx_room_id": "<WeChat callback passthrough>",
"wx_session_token": "<WeChat callback passthrough>",
"wx_app_id": "<Server config>",
"device_id": "<WeChat callback Sn>",
"wx_payload": "<WeChat callback Payload raw>",
"wx_model_id": "<Server config>",
"calling_timeout_sec": "<Device profile>",
"no_video": "<Device profile>",
"video_mt": "<Device profile>",
"screen_width": "<Device profile>",
"screen_height": "<Device profile>",
"audio_rate": "<Device profile>",
"audio_channels": "<Device profile>"
}Response:
{ "code": 0, "data": { "peer_id": "whips://wxvoip?...", "token": "v1.eyJ..." } }Do not parse peer_id or token. Place them as-is into the downlink call_incoming.
V. Device Downlink Channel
The business server needs a long-connection channel to the device for pushing messages. The transport is up to you (MQTT, WebSocket, custom TCP, etc.). The only requirement: the server can address a specific device when it is online.
The following describes the message contract, independent of transport. For example with MQTT: the server connects to the broker as an MQTT client, sending commands to device/sn_{device_id}/cmd and notifications to device/sn_{device_id}/notify. The same pattern applies to WebSocket or custom TCP — only the routing mechanism differs.
5.1 Message Envelope
All downlink messages use the same JSON structure:
{ "type": "<message type>", "channel": "wx", "payload": { ... } }5.2 Message Categories
| Category | Description | Requires device ACK |
|---|---|---|
| Command (cmd) | Critical operations such as incoming call notifications | Yes |
| Notification (notify) | Informational messages such as cancel or authorization changes | No |
How to distinguish the two categories depends on the transport. MQTT can use different topics (/cmd vs /notify); WebSocket can add a field like "category": "cmd".
5.3 call_incoming — Incoming Call Notification
Command-type message. Pushed after WeChat callback processing succeeds.
{
"type": "call_incoming",
"channel": "wx",
"payload": {
"peer_id": "whips://wxvoip?...",
"token": "v1.eyJ...",
"wx_app_id": "wx27d4b2d7eb37eb58",
"wx_model_id": "HRHY_vJ9mHI2KQhd6yvj9Q",
"wx_room_id": "wxf830863...",
"wx_user_openid": "o4DLd5...",
"wx_user_nickname": "",
"wx_server_token": "...",
"wx_session_key": "...",
"wx_payload": "...",
"wx_call_id": "...",
"wx_from": "..."
}
}Field sources:
| Field | Source | Device usage |
|---|---|---|
peer_id | Tange Cloud Token API response | First argument to TiRtcWhipConnect |
token | Tange Cloud Token API response | Second argument to TiRtcWhipConnect |
wx_app_id | Server config | Identifies the Mini Program |
wx_model_id | Server config | IoT model ID |
wx_room_id | WeChat callback RoomId (passthrough) | Needed for hangup/reject |
wx_user_openid | WeChat callback URL query openid (passthrough) | Caller identity |
wx_server_token | WeChat callback ServerToken (passthrough) | Pass to TiRtcServiceRequest on reject |
wx_session_key | WeChat callback SessionKey (passthrough) | Session key |
wx_payload | WeChat callback Payload (passthrough) | Custom data, Base64 raw |
wx_call_id | Payload decoded .id (passthrough) | Call identifier |
wx_from | Payload decoded .from (passthrough) | Caller side: "miniapp" or "device" |
5.4 call_cancel — Cancel Call
Notification-type message.
{ "type": "call_cancel", "channel": "wx", "payload": { "wx_room_id": "wxf830863..." } }Triggered when the Mini Program calls /v1/voip/user/cancel. The device matches wx_room_id against the current call and stops ringing or disconnects.
5.5 callers_update — Authorization Change
Notification-type message.
{ "type": "callers_update", "channel": "wx", "payload": {} }Triggered when a user reports or deletes an authorization. The device should re-call GET /v1/voip/device/callers to refresh the local list.
VI. Mini Program–Facing APIs
Called by the Mini Program over HTTPS. Authentication is up to you.
6.1 WeChat Login
POST /v1/voip/user/wechat-mini-login
// Request
{ "code": "wx_login_code" }
// Response
{ "code": 0, "data": { "wx_user_openid": "o4DLd5..." } }Internally calls WeChat jscode2session.
6.2 VoIP Authorization Binding
POST /v1/voip/user/report-auth
Called after the Mini Program completes wx.requestDeviceVoIP.
// Request
{ "device_id": "TIRZ00000001", "wx_open_id": "o4DLd5...", "wx_model_id": "HRHY_xxx" }
// Response
{ "code": 0 }After storing the authorization, push callers_update to the device via the downlink channel.
6.3 Delete Authorization
POST /v1/voip/user/delete-auth
// Request
{ "device_id": "TIRZ00000001", "wx_open_id": "o4DLd5..." }
// Response
{ "code": 0 }6.4 SN Ticket
POST /v1/voip/user/sn-ticket
// Request
{ "device_id": "TIRZ00000001" }
// Response
{ "code": 0, "data": { "sn_ticket": "..." } }Internally calls WeChat getsnticket.
6.5 Cancel Call
POST /v1/voip/user/cancel
// Request
{ "device_id": "TIRZ00000001", "wx_room_id": "wxf830863..." }
// Response
{ "code": 0 }Pushes call_cancel to the device via the downlink channel. Only used when the Mini Program is the caller and the device has not yet answered.
VII. Device-Facing APIs
Called by the device over HTTPS. Authentication is up to you (e.g. JWT).
7.1 Upload Media Capabilities
POST /v1/voip/device/profile
Must be called on startup. Without it, the server cannot retrieve device capabilities when a WeChat callback arrives and the call notification will fail.
// Request
{
"screen_width": 0,
"screen_height": 0,
"audio_rate": 8000,
"audio_channels": 1,
"video_mt": "",
"no_video": true,
"calling_timeout_sec": 30
}
// Response
{ "code": 0 }These fields are assembled into the Tange Cloud Token request. Allowed values follow the Token API:
| Profile field | Token field | Allowed values (see Server API) |
|---|---|---|
audio_rate | audio_rate | 8000 / 16000 |
audio_channels | audio_channels | 1 / 2 |
video_mt | video_mt | h264 / mjpeg / none |
no_video | no_video | Set true for audio-only devices |
screen_width/height | screen_width / screen_height | Required for non-audio-only |
calling_timeout_sec | calling_timeout_sec | Ring timeout in seconds, default 30 |
For finer directional control, add optional fields: up_video_mt (h264/h265/mjpeg/none), down_video_mt (h264/mjpeg/none), down_audio_mt (alaw/amr/opus). Do not mix video_mt with up_video_mt/down_video_mt.
Call once per startup. The server overwrites on each update, then uses the stored values when assembling Token requests.
7.2 List Authorized Callers
GET /v1/voip/device/callers
// Response
{ "code": 0, "data": { "list": [
{ "wx_open_id": "o4DLd5...", "wx_app_id": "wx27d...", "wx_model_id": "HRHY...", "created_at": "..." }
] } }The device selects a target user from this list before placing an active call.
7.3 Device Calls Mini Program
POST /v1/voip/device/call
// Request
{ "device_id": "TIRZ00000001", "wx_user_openid": "o4DLd5...", "wx_room_type": "voice" }
// Response
{ "code": 0 }Internally calls WeChat iot/voip/call. After calling:
- WeChat pushes an incoming call ring to the Mini Program
- User answers → WeChat callbacks
join_voip_room - Subsequent flow is identical to the Mini Program–initiated scenario
VIII. Differences Between the Two Scenarios
The only difference is who initiates the call.
Scenario 1: Mini Program Calls Device
Mini Program VoIP plugin ──▶ WeChat server ── join_voip_room ──▶ Business server
│
① Verify/decrypt
② Look up profile
③ Call Tange Token API
④ Downlink call_incoming ──▶ Device
│
⑤ TiRtcWhipConnect
⑥ Receives cmdw=0x2000 (both sides established)Server role: purely passive — receives WeChat callback, forwards to device.
Scenario 2: Device Calls Mini Program
Device ── POST /v1/voip/device/call ──▶ Business server ── WeChat iot/voip/call ──▶ WeChat server
│
Mini Program rings
User answers
│
Business server ◀── join_voip_room ──── WeChat server
│
① Verify/decrypt
② Look up profile
③ Call Tange Token API
④ Downlink call_incoming ──▶ Device
│
⑤ TiRtcWhipConnect
⑥ Receives cmdw=0x2000 (both sides established)Server role: first proactively calls WeChat API to initiate, then passively handles the callback — the callback processing logic is identical to Scenario 1.
Comparison
| Scenario 1 (Mini Program calls) | Scenario 2 (Device calls) | |
|---|---|---|
| Call initiation | Mini Program VoIP plugin | Device calls POST /v1/voip/device/call |
| Extra server step | None | Call WeChat iot/voip/call |
join_voip_room handling | Same | Same |
| Tange Token API | Same | Same |
| Downlink push | Same | Same |
| cmdw=0x2000 | Received after WhipConnect, both sides established | Same |