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

Quick setup

  1. Install the plugin (above).
  2. Login (QR, on the Gateway machine):
    • apus channels login --channel zalouser
    • Scan the QR code with the Zalo mobile app.
  3. Enable the channel:
{
  channels: {
    zalouser: {
      enabled: true,
      dmPolicy: "pairing",
    },
  },
}
  1. Check apus channels status --probe; start the Gateway if it is offline. Config changes follow hot reload.
  2. DM access defaults to pairing; approve the pairing code on first contact.

What it is

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

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:

Group access (optional)

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

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:

  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 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

Troubleshooting

Login doesn't stick:

Allowlist/group name didn't resolve:

Upgraded from an old external zca/CLI-based setup:

DM history is missing after an upgrade:

Related