Free and open-source CLI for Zalo, built on zca-js. Command structure compatible with zca-cli.dev/docs.
npm install -g openzca@latestCommand aliases: openzca, zca.
Or run without installing:
npx openzca --helpRequires Node.js 18+.
# Login with QR code
openzca auth login
# Check your account
openzca me info
# Send a message
openzca msg send USER_ID "Hello"
# Send to a group
openzca msg send GROUP_ID "Hello team" --group
# Mention a group member by display name, username, or member id
openzca msg send GROUP_ID "Hi @Alice Nguyen" --group
openzca msg send GROUP_ID "Hi @123456789" --group
# Listen for incoming messages
openzca listen| Command | Description |
|---|---|
openzca auth login |
Login with QR code (--qr-path <path> to save QR image) |
openzca auth login-cred [file] |
Login using a credential JSON file |
openzca auth logout |
Remove saved credentials |
openzca auth status |
Show login status |
openzca auth cache-refresh |
Refresh friends/groups cache |
openzca auth cache-info |
Show cache metadata |
openzca auth cache-clear |
Clear local cache |
QR login renders inline in supported terminals (Ghostty, Kitty, WezTerm, iTerm2) with ASCII fallback for others.
If QR is not visible in your terminal, use openzca auth login --open-qr (macOS/Linux desktop) or set OPENZCA_QR_OPEN=1.
In non-interactive environments, openzca auto-opens the QR image by default (set OPENZCA_QR_AUTO_OPEN=0 to disable).
You can also open the saved file manually (for example: open qr.png on macOS).
| Command | Description |
|---|---|
openzca msg send <threadId> <message> |
Send text message |
openzca msg image <threadId> [file] |
Send image(s) from file or URL |
openzca msg video <threadId> [file] |
Send video(s) from file or URL |
openzca msg voice <threadId> [file] |
Send voice message from local file or URL (.aac, .mp3, .m4a, .wav, .ogg) |
openzca msg sticker <threadId> <stickerId> |
Send a sticker |
openzca msg link <threadId> <url> |
Send a link |
openzca msg card <threadId> <contactId> |
Send a contact card |
openzca msg react <msgId> <cliMsgId> <threadId> <reaction> |
React to a message |
openzca msg typing <threadId> |
Send typing indicator |
openzca msg forward <message> <targets...> |
Forward text to multiple targets |
openzca msg delete <msgId> <cliMsgId> <uidFrom> <threadId> |
Delete a message |
openzca msg undo <msgId> <cliMsgId> <threadId> |
Recall a sent message |
openzca msg upload <arg1> [arg2] |
Upload and send file(s) |
openzca msg recent <threadId> |
List recent messages (-n, --json, newest-first); group mode prefers direct group-history endpoint (websocket fallback) |
Media commands accept local files, file:// paths, and repeatable --url options. Add --group for group threads.
Local paths using ~ are expanded automatically (for positional file args, --url, and OPENZCA_LISTEN_MEDIA_DIR).
Group text sends via openzca msg send --group resolve unique @Name or @userId mentions against the current group member list using member ids, display names, and usernames. Mention offsets are computed after formatting markers are parsed, so messages like **@Alice Nguyen** hello work. If multiple members share the same label, the command fails instead of guessing.
Use debug mode to write copyable logs for support/debugging:
# One-off debug run
openzca --debug msg image <threadId> ~/Desktop/screenshot.png
# Custom debug log path
openzca --debug --debug-file ~/Desktop/openzca-debug.log msg image <threadId> ~/Desktop/screenshot.png
# Or enable by environment
OPENZCA_DEBUG=1 openzca listen --rawDefault debug log file:
~/.openzca/logs/openzca-debug.log
Useful command to copy recent debug logs:
tail -n 200 ~/.openzca/logs/openzca-debug.logFor media debugging, grep these events in the debug log:
listen.media.detectedlisten.media.cache_error
| Command | Description |
|---|---|
openzca group list |
List groups |
openzca group info <groupId> |
Get group details |
openzca group members <groupId> |
List members |
openzca group create <name> <members...> |
Create a group |
openzca group poll create <groupId> |
Create a poll (--question, repeatable --option, optional poll flags) |
openzca group poll detail <pollId> |
Get poll details |
openzca group poll vote <pollId> |
Vote on a poll with repeatable --option <id> |
openzca group poll lock <pollId> |
Close a poll |
openzca group poll share <pollId> |
Share a poll |
openzca group rename <groupId> <name> |
Rename group |
openzca group avatar <groupId> <file> |
Change group avatar |
openzca group settings <groupId> |
Update settings (--lock-name, --sign-admin, etc.) |
openzca group add <groupId> <userIds...> |
Add members |
openzca group remove <groupId> <userIds...> |
Remove members |
openzca group add-deputy <groupId> <userId> |
Promote to deputy |
openzca group remove-deputy <groupId> <userId> |
Demote deputy |
openzca group transfer <groupId> <newOwnerId> |
Transfer ownership |
openzca group block <groupId> <userId> |
Block a member |
openzca group unblock <groupId> <userId> |
Unblock a member |
openzca group blocked <groupId> |
List blocked members |
openzca group enable-link <groupId> |
Enable invite link |
openzca group disable-link <groupId> |
Disable invite link |
openzca group link-detail <groupId> |
Get invite link |
openzca group join-link <linkId> |
Join via invite link |
openzca group pending <groupId> |
List pending requests |
openzca group review <groupId> <userId> <action> |
Approve or deny join request |
openzca group leave <groupId> |
Leave group |
openzca group disperse <groupId> |
Disperse group |
Poll creation currently targets group threads only and maps to the existing zca-js group poll APIs. group poll create requires --question plus at least two --option values, and also supports --multi, --allow-add-option, --hide-vote-preview, --anonymous, and --expire-ms.
| Command | Description |
|---|---|
openzca friend list |
List all friends |
openzca friend find <query> |
Find user by phone, username, or name |
openzca friend online |
List online friends |
openzca friend recommendations |
Get friend recommendations |
openzca friend add <userId> |
Send friend request (-m for message) |
openzca friend accept <userId> |
Accept friend request |
openzca friend reject <userId> |
Reject friend request |
openzca friend cancel <userId> |
Cancel sent friend request |
openzca friend sent |
List sent requests |
openzca friend remove <userId> |
Remove a friend |
openzca friend alias <userId> <alias> |
Set friend alias |
openzca friend remove-alias <userId> |
Remove alias |
openzca friend aliases |
List all aliases |
openzca friend block <userId> |
Block user |
openzca friend unblock <userId> |
Unblock user |
openzca friend block-feed <userId> |
Block user from viewing your feed |
openzca friend unblock-feed <userId> |
Unblock user from viewing your feed |
| Command | Description |
|---|---|
openzca me info |
Get account info |
openzca me id |
Get your user ID |
openzca me update |
Update profile (--name, --gender, --birthday) |
openzca me avatar <file> |
Change avatar |
openzca me avatars |
List avatar history |
openzca me delete-avatar <id> |
Delete an avatar |
openzca me reuse-avatar <id> |
Reuse a previous avatar |
openzca me status <online|offline> |
Set online status |
openzca me last-online <userId> |
Check last online time |
| Command | Description |
|---|---|
openzca listen |
Listen for incoming messages |
openzca listen --echo |
Auto-reply with received message |
openzca listen --prefix <prefix> |
Only process messages matching prefix |
openzca listen --webhook <url> |
POST message payload to a webhook URL |
openzca listen --raw |
Output raw JSON per line |
openzca listen --keep-alive |
Auto-reconnect on disconnect |
openzca listen --supervised --raw |
Supervisor mode with lifecycle JSON events (session_id, connected, heartbeat, error, closed) |
openzca listen --keep-alive --recycle-ms <ms> |
Periodically recycle listener process to avoid stale sessions |
listen --raw includes inbound media metadata when available:
mediaPath,mediaPathsmediaUrl,mediaUrlsmediaType,mediaTypesmediaKind
It also includes stable routing fields for downstream tools:
threadId,targetId,conversationIdsenderId,toId,chatType,msgType,timestampmentions(normalized mention entities:uid,pos,len,type, optionaltext)mentionIds(flattened mention user IDs)metadata.threadId,metadata.targetId,metadata.senderId,metadata.toIdmetadata.mentions,metadata.mentionIds,metadata.mentionCountquoteandmetadata.quotewhen the inbound message is a reply to a previous message- Includes parsed
quote.attachand extractedquote.mediaUrlswhen attachment URLs are present.
- Includes parsed
quoteMediaPath,quoteMediaPaths,quoteMediaUrl,quoteMediaUrls,quoteMediaType,quoteMediaTypes- Present when quoted attachment URLs can be resolved/downloaded.
For direct messages, metadata.senderName is intentionally omitted so consumers can prefer numeric IDs for routing instead of display-name targets.
When a reply/quoted message is detected, content also appends a compact line:
[reply context: <sender-or-owner-id>: <quoted summary>]
This helps downstream consumers that only read content (without parsing quote) still see reply context.
listen also normalizes JSON-string message payloads (common for chat.voice and share.file) so media URLs are extracted/cached instead of being forwarded as raw JSON text.
For non-text inbound messages (image/video/audio/file), content is emitted as a media note:
[media attached: /abs/path/to/file.ext (mime/type) | https://source-url]
or for multiple attachments:
[media attached: 2 files]
[media attached 1/2: /abs/path/one.png (image/png) | https://...]
[media attached 2/2: /abs/path/two.pdf (application/pdf) | https://...]
This format is compatible with OpenClaw media parsing.
By default, inbound media downloaded by listen is stored under OpenClaw state:
~/.openclaw/media/openzca/<profile>/inbound
If OPENCLAW_STATE_DIR (or CLAWDBOT_STATE_DIR) is set, that directory is used instead of ~/.openclaw.
Optional overrides:
OPENZCA_LISTEN_MEDIA_DIR: explicit inbound media cache directoryOPENZCA_LISTEN_MEDIA_MAX_BYTES: max bytes per inbound media file (default20971520, 20MB)OPENZCA_LISTEN_MEDIA_MAX_FILES: max inbound media files extracted per message (default4, max16)OPENZCA_LISTEN_MEDIA_FETCH_TIMEOUT_MS: max download time per inbound media URL (default10000)- Set to
0to disable timeout.
- Set to
OPENZCA_LISTEN_MEDIA_LEGACY_DIR=1: use legacy storage at~/.openzca/profiles/<profile>/inbound-media
Listener resilience override:
OPENZCA_LISTEN_RECYCLE_MS: whenlisten --keep-aliveis used, force listener recycle after N milliseconds.- Default:
1800000(30 minutes) if not set. - Set to
0to disable auto recycle. - On recycle,
openzcaexits with code75so external supervisors (like OpenClaw Gateway) can auto-restart it.
- Default:
OPENZCA_LISTEN_HEARTBEAT_MS: heartbeat interval forlisten --supervised --rawlifecycle events.- Default:
30000(30 seconds). - Set to
0to disable heartbeat events.
- Default:
OPENZCA_LISTEN_INCLUDE_QUOTE_CONTEXT: include reply context/quoted-media helper lines incontent.- Default: enabled.
- Set to
0to disable.
OPENZCA_LISTEN_DOWNLOAD_QUOTE_MEDIA: download quoted attachment URLs (if present) into inbound media cache.- Default: enabled.
- Set to
0to keep only quote metadata/URLs without downloading.
OPENZCA_RECENT_USER_MAX_PAGES: max websocket history pages to scan formsg recentin user/DM mode.- Default:
20. - Increase if a DM thread is old and not found in the first page.
- Default:
OPENZCA_RECENT_GROUP_MAX_PAGES: max websocket history pages to scan formsg recent -gwhen direct group-history path fails.- Default:
20. - Increase if a group thread is old and not found quickly.
- Default:
OPENZCA_LISTEN_ENFORCE_SINGLE_OWNER: enforce onelistenowner process per profile.- Default: enabled.
- Set to
0to allow multiple listeners on the same profile (not recommended).
OPENZCA_LISTEN_IPC: expose local IPC socket fromlistensomsg uploadcan reuse the active websocket session.- Default: enabled.
- Set to
0to disable IPC.
OPENZCA_LISTEN_KEEPALIVE_RESTART_DELAY_MS: when--keep-aliveis on, restart listener after close code1000/3000with this delay.- Default:
2000.
- Default:
OPENZCA_LISTEN_KEEPALIVE_RESTART_ON_ANY_CLOSE: force keepalive fallback restart for any close code.- Default: disabled.
- Set to
1if your environment closes with non-retry codes.
Supervised mode notes:
- Use
listen --supervised --rawwhen an external process manager owns restart logic. - In supervised mode, internal websocket retry ownership is disabled (equivalent to forcing
retryOnClose=false).
Upload/listener coordination overrides:
OPENZCA_UPLOAD_IPC: try upload via active listener IPC first.- Default: enabled.
- Set to
0to disable IPC path.
OPENZCA_UPLOAD_IPC_CONNECT_TIMEOUT_MS: timeout for connecting to listener IPC socket.- Default:
1000.
- Default:
OPENZCA_UPLOAD_IPC_TIMEOUT_MS: timeout waiting for listener IPC upload response.- Default:
OPENZCA_UPLOAD_TIMEOUT_MS + 5000.
- Default:
OPENZCA_UPLOAD_IPC_HANDLER_TIMEOUT_MS: timeout applied by listener IPC while executing upload.- Default: same as
OPENZCA_UPLOAD_TIMEOUT_MS(120000 if unset).
- Default: same as
OPENZCA_UPLOAD_ENFORCE_SINGLE_OWNER: when an active listener owner exists but IPC is unavailable, fail fast instead of starting a second listener.- Default: enabled.
- Set to
0to allow fallback listener startup (may disconnect active listener due duplicate websocket ownership).
OPENZCA_UPLOAD_AUTO_THREAD_TYPE: auto-detectmsg uploadthread type (group/user) when--groupis not provided.- Default: disabled (
0) for safer routing. - Set to
1to enable cache/probe-based detection.
- Default: disabled (
OPENZCA_UPLOAD_GROUP_PROBE: allowmsg uploadto probegetGroupInfowhen auto thread-type detection is enabled.- Default: enabled.
- Set to
0to skip probe and rely only on cache matches.
| Command | Description |
|---|---|
openzca account list |
List all profiles |
openzca account current |
Show active profile |
openzca account switch <name> |
Set default profile |
openzca account add [name] |
Create a new profile |
openzca account label <name> <label> |
Label a profile |
openzca account remove <name> |
Remove a profile |
Use --profile <name> or set OPENZCA_PROFILE=<name> (or legacy ZCA_PROFILE=<name>) to switch between accounts. Manage profiles with the account commands.
Profile data is stored in ~/.openzca/ (override with OPENZCA_HOME):
~/.openzca/
profiles.json
profiles/<name>/credentials.json
profiles/<name>/cache/*.json
git clone https://github.com/darkamenosa/openzca.git
cd openzca
npm install
npm run build
node dist/cli.js --helpMIT