Skip to content

Calls API

Use the control WebSocket for call commands and events. Its winning owner can attach the separate raw PCM media socket. Rust crate architecture explains package responsibilities.

Routes and snapshot

All routes require an account-authorized calls bearer token in the Authorization header, including WebSocket upgrades. URL-encode identifiers; never put credentials in URLs.

MethodPathResponse
GET/v1/accounts/{account}/calls/currentJSON snapshot
WebSocket/v1/accounts/{account}/calls/socketJSON control messages
WebSocket/v1/accounts/{account}/calls/{callId}/mediaJSON setup and binary PCM
Terminal window
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/accounts/$ACCOUNT/calls/current"
{
"capabilities": {
"directAudio": true
},
"availability": {
"busy": false,
"ownedByThisClient": false
},
"currentOffer": null,
"currentPendingOffer": null,
"currentCall": null,
"audioFormat": {
"sampleRate": 16000,
"channels": 1,
"frameMs": 20
}
}
FieldMeaning
capabilitiesAvailable flags only; missing flags mean unavailable; {} means no calling
currentPendingOfferIncoming candidate waiting for readiness or preparation; cannot be answered
currentOfferActionable incoming offer; clears when claimed or ended
currentCallActive call metadata, including kind, callType, direction, state and reported participant controls
audioFormatClient/daemon PCM format, independent of the WeChat codec

Same-account observers see call metadata without owner tokens or audio access. HTTP ownership is always false: ownership belongs to a control socket. Global busy also covers other accounts, whose call details stay hidden.

Client messages

Send these JSON text messages on the control socket. Every command has a requestId; reuse it only for an identical command. Use the latest received revision for answer/reject/cancel/hangup. Example revisions below are illustrative.

Readiness

Prepare your audio path, then advertise readiness. Connecting alone stays passive.

{
"type": "call.ready",
"requestId": "ready-outgoing",
"ready": true,
"format": {
"sampleRate": 16000,
"channels": 1,
"frameMs": 20
},
"scope": {
"kind": "outgoing"
}
}

Ready for one pending incoming call:

{
"type": "call.ready",
"requestId": "ready-offer",
"ready": true,
"format": {
"sampleRate": 16000,
"channels": 1,
"frameMs": 20
},
"scope": {
"kind": "offer",
"callId": "example-call",
"expectedRevision": 1
}
}

Ready for incoming and outgoing calls, or withdraw readiness:

{
"type": "call.ready",
"requestId": "ready-all",
"ready": true,
"format": {
"sampleRate": 16000,
"channels": 1,
"frameMs": 20
},
"scope": {
"kind": "all"
}
}
{
"type": "call.ready",
"requestId": "not-ready",
"ready": false,
"format": {
"sampleRate": 16000,
"channels": 1,
"frameMs": 20
}
}

Omitting scope means all.

Start or answer

{
"type": "call.dial",
"requestId": "dial-1",
"target": {
"kind": "direct",
"contactId": "example-contact"
},
"callType": "voice"
}
{
"type": "call.answer",
"requestId": "answer-1",
"callId": "example-call",
"expectedRevision": 3
}

The initiating socket owns an outgoing call. For incoming calls, the first successful answer wins; competing clients receive already_claimed.

Reject, cancel or hang up

Reject an incoming offer, cancel your outgoing call before pickup, or hang up after answering/connecting:

{
"type": "call.reject",
"requestId": "reject-1",
"callId": "example-call",
"expectedRevision": 3
}
{
"type": "call.cancel",
"requestId": "cancel-1",
"callId": "example-call",
"expectedRevision": 4
}
{
"type": "call.hangup",
"requestId": "hangup-1",
"callId": "example-call",
"expectedRevision": 5
}

Audio controls

Mute outgoing audio; send muted: false to unmute. Clear queued outgoing audio when interrupting a response; see stream generations. call.mute silences this client’s outgoing stream in the daemon. It does not send a remote microphone-control request or change the peer’s microphone.

{
"type": "call.mute",
"requestId": "mute-1",
"callId": "example-call",
"muted": true
}
{
"type": "call.clear_audio",
"requestId": "clear-1",
"callId": "example-call"
}

Server messages

These JSON text messages arrive on the control socket. Media attachment has its own media.ready response.

Hello

The first event includes the connection’s clientId and a current snapshot:

{
"type": "hello",
"protocolVersion": 1,
"clientId": "example-client",
"capabilities": {
"directAudio": true
},
"availability": {
"busy": false,
"ownedByThisClient": false
},
"currentOffer": null,
"currentPendingOffer": null,
"currentCall": null,
"audioFormat": {
"sampleRate": 16000,
"channels": 1,
"frameMs": 20
}
}

Command results

Match requestId to your command. Readiness succeeds without claiming a call:

{
"type": "command.result",
"requestId": "ready-outgoing",
"status": "accepted",
"callId": null,
"ownerToken": null,
"code": null
}

A successful claim includes a secret token for media attachment:

{
"type": "command.result",
"requestId": "dial-1",
"status": "accepted",
"callId": "example-call",
"ownerToken": "<secret owner token>",
"code": null
}

Acceptance starts the operation; it does not mean connected. An identical request can return replayed. A rejected command returns error and a fixed code:

{
"type": "command.result",
"requestId": "ready-outgoing",
"status": "replayed",
"callId": null,
"ownerToken": null,
"code": null
}
{
"type": "command.result",
"requestId": "answer-1",
"status": "error",
"callId": null,
"ownerToken": null,
"code": "already_claimed"
}

Pending and actionable offers

A pending candidate has phase waiting_for_ready or preparing:

{
"type": "call.pending",
"offer": {
"callId": "example-call",
"revision": 1,
"kind": "direct",
"callType": "voice",
"callerId": "example-contact",
"phase": "waiting_for_ready"
}
}
{
"type": "call.pending",
"offer": null
}

An actionable offer can be answered or rejected using its revision:

{
"type": "call.offered",
"offer": {
"callId": "example-call",
"revision": 3,
"kind": "direct",
"callType": "voice",
"callerId": "example-contact"
}
}

State and availability

{
"type": "call.state",
"call": {
"callId": "example-call",
"revision": 5,
"kind": "direct",
"callType": "voice",
"direction": "incoming",
"state": "connected",
"ownedByThisClient": true,
"controls": [
{
"participantId": "example-contact",
"control": {
"kind": "microphone",
"muted": false
}
}
]
}
}
{
"type": "call.state",
"call": {
"callId": "example-call",
"revision": 6,
"kind": "direct",
"callType": "voice",
"direction": "incoming",
"state": "ending",
"ownedByThisClient": true
}
}
{
"type": "call.state",
"call": {
"callId": "example-call",
"revision": 7,
"kind": "direct",
"callType": "voice",
"direction": "incoming",
"state": "ended",
"ownedByThisClient": true
}
}
{
"type": "calls.availability",
"availability": {
"busy": false,
"ownedByThisClient": false
}
}

States are offered, dialing, ringing, connecting, connected, ending, ended and uncertain. Stop audio at ending; keep control connected while the daemon finishes cleanup. uncertain reports an unconfirmed backend outcome; its network receipts are retained even when the local session closes.

Participant control events

call.control reports a participant’s control state. The current control kind is microphone; muted: true means muted, and false means unmuted. participantId is the contact ID used by the direct-call target or incoming offer.

{
"type": "call.control",
"callId": "example-call",
"participantId": "example-contact",
"control": {
"kind": "microphone",
"muted": true
}
}
{
"type": "call.control",
"callId": "example-call",
"participantId": "example-contact",
"control": {
"kind": "microphone",
"muted": false
}
}

These events go to the owner and same-account observers. They need no reply; clients can ignore them. Repeated unchanged microphone state produces no event. The latest known values also appear in currentCall.controls in HTTP snapshots, hello and call.state. An omitted controls means no state has been reported. Control updates do not change the call-state revision or PCM format, end the call, or interrupt audio in the other direction.

Audio generation and errors

After successful call.clear_audio, the send track advances:

{
"type": "track.updated",
"callId": "example-call",
"track": {
"id": 1,
"generation": 2,
"direction": "send",
"format": {
"sampleRate": 16000,
"channels": 1,
"frameMs": 20
}
}
}

A socket-level failure uses call.error:

{
"type": "call.error",
"code": "forbidden"
}

Error codes are invalid_request, busy, already_claimed, offer_expired, not_owner, unsupported, media_not_ready, outcome_unknown, capacity, stale, forbidden and closed. Lost replies do not authorize automatic retry.