Zalo Personal Channel Setup with zca-js QR Login
Covers the experimental Zalo personal account integration through native zca-js with QR login, its capabilities, and configuration. Aimed at operators setting up the external plugin and approving DM pairing.
Read this when
- Setting up Zalo Personal for OpenClaw
- Debugging Zalo Personal login or message flow
Status: experimental. This integration drives a personal Zalo account through native zca-js inside the process, so no external CLI binary is needed.
Warning
The integration is unofficial and could get your account suspended or banned. You accept that risk by using it.
Install
Zalo Personal ships as an official external plugin and is not part of core. Install it first:
openclaw plugins install @openclaw/zalouser
- To pin a version:
openclaw plugins install @openclaw/zalouser@<version> - To install from a source checkout:
openclaw plugins install ./path/to/local/zalouser-plugin - More information: Plugins
Quick setup
- Install the plugin (see above).
- Log in with a QR code on the Gateway machine:
openclaw channels login --channel zalouser- Use the Zalo mobile app to scan the QR code.
- Turn on the channel:
{
channels: {
zalouser: {
enabled: true,
dmPolicy: "pairing",
},
},
}
- Inspect
openclaw channels status --probeand start the Gateway when it is offline. Configuration edits are picked up per hot reload. - Direct messages are gated by pairing by default; on first contact, approve the pairing code.
What it is
- Everything runs in-process through the
zca-jslibrary (no externalzca/openzcabinary is involved). - Inbound messages arrive through native event listeners (
message,error). - Replies go out straight from the JS API (text, media, or link).
- Built for "personal account" scenarios in which the Zalo Bot API cannot be used.
Naming
The channel id zalouser makes clear that this automates an unofficial personal Zalo user account. A possible future official Zalo API integration is what zalo is held for.
Finding IDs (directory)
openclaw directory self --channel zalouser
openclaw directory peers list --channel zalouser --query "name"
openclaw directory groups list --channel zalouser --query "work"
Limits
- Outbound text is split into chunks of 2000 characters, which is the Zalo client limit.
- If a send is canceled or replaced, its later requests stop once preparation is done. Messages already submitted may still be delivered, and message IDs reported earlier stay on record if a later chunk or audio step fails.
channels.zalouser.mediaMaxMbcaps each outbound attachment in MiB. AnmediaMaxMbset on the selected channel account takes precedence over the root value, andagents.defaults.mediaMaxMbprovides the fallback after that. Images may be optimized; when a limit is omitted, the shared loader defaults apply.- Streaming has no support.
- Inbound message ids that have completed are kept for 30 days, capped at the 1000 most recent entries per account.
The optional zalouser tool picks a credential profile rather than a channel account.
Its image action applies the current delivery account's cap only if that account
uses the chosen profile. In every other case it relies on the channel root and the
agent fallback, and it never searches other accounts that happen to share the
profile. Profile selection, along with the tool's literal default profile, stays
as it is.
Inbound durability
Before processing, OpenClaw persists every raw zca-js message callback. After a Gateway restart, pending messages continue from the account queue, and work remains serialized per direct chat or group.
No delivery acknowledgement is exposed by the zca-js socket listener, and it does not replay old messages automatically on reconnect. So the durable queue covers only the local crash window once a callback has reached OpenClaw; a message the socket never delivered cannot be recovered. Replay tombstones chiefly guard against a repeated callback carrying the same Zalo message id.
Access control (DMs)
channels.zalouser.dmPolicy: pairing | allowlist | open | disabled (default: pairing).
channels.zalouser.allowFrom ought to contain stable Zalo user IDs. Static sender access groups (accessGroup:<name>) may be referenced as well. When setup runs interactively, names you enter can be resolved to IDs through the plugin's in-process contact lookup.
Should a raw name still sit in config, startup resolves it only if channels.zalouser.dangerouslyAllowNameMatching: true is enabled. Absent that opt-in, runtime sender checks work on IDs alone, and raw names carry no weight for authorization.
To approve:
openclaw pairing list zalouseropenclaw pairing approve zalouser <code>
Group access (optional)
- Default:
channels.zalouser.groupPolicy = "allowlist"(a group needs an explicit allowlist entry). - To open every group:
channels.zalouser.groupPolicy = "open". - To block every group:
channels.zalouser.groupPolicy = "disabled". - When
groupPolicy = "allowlist"is set:channels.zalouser.groupskeys should be stable group IDs; names become IDs at startup only ifchannels.zalouser.dangerouslyAllowNameMatching: trueis enabled.- Which senders in allowed groups may trigger the bot is governed by
channels.zalouser.groupAllowFrom; static sender access groups can be referenced viaaccessGroup:<name>.
- The configure wizard is able to prompt for group allowlists.
- Group allowlist matching is ID-only unless changed. Names that stay unresolved are ignored for auth unless
channels.zalouser.dangerouslyAllowNameMatching: trueis enabled. channels.zalouser.dangerouslyAllowNameMatching: trueis a break-glass compatibility mode: it turns mutable startup name resolution and runtime group-name matching back on.- For normal group messages,
groupAllowFromdoes not fall back toallowFrom: if you leave it empty on an allowlisted group, any sender can reach that group. Authorized control commands (/new, for instance) are the exception; whengroupAllowFromis empty, command sender checks fall back toallowFrom.
Example:
{
channels: {
zalouser: {
groupPolicy: "allowlist",
groupAllowFrom: ["1471383327500481391"],
groups: {
"123456789": { enabled: true },
"Work Chat": { enabled: true },
},
},
},
}
Note
channels.zalouser.groups.<id>.allowis an older field name; the current config usesenabled.openclaw doctor --fixconvertsallowtoenabledon its own.
Group mention gating
- Whether group replies need a mention is governed by
channels.zalouser.groups.<group>.requireMention. - Lookup order: group id ->
group:<id>alias -> group name/slug (name-based candidates apply only whendangerouslyAllowNameMatching: true) ->*-> default (true). - Works for allowlisted groups and for open group mode alike.
- An implicit mention for group activation is counted when someone quotes a bot message.
- Authorized control commands (
/new, for instance) may skip mention gating. - If a group message is skipped for lacking a required mention, OpenClaw saves it as pending group history and adds it to the next group message it processes.
- Group history limit:
channels.zalouser.historyLimit, thenmessages.groupChat.historyLimit, with a fallback of50.
Example:
{
channels: {
zalouser: {
groupPolicy: "allowlist",
groups: {
"*": { enabled: true, requireMention: true },
"Work Chat": { enabled: true, requireMention: false },
},
},
},
}
Multi-account
In OpenClaw state, accounts correspond to zalouser profiles. Example:
{
channels: {
zalouser: {
enabled: true,
groupPolicy: "allowlist",
defaultAccount: "work",
accounts: {
work: { enabled: true, profile: "work", groupPolicy: "allowlist" },
},
},
},
}
Environment variables
Environment variables can also supply the profile selection:
| Var | Purpose |
|---|---|
ZALOUSER_PROFILE | Profile name used when channel or account config sets no profile. |
ZCA_PROFILE | Legacy fallback, consulted only if ZALOUSER_PROFILE is unset. |
Saved Zalo login credentials in OpenClaw state are chosen by profile name. Resolution order:
- An explicit
profilein config. ZALOUSER_PROFILE.ZCA_PROFILE.- The account id for non-default accounts, or
defaultfor the default account.
With multiple accounts, set profile on each account in config where possible, so a single environment variable does not cause several accounts to share one login session.
Typing, reactions, and delivery acknowledgements
- Before a reply is dispatched, OpenClaw sends a typing event (best-effort).
- The message reaction action
reactis supported forzalouserin channel actions.- A specific reaction emoji can be removed from a message with
remove: true. - Reaction semantics: Reactions
- A specific reaction emoji can be removed from a message with
- For inbound messages carrying event metadata, OpenClaw sends delivered + seen acknowledgements (best-effort).
Troubleshooting
Login doesn't stick:
openclaw channels status --probe- To log in again:
openclaw channels logout --channel zalouser && openclaw channels login --channel zalouser
Allowlist/group name didn't resolve:
- Put numeric IDs in
allowFrom/groupAllowFromand stable group IDs ingroups. If exact friend/group names are genuinely needed, turn onchannels.zalouser.dangerouslyAllowNameMatching: true.
Upgraded from an old external zca/CLI-based setup:
- Drop any assumptions about an external
zcaprocess; the channel now runs entirely in-process throughzca-js, with no external CLI binary.
DM history is missing after an upgrade:
- Run
openclaw doctor --fixto repair supported legacy DM session keys. Doctor inspects every configured agent, named agents included, and leaves genuine group sessions intact.
Related
- Channels Overview - all supported channels
- Pairing - DM authentication and pairing flow
- Groups - group chat behavior and mention gating
- Channel routing - session routing for messages
- Security - access model and hardening