# zca-js: an unofficial Zalo client for JavaScript

> zca-js is a TypeScript library that drives Zalo Web the way a browser would, so you can log in by QR code, listen for messages and send replies from Node or Bun. It is a personal account tool with a real risk of getting the account locked, and version 2 moved image metadata handling out of the package and into your code.

**RFS-ADRENO/zca-js** — Unofficial Zalo API for JavaScript

- Repository: https://github.com/RFS-ADRENO/zca-js
- Website: https://zca-js.tdung.com
- Stars: 688 · Forks: 323
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/rfs-adreno-zca-js

## It works by pretending to be the browser

zca-js is described as an unofficial Zalo API for JavaScript, and the README is direct about the mechanism: it works by simulating the browser to interact with Zalo Web. There is no official endpoint being wrapped here in the way a normal SDK wraps a public API. The library drives the same web session a person would use.

That single fact explains most of what the library does and most of its risk. The audience is developers building personal tools on top of Zalo, chat bots, notification relays and account automation, who need a scriptable client rather than the official mobile or desktop application. The repository topics name JavaScript, Node, Bun and Zalo, and the package ships as an ES module with a CommonJS build alongside it.

The README carries a warning that should be read before anything else: using this API could get your account locked or banned, the maintainers are not responsible for any issues that may happen, and use is at your own risk. That is not a formality. You are automating an account on a service that decides when to close it, and the library has no way to promise that tomorrow's Zalo Web still behaves the way today's does.

## Login happens by scanning a QR code

The entry point is the Zalo class and a method called loginQR, and there is no username and password path in the documented usage. The example is three lines:

```javascript
import { Zalo } from "zca-js";

const zalo = new Zalo();
const api = await zalo.loginQR();
```

The result is an api object, and that object is what every other operation hangs off. The session is established the way a person establishes it on a new device, by scanning a code, which means an interactive step is part of the flow even in an automated program.

The runtime requirements are modest. The package metadata declares engines node >=18.0.0, and the README shows installation with either package manager, since the project is developed with Bun and npm works as well:

```bash
bun add zca-js # or npm install zca-js
```

The dependency list explains the browser simulation. There is a websocket client, a cookie jar, form data handling, zlib compression through pako, hashing and crypto helpers, a JSON parser that preserves big integers, and a semver comparison library. Those are the parts of a browser session that a web client needs to reproduce.

## One web listener per account, and the browser will kill it

Listening for messages works through an event emitter on the api object. The documented pattern registers a handler for the message event, inspects the thread type, and then starts the listener:

```javascript
import { Zalo, ThreadType } from "zca-js";

const zalo = new Zalo();
const api = await zalo.loginQR();

api.listener.on("message", (message) => {
    const isPlainText = typeof message.data.content === "string";

    switch (message.type) {
        case ThreadType.User: {
            if (isPlainText) {
                // received plain text direct message
            }
            break;
        }
        case ThreadType.Group: {
            if (isPlainText) {
                // received plain text group message
            }
            break;
        }
    }
});

api.listener.start();
```

Two details in that snippet carry real weight. The handler has to check whether the content is a plain string, because a message can carry something other than text, and the thread type distinguishes a direct message from a group message, so a bot that ignores the switch will respond in group chats it was never meant to answer in.

Then there is the constraint the README marks as important: only one web listener can run per account at a time, and if you open Zalo in the browser while the listener is active, the listener is stopped automatically. That is a single session model, and it means a personal bot and a human using Zalo on the same account cannot coexist. It also means a bot that crashes and restarts may find its session taken over, with no obvious error at the point of restart.

## Version 2 moved image handling into your code

The most concrete upgrade note in the README concerns images. Since version 2.0.0, zca-js no longer depends on sharp for image metadata extraction. If you want to send images or gifs by file path, you have to provide your own imageMetadataGetter function when you construct the Zalo class.

The point of that change is to keep a native image dependency out of a library that many people install, at the cost of moving that work to the consumer. The documented shape of the function is small, it takes a file path and returns height, width and size:

```javascript
import { Zalo } from "zca-js";
import sharp from "sharp";
import fs from "node:fs";

async function imageMetadataGetter(filePath) {
    const data = await fs.promises.readFile(filePath);
    const metadata = await sharp(data).metadata();
    return {
        height: metadata.height,
        width: metadata.width,
        size: metadata.size || data.length,
    };
}

const zalo = new Zalo({
    imageMetadataGetter,
});
```

If you want that helper, the README shows the dependency being added separately, and sharp is your choice of implementation rather than a requirement of the package.

For a chat bot that only handles text, the change is invisible. For a bot that forwards images from disk, it means a migration step, and it is the clearest signal in the repository about how the package treats its dependency surface.

## Sending replies, quoting and stickers

Sending a message goes through api.sendMessage, which takes a payload, a thread id and a thread type. The payload carries the text and, optionally, the message being replied to, which is how a quote or reply is attached:

```javascript
            api.sendMessage(
                {
                    msg: "echo: " + message.data.content,
                    quote: message.data, // the message to reply to (optional)
                },
                message.threadId,
                message.type, // ThreadType.User or ThreadType.Group
            );
```

The thread id and thread type come from the incoming message, so a reply loop is a matter of echoing the fields back. The echo bot example in the README is a complete pattern for that, and it also shows the guard that keeps a bot from replying to itself, by skipping messages where message.isSelf is set, and skipping anything that is not plain text.

Stickers have their own three step flow: search for stickers by keyword, fetch the detail of the one you want, then send that sticker object to the thread.

```javascript
api.getStickers("hello").then(async (stickerIds) => {
    // Get the first sticker
    const stickerObject = await api.getStickersDetail(stickerIds[0]);
    api.sendMessageSticker(
        stickerObject,
        message.threadId,
        message.type, // ThreadType.User or ThreadType.Group
    );
});
```

The examples directory in the repository holds the same code in runnable form, with login and echobot scripts.

## Other ways in: a browser extension and DevTools snippets

The README lists related projects, and two of them point at a different approach to the same goal.

The first is a browser extension that extracts the IMEI, cookies and user agent from Zalo Web. That is a different shape entirely: instead of a library reproducing a browser session, you take the session data that a real browser already has and hand it to whatever tool you like. The second is a collection of JavaScript snippets meant to be pasted into DevTools to change how Zalo Web behaves, plus a variant that toggles hidden modes in the web client. These are modifications to the web application itself rather than a client library.

A third is a node package for n8n, the workflow automation tool, which suggests the same functionality delivered as a workflow node rather than as code you write yourself.

The differences are practical. The library approach gives you an event listener and typed methods but requires you to run a headless browser session and accept the account risk. The extension approach reuses a session you already have in a real browser, at the cost of only working from a browser tab and only for the data the extension chooses to export. The DevTools approach changes the client rather than automating it, so it survives as long as the web app keeps the hooks it relies on.

## Package shape, tooling and what a major version costs

The published package is version 2.2.0, and the build pipeline is a Bun and Rollup setup that emits both module formats:

```json
    "name": "zca-js",
    "version": "2.2.0",
    "type": "module",
    "exports": {
        ".": {
            "require": "./dist/cjs/index.cjs",
            "types": "./index.d.ts",
            "default": "./dist/index.js"
        }
    },
```

A prebuild step runs a script that builds the API surface, then TypeScript compiles the module build and Rollup produces the CommonJS bundle. The scripts section also carries a feature test runner, lint and format commands, and a husky prepare hook, so the repository treats releases as a pipeline rather than a manual step.

On releases, the tags are v2.2.0 from 2026-09-09, v2.1.2 from 2026-03-17 and v2.1.1 from 2026-02-24, and the repository is not archived with a last push on 2026-10-13, so the package is still being worked on. The licence is MIT, so the code can be reused and modified freely, though the licence covers the library and not the Zalo service it talks to.

The upgrade cost is where caution is warranted. Moving from version 1 to version 2 required writing an imageMetadataGetter, and the library depends on a website that can change without notice. Treat the dependency as unpinned in practice, and re-test the login and send paths whenever you bump the version.

## Conclusion

Adopt zca-js if you are building a personal automation on top of Zalo Web and can accept that an unofficial client may break without notice and may get the account locked. Do not adopt it for a business account or a multi account setup, and do not adopt it expecting a stable contract, since the package has already changed its image handling API between major versions. Verify first that a QR login works for your account and that you can supply your own imageMetadataGetter if you need to send images or gifs by file path, since version 2.0.0 removed the built in sharp dependency.

## FAQ

### How do I install zca-js and log in to a Zalo account?

Install the package with bun add zca-js or npm install zca-js, then create a Zalo instance and call loginQR, which returns an api object after you scan the QR code. Node 18.0.0 or newer is required.

### Why does zca-js need my own imageMetadataGetter?

Since version 2.0.0 the package no longer depends on sharp for image metadata extraction. If you send images or gifs by file path, you must pass an imageMetadataGetter function to the Zalo constructor that returns the height, width and size of the file.

### Can I run a zca-js listener while using Zalo in the browser?

No. The README states that only one web listener can run per account at a time, and that opening Zalo in the browser while the listener is active stops the listener automatically.

### Is it safe to use zca-js with a personal Zalo account?

The README carries an explicit warning that using this API could get your account locked or banned, that the maintainers are not responsible for issues that result, and that use is at your own risk. The library is unofficial and works by simulating the browser against Zalo Web.

## Sources

- [License: MIT](https://github.com/RFS-ADRENO/zca-js/blob/main/LICENSE)
- [Project website](https://zca-js.tdung.com)
- [README](https://github.com/RFS-ADRENO/zca-js/blob/main/README.md)
- [Releases](https://github.com/RFS-ADRENO/zca-js/releases)
- [RFS-ADRENO/zca-js on GitHub](https://github.com/RFS-ADRENO/zca-js)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rfs-adreno-zca-js
