Status: experimental. This integration automates a personal Zalo account via native zca-js, in-process, with no external CLI binary.
This is an unofficial integration and may result in account suspension or ban. Use at your own risk.
Install
Zalo Personal is an official external plugin, not bundled in core. Install it before use:
apus plugins install @openclaw/zalouser
- Pin a version:
apus plugins install @openclaw/zalouser@<version> - From a source checkout:
apus plugins install ./path/to/local/zalouser-plugin - Details: Plugins
Quick setup
- Install the plugin (above).
- Login (QR, on the Gateway machine):
apus channels login --channel zalouser- Scan the QR code with the Zalo mobile app.
- Enable the channel:
{
channels: {
zalouser: {
enabled: true,
dmPolicy: "pairing",
},
},
}
- Check
apus channels status --probe; start the Gateway if it is offline. Config changes follow hot reload. - DM access defaults to pairing; approve the pairing code on first contact.
What it is
- Runs entirely in-process via the
zca-jslibrary (no externalzca/openzcabinary). - Uses native event listeners (
message,error) to receive inbound messages. - Sends replies directly through the JS API (text/media/link).
- Designed for "personal account" use cases where the Zalo Bot API is not available.
Naming
Channel id is zalouser to make it explicit this automates a personal Zalo user account (unofficial). zalo is reserved for a potential future official Zalo API integration.
Finding IDs (directory)
apus directory self --channel zalouser
apus directory peers list --channel zalouser --query "name"
apus directory groups list --channel zalouser --query "work"
Limits
- Outbound text is chunked to 2000 characters (Zalo client limit).
- Canceling or replacing a send stops its later requests after preparation. Messages already submitted may still arrive, and previously reported message IDs remain recorded when a later chunk or audio step fails.
channels.zalouser.mediaMaxMblimits each outbound attachment in MiB. The selected channel account'smediaMaxMboverrides the root, thenagents.defaults.mediaMaxMbsupplies the fallback. Images may be optimized; omitted limits preserve the shared loader defaults.- Streaming is not supported.
- Completed inbound message ids are retained for 30 days, bounded to the 1000 most recent entries per account.
The optional zalouser tool selects a credential profile, not a channel account.
Its image action uses the current delivery account's cap only when that account
uses the selected profile. Otherwise it uses the channel root and agent fallback;
it does not search other accounts that happen to share the profile. Profile
selection and the tool's literal default profile remain unchanged.
Inbound durability
Apus stores each raw zca-js message callback before processing it. Pending messages resume from the account queue after a Gateway restart, and processing stays serialized per direct chat or group.
The zca-js socket listener does not expose a delivery acknowledgement or automatically replay old messages after reconnect. The durable queue therefore protects the local crash window after a callback reaches Apus; it cannot recover a message the socket never delivered. Replay tombstones are mostly a safeguard against a repeated callback with the same Zalo message id.
Access control (DMs)
channels.zalouser.dmPolicy: pairing | allowlist | open | disabled (default: pairing).
channels.zalouser.allowFrom should use stable Zalo user IDs. It can also reference static sender access groups (accessGroup:<name>). During interactive setup, entered names can be resolved to IDs using the plugin's in-process contact lookup.
If a raw name remains in config, startup resolves it only when channels.zalouser.dangerouslyAllowNameMatching: true is enabled. Without that opt-in, runtime sender checks are ID-only and raw names are ignored for authorization.
Approve via:
apus pairing list zalouserapus pairing approve zalouser <code>
Group access (optional)
- Default:
channels.zalouser.groupPolicy = "allowlist"(groups require an explicit allowlist entry). - Open all groups:
channels.zalouser.groupPolicy = "open". - Block all groups:
channels.zalouser.groupPolicy = "disabled". - With
groupPolicy = "allowlist":channels.zalouser.groupskeys should be stable group IDs; names resolve to IDs on startup only whenchannels.zalouser.dangerouslyAllowNameMatching: trueis enabled.channels.zalouser.groupAllowFromcontrols which senders in allowed groups can trigger the bot; static sender access groups can be referenced withaccessGroup:<name>.
- The configure wizard can prompt for group allowlists.
- Group allowlist matching is ID-only by default. Unresolved names are ignored for auth unless
channels.zalouser.dangerouslyAllowNameMatching: trueis enabled. channels.zalouser.dangerouslyAllowNameMatching: trueis a break-glass compatibility mode that re-enables mutable startup name resolution and runtime group-name matching.groupAllowFromdoes not fall back toallowFromfor normal group messages: leaving it empty on an allowlisted group opens that group to any sender. Authorized control commands (for example/new) are the exception; command sender checks fall back toallowFromwhengroupAllowFromis empty.
Example:
{
channels: {
zalouser: {
groupPolicy: "allowlist",
groupAllowFrom: ["1471383327500481391"],
groups: {
"123456789": { enabled: true },
"Work Chat": { enabled: true },
},
},
},
}
channels.zalouser.groups.<id>.allow is a legacy field name; current config uses enabled. apus doctor --fix migrates allow to enabled automatically.
Group mention gating
channels.zalouser.groups.<group>.requireMentioncontrols whether group replies require a mention.- Resolution order: group id ->
group:<id>alias -> group name/slug (name-based candidates only apply whendangerouslyAllowNameMatching: true) ->*-> default (true). - Applies both to allowlisted groups and open group mode.
- Quoting a bot message counts as an implicit mention for group activation.
- Authorized control commands (for example
/new) can bypass mention gating. - When a group message is skipped because a mention is required, Apus stores it as pending group history and includes it on the next processed group message.
- Group history limit:
channels.zalouser.historyLimit, thenmessages.groupChat.historyLimit, then a fallback of50.
Example:
{
channels: {
zalouser: {
groupPolicy: "allowlist",
groups: {
"*": { enabled: true, requireMention: true },
"Work Chat": { enabled: true, requireMention: false },
},
},
},
}
Multi-account
Accounts map to zalouser profiles in Apus state. Example:
{
channels: {
zalouser: {
enabled: true,
groupPolicy: "allowlist",
defaultAccount: "work",
accounts: {
work: { enabled: true, profile: "work", groupPolicy: "allowlist" },
},
},
},
}
Environment variables
Profile selection can also come from environment variables:
| Var | Purpose |
|---|---|
ZALOUSER_PROFILE |
Profile name to use when no profile is set in channel or account config. |
ZCA_PROFILE |
Legacy fallback, used only when ZALOUSER_PROFILE is not set. |
Profile names select the saved Zalo login credentials in Apus state. Resolution order:
- Explicit
profilein config. ZALOUSER_PROFILE.ZCA_PROFILE.- The account id for non-default accounts, or
defaultfor the default account.
For multi-account setups, prefer setting profile on each account in config so one environment variable does not make multiple accounts share the same login session.
Typing, reactions, and delivery acknowledgements
- Apus sends a typing event before dispatching a reply (best-effort).
- Message reaction action
reactis supported forzalouserin channel actions.- Use
remove: trueto remove a specific reaction emoji from a message. - Reaction semantics: Reactions
- Use
- For inbound messages that include event metadata, Apus sends delivered + seen acknowledgements (best-effort).
Troubleshooting
Login doesn't stick:
apus channels status --probe- Re-login:
apus channels logout --channel zalouser && apus channels login --channel zalouser
Allowlist/group name didn't resolve:
- Use numeric IDs in
allowFrom/groupAllowFromand stable group IDs ingroups. If you intentionally need exact friend/group names, enablechannels.zalouser.dangerouslyAllowNameMatching: true.
Upgraded from an old external zca/CLI-based setup:
- Remove any external
zcaprocess assumptions; the channel now runs fully in-process viazca-js, with no external CLI binary.
DM history is missing after an upgrade:
- Run
apus doctor --fixto repair supported legacy DM session keys. Doctor checks every configured agent, including named agents, and preserves genuine group sessions.
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