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:
- Start a small run in the Apify Console.
- Open
Outputand selectFirst run: scan this Zalo QR. - Scan and approve the QR in the Zalo mobile app.
- 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.
Install the Python client
Create a virtual environment if your project does not already have one, then
install the client:
pip install apify-client
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"
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")
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)
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"),
})
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,
}
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"
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.
Top comments (0)