Zalo Personal Channel: QR Login Setup and Configuration

Learn how to connect your personal Zalo account using the experimental zalouser plugin with QR login, including installation, setup, and important warnings about account risks.

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, running in-process with no separate CLI binary.

Warning

This is not an official integration, and using it could lead to account suspension or a ban. Proceed at your own risk.

Install

Zalo Personal ships as an official external plugin, not part of the core bundle. Install it before you can use it:

openclaw plugins install @openclaw/zalouser
  • Lock to a specific version: openclaw plugins install @openclaw/zalouser@<version>
  • Build from a source checkout: openclaw plugins install ./path/to/local/zalouser-plugin
  • Learn more: Plugins

Quick setup

  1. Get the plugin installed (steps above).
  2. Sign in via QR code, which must be done on the Gateway machine:
    • Run openclaw channels login --channel zalouser
    • Scan the displayed QR using the Zalo mobile app.
  3. Turn on the channel:
{
  channels: {
    zalouser: {
      enabled: true,
      dmPolicy: "pairing",
    },
  },
}
  1. Reboot the Gateway, or wrap up the remaining setup.
  2. By default, DM access relies on pairing; the pairing code needs approval the first time a contact reaches out.

What it is

  • Everything executes in-process through the zca-js library, with no external zca/openzca binary involved.
  • Inbound messages arrive through native event hooks (message, error).
  • Outbound replies go straight out via the JS API, covering text, media, and links.
  • Built for "personal account" scenarios where the Zalo Bot API is unavailable.

Naming

The channel id is zalouser, making it clear this automates a personal Zalo user account (unofficial). zalo stays reserved in case an official Zalo API integration appears later.

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 gets split into chunks of 2000 characters, matching the Zalo client cap.
  • channels.zalouser.mediaMaxMb caps each outbound attachment size in MiB. The selected channel account's mediaMaxMb takes priority over the root, and agents.defaults.mediaMaxMb provides the fallback. Images may undergo optimization; when limits are absent, the shared loader defaults apply.
  • Streaming is not available.
  • Completed inbound message ids stay stored for 30 days, limited to the 1000 newest entries for each account.

The optional zalouser tool picks a credential profile, not a channel account. Its image action only applies the current delivery account's cap when that account is tied to the chosen profile. Otherwise, it relies on the channel root and agent fallback; other accounts sharing the profile are not examined. Profile selection and the tool's literal default profile are left untouched.

Inbound durability

OpenClaw stores every raw zca-js message callback before processing it. After a Gateway restart, pending messages resume from the account queue, and processing stays serialized for each direct chat or group.

The zca-js socket listener offers no delivery acknowledgement and does not automatically replay old messages after reconnecting. So the durable queue only shields the local crash window once a callback reaches OpenClaw; it cannot retrieve a message the socket never delivered. Replay tombstones mostly guard against a duplicate callback carrying the same Zalo message id.

Access control (DMs)

channels.zalouser.dmPolicy: pairing | allowlist | open | disabled (default: pairing).

channels.zalouser.allowFrom should rely on stable Zalo user IDs. It can also point to static sender access groups (accessGroup:<name>). During interactive setup, typed names can be turned into IDs using the plugin's in-process contact lookup.

If a raw name stays in config, startup resolves it only when channels.zalouser.dangerouslyAllowNameMatching: true is turned on. Without that opt-in, runtime sender checks work purely on IDs, and raw names are ignored for authorization.

Approve through:

  • openclaw pairing list zalouser
  • openclaw pairing approve zalouser <code>

Group access (optional)

  • Default: channels.zalouser.groupPolicy = "allowlist" (groups must be explicitly allowlisted).
  • Permit every group: channels.zalouser.groupPolicy = "open".
  • Deny every group: channels.zalouser.groupPolicy = "disabled".
  • With groupPolicy = "allowlist":
    • channels.zalouser.groups keys should be stable group IDs; names resolve to IDs at startup only when channels.zalouser.dangerouslyAllowNameMatching: true is enabled.
    • channels.zalouser.groupAllowFrom decides which senders inside allowed groups can trigger the bot; static sender access groups can be referenced via accessGroup:<name>.
  • The configure wizard can ask for group allowlists.
  • Group allowlist matching defaults to ID-only. Unresolved names are ignored for auth unless channels.zalouser.dangerouslyAllowNameMatching: true is enabled.
  • channels.zalouser.dangerouslyAllowNameMatching: true acts as a break-glass compatibility mode, bringing back mutable startup name resolution and runtime group-name matching.
  • groupAllowFrom does not fall back to allowFrom for ordinary group messages: leaving it empty on an allowlisted group opens that group to any sender. Authorized control commands (such as /new) are the exception; command sender checks fall back to allowFrom when groupAllowFrom is empty.

Example:

{
  channels: {
    zalouser: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["1471383327500481391"],
      groups: {
        "123456789": { enabled: true },
        "Work Chat": { enabled: true },
      },
    },
  },
}

Note

channels.zalouser.groups.<id>.allow is an older field name; the current config uses enabled. openclaw doctor --fix migrates allow to enabled on its own.

Group mention gating

  • channels.zalouser.groups.<group>.requireMention decides if a mention is mandatory for group replies.
  • Lookup sequence: group id -> group:<id> alias -> group name/slug (name-based matches apply only when dangerouslyAllowNameMatching: true) -> * -> fallback (true).
  • This holds for both allowlisted groups and open group mode.
  • When a bot message is quoted, that counts as an implicit mention for activating the group.
  • Authorized control commands (such as /new) can skip the mention requirement.
  • If a group message gets skipped due to the mention rule, OpenClaw saves it as pending group history and attaches it to the next group message that gets processed.
  • Group history cap: channels.zalouser.historyLimit, then messages.groupChat.historyLimit, then a fallback of 50.

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 determine profile selection:

VarPurpose
ZALOUSER_PROFILEProfile name applied when no profile is present in channel or account config.
ZCA_PROFILEOlder fallback, only used when ZALOUSER_PROFILE is absent.

Profile names pick the saved Zalo login credentials stored in OpenClaw state. Lookup order:

  1. Explicit profile in config.
  2. ZALOUSER_PROFILE.
  3. ZCA_PROFILE.
  4. The account id for non-default accounts, or default for the default account.

For setups with multiple accounts, it's best to set profile on each account in config so a single environment variable doesn't force multiple accounts to share one login session.

Typing, reactions, and delivery acknowledgements

  • Before sending a reply, OpenClaw fires a typing event (best-effort).
  • The message reaction action react works for zalouser in channel actions.
    • To strip a particular reaction emoji from a message, use remove: true.
    • Reaction behavior is described in Reactions
  • For inbound messages carrying event metadata, OpenClaus sends delivered plus seen acknowledgements (best-effort).

Troubleshooting

Login won't persist:

  • openclaw channels status --probe
  • Log in again: openclaw channels logout --channel zalouser && openclaw channels login --channel zalouser

Allowlist/group name failed to resolve:

  • Put numeric IDs in allowFrom/groupAllowFrom and stable group IDs in groups. If you really need exact friend/group names, turn on channels.zalouser.dangerouslyAllowNameMatching: true.

Moved from an older external zca/CLI setup:

  • Drop any external zca process expectations; the channel now runs entirely in-process through zca-js, with no external CLI binary involved.
1,331 words · updated Sep 1, 2026