Chuyển tới nội dung chính

Gửi email

POST /emails gửi một email tới tối đa 50 người nhận. POST /emails/batch gửi tối đa 100 email khác nhau trong một lần gọi. Trang này đi qua từng trường, response, hẹn giờ, idempotency, các giới hạn, và cách theo dõi một email qua các trạng thái và sự kiện.

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

  • Một tên miền đã xác minh. Xem Tên miền.
  • API key có phạm vi email:send. Xem Xác thực.
  • Request gửi tới https://api.hatang.dev với header Authorization: Bearer ht_... và Content-Type: application/json.

Gửi một email​

curl -X POST https://api.hatang.dev/emails \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1024-confirmation" \
-d '{
"from": "Shop <[email protected]>",
"to": ["[email protected]"],
"cc": ["[email protected]"],
"reply_to": "[email protected]",
"subject": "Đơn #1024 đã được xác nhận",
"html": "<p>Chào Lan, đơn <strong>#1024</strong> của bạn đã được xác nhận.</p>",
"text": "Chào Lan, đơn #1024 của bạn đã được xác nhận.",
"tags": [{ "name": "category", "value": "order_confirmation" }],
"headers": { "X-Order-Id": "1024" }
}'

Response là 200 kèm id của email mới (một UUID):

{ "id": "0199e6c1-7a2b-7f00-9c1e-3f6a2b1d9e10" }

200 nghĩa là Hạ Tầng đã nhận email và đưa vào hàng đợi. Việc giao thư diễn ra sau đó; theo dõi bằng trạng thái và sự kiện.

Các trường của request​

TrườngKiểuMô tả
fromstringNgười gửi, [email protected] hoặc Shop <[email protected]>. Tên miền phải đã xác minh. Chỉ được bỏ trống khi template đã có sẵn.
tostring hoặc mảngNgười nhận.
ccstring hoặc mảngNgười nhận đồng gửi.
bccstring hoặc mảngNgười nhận ẩn. Không bao giờ xuất hiện trong header thư.
reply_tostring hoặc mảngĐịa chỉ Reply-To, tối đa 50.
subjectstringTối đa 998 ký tự. Bắt buộc, trừ khi template đã có sẵn.
htmlstringNội dung HTML, tối đa 10.000.000 ký tự.
textstringNội dung văn bản thuần, tối đa 10.000.000 ký tự.
templateobject{ "id": "tpl_… hoặc alias", "variables": { … } } gửi một template đã publish thay cho html/text. Xem Templates.
headersobjectHeader thêm, ví dụ { "X-Order-Id": "1024" }.
tagsmảngTối đa 50 cặp { "name", "value" }, được gửi kèm trong webhook.
attachmentsmảngTối đa 50 tệp. Xem Tệp đính kèm.
scheduled_atstringGửi sau. Xem Hẹn giờ gửi.
topic_idstringBỏ qua người nhận đã rời topic này. Xem Contacts, segments và topics.

Phải có html, text hoặc template. Request có template thì không được có thêm html hay text.

Gửi cả HTML lẫn văn bản thuần

Hạ Tầng chỉ tự tạo phần văn bản thuần từ HTML khi gửi bằng template. Khi gửi trực tiếp, hãy truyền cả html và text: một số ứng dụng email và bộ lọc spam có xem phần văn bản.

Người nhận​

  • Địa chỉ viết dạng thường ([email protected]) hoặc kèm tên hiển thị (Lan <[email protected]>).
  • to, cc và bcc cộng lại tối đa 50 địa chỉ. Muốn gửi cho nhiều người hơn, gửi mỗi người một email (bằng /emails/batch) hoặc dùng chiến dịch email.
  • Mỗi người nhận được giao và theo dõi riêng. Người nhận nằm trong danh sách chặn bị bỏ qua và được báo là bị trả lại.

Dùng headers cho các header riêng như X-Order-Id hay List-Unsubscribe. Hạ Tầng bỏ các header do chính nó đặt: From, To, Cc, Bcc, Subject, Date, Message-ID, Content-Type, Content-Transfer-Encoding, MIME-Version, Return-Path, Received, DKIM-Signature và Authentication-Results. Mọi thư còn có header X-Entity-Ref-ID chứa id của email.

Tag​

Tag dùng để đánh dấu email cho hệ thống của bạn. name và value mỗi trường dài 1 tới 256 ký tự, chỉ gồm chữ cái ASCII, số, _ và -. Trong webhook, tag có dạng object, ví dụ "tags": { "category": "order_confirmation" }.

Tệp đính kèm​

Gửi nội dung mỗi tệp dạng base64 trong content:

{
"attachments": [
{ "filename": "hoa-don-1024.pdf", "content": "JVBERi0xLjcKJc…", "content_type": "application/pdf" },
{ "filename": "logo.png", "content": "iVBORw0KGgo…", "content_type": "image/png", "content_id": "logo" }
]
}
TrườngMô tả
filenameTên tệp người nhận thấy, tối đa 255 ký tự.
contentTệp dạng chuỗi base64. SDK Node.js nhận cả Buffer hoặc Uint8Array và tự mã hoá.
content_typeKiểu MIME. Không bắt buộc.
content_idĐặt để hiển thị tệp ngay trong nội dung: trong HTML viết <img src="cid:logo">.
Chưa hỗ trợ path

Tệp đính kèm theo URL (path) bị từ chối với lỗi 422 invalid_attachment. Hãy tự tải tệp về rồi gửi bằng content.

Toàn bộ email, gồm nội dung và tệp đính kèm sau khi giải mã, phải dưới 40 MB.

React Email​

SDK Node.js nhận một phần tử react và render ra html và text trước khi gửi request (bản thân API chỉ nhận html và text). Cài thêm @react-email/render và react cạnh SDK:

import { Hatang } from "@hatang/sdk";
import { WelcomeEmail } from "./emails/welcome";

const hatang = new Hatang(process.env.HATANG_API_KEY, { baseUrl: "https://api.hatang.dev" });

await hatang.emails.send({
from: "Shop <[email protected]>",
subject: "Chào mừng bạn đến với Shop",
react: <WelcomeEmail name="Lan" />,
});

Hẹn giờ gửi​

Đặt scheduled_at là một thời điểm ISO 8601 có múi giờ, hoặc một cụm như in 1 hour, in 30 minutes, in 2 days. Thời điểm ISO tối đa 30 ngày tới.

{ "from": "Shop <[email protected]>", "to": ["[email protected]"], "subject": "Ngày mai hết hạn dùng thử", "html": "<p>…</p>", "scheduled_at": "2026-10-20T09:00:00+07:00" }

Email có trạng thái scheduled. Trước khi gửi, bạn có thể dời giờ hoặc huỷ:

# Dời giờ
curl -X PATCH https://api.hatang.dev/emails/$EMAIL_ID \
-H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
-d '{ "scheduled_at": "in 3 hours" }'

# Huỷ
curl -X POST https://api.hatang.dev/emails/$EMAIL_ID/cancel -H "Authorization: Bearer $HATANG_API_KEY"

Cả hai trả về { "object": "email", "id": "…" }. Dời giờ hoặc huỷ một email không còn ở trạng thái scheduled sẽ nhận lỗi 422. Email đã huỷ có trạng thái canceled.

Gửi theo lô​

POST /emails/batch nhận một mảng từ 1 tới 100 email. Mỗi phần tử có các trường giống POST /emails, trừ attachments và scheduled_at không được dùng.

curl -X POST https://api.hatang.dev/emails/batch \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{ "from": "Shop <[email protected]>", "to": ["[email protected]"], "subject": "Chào Lan", "html": "<p>Chào Lan</p>" },
{ "from": "Shop <[email protected]>", "to": ["[email protected]"], "subject": "Chào Minh", "html": "<p>Chào Minh</p>" }
]'
{ "data": [{ "id": "0199e6c1-7a2b-7f00-9c1e-3f6a2b1d9e10" }, { "id": "0199e6c1-7a2c-7c41-8f0e-61f1b2a3c4d5" }] }

Các id theo đúng thứ tự các phần tử.

Gửi theo lô không phải "tất cả hoặc không gì cả"

Hạ Tầng kiểm tra cấu trúc của mọi phần tử trước khi gửi. Những kiểm tra cần tới tài khoản của bạn (tên miền của from, template, hạn mức ngày) chạy lần lượt từng phần tử trong lúc gửi, nên nếu phần tử thứ 5 lỗi thì phần tử 1 tới 4 đã được gửi và lệnh gọi trả về lỗi. Trước một lô lớn, hãy chắc chắn mọi tên miền from đã xác minh và hạn mức còn đủ. Endpoint batch không hỗ trợ Idempotency-Key.

Idempotency​

Lỗi mạng là chuyện thường. Để gọi lại POST /emails an toàn, gửi kèm header Idempotency-Key (trong SDK Node.js là tuỳ chọn idempotencyKey). Dùng một key định danh thư trong hệ thống của bạn, ví dụ order-1024-confirmation.

Tình huốngKết quả
Request đầu tiên với keyEmail được tạo và gửi.
Cùng key, cùng nội dung200 kèm id của email đầu tiên. Không gửi lại và không trừ hạn mức.
Cùng key, nội dung khác400 invalid_idempotent_request
Cùng key khi request đầu còn đang chạy409 concurrent_idempotent_requests. Đợi một chút rồi thử lại.

Key thuộc về doanh nghiệp của bạn: hai doanh nghiệp khác nhau dùng cùng một key không xung đột.

Giới hạn​

Giới hạnGiá trị
Người nhận mỗi email50, tính chung to, cc và bcc
Địa chỉ reply_to50
Tiêu đề998 ký tự
html, textMỗi trường 10.000.000 ký tự
Tệp đính kèm50 tệp mỗi email
Kích thước email40 MB, gồm nội dung và tệp đính kèm
Tag50 tag mỗi email, mỗi tên hoặc giá trị tối đa 256 ký tự
Gửi theo lô100 email mỗi lần gọi
Hẹn giờTối đa 30 ngày tới
Tốc độ gọi10 request mỗi giây cho mỗi doanh nghiệp. Response có header ratelimit-limit, ratelimit-remaining và ratelimit-reset; lỗi 429 có thêm retry-after.
Hạn mức ngàyMỗi email (mỗi phần tử của lô, mỗi email của chiến dịch) tính là một. Đặt lại lúc nửa đêm UTC. GET /usage cho biết còn bao nhiêu.

Lỗi​

Lỗi ở mọi nơi có cùng một dạng:

{ "statusCode": 403, "name": "validation_error", "message": "The shop.vn domain is not verified. Please, add and verify your domain first." }
MãnameKhi nào
422validation_errorThiếu hoặc sai trường, ví dụ không có html/text/template, quá 50 người nhận, hoặc scheduled_at quá 30 ngày.
403validation_errorTên miền của from chưa được thêm vào doanh nghiệp hoặc chưa xác minh.
401restricted_api_keyAPI key bị giới hạn vào tên miền gửi khác.
422invalid_attachmentTệp đính kèm dùng path.
403application_errorDoanh nghiệp đang tạm dừng gửi (Cài đặt email).
429rate_limit_exceededQuá 10 request mỗi giây.
429daily_quota_exceededĐã dùng hết hạn mức ngày.
400invalid_idempotent_requestIdempotency key đã được dùng với nội dung khác.
409concurrent_idempotent_requestsMột request khác cùng idempotency key đang chạy.

Danh sách đầy đủ ở Lỗi.

Xem và liệt kê email​

GET /emails/{id} trả về một email:

{
"object": "email",
"id": "0199e6c1-7a2b-7f00-9c1e-3f6a2b1d9e10",
"to": ["[email protected]"],
"from": "Shop <[email protected]>",
"created_at": "2026-10-14T09:12:03.000Z",
"subject": "Đơn #1024 đã được xác nhận",
"html": "<p>Chào Lan, …</p>",
"text": "Chào Lan, …",
"bcc": null,
"cc": ["[email protected]"],
"reply_to": ["[email protected]"],
"last_event": "delivered",
"status": "delivered",
"scheduled_at": null,
"tags": [{ "name": "category", "value": "order_confirmation" }],
"headers": { "X-Order-Id": "1024" }
}

GET /emails liệt kê email, mới nhất trước:

Tham số queryMô tả
limit1 tới 100, mặc định 20.
after, beforeCon trỏ phân trang: truyền id của email cuối (hoặc đầu) của trang đang có.
statusMột hoặc nhiều trạng thái cách nhau bởi dấu phẩy, ví dụ bounced,complained.
sourceapi, smtp, broadcast, chat hoặc system.
qTìm theo tiêu đề, người gửi, địa chỉ to, hoặc đúng id email.

GET /emails/{id}/events trả về mọi sự kiện của email và trạng thái của từng người nhận, kèm phản hồi SMTP gần nhất:

{
"object": "list",
"data": [
{ "id": "evt_…", "type": "email.sent", "created_at": "2026-10-14T09:12:03.000Z", "data": { "source": "api" } },
{ "id": "evt_…", "type": "email.delivered", "created_at": "2026-10-14T09:12:05.000Z", "data": { "address": "[email protected]", "code": 250 } }
],
"recipients": [
{ "address": "[email protected]", "kind": "to", "status": "delivered", "attempts": 1, "last_smtp_code": 250, "last_smtp_response": "250 Ok", "next_attempt_at": null }
]
}

Trạng thái và sự kiện​

status của email tổng hợp từ các người nhận:

Trạng tháiÝ nghĩa
scheduledĐang chờ tới scheduled_at.
queuedĐã nhận, đang chờ chuyển cho máy chủ thư.
sentĐã chuyển đi; còn ít nhất một người nhận chưa có kết quả cuối.
deliveredMọi người nhận được gửi tới đều đã nhận được thư. Khi một số người nhận bị trả lại còn số khác đã nhận, trạng thái cũng là delivered; xem chi tiết ở từng người nhận.
delivery_delayedĐang thử giao lại cho ít nhất một người nhận.
bouncedKhông giao được cho người nhận nào, kể cả khi mọi người nhận đều nằm trong danh sách chặn.
complainedMột người nhận đánh dấu email là spam.
canceledEmail hẹn giờ đã bị huỷ.
failedHạ Tầng không gửi được email.

Mỗi người nhận có trạng thái riêng trong GET /emails/{id}/events: pending, sent, delivered, delayed, bounced, suppressed hoặc canceled.

Các sự kiện webhook sau theo dõi một email. Đăng ký ở mục Webhooks trong dashboard hoặc bằng POST /webhooks; xem Webhook.

Sự kiệnKhi nào
email.sentHạ Tầng đã nhận email.
email.deliveredMáy chủ thư của người nhận đã nhận thư.
email.delivery_delayedĐang thử giao lại. Gửi ở lần chậm đầu tiên, sau đó tối đa mỗi giờ một lần cho mỗi người nhận.
email.bouncedMột người nhận bị trả lại, hoặc bị bỏ qua vì địa chỉ nằm trong danh sách chặn (bounce.subType là Suppressed).
email.complainedMột người nhận báo email là spam.
email.openedMột người nhận mở email. Cần bật theo dõi mở; tối đa mỗi giờ một lần cho mỗi người nhận.
email.clickedMột người nhận nhấp vào link. Cần bật theo dõi nhấp.
email.failedKhông gửi được email.

Webhook bị trả lại có kèm lý do:

{
"type": "email.bounced",
"created_at": "2026-10-14T09:12:07.000Z",
"data": {
"email_id": "0199e6c1-7a2b-7f00-9c1e-3f6a2b1d9e10",
"from": "Shop <[email protected]>",
"to": ["[email protected]"],
"subject": "Đơn #1024 đã được xác nhận",
"created_at": "2026-10-14T09:12:03.000Z",
"tags": { "category": "order_confirmation" },
"bounce": { "type": "Permanent", "subType": "NoEmail", "message": "550 5.1.1 The email account that you tried to reach does not exist." }
}
}

Bước tiếp theo​

Xem trang này dạng Markdown