# Chiến dịch email

Chiến dịch email (broadcast) là email marketing, như bản tin, chương trình giảm giá hay thông báo sản phẩm mới, gửi tới mọi contact của một segment. Hạ Tầng tách chiến dịch thành mỗi contact một email, điền tên và thuộc tính của từng người, bỏ qua người đã huỷ đăng ký, rồi thêm link huỷ đăng ký và header `List-Unsubscribe` một chạm.

## Trước khi bắt đầu

- Một tên miền đã xác minh cho địa chỉ `From`. Xem [Tên miền](https://hatang.dev/vi/docs/guides/email/domains.md).
- Một segment có contact, và các topic nếu muốn người nhận tự chọn loại thư. Xem [Contacts, segments và topics](https://hatang.dev/vi/docs/guides/email/audiences.md).
- Nếu dùng API, key có `email:write`.

## Chiến dịch chọn người nhận thế nào

Một contact nhận chiến dịch khi thoả cả bốn điều kiện:

1. Contact thuộc segment của chiến dịch.
2. Contact chưa huỷ đăng ký mọi email.
3. Nếu chiến dịch có topic: contact đang nhận topic đó. Với topic `opt_in`, nghĩa là contact chưa rời topic; với topic `opt_out`, contact phải đã tự đăng ký.
4. Địa chỉ không nằm trong danh sách chặn (trả lại cứng, báo spam, chặn thủ công). Địa chỉ bị chặn được bỏ qua và báo là bị trả lại. Xem [Danh sách chặn và thư trả lại](https://hatang.dev/vi/docs/guides/email/suppressions-and-bounces.md).

## Soạn

Trong dashboard, mở **Email → Chiến dịch email → Chiến dịch email mới**:

- **Segment**: ai nhận. **Topic**: không bắt buộc; **Không gắn topic (mọi người đang nhận)** gửi cho mọi contact đang nhận trong segment.
- **Tên nội bộ**, **Người gửi** và **Tiêu đề**.
- **Bắt đầu từ template** chép HTML, văn bản, người gửi và tiêu đề của một template đã publish vào chiến dịch.
- Nội dung HTML và bản **Văn bản thuần (không bắt buộc)**, kèm **Xem trước**.

Lưu bằng **Tạo nháp**. Chiến dịch sửa được khi còn là bản nháp hoặc đang hẹn giờ.

### Thẻ trộn

Thẻ trộn được điền cho từng contact:

| Thẻ | Giá trị |
|---|---|
| `{{{FIRST_NAME}}}`, `{{{LAST_NAME}}}`, `{{{EMAIL}}}` | Tên và địa chỉ của contact. |
| `{{{contact.first_name}}}`, `{{{contact.plan}}}`… | Trường của contact và thuộc tính tuỳ chỉnh. |
| `{{{UNSUBSCRIBE_URL}}}` | Trang tuỳ chọn của contact. `{{{RESEND_UNSUBSCRIBE_URL}}}` cũng dùng được. |

Thêm giá trị mặc định sau dấu `|`: `Chào {{{FIRST_NAME|bạn}}}`. Thuộc tính chưa có giá trị thì lấy giá trị dự phòng. Thẻ không có giá trị nào sẽ thành chuỗi rỗng; không làm dừng việc gửi. Giá trị được escape trong phần HTML.

```html
<p>Chào {{{FIRST_NAME|bạn}}},</p>
<p>Gói {{{contact.plan|free}}} của bạn giờ có thêm 10 GB lưu trữ.</p>
<p><a href="{{{UNSUBSCRIBE_URL}}}">Huỷ đăng ký hoặc chọn loại email muốn nhận</a></p>
```

### Tạo bằng API

**cURL**

```bash
curl -X POST https://api.hatang.dev/broadcasts \
  -H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Bản tin tháng 10",
    "segment_id": "aud_0199e6c17a2b7f009c1e3f6a2b1d9e10",
    "topic_id": "topic_0199e6c17a2c7c418f0e61f1b2a3c4d5",
    "from": "Shop <news@updates.shop.vn>",
    "subject": "Có gì mới tháng 10, {{{FIRST_NAME|bạn}}}",
    "html": "<p>Chào {{{FIRST_NAME|bạn}}}, …</p><p><a href=\"{{{UNSUBSCRIBE_URL}}}\">Huỷ đăng ký</a></p>"
  }'
# { "object": "broadcast", "id": "bc_…" }
```

**Node.js**

```ts
const { data, error } = await hatang.broadcasts.create({
  name: "Bản tin tháng 10",
  segment_id: "aud_0199e6c17a2b7f009c1e3f6a2b1d9e10",
  topic_id: "topic_0199e6c17a2c7c418f0e61f1b2a3c4d5",
  from: "Shop <news@updates.shop.vn>",
  subject: "Có gì mới tháng 10, {{{FIRST_NAME|bạn}}}",
  html: '<p>Chào {{{FIRST_NAME|bạn}}}, …</p><p><a href="{{{UNSUBSCRIBE_URL}}}">Huỷ đăng ký</a></p>',
});
```

**Python**

```python
res = requests.post(
    "https://api.hatang.dev/broadcasts",
    headers={"Authorization": f"Bearer {os.environ['HATANG_API_KEY']}"},
    json={
        "name": "Bản tin tháng 10",
        "segment_id": "aud_0199e6c17a2b7f009c1e3f6a2b1d9e10",
        "from": "Shop <news@updates.shop.vn>",
        "subject": "Có gì mới tháng 10",
        "html": '<p>Chào {{{FIRST_NAME|bạn}}}, …</p><p><a href="{{{UNSUBSCRIBE_URL}}}">Huỷ đăng ký</a></p>',
    },
)
broadcast_id = res.json()["id"]
```

**PHP**

```php
$res = $client->post('/broadcasts', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
    'json' => [
        'name' => 'Bản tin tháng 10',
        'segment_id' => 'aud_0199e6c17a2b7f009c1e3f6a2b1d9e10',
        'from' => 'Shop <news@updates.shop.vn>',
        'subject' => 'Có gì mới tháng 10',
        'html' => '<p>Chào {{{FIRST_NAME|bạn}}}, …</p><p><a href="{{{UNSUBSCRIBE_URL}}}">Huỷ đăng ký</a></p>',
    ],
]);
$broadcastId = json_decode($res->getBody(), true)['id'];
```

| Trường | Mô tả |
|---|---|
| `segment_id` | Bắt buộc. Segment nhận chiến dịch. `audience_id` là tên cũ, vẫn dùng được. |
| `topic_id` | Không bắt buộc. Chỉ gửi cho contact đang nhận topic này. |
| `template_id` | Không bắt buộc. Chép `html`, `text`, `from`, `subject` và `reply_to` từ template đã publish này. Trường bạn tự gửi được ưu tiên. |
| `from` | Bắt buộc, trừ khi template đã có. |
| `subject` | Bắt buộc, trừ khi template đã có. Tối đa 998 ký tự. |
| `reply_to` | Một địa chỉ hoặc một danh sách. |
| `html`, `text` | Nội dung. Phải có ít nhất một trong hai trước khi gửi. |
| `name` | Tên nội bộ, tối đa 200 ký tự. Mặc định là tiêu đề. |

Template được chép lúc tạo chiến dịch; publish phiên bản template mới sau đó không làm thay đổi chiến dịch.

## Gửi hoặc hẹn giờ

Trong trình soạn, ô **Gửi** nhận `now`, một thời điểm ISO hoặc một cụm như `in 2 hours`. Lưu thay đổi trước khi bấm **Gửi chiến dịch**. Bằng API:

**cURL**

```bash
# Gửi ngay
curl -X POST https://api.hatang.dev/broadcasts/$BROADCAST_ID/send -H "Authorization: Bearer $HATANG_API_KEY"

# Hẹn giờ
curl -X POST https://api.hatang.dev/broadcasts/$BROADCAST_ID/send \
  -H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
  -d '{ "scheduled_at": "2026-10-20T09:00:00+07:00" }'
# { "object": "broadcast", "id": "bc_…", "status": "scheduled" }
```

**Node.js**

```ts
await hatang.broadcasts.send(broadcastId); // gửi ngay
await hatang.broadcasts.send(broadcastId, { scheduledAt: "in 2 hours" }); // hẹn giờ
```

**Python**

```python
headers = {"Authorization": f"Bearer {os.environ['HATANG_API_KEY']}"}
requests.post(f"https://api.hatang.dev/broadcasts/{broadcast_id}/send", headers=headers, json={"scheduled_at": "in 2 hours"})
```

**PHP**

```php
$client->post("/broadcasts/$broadcastId/send", [
    'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
    'json' => ['scheduled_at' => 'in 2 hours'],
]);
```

`scheduled_at` nhận các giá trị giống email đơn lẻ: thời điểm ISO 8601 trong vòng 30 ngày tới, hoặc `in N minutes|hours|days`. Gửi lại một chiến dịch đang hẹn giờ sẽ thay lịch cũ.

`POST /broadcasts/{id}/cancel` dừng chiến dịch đang hẹn giờ hoặc còn trong hàng đợi. Chiến dịch đang gửi thì không huỷ hay xoá được.

| Trạng thái | Ý nghĩa |
|---|---|
| `draft` | Chưa gửi. Sửa được. |
| `scheduled` | Đang chờ tới giờ. Vẫn sửa hoặc huỷ được. |
| `queued` | Sắp bắt đầu. |
| `sending` | Đang tạo email cho từng contact. |
| `sent` | Đã xử lý xong mọi người nhận. |
| `canceled` | Đã huỷ trước khi bắt đầu. |

`GET /broadcasts/{id}` cho biết `total` (số contact được chọn) và `sent_count` (số email đã tạo). Mỗi email của chiến dịch là một email bình thường có tag `broadcast_id`, nên bạn theo dõi được ở **Email → Email** (lọc **Chiến dịch email**), qua `GET /emails?source=broadcast` và các webhook quen thuộc. Nếu việc gửi bị gián đoạn, Hạ Tầng tiếp tục từ chỗ dừng và không bao giờ gửi cùng một chiến dịch hai lần cho một contact.

> **Caution: Hạn mức**
>
> Mỗi email của chiến dịch đều tính vào hạn mức ngày. Chiến dịch gửi cho nhiều contact hơn hạn mức còn lại sẽ dừng gửi khi hết hạn mức; các contact còn lại được đánh dấu lỗi. Xem **Cài đặt email** trước khi gửi số lượng lớn.

## Huỷ đăng ký {#unsubscribe}

Mỗi email của chiến dịch có hai lối thoát:

- Link `{{{UNSUBSCRIBE_URL}}}` trong nội dung mở trang tuỳ chọn, nơi contact chọn topic hoặc huỷ đăng ký mọi email. Chỉ mở trang thì không thay đổi gì.
- Header `List-Unsubscribe` cùng `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058), để Gmail, Apple Mail và các ứng dụng khác hiện nút huỷ đăng ký của riêng chúng. Một chạm sẽ gỡ contact khỏi topic của chiến dịch, hoặc khỏi mọi email nếu chiến dịch không gắn topic.

Hạ Tầng tự thêm header. Hãy đặt cả link trong nội dung: một số ứng dụng email không hiện nút, và pháp luật nhiều nước yêu cầu cách huỷ đăng ký phải nhìn thấy được.

Khi contact huỷ đăng ký mọi email, họ không còn nhận chiến dịch email và email của automation, đồng thời địa chỉ được đưa vào danh sách chặn với lý do `unsubscribe`, chặn luôn các email khác gửi tới địa chỉ đó. Xem [Danh sách chặn và thư trả lại](https://hatang.dev/vi/docs/guides/email/suppressions-and-bounces.md).

Đổi giao diện trang tuỳ chọn ở **Cài đặt email → Trang huỷ đăng ký**: tiêu đề, mô tả, logo và màu.

## Tham chiếu API

| Endpoint | Mô tả |
|---|---|
| `POST /broadcasts` | Tạo bản nháp. Trả về `201` kèm id. |
| `GET /broadcasts`, `GET /broadcasts/{id}` | Liệt kê, hoặc xem một chiến dịch kèm `status`, `total`, `sent_count`, `scheduled_at` và `sent_at`. |
| `PATCH /broadcasts/{id}` | Sửa chiến dịch nháp hoặc đang hẹn giờ. |
| `POST /broadcasts/{id}/send` | Gửi ngay hoặc hẹn giờ (`scheduled_at`). |
| `POST /broadcasts/{id}/cancel` | Huỷ chiến dịch đang hẹn giờ hoặc trong hàng đợi. |
| `DELETE /broadcasts/{id}` | Xoá, trừ khi đang gửi. |

## Bước tiếp theo

- [Contacts, segments và topics](https://hatang.dev/vi/docs/guides/email/audiences.md)
- [Templates](https://hatang.dev/vi/docs/guides/email/templates.md)
- [Khả năng vào hộp thư](https://hatang.dev/vi/docs/guides/email/deliverability.md)
- [Số liệu và logs](https://hatang.dev/vi/docs/guides/email/metrics-and-logs.md)

Source: https://hatang.dev/vi/docs/guides/email/broadcasts
