A password-protected Zello Consumer channel cannot be used by the Channels API at all — and the proxy discards the field where Zello says so. The result is a dispatch console that authenticates cleanly, reports the channel offline with no reason given, and silently drops every transmission.
Zello diagnosed it correctly on the very first connect, in the same frame the proxy was already parsing.
What Zello actually sends
on_channel_status, on a password-protected channel:
{"command":"on_channel_status","channel":"TicketsCad-CleveOps","status":"offline",
"users_online":0,"images_supported":false,"texting_supported":false,
"locations_supported":false,
"error":"invalid password","error_type":"configuration"}
Same channel, same account, same key, after removing the channel password:
{"command":"on_channel_status","channel":"TicketsCad-CleveOps","status":"online",
"users_online":3,"images_supported":true,"texting_supported":true,
"locations_supported":true,"full_duplex":false}
Note it is not only audio: images_supported, texting_supported and locations_supported are all false while the channel is refused, because the session is never attached to the channel. They flip to true the moment the password is gone.
The bug
proxy/ZelloUpstream.php, the on_channel_status handler, reads three fields and drops the two that explain failure:
$ch = (string) ($data['channel'] ?? '?');
$st = (string) ($data['status'] ?? '?');
\plog("[Upstream] Channel status: {$ch} - {$st}");
($this->onStatus)('channel_status', "Channel '{$ch}' is {$st}");
// ...
if (isset($data['images_supported'])) { ... }
error and error_type are never looked at. So the operator gets Channel 'X' is offline — true, unactionable, and indistinguishable from an empty channel, a bad key, a missing membership, or a wrong channel name.
Downstream the failure is silent in the worst way: logon succeeds, the widget shows connected, PTT lights up and the timer runs, start_stream goes out, and Zello simply never answers it. Frames pile up as Queued N frames (pending, no stream_id yet) and are discarded at PTT release. Nothing anywhere says why. start_stream FAILED never fires either, because Zello does not reject the command — it ignores it.
Fix
Surface what Zello already told us:
$err = trim((string) ($data['error'] ?? ''));
$errType = trim((string) ($data['error_type'] ?? ''));
$suffix = $err !== '' ? ' — ' . $err . ($errType !== '' ? " ({$errType})" : '') : '';
\plog("[Upstream] Channel status: {$ch} - {$st}{$suffix}");
($this->onStatus)('channel_status', "Channel '{$ch}' is {$st}{$suffix}");
Running locally. The widget now reads Channel 'TicketsCad-CleveOps' is offline — invalid password (configuration), which points straight at the cause. Happy to send it as a PR.
The documentation half, which is what actually misled me
docs/ZELLO-SETUP-GUIDE.md:125 is the only mention of channel passwords in the repo:
Password / private vs. public. A private or passworded channel only admits users who have been approved or have the password. Important: you do not type a channel password into TicketsCAD. Instead, the console's Zello account (dispatch-console) must itself have joined and been admitted to the channel. If the console account isn't an admitted member, the proxy will log in to Zello fine but see an empty channel.
It predicts the symptom exactly — "log in to Zello fine but see an empty channel" is precisely success: true followed by status: offline, users_online: 0.
But the remedy does not work. The console account (dispatch-cad) was the channel's owner. You cannot be more admitted than that, and Zello still returned invalid password. Being an admitted member does not satisfy the Channels API on a passworded channel — the API session itself is refused, and the guide correctly states there is nowhere in TicketsCAD to put a channel password.
So the guidance sends you to check membership, membership is already correct, and the actual answer — remove the password from the channel — appears nowhere. I read that paragraph during the investigation, confirmed ownership, concluded passwords were not my problem, and moved on. It is the specific sentence that steered me wrong.
It also interacts badly with the partial fix from #6
In #6 you said:
Fixed in 18b37a2. The steps now follow the real console flow (Keys → Add Key → New Key Details) instead of the older "channel admin → Channels API" wording…
That shipped in v4.2.0 and the steps are correct now. But the framing paragraph above them was not updated. ZELLO-SETUP-GUIDE.md:173 is byte-identical to the pre-v4.2.0 version and still asserts:
that key pair is generated inside one specific channel's admin settings, and it authorizes only that one channel … The Issuer + Private Key are per channel.
The shipped guide therefore contradicts itself: the prose says keys are per-channel and minted from channel admin, and the numbered steps twelve lines later send you to an account-level Keys page with no channel field anywhere — which matches the real console, and matches Zello's own support article for acquiring a token, which states the flow involves no channel selection.
The troubleshooting entry at line 499 is built on the same stale premise:
"Channel offline" right after a successful login
Cause: the Issuer / Private Key don't authorize that channel.
That is the exact symptom of a passworded channel, attributed to the wrong cause. Following it, I regenerated the key twice, deleted and re-minted it, and rebuilt the account — none of which could have helped, because the key was valid every time.
Suggested doc changes
- Correct
:125 — on a passworded Consumer channel the Channels API is refused outright, even for the channel owner. The fix is to remove the channel password, not to check membership.
- Add "channel has a password" as the first cause under the
"Channel offline" troubleshooting entry, with error: "invalid password" as the identifying signature.
- Finish
18b37a2 — retire the "per channel / channel admin settings" premise at :173 so it stops contradicting the corrected steps.
settings.php:4700 tells the reader to look for Channel status: NAME - online and explains Authentication failed, but says nothing about what an offline channel means. That is the state a stuck install is actually in.
Should TicketsCAD have a channel-password field? No — and that is the problem
Worth settling, because it is the obvious first instinct and it is a dead end. The Channels API spec lists the complete logon field set:
command, seq, auth_token, refresh_token, username, password,
channels, listen_only, version, platform_type, platform_name,
language, features
There is no channel-password field. password is the account password ("Required if username provided"). So a Channel Password box in Settings would have nowhere to send its value. The guide is right that you do not type one into TicketsCAD — there is no such thing to type. Only its remedy ("be an admitted member") is wrong.
That makes this a hard constraint rather than a missing feature: on Consumer, a channel usable by TicketsCAD must have no channel password. Nothing in the app can work around it.
Why this matters more for this project than it might look
TicketsCAD is open source and aimed squarely at agencies without budget, and free Zello Consumer is the obvious radio bridge for exactly that group — a paid Work network is the thing they do not have. So the tier most likely to be used here is the tier with this constraint.
The operational consequence deserves stating in the guide, because it is a real trade-off rather than a formality: the only way to make a Consumer channel work with TicketsCAD is to remove its password, which removes the access control the operator likely put there on purpose. For a dispatch channel carrying incident traffic, "anyone who finds the channel name can join and listen" is a different security posture from "you need the password," and an agency should be choosing that knowingly rather than discovering it as a side effect of getting PTT to work.
One thing I have not tested and am flagging rather than asserting: Zello Consumer channels can be restricted by approval / private membership as an alternative to a password. If approval-based access is honoured by the Channels API — plausible, since the refusal here is specifically invalid password and approval is not password-based — then that would be the right recommendation for anyone who needs a closed channel: private-by-approval, no password, console account admitted. Worth someone verifying before it goes in the guide; I did not want to swap one untested instruction for another.
Reproduction — and a request
On a free Zello Consumer account, which is the tier #6 was about:
- Create a channel you own and set a channel password on it.
- Point
zello_dispatch_channel at it with a valid Issuer + Private Key.
- Start the proxy. Logon succeeds; channel reports offline.
- Press PTT.
start_stream is sent, no stream_id ever arrives, frames are discarded at release. No error surfaces anywhere.
- Remove the channel password, restart the proxy — channel goes online and PTT works immediately.
I would value someone else reproducing step 1–4 on a free Consumer account before you act on this. My conclusion rests on one account and one channel, and while the before/after frames are unambiguous on this install, "passworded Consumer channel is refused by the Channels API" is a claim about Zello's behaviour rather than about this codebase, and it deserves a second data point.
I cannot speak to Zello Work at all. I have no Work network to test against. Work uses a different auth path — getAuthToken() returns an empty string for service=work and relies on the logon username/password — and Work channels have roles and per-channel permissions that do not exist on Consumer. It is entirely possible Work behaves differently, or that a Work network token spans passworded channels without complaint. Please do not let me imply otherwise; the doc wording should probably scope the warning to Consumer unless someone can confirm Work.
The code fix stands on its own regardless of tier: whatever error Zello sends on on_channel_status, showing it to the operator is strictly better than discarding it.
Environment
v4.2.2, Windows 11 / IIS, PHP 8.4.22, MySQL 8.0. Zello Consumer (wss://zello.io/ws), free account, self-minted production token (Issuer + Private Key, RS256). Not environment-specific — it is a dropped field in a JSON handler plus documentation.
Footnote on how long this took
For calibration on the cost of the dropped field: authentication succeeded on every attempt throughout, so every signal pointed at the credentials. Over the session this produced two full key regenerations, one key deleted and re-minted, a rebuilt Zello account and channel, an audit of channel ownership and talk permissions, and a detour into whether developers.zello.com binds keys to channels at all. The answer was in the first on_channel_status frame of the very first connect, and the proxy had been receiving and discarding it every reconnect since.
A password-protected Zello Consumer channel cannot be used by the Channels API at all — and the proxy discards the field where Zello says so. The result is a dispatch console that authenticates cleanly, reports the channel offline with no reason given, and silently drops every transmission.
Zello diagnosed it correctly on the very first connect, in the same frame the proxy was already parsing.
What Zello actually sends
on_channel_status, on a password-protected channel:{"command":"on_channel_status","channel":"TicketsCad-CleveOps","status":"offline", "users_online":0,"images_supported":false,"texting_supported":false, "locations_supported":false, "error":"invalid password","error_type":"configuration"}Same channel, same account, same key, after removing the channel password:
{"command":"on_channel_status","channel":"TicketsCad-CleveOps","status":"online", "users_online":3,"images_supported":true,"texting_supported":true, "locations_supported":true,"full_duplex":false}Note it is not only audio:
images_supported,texting_supportedandlocations_supportedare allfalsewhile the channel is refused, because the session is never attached to the channel. They flip totruethe moment the password is gone.The bug
proxy/ZelloUpstream.php, theon_channel_statushandler, reads three fields and drops the two that explain failure:erroranderror_typeare never looked at. So the operator getsChannel 'X' is offline— true, unactionable, and indistinguishable from an empty channel, a bad key, a missing membership, or a wrong channel name.Downstream the failure is silent in the worst way: logon succeeds, the widget shows connected, PTT lights up and the timer runs,
start_streamgoes out, and Zello simply never answers it. Frames pile up asQueued N frames (pending, no stream_id yet)and are discarded at PTT release. Nothing anywhere says why.start_stream FAILEDnever fires either, because Zello does not reject the command — it ignores it.Fix
Surface what Zello already told us:
Running locally. The widget now reads
Channel 'TicketsCad-CleveOps' is offline — invalid password (configuration), which points straight at the cause. Happy to send it as a PR.The documentation half, which is what actually misled me
docs/ZELLO-SETUP-GUIDE.md:125is the only mention of channel passwords in the repo:It predicts the symptom exactly — "log in to Zello fine but see an empty channel" is precisely
success: truefollowed bystatus: offline, users_online: 0.But the remedy does not work. The console account (
dispatch-cad) was the channel's owner. You cannot be more admitted than that, and Zello still returnedinvalid password. Being an admitted member does not satisfy the Channels API on a passworded channel — the API session itself is refused, and the guide correctly states there is nowhere in TicketsCAD to put a channel password.So the guidance sends you to check membership, membership is already correct, and the actual answer — remove the password from the channel — appears nowhere. I read that paragraph during the investigation, confirmed ownership, concluded passwords were not my problem, and moved on. It is the specific sentence that steered me wrong.
It also interacts badly with the partial fix from #6
In #6 you said:
That shipped in v4.2.0 and the steps are correct now. But the framing paragraph above them was not updated.
ZELLO-SETUP-GUIDE.md:173is byte-identical to the pre-v4.2.0 version and still asserts:The shipped guide therefore contradicts itself: the prose says keys are per-channel and minted from channel admin, and the numbered steps twelve lines later send you to an account-level Keys page with no channel field anywhere — which matches the real console, and matches Zello's own support article for acquiring a token, which states the flow involves no channel selection.
The troubleshooting entry at line 499 is built on the same stale premise:
That is the exact symptom of a passworded channel, attributed to the wrong cause. Following it, I regenerated the key twice, deleted and re-minted it, and rebuilt the account — none of which could have helped, because the key was valid every time.
Suggested doc changes
:125— on a passworded Consumer channel the Channels API is refused outright, even for the channel owner. The fix is to remove the channel password, not to check membership."Channel offline"troubleshooting entry, witherror: "invalid password"as the identifying signature.18b37a2— retire the "per channel / channel admin settings" premise at:173so it stops contradicting the corrected steps.settings.php:4700tells the reader to look forChannel status: NAME - onlineand explainsAuthentication failed, but says nothing about what an offline channel means. That is the state a stuck install is actually in.Should TicketsCAD have a channel-password field? No — and that is the problem
Worth settling, because it is the obvious first instinct and it is a dead end. The Channels API spec lists the complete
logonfield set:There is no channel-password field.
passwordis the account password ("Required if username provided"). So a Channel Password box in Settings would have nowhere to send its value. The guide is right that you do not type one into TicketsCAD — there is no such thing to type. Only its remedy ("be an admitted member") is wrong.That makes this a hard constraint rather than a missing feature: on Consumer, a channel usable by TicketsCAD must have no channel password. Nothing in the app can work around it.
Why this matters more for this project than it might look
TicketsCAD is open source and aimed squarely at agencies without budget, and free Zello Consumer is the obvious radio bridge for exactly that group — a paid Work network is the thing they do not have. So the tier most likely to be used here is the tier with this constraint.
The operational consequence deserves stating in the guide, because it is a real trade-off rather than a formality: the only way to make a Consumer channel work with TicketsCAD is to remove its password, which removes the access control the operator likely put there on purpose. For a dispatch channel carrying incident traffic, "anyone who finds the channel name can join and listen" is a different security posture from "you need the password," and an agency should be choosing that knowingly rather than discovering it as a side effect of getting PTT to work.
One thing I have not tested and am flagging rather than asserting: Zello Consumer channels can be restricted by approval / private membership as an alternative to a password. If approval-based access is honoured by the Channels API — plausible, since the refusal here is specifically
invalid passwordand approval is not password-based — then that would be the right recommendation for anyone who needs a closed channel: private-by-approval, no password, console account admitted. Worth someone verifying before it goes in the guide; I did not want to swap one untested instruction for another.Reproduction — and a request
On a free Zello Consumer account, which is the tier #6 was about:
zello_dispatch_channelat it with a valid Issuer + Private Key.start_streamis sent, nostream_idever arrives, frames are discarded at release. No error surfaces anywhere.I would value someone else reproducing step 1–4 on a free Consumer account before you act on this. My conclusion rests on one account and one channel, and while the before/after frames are unambiguous on this install, "passworded Consumer channel is refused by the Channels API" is a claim about Zello's behaviour rather than about this codebase, and it deserves a second data point.
I cannot speak to Zello Work at all. I have no Work network to test against. Work uses a different auth path —
getAuthToken()returns an empty string forservice=workand relies on the logon username/password — and Work channels have roles and per-channel permissions that do not exist on Consumer. It is entirely possible Work behaves differently, or that a Work network token spans passworded channels without complaint. Please do not let me imply otherwise; the doc wording should probably scope the warning to Consumer unless someone can confirm Work.The code fix stands on its own regardless of tier: whatever
errorZello sends onon_channel_status, showing it to the operator is strictly better than discarding it.Environment
v4.2.2, Windows 11 / IIS, PHP 8.4.22, MySQL 8.0. Zello Consumer (
wss://zello.io/ws), free account, self-minted production token (Issuer + Private Key, RS256). Not environment-specific — it is a dropped field in a JSON handler plus documentation.Footnote on how long this took
For calibration on the cost of the dropped field: authentication succeeded on every attempt throughout, so every signal pointed at the credentials. Over the session this produced two full key regenerations, one key deleted and re-minted, a rebuilt Zello account and channel, an audit of channel ownership and talk permissions, and a detour into whether
developers.zello.combinds keys to channels at all. The answer was in the firston_channel_statusframe of the very first connect, and the proxy had been receiving and discarding it every reconnect since.