Mode B — request and response
The framed-PCM walkie: POST an utterance to /voice, poll /sse/notifications for spoken answers, and use /voiceecho to settle audio arguments during bring-up.
8 min read
On this page
The device records a full utterance, posts it, and gets an answer. Simple to implement, tolerant of bad networks, and the only mode that works through a proxy that will not pass UDP.
Uplink
POST /voice HTTP/1.1
x-device-id: AA:BB:CC:DD:EE:FF
x-mac-address: AA:BB:CC:DD:EE:FF
x-property-id: 4
x-organization_id: 2
x-team-id: 7
x-user-id: 19
Content-Type: application/octet-stream
Transfer-Encoding: chunked
FF FF FF FF <s16le PCM, 16 kHz mono> EE EE EE EEThe body is framed PCM: a four-byte start marker 0xFFFFFFFF, the audio, and a four-byte end marker 0xEEEEEEEE. Stream it — do not buffer the whole utterance before sending the first byte.
Identity headers resolve with fallbacks — a user_id alone will resolve the organization and a property from that user's memberships, and a team_id alone resolves its property and organization. Send x-property-id explicitly anyway; the fallbacks exist for legacy units, not as a design.
Response
200 OK
Content-Type: text/plain
x-request-id: 4f2a91c7
x-request-time: 1.84s
okDeliberately minimal: the transcript is saved asynchronously and everything interesting happens server-side afterwards. Error responses are 4xx or 5xx with the reason in an X-Error header and an empty body. A 499 means the client disconnected before sending audio — if you see those in volume, your button or your network is dropping the connection mid-utterance.
The bring-up endpoint that settles arguments
/voiceecho takes exactly the same request and returns the 16 kHz WAV the recogniser actually received, with the transcript in headers. Hearing back exactly what the server heard settles arguments about clipping, gain and sample rate in one listen.
# Frame a raw PCM file: 0xFFFFFFFF + audio + 0xEEEEEEEE
{ printf '\xFF\xFF\xFF\xFF'; cat utterance-16k-mono.raw; printf '\xEE\xEE\xEE\xEE'; } \
| curl -s -D headers.txt -X POST https://api.yourcompany.com/voiceecho \
-H "x-device-id: AA:BB:CC:DD:EE:FF" \
-H "x-property-id: 4" \
-H "Content-Type: application/octet-stream" \
--data-binary @- -o echo.wav
grep -i '^x-' headers.txt
# x-transcript: check the parking gate
# x-detected-language: en
# x-stt-time: 0.93s
# x-request-time: 1.84s
afplay echo.wav # or: aplay echo.wavDownlink — notification polling
Mode B's speaker path is a poll. 204 No Content means nothing is waiting and is the normal case — treat it as cheap and expected. A 200 carries audio framed exactly like the uplink.
GET /sse/notifications HTTP/1.1
x-device-id: AA:BB:CC:DD:EE:FF
──────────────────────────────────────────────
200 OK
Content-Type: application/octet-stream
x-notification-type: order-ready
x-order-id: 344
x-notification-text: Your order for table 5 is ready
FF FF FF FF <s16le PCM, 16 kHz mono> EE EE EE EEMAC matching here is the forgiving one — colon, dash, bare hex and mixed case all resolve. x-notification-text is the spoken text: show it on a display if you have one, and use it as your fallback if audio playback fails.
A complete mode B client
import requests
HOST = "https://api.yourcompany.com"
HEADERS = {
"x-device-id": "AA:BB:CC:DD:EE:FF",
"x-mac-address": "AA:BB:CC:DD:EE:FF",
"x-property-id": "4",
"x-organization_id": "2", # underscore — this is the contract
"x-user-id": "19",
}
START, END = b"\xFF\xFF\xFF\xFF", b"\xEE\xEE\xEE\xEE"
def framed(mic):
"""Generator: start marker, live PCM while the button is held, end marker."""
yield START
for chunk in mic: # 20 ms s16le buffers straight off the ADC
yield chunk
yield END
def speak(mic):
r = requests.post(f"{HOST}/voice", headers={**HEADERS,
"Content-Type": "application/octet-stream"},
data=framed(mic), timeout=60)
if r.status_code != 200:
raise RuntimeError(r.headers.get("X-Error", r.text))
return r.text # "ok"
def poll_downlink():
"""Call every 2–5 s. Returns (text, pcm) or None."""
r = requests.get(f"{HOST}/sse/notifications",
headers={"x-device-id": HEADERS["x-device-id"]}, timeout=10)
if r.status_code == 204:
return None # normal, cheap, expected
body = r.content
if body.startswith(START) and body.endswith(END):
body = body[len(START):-len(END)]
return r.headers.get("x-notification-text", ""), bodySomething wrong or missing on this page? Tell us.

