DEV Community

KazKN
KazKN

Posted on

How to Run a Zalo Data Export from Python with the Apify API

A useful Zalo export should end as records your code can process. For a group
roster, that means separate group, member, and profile rows with stable IDs. For
a chat backup, it means conversation summaries and message rows with explicit
limits.

This guide runs a bounded export with Python, retrieves the Apify Dataset, and
splits the result by recordType.

Disclosure: I built the Zalo Data Exporter used below. I may earn revenue
from Apify when the Actor is run through links in this article, at no extra
cost to you.

The first connection is interactive

The Actor connects to your own Zalo account. It does not scrape anonymous or
hidden account data.

Your first run for an account alias requires a temporary QR:

  1. Start a small run in the Apify Console.
  2. Open Output and select First run: scan this Zalo QR.
  3. Scan and approve the QR in the Zalo mobile app.
  4. Wait for the Dataset to appear.

The QR expires after roughly 100 seconds. Once the connection succeeds, later
runs under the same Apify user and accountAlias can reuse the encrypted saved
login while it remains valid.

Open the Zalo Data Exporter

Install the Python client

Create a virtual environment if your project does not already have one, then
install the client:

pip install apify-client
Enter fullscreen mode Exit fullscreen mode

Load the Apify token from an environment variable. Do not put it in the script
or paste it into a public prompt.

export APIFY_API_TOKEN="your-token-from-apify-console"
Enter fullscreen mode Exit fullscreen mode

Run a bounded group roster export

The starter input asks for one joined group and at most 100 visible members.
Those limits keep the first automated call easy to inspect.

import os

from apify_client import ApifyClient


client = ApifyClient(os.environ["APIFY_API_TOKEN"])

run_input = {
    "exportMode": "groupRoster",
    "accountAlias": "main",
    "maxGroups": 1,
    "maxMembersPerGroup": 100,
}

run = client.actor("kazkn/zalo-member-profile-exporter").call(
    run_input=run_input,
)

items = client.dataset(run["defaultDatasetId"]).list_items().items
print(f"Received {len(items)} records")
Enter fullscreen mode Exit fullscreen mode

The Actor writes a normalized Dataset. A single run can contain several record
families, so keep recordType when you store or transform the data.

from collections import Counter


counts = Counter(item.get("recordType") for item in items)
for record_type, count in sorted(counts.items()):
    print(record_type, count)
Enter fullscreen mode Exit fullscreen mode

A group roster can return group, groupMember, and profile records. A
conversation backup can return conversation and message records.

Select the records your job needs

Filtering locally is straightforward:

members = [
    item for item in items
    if item.get("recordType") == "groupMember"
]

profiles = {
    item["userId"]: item
    for item in items
    if item.get("recordType") == "profile" and item.get("userId")
}

member_rows = []
for member in members:
    profile = profiles.get(member.get("userId"), {})
    member_rows.append({
        "groupId": member.get("groupId"),
        "userId": member.get("userId"),
        "displayName": member.get("displayName"),
        "zaloName": profile.get("zaloName"),
        "avatarUrl": profile.get("avatarUrl"),
    })
Enter fullscreen mode Exit fullscreen mode

Profile fields are nullable because Zalo does not expose every field for every
visible member. Keep the member row even when its linked profile is incomplete.

Switch to another export preset

The same API call supports four outcomes:

Job exportMode Main record types
Joined-group roster groupRoster group, groupMember, profile
Visible contacts contactsAndProfiles profile
Accessible chat backup conversationBackup conversation, message
Visible-account snapshot completeSnapshot All five record types

A bounded conversation export looks like this:

run_input = {
    "exportMode": "conversationBackup",
    "accountAlias": "main",
    "messagesSince": "2026-07-01",
    "maxConversations": 3,
    "maxMessagesPerConversation": 200,
    "includeMessageText": True,
    "includeAttachmentMetadata": True,
}
Enter fullscreen mode Exit fullscreen mode

Conversation records report whether the read reached the start returned by
Zalo Web, stopped at your limit, or hit another bounded platform condition. Do
not label a partial conversation as a complete archive.

Handle reconnects without breaking the workflow

If the saved login expires, the run can ask for another QR. Set
reconnectZalo to true for one refresh run and approve the new QR in the
Console. Keep the same alias when refreshing the same Zalo account.

Use a different alias for a different Zalo account:

run_input["accountAlias"] = "client-vietnam-02"
Enter fullscreen mode Exit fullscreen mode

The first call for that alias will require its own QR approval.

Production checklist

  • Start with explicit record limits.
  • Keep recordType, stable IDs, and completeness fields.
  • Store the Apify token in an environment variable.
  • Expect a manual QR step for a new or expired saved login.
  • Use one account alias per connected Zalo account.
  • Retry only after reading the public error code and run status.

The API automates the repeatable part of the workflow. Authentication remains
attached to the account owner, and the Dataset only contains data visible to
that connected account.

👉 Run a bounded Zalo export from Python

Top comments (0)