Skip to content

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/call server APIs, and iot_voip_notify callback 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:

#ModulePurpose
1WeChat API wrapperEncapsulate access_token retrieval and caching — all other WeChat APIs depend on it
2WeChat callback handlingReceive join_voip_room → call Tange Cloud Token API → push downlink message to device
3Device downlink channelServer-to-device push channel for call notifications, cancel, and authorization updates
4Mini Program APIsWeChat login, VoIP authorization bind/unbind, SN Ticket issuance
5Device APIsDevice media profile upload, authorized caller list, active call initiation
6Call cancellationMini 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}

json
{ "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}

json
{
  "model_id":  "HRHY_xxx",
  "sn":        "TIRZ00000001",
  "openid":    "o4DLd5...",
  "room_type": "voice",
  "listener_name": "device",
  "query": "",
  "version_type": 2,
  "caller_camera_status": 0,
  "listener_camera_status": 0,
  "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 WeChat

3.3 Field Flow Summary ​

"→ Token" means the field goes into the Tange Cloud Token request. "→ Downlink" means it goes into the call_incoming message.

SourceField→ Token→ DownlinkNotes
WeChat XMLSn✓—Device ID, used for profile lookup
WeChat XMLRoomId✓✓As wx_room_id
WeChat XMLSessionKey✓✓As wx_session_key
WeChat XMLServerToken✓✓As wx_server_token
WeChat XMLPayload✓✓Raw Base64 passthrough
WeChat URLopenid—✓Caller identity
Device profileAll fields✓—A/V parameters
Server configwx_app_id✓✓
Server configwx_model_id✓✓
Tange Cloud responsepeer_id—✓Device WHIP connect address
Tange Cloud responsetoken—✓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:

json
{
  "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:

json
{ "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.


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:

json
{ "type": "<message type>", "channel": "wx", "payload": { ... } }

5.2 Message Categories ​

CategoryDescriptionRequires device ACK
Command (cmd)Critical operations such as incoming call notificationsYes
Notification (notify)Informational messages such as cancel or authorization changesNo

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.

json
{
  "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:

FieldSourceDevice usage
peer_idTange Cloud Token API responseFirst argument to TiRtcWhipConnect
tokenTange Cloud Token API responseSecond argument to TiRtcWhipConnect
wx_app_idServer configIdentifies the Mini Program
wx_model_idServer configIoT model ID
wx_room_idWeChat callback RoomId (passthrough)Needed for hangup/reject
wx_user_openidWeChat callback URL query openid (passthrough)Caller identity
wx_server_tokenWeChat callback ServerToken (passthrough)Pass to TiRtcServiceRequest on reject
wx_session_keyWeChat callback SessionKey (passthrough)Session key
wx_payloadWeChat callback Payload (passthrough)Custom data, Base64 raw
wx_call_idPayload decoded .id (passthrough)Call identifier
wx_fromPayload decoded .from (passthrough)Caller side: "miniapp" or "device"

5.4 call_cancel — Cancel Call ​

Notification-type message.

json
{ "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.

json
{ "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

json
// 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.

json
// 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

json
// Request
{ "device_id": "TIRZ00000001", "wx_open_id": "o4DLd5..." }
// Response
{ "code": 0 }

6.4 SN Ticket ​

POST /v1/voip/user/sn-ticket

json
// Request
{ "device_id": "TIRZ00000001" }
// Response
{ "code": 0, "data": { "sn_ticket": "..." } }

Internally calls WeChat getsnticket.

6.5 Cancel Call ​

POST /v1/voip/user/cancel

json
// 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.

json
// Request
{
  "screen_width":        0,
  "screen_height":       0,
  "audio_rate":          8000,
  "audio_channels":      1,
  "down_audio_mt":       "alaw",
  "up_video_mt":         "none",
  "down_video_mt":       "none",
  "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 fieldToken fieldAllowed values (see Server API)
audio_rateaudio_rate8000 / 16000
audio_channelsaudio_channels1 / 2
video_mtvideo_mth264 / mjpeg / none
down_audio_mtdown_audio_mtalaw / amr / opus
up_video_mtup_video_mth264 / h265 / mjpeg / none
down_video_mtdown_video_mth264 / mjpeg / none
no_videono_videoSet true for audio-only devices
screen_width/heightscreen_width / screen_heightRequired for non-audio-only
calling_timeout_seccalling_timeout_secRing timeout in seconds, default 30

New integrations should use the directional fields above and pass them through to the Token request. Keep video_mt only as a compatibility field that sets both video directions, and do not mix it with up_video_mt or 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

json
// 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

json
// Request
{ "device_id": "TIRZ00000001", "wx_user_openid": "o4DLd5...", "wx_room_type": "voice" }
// Response
{ "code": 0 }

Internally calls WeChat iot/voip/call. After calling:

  1. WeChat pushes an incoming call ring to the Mini Program
  2. User answers → WeChat callbacks join_voip_room
  3. 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 initiationMini Program VoIP pluginDevice calls POST /v1/voip/device/call
Extra server stepNoneCall WeChat iot/voip/call
join_voip_room handlingSameSame
Tange Token APISameSame
Downlink pushSameSame
cmdw=0x2000Received after WhipConnect, both sides establishedSame

WeChat VoIP calling