PHPackages                             field-vn/zalo - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. field-vn/zalo

ActiveLibrary

field-vn/zalo
=============

Zalo OA &amp; Bot SDK cho Laravel — quản lý nhiều OA, tự refresh token, có UI cấu hình

v0.2.0(today)07↑2900%[1 PRs](https://github.com/field-vn/zalo/pulls)MITPHPPHP ^8.2CI failing

Since Aug 25Pushed todayCompare

[ Source](https://github.com/field-vn/zalo)[ Packagist](https://packagist.org/packages/field-vn/zalo)[ Docs](https://github.com/field-vn/zalo)[ RSS](/packages/field-vn-zalo/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (8)Versions (4)Used By (0)

Zalo
====

[](#zalo)

[![Tests](https://github.com/field-vn/zalo/actions/workflows/tests.yml/badge.svg)](https://github.com/field-vn/zalo/actions/workflows/tests.yml)[![Latest Version](https://camo.githubusercontent.com/763477463456a6a00363e7df75be7cdfeccb30660fb2ed206c3a32898a07c1d6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6669656c642d766e2f7a616c6f2e737667)](https://packagist.org/packages/field-vn/zalo)[![Downloads](https://camo.githubusercontent.com/ba799d937ce6498572e07523a976cc63c7e6c019ef743c92b98ddb15057f5e6e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6669656c642d766e2f7a616c6f2e737667)](https://packagist.org/packages/field-vn/zalo)[![License](https://camo.githubusercontent.com/26ca42d82329ff94e5bc29dc40d86db5e95eba0461f95cbed743092e2bba14f1/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6669656c642d766e2f7a616c6f2e737667)](LICENSE)

SDK Laravel cho **Zalo Official Account** và **Zalo Bot**: quản lý nhiều OA, tự refresh token, nhận webhook, kèm giao diện cấu hình.

Mục lục
-------

[](#mục-lục)

- [Yêu cầu](#y%C3%AAu-c%E1%BA%A7u)
- [Cài đặt](#c%C3%A0i-%C4%91%E1%BA%B7t)
- [Kết nối Official Account](#k%E1%BA%BFt-n%E1%BB%91i-official-account)
- [Kết nối Bot](#k%E1%BA%BFt-n%E1%BB%91i-bot)
- [Gửi tin nhắn](#g%E1%BB%ADi-tin-nh%E1%BA%AFn)
- [Nhận tin nhắn](#nh%E1%BA%ADn-tin-nh%E1%BA%AFn)
- [Chi phí và giới hạn](#chi-ph%C3%AD-v%C3%A0-gi%E1%BB%9Bi-h%E1%BA%A1n)
- [Cấu hình](#c%E1%BA%A5u-h%C3%ACnh)
- [Giao diện web](#giao-di%E1%BB%87n-web)
- [Commands](#commands)
- [Testing](#testing)
- [Mở rộng](#m%E1%BB%9F-r%E1%BB%99ng)

Yêu cầu
-------

[](#yêu-cầu)

- PHP 8.2+
- Laravel 10, 11, 12 hoặc 13

Cài đặt
-------

[](#cài-đặt)

```
composer require field-vn/zalo
```

Thêm credential của Zalo App vào `.env`:

```
ZALO_APP_ID=
ZALO_APP_SECRET=
```

Chạy trình cài đặt:

```
php artisan zalo:install
```

Lệnh này kiểm tra env, publish config, chạy migration và nhắc các bước còn thiếu.

### Xác thực domain

[](#xác-thực-domain)

Zalo chỉ chấp nhận URL thuộc domain đã xác thực. Làm bước này **trước**, nếu không webhook sẽ báo *"chưa được xác thực domain"* và OAuth trả `-14003 Invalid redirect uri`.

Vào Zalo Developers → App → Xác thực domain, tải file HTML được cấp, đặt vào thư mục `public/` của dự án, rồi bấm xác thực.

Kết nối Official Account
------------------------

[](#kết-nối-official-account)

```
php artisan zalo:oa:add
```

Nhập tên, slug và OA ID (lấy ở trang quản trị Zalo OA). Lệnh sẽ hỏi có cấp quyền luôn không.

Luồng cấp quyền in ra một link — mở bằng tài khoản **admin của OA** và bấm đồng ý. Nếu callback truy cập được từ Internet thì token tự lưu; nếu đang chạy localhost, copy giá trị `code` trên thanh địa chỉ rồi dán vào terminal.

Redirect URI phải khớp chính xác giá trị khai trong Zalo Developers, kể cả dấu `/` cuối. Dashboard `/zalo` hiển thị sẵn giá trị đúng kèm nút copy.

Kiểm tra kết nối:

```
php artisan zalo:oa:list
php artisan zalo:oa:test cskh
```

### Scheduler

[](#scheduler)

Thêm cron sau, nếu không token sẽ hết hạn:

```
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1
```

`refresh_token` của Zalo sống khoảng ba tháng và xoay vòng mỗi lần dùng. Package đăng ký sẵn `zalo:token:refresh --all` chạy hàng giờ.

Kết nối Bot
-----------

[](#kết-nối-bot)

Bot dùng token tĩnh, không cần OAuth và không dính Zalo App. Lấy token tại [bot.zaloplatforms.com](https://bot.zaloplatforms.com).

```
php artisan zalo:bot:add
```

Lệnh gọi `getMe` ngay để kiểm tra token. Nếu token sai, bản ghi vừa tạo sẽ bị xoá.

### Lấy `chat_id`

[](#lấy-chat_id)

Bot cần `chat_id` để gửi tin. Zalo không có API liệt kê `chat_id`, nên phải cắm webhook để package ghi lại khi có người nhắn tới.

```
# Chuỗi do bạn tự đặt, dài 8–256 ký tự
ZALO_BOT_WEBHOOK_SECRET=
```

```
php artisan zalo:bot:webhook support --set
# Mở Zalo, nhắn cho bot một câu
php artisan zalo:bot:chats support
php artisan zalo:bot:send support  "Xin chào"
```

Mọi người nhắn tới bot được lưu vào bảng `zl_bot_chats`. Có thể làm toàn bộ các bước trên bằng giao diện tại `/zalo/bots/{slug}`.

`getUpdates` và webhook loại trừ nhau — Zalo trả lỗi 400 nếu gọi `getUpdates` khi bot đang cắm webhook.

Gửi tin nhắn
------------

[](#gửi-tin-nhắn)

```
use FieldVn\Zalo\Laravel\Facades\Zalo;

Zalo::oa('cskh')->messages()->text($userId, 'Đơn hàng đã được xác nhận');
Zalo::bot('support')->text($chatId, 'Xin chào');
```

Hoặc dùng helper toàn cục:

```
zalo_oa('cskh')->messages()->text($userId, 'Xin chào');
zalo_bot('support')->text($chatId, 'Xin chào');
```

### Official Account

[](#official-account)

**Tin có nút bấm**

```
use FieldVn\Zalo\Core\Channels\OA\Messages\Button;
use FieldVn\Zalo\Core\Channels\OA\Messages\TextMessage;

Zalo::oa('cskh')->messages()->send(
    TextMessage::to($userId)
        ->text('Đơn hàng #1234 đang giao')
        ->button(Button::url('Theo dõi', 'https://shop.vn/don/1234'))
        ->button(Button::phone('Gọi shipper', '0900000000'))
);
```

Message object là immutable — mỗi lần gọi trả về bản sao mới, nên dựng sẵn tin mẫu rồi tuỳ biến cho từng người nhận là an toàn.

**Gửi ảnh**

OA yêu cầu upload ảnh trước để lấy `attachment_id`:

```
$id = Zalo::oa('cskh')->uploads()->image('/duong/dan/anh.jpg');

Zalo::oa('cskh')->messages()->image($userId, $id, 'Ảnh sản phẩm');
```

`attachment_id` dùng lại được, nên gửi cùng một ảnh cho nhiều người chỉ cần upload một lần. Package kiểm tra file tồn tại, dung lượng dưới 1 MB và đúng định dạng trước khi gọi API.

**Payload tuỳ ý**

Với những dạng tin package chưa bọc thành class (list, carousel, `request_user_info`, file):

```
use FieldVn\Zalo\Core\Channels\OA\Messages\RawMessage;

Zalo::oa('cskh')->messages()->send(
    RawMessage::to($userId)->message([
        'attachment' => [
            'type' => 'template',
            'payload' => ['template_type' => 'list', 'elements' => [/* … */]],
        ],
    ])
);
```

Gọi thẳng endpoint chưa được bọc:

```
Zalo::oa('cskh')->request()->get('/v3.0/oa/duong-dan-moi', ['param' => 'x']);
```

### Bot

[](#bot)

```
$bot = Zalo::bot('support');

$bot->text($chatId, 'Xin chào');
$bot->photo($chatId, 'https://…/anh.png', 'Chú thích');
$bot->sticker($chatId, $stickerId);
$bot->typing($chatId);
```

Bot nhận thẳng URL ảnh, không cần upload trước như OA.

### Nhiều OA

[](#nhiều-oa)

```
Zalo::oa();                  // OA active đầu tiên
Zalo::oa('marketing');       // theo slug
Zalo::availableOas();        // Collection, dùng cho dropdown

Zalo::oas(fn ($oa) => in_array('cskh', $oa->tags ?? []))
    ->each(fn ($channel) => $channel->messages()->text($userId, $noiDung));
```

Nhận tin nhắn
-------------

[](#nhận-tin-nhắn)

OA và Bot dùng hai URL và hai cơ chế xác thực khác nhau:

KênhURLXác thựcOA`/zalo/webhook`chữ ký `X-ZEvent-Signature`Bot`/zalo/webhook/bot/{slug}`secret ở header `X-Bot-Api-Secret-Token`Mỗi bot có URL riêng vì payload Zalo gửi không kèm định danh bot.

```
ZALO_WEBHOOK_SECRET=          # OA Secret Key, lấy trong cài đặt webhook của App
ZALO_BOT_WEBHOOK_SECRET=      # chuỗi bạn tự đặt cho bot, 8–256 ký tự
```

`ZALO_WEBHOOK_SECRET` khác `ZALO_APP_SECRET`. Nó là *OA Secret Key* nằm ở phần cài đặt webhook, không phải secret của ứng dụng.

### Lắng nghe event

[](#lắng-nghe-event)

```
use FieldVn\Zalo\Laravel\Events\ZaloMessageReceived;

class TraLoiOa
{
    public function handle(ZaloMessageReceived $e): void
    {
        if ($e->text === null || $e->oa === null) {
            return;
        }

        zalo_oa($e->oa->slug)->messages()->text($e->userId, "Bạn vừa nói: {$e->text}");
    }
}
```

```
use FieldVn\Zalo\Laravel\Events\ZaloBotMessageReceived;

class TraLoiBot
{
    public function handle(ZaloBotMessageReceived $e): void
    {
        zalo_bot($e->bot->slug)->text($e->chatId, "Đã nhận: {$e->text}");
    }
}
```

EventKhi nào`ZaloWebhookReceived`Mọi sự kiện của OA, kèm payload gốc`ZaloMessageReceived`Người dùng gửi tin nhắn tới OA`ZaloFollowerAdded`Người dùng quan tâm OA`ZaloFollowerRemoved`Người dùng bỏ quan tâm`ZaloOaConnected`OA vừa được cấp quyền`ZaloOaDisconnected`OA mất kết nối, cần cấp quyền lại`ZaloBotUpdateReceived`Mọi update của Bot, kèm payload gốc`ZaloBotMessageReceived`Người dùng nhắn cho Bot`ZaloWebhookReceived` và `ZaloBotUpdateReceived` được bắn cho mọi loại sự kiện, kể cả loại package chưa bọc riêng.

### Hành vi cần biết

[](#hành-vi-cần-biết)

- Route webhook không đi qua auth của giao diện. Chữ ký (OA) hoặc secret header (Bot) là lớp bảo vệ duy nhất.
- Chưa cấu hình secret thì webhook bị từ chối 401.
- Webhook của Bot yêu cầu HTTPS vì secret đi nguyên văn trong header.
- Mặc định xử lý qua queue (`ZALO_WEBHOOK_QUEUE=true`).
- Lỗi trong listener của bạn không làm webhook trả 500. Nếu trả 500, Zalo sẽ gửi lại và bạn xử lý trùng.
- Chống trùng nên dựa vào `$e->messageId`.

Bật `ZALO_WEBHOOK_LOG=true` để ghi payload vào `zl_webhook_logs` khi cần debug. Mặc định tắt vì payload chứa nội dung tin nhắn của người dùng.

Package không tự gửi cảnh báo khi OA mất kết nối. Lắng nghe `ZaloOaDisconnected` để tự xử lý.

Chi phí và giới hạn
-------------------

[](#chi-phí-và-giới-hạn)

### Tin Tư vấn (OA)

[](#tin-tư-vấn-oa)

Tính từ tương tác cuối của người dùng:

Khoảng thời gianQua OpenAPITrong 48 giờGửi được, miễn phí48 giờ đến 7 ngàyGửi được, Zalo tính phíSau 7 ngàyBị từ chốiPackage không tự chặn khi quá 48 giờ vì nó không biết thời điểm tương tác cuối. Nếu cần kiểm soát chi phí, hãy tự lưu mốc tương tác từ webhook.

"Tương tác" gồm: gửi tin nhắn tới OA, gửi tin trong nhóm GMF, gọi thoại tới OA, đồng ý nhận cuộc gọi, bình luận bài viết, tương tác chatbot, bấm Menu hoặc CTA, bấm widget.

### Khả năng của từng kênh

[](#khả-năng-của-từng-kênh)

BotOAText✅✅Ảnh✅ (URL)✅ (upload trước)Sticker✅❌Trạng thái đang soạn tin✅❌Nút bấm❌✅List, carousel❌✅Giới hạn thời gianKhôngCó, xem bảng trên### ZBS Template Message

[](#zbs-template-message)

Từ 01/01/2026 Zalo hợp nhất ZNS, tin UID Giao dịch và tin UID Truyền thông thành **ZBS Template Message**, gửi qua UID hoặc số điện thoại theo template đã duyệt.

Package chưa hỗ trợ. Hai method `transaction()` và `promotion()` trỏ tới endpoint có trước thời điểm hợp nhất.

Cấu hình
--------

[](#cấu-hình)

### Zalo App

[](#zalo-app)

```
ZALO_APP_ID=
ZALO_APP_SECRET=
ZALO_APP_REDIRECT=            # để trống thì tự suy ra từ ZALO_UI_PATH
```

App credential chỉ đọc từ env, không lưu vào DB hay sửa qua giao diện.

### Prefix bảng

[](#prefix-bảng)

```
ZALO_TABLE_PREFIX=zl_
```

Chốt giá trị này **trước lần migrate đầu tiên**. Đổi sau khi đã migrate sẽ khiến code tìm bảng theo tên mới trong khi DB giữ tên cũ.

Prefix cộng dồn với prefix của DB connection: `DB_PREFIX=app_` cộng `zl_` cho ra bảng `app_zl_oas`.

Package tạo 6 bảng: `oas`, `oa_tokens`, `bots`, `bot_chats`, `audit_logs`, `webhook_logs`.

### Toàn bộ biến env

[](#toàn-bộ-biến-env)

```
# Zalo App
ZALO_APP_ID=
ZALO_APP_SECRET=
ZALO_APP_KEY=default
ZALO_APP_REDIRECT=

# Webhook
ZALO_WEBHOOK_ENABLED=true
ZALO_WEBHOOK_PATH=zalo/webhook
ZALO_WEBHOOK_SECRET=
ZALO_WEBHOOK_QUEUE=true
ZALO_WEBHOOK_QUEUE_NAME=
ZALO_WEBHOOK_TOLERANCE=300
ZALO_WEBHOOK_LOG=false

# Bot
ZALO_BOT_WEBHOOK_SECRET=

# Giao diện
ZALO_UI_ENABLED=true
ZALO_UI_PATH=zalo
ZALO_UI_USER=admin
ZALO_UI_PASSWORD=
ZALO_UI_ALLOWED_IPS=

# Khác
ZALO_TABLE_PREFIX=zl_
ZALO_SCHEDULER=true
ZALO_HTTP_TIMEOUT=10
ZALO_HTTP_CONNECT_TIMEOUT=5
ZALO_HTTP_RETRY=3
```

### `APP_KEY`

[](#app_key)

Token lưu trong DB được mã hoá bằng `APP_KEY`. Đổi `APP_KEY` sẽ làm mất toàn bộ token và phải cấp quyền lại cho mọi OA.

Giao diện web
-------------

[](#giao-diện-web)

Truy cập `https://your-app.com/zalo`.

- **Tổng quan** — sức khoẻ token, Redirect URI và Webhook URL kèm nút copy
- **Official Account** — danh sách; mỗi OA có trang riêng để sửa, cấp quyền, gửi tin thử
- **Bot** — danh sách; mỗi bot có trang riêng để sửa, cắm webhook, xem `chat_id`, gửi tin thử

Giao diện không cần build step và không cần `vendor:publish`. Muốn tuỳ biến thì chạy `php artisan vendor:publish --tag=zalo-views`.

```
ZALO_UI_ENABLED=true
ZALO_UI_PATH=zalo
ZALO_UI_USER=admin
ZALO_UI_PASSWORD=             # để trống thì giao diện chỉ chạy ở môi trường local
ZALO_UI_ALLOWED_IPS=          # ví dụ: 113.161.0.0/16,203.0.113.5
```

Basic Auth gửi credential ở mọi request nên site phải chạy HTTPS.

Nếu dự án đã có hệ thống auth riêng, định nghĩa gate — nó được ưu tiên hơn basic auth:

```
// AppServiceProvider::boot()
Zalo::auth(fn ($request) => $request->user()?->is_admin === true);
```

Token và secret không hiển thị đầy đủ trên giao diện.

Commands
--------

[](#commands)

CommandMô tả`zalo`Trạng thái OA, Bot và sức khoẻ token`zalo:install`Cài đặt: kiểm env, publish config, migrate`zalo:doctor`Chẩn đoán cấu hình kèm hướng dẫn sửa`zalo:oa:add`Thêm OA`zalo:oa:list`Liệt kê OA và trạng thái token`zalo:oa:test {oa}`Gọi thử API để xác nhận kết nối`zalo:authorize {oa}`Cấp quyền và lấy token lần đầu`zalo:token:refresh``{oa?}` · `--all` · `--force``zalo:bot:add`Thêm Bot, tự kiểm tra token`zalo:bot:list`Liệt kê Bot`zalo:bot:test {bot}`Kiểm tra token bot`zalo:bot:webhook {bot}`Xem · `--set` · `--delete` · `--url=``zalo:bot:chats {bot?}`Liệt kê `chat_id` đã ghi nhận`zalo:bot:send {bot} {chat} {text?}`Gửi tin · `--photo=` · `--sticker=`Gặp vấn đề thì chạy `zalo:doctor` trước — lệnh này kiểm credential, redirect URI, bảng, mã hoá, giao diện, scheduler, từng OA và từng Bot.

Testing
-------

[](#testing)

`Zalo::fake()` chặn mọi lời gọi tới Zalo và cho phép assert những gì đã gửi:

```
use FieldVn\Zalo\Laravel\Facades\Zalo;

it('gửi xác nhận khi đặt hàng', function () {
    Zalo::fake();

    $this->post('/don-hang', ['san_pham' => 1]);

    Zalo::assertSentTo('user-1', 'Đơn hàng đã được xác nhận');
});
```

Không cần OA trong DB, không cần token, không cần giả lập OAuth.

AssertionMô tả`assertSentTo($userId, $text?)`Đã gửi tin nhắn tới người này`assertNotSentTo($userId)`Chưa gửi cho người này`assertSentVia($slug)`Đã gửi qua đúng OA đó`assertSent($callback)`Điều kiện tuỳ ý`assertNotSent($callback)``assertNothingSent()``assertSentCount($n)``sent()`Collection các request đã ghiĐặt response giả:

```
Zalo::fake()->push(['error' => -216, 'message' => 'Token hết hạn']);
```

`fake()` chỉ thay tầng mạng. Message builder, resource và phần validate payload vẫn chạy code thật, nên tin nhắn dựng sai (quá 2000 ký tự, nút không phải HTTPS) vẫn bị bắt trong test.

Mở rộng
-------

[](#mở-rộng)

Thay thành phần mà không cần fork:

```
// Đổi tầng HTTP
$this->app->bind(FieldVn\Zalo\Contracts\Transport::class, MyTransport::class);

// Đổi nguồn danh sách OA (multi-tenant, config thuần, API nội bộ…)
$this->app->bind(FieldVn\Zalo\Contracts\OaRepository::class, TenantOaRepository::class);
```

`OAChannel`, `BotChannel` và các `Resource` đều dùng `Macroable`.

Phát triển package
------------------

[](#phát-triển-package)

```
composer test
composer analyse
composer format
```

Đóng góp
--------

[](#đóng-góp)

Xem [CONTRIBUTING.md](CONTRIBUTING.md) và [CODE\_OF\_CONDUCT.md](CODE_OF_CONDUCT.md).

Bảo mật
-------

[](#bảo-mật)

Phát hiện lỗ hổng? Xem [SECURITY.md](SECURITY.md) — vui lòng không mở public issue.

License
-------

[](#license)

MIT — xem [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/11310549?v=4)[Trieu Nguyen](/maintainers/trieunguyen1988)[@trieunguyen1988](https://github.com/trieunguyen1988)

---

Top Contributors

[![trieunguyen1988](https://avatars.githubusercontent.com/u/11310549?v=4)](https://github.com/trieunguyen1988 "trieunguyen1988 (34 commits)")

---

Tags

phplaravelchatbotzalovietnamzalo-botzalo-oa

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/field-vn-zalo/health.svg)

```
[![Health](https://phpackages.com/badges/field-vn-zalo/health.svg)](https://phpackages.com/packages/field-vn-zalo)
```

###  Alternatives

[spatie/laravel-health

Monitor the health of a Laravel application

88412.7M190](/packages/spatie-laravel-health)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M341](/packages/laravel-ai)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9922.4M147](/packages/roots-acorn)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

265.2k](/packages/aedart-athenaeum)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[forjedio/inertia-table

Backend-driven dynamic tables for Laravel + Inertia.js

272.0k](/packages/forjedio-inertia-table)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
