Hóa đơn điện tử
Hạ Tầng phát hành hóa đơn điện tử theo mã số thuế của bạn, qua tài khoản riêng của bạn tại nhà cung cấp hóa đơn. Thêm khối invoice vào payment và hóa đơn được tạo khi đơn đã thanh toán; hoặc tự tạo hóa đơn cho bất kỳ payment hay giao dịch ngân hàng nào. Trang này hướng dẫn kết nối nhà cung cấp, vòng đời hóa đơn, xử lý lỗi, và đối soát tiền vào với hóa đơn.
Trước khi bắt đầu
- Tài khoản hóa đơn điện tử Mắt Bão có quyền dùng API. Mã số thuế, tài khoản và mật khẩu API nằm trong mục "Cấu hình API" trên cổng Mắt Bão.
- Ít nhất một mẫu số và ký hiệu đã đăng ký trên tài khoản đó.
- API key có
payments:write. Hóa đơn điện tử dùng chung scope và môi trường với Thanh toán.
Ở development, hãy kết nối tài khoản sandbox demo của Mắt Bão: hóa đơn là hóa đơn sandbox thật nhưng không bao giờ gửi tới cơ quan thuế.
1. Kết nối nhà cung cấp
Làm một lần cho mỗi môi trường, trên dashboard (Cài đặt thanh toán → Nhà cung cấp hoá đơn) hoặc qua API. Trước tiên kiểm tra thông tin đăng nhập và liệt kê mẫu hóa đơn của tài khoản; bước này không lưu gì:
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/einvoices/connections/check \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "provider": "matbao", "tax_code": "0109876543", "username": "api-user", "password": "..." }'
const { data: check, error } = await hatang.einvoices.connections.check({
provider: "matbao",
tax_code: "0109876543",
username: "api-user",
password: process.env.MATBAO_PASSWORD!,
});
check = requests.post("https://api.hatang.dev/einvoices/connections/check", headers=HEADERS, json={
"provider": "matbao", "tax_code": "0109876543", "username": "api-user", "password": os.environ["MATBAO_PASSWORD"],
}, timeout=30).json()
$check = json_decode($client->post('/einvoices/connections/check', ['json' => [
'provider' => 'matbao', 'tax_code' => '0109876543', 'username' => 'api-user', 'password' => getenv('MATBAO_PASSWORD'),
]])->getBody(), true);
{
"object": "einvoice_connection_check",
"ok": true,
"provider": "matbao",
"seller_tax_code": "0109876543",
"seller_name": "CONG TY TNHH VI DU",
"templates": [
{ "pattern": "1", "serial": "C26TAT", "name": "Hóa đơn giá trị gia tăng", "remaining": 4980 }
]
}
Sau đó kết nối với mẫu bạn chọn. Body gồm:
| Trường | Kiểu | Bắt buộc | Mặc định | Ý nghĩa |
|---|---|---|---|---|
provider | chuỗi | Có | – | matbao (GET /einvoices/providers liệt kê các nhà cung cấp) |
tax_code, username, password | chuỗi | Có với matbao | – | Mã số thuế người bán và tài khoản API của nhà cung cấp; được lưu mã hóa |
pattern | chuỗi, 1 đến 6 | Có | – | Mẫu số mặc định cho hóa đơn mới; phải là một mẫu của tài khoản |
serial | chuỗi, đúng 6 | Có | – | Ký hiệu mặc định, ví dụ C26TAT |
default_mode | draft | publish | Không | draft | Khi đơn đã thanh toán yêu cầu hóa đơn mà không có mode: draft dừng ở nháp, publish gửi ký luôn |
is_default | boolean | Không | true với nhà cung cấp đầu tiên | Hóa đơn mới dùng nhà cung cấp mặc định |
import_external | boolean | Không | false | Nhập cả hóa đơn tạo trực tiếp trên trang nhà cung cấp |
label | chuỗi, tối đa 100 | Không | – | Tên bạn đặt cho kết nối |
Schema OpenAPI: POST /einvoices/connections
curl https://api.hatang.dev/einvoices/connections \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "provider": "matbao", "tax_code": "0109876543", "username": "api-user", "password": "...",
"pattern": "1", "serial": "C26TAT", "default_mode": "publish" }'
Phản hồi (201) là kết nối, kèm callback_url. Gửi URL này cho bộ phận hỗ trợ Mắt Bão để thay đổi phía họ về Hạ Tầng ngay; nếu không, Hạ Tầng kiểm tra lại các hóa đơn còn đang chờ sau 1 giờ kể từ khi tạo, 15 phút một lần, trong tối đa 7 ngày. Bản nháp bạn cố ý để lại (mode: "draft") không được kiểm tra: gọi POST /einvoices/{id}/sync để lấy các chỉnh sửa trên trang nhà cung cấp. Thông tin đăng nhập được lưu mã hóa. Nếu nhà cung cấp bắt đầu từ chối thông tin này, kết nối chuyển sang needs_reauth và hóa đơn tạm dừng cho tới khi bạn lưu thông tin mới bằng PATCH /einvoices/connections/{id}.
| Endpoint | Chức năng |
|---|---|
GET /einvoices/providers | Các nhà cung cấp kết nối được, kèm các trường mỗi bên cần |
GET /einvoices/connections / GET /einvoices/connections/{id} | Các kết nối của bạn |
PATCH /einvoices/connections/{id} | Đổi chế độ mặc định, mẫu, thông tin đăng nhập, nhãn |
POST /einvoices/connections/{id}/default | Đặt làm nhà cung cấp mặc định |
GET /einvoices/connections/{id}/templates | Các mẫu tài khoản được phép phát hành, đọc trực tiếp |
POST /einvoices/connections/{id}/sync | Lấy thay đổi từ nhà cung cấp ngay |
DELETE /einvoices/connections/{id} | Gỡ liên kết. Hóa đơn vẫn còn; nhà cung cấp tiếp theo trở thành mặc định |
2. Thêm hóa đơn vào payment
Thêm khối invoice khi tạo payment. Không có gì được gửi tới nhà cung cấp cho tới khi payment ở trạng thái paid hoặc overpaid.
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/payments \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Idempotency-Key: order-1002" \
-H "Content-Type: application/json" \
-d '{
"amount": 275000,
"description": "Đơn hàng #1002",
"ipn_url": "https://shop.example.vn/hatang/ipn",
"metadata": { "order_id": "1002" },
"invoice": {
"mode": "publish",
"template": { "pattern": "1", "serial": "C26TAT" },
"buyer": {
"name": "Nguyễn Văn Khách",
"legal_name": "Công ty TNHH Khách Hàng",
"tax_code": "0109876543",
"address": "1 Tràng Tiền, Hà Nội",
"email": "[email protected]"
},
"items": [{ "name": "Gói dịch vụ tháng 9", "unit": "gói", "quantity": 1, "unit_price": 250000, "vat_rate": 10 }],
"payment_method": "CK",
"send_email": true
}
}'
const { data: payment, error } = await hatang.payments.createOrder({
order_id: "1002",
amount: 275_000,
description: "Đơn hàng #1002",
ipn_url: "https://shop.example.vn/hatang/ipn",
invoice: {
mode: "publish",
template: { pattern: "1", serial: "C26TAT" },
buyer: { name: "Nguyễn Văn Khách", legal_name: "Công ty TNHH Khách Hàng", tax_code: "0109876543", address: "1 Tràng Tiền, Hà Nội", email: "[email protected]" },
items: [{ name: "Gói dịch vụ tháng 9", unit: "gói", quantity: 1, unit_price: 250_000, vat_rate: 10 }],
payment_method: "CK",
send_email: true,
},
});
payment = hatang.payments.create({
"amount": 275000,
"description": "Đơn hàng #1002",
"ipn_url": "https://shop.example.vn/hatang/ipn",
"metadata": {"order_id": "1002"},
"invoice": {
"mode": "publish",
"template": {"pattern": "1", "serial": "C26TAT"},
"buyer": {"name": "Nguyễn Văn Khách", "legal_name": "Công ty TNHH Khách Hàng", "tax_code": "0109876543",
"items": [{"name": "Gói dịch vụ tháng 9", "unit": "gói", "quantity": 1, "unit_price": 250000, "vat_rate": 10}],
"payment_method": "CK",
"send_email": True,
},
}, idempotency_key="order-1002")
$payment = json_decode($client->post('/payments', [
'headers' => ['Idempotency-Key' => 'order-1002'],
'json' => [
'amount' => 275000,
'description' => 'Đơn hàng #1002',
'ipn_url' => 'https://shop.example.vn/hatang/ipn',
'metadata' => ['order_id' => '1002'],
'invoice' => [
'mode' => 'publish',
'template' => ['pattern' => '1', 'serial' => 'C26TAT'],
'buyer' => ['name' => 'Nguyễn Văn Khách', 'legal_name' => 'Công ty TNHH Khách Hàng', 'tax_code' => '0109876543',
'items' => [['name' => 'Gói dịch vụ tháng 9', 'unit' => 'gói', 'quantity' => 1, 'unit_price' => 250000, 'vat_rate' => 10]],
'payment_method' => 'CK',
'send_email' => true,
],
],
])->getBody(), true);
| Trường | Kiểu | Bắt buộc | Mặc định | Ý nghĩa |
|---|---|---|---|---|
mode | draft | publish | Không | default_mode của kết nối | draft dừng ở bản nháp, bạn xem lại rồi gọi POST /einvoices/{id}/publish; publish tạo nháp rồi gửi ký luôn |
template | object { pattern, serial } | Không | mẫu của kết nối | pattern: mẫu số, 1 đến 6 ký tự; serial: ký hiệu, đúng 6 ký tự |
buyer | người mua | Có | – | Cần name hoặc legal_name |
items | mảng dòng hàng, 1 đến 200 | Có | – | Các dòng hàng trên hóa đơn |
payment_method | chuỗi, tối đa 50 | Không | TM/CK | Hình thức thanh toán, in trên hóa đơn |
note | chuỗi, tối đa 500 | Không | – | Ghi chú, in trên hóa đơn |
send_email | boolean | Không | false | Gửi email hóa đơn đã phát hành cho người mua (tới buyer.email) |
Schema OpenAPI: POST /payments, POST /einvoices
Hãy để tổng hóa đơn (sau VAT) bằng amount của payment: ở ví dụ trên 250.000 + 10% = 275.000. Hạ Tầng không kiểm tra điều này. Mỗi dòng được làm tròn tới đồng riêng; khi giá của bạn đã gồm VAT, hãy gửi vat_amount cho từng dòng để tổng khớp tới từng đồng. Xem Tổng tiền và làm tròn VAT.
3. Sau khi thanh toán
Mỗi hóa đơn đi qua cùng hai bước, tạo nháp và gửi ký; mỗi bước được xếp hàng và tự thử lại độc lập:
invoice.status | Ý nghĩa |
|---|---|
waiting_for_payment | Chưa trả; chưa gửi gì (chỉ có trên payment) |
creating | Đã trả; đang tạo nháp tại nhà cung cấp |
draft | Đã có nháp. Dừng ở đây nếu mode là draft |
publishing | Đã gửi ký |
issued | Đã ký: có number và lookup_code; chờ cơ quan thuế |
tax_accepted / tax_rejected | Phản hồi của cơ quan thuế (tax_authority_code khi chấp nhận) |
failed | Nhà cung cấp từ chối một bước: error cho biết lý do, failed_step cho biết bước nào |
canceled / replaced / adjusted | Thay đổi sau đó, trên trang nhà cung cấp hoặc bằng hóa đơn thay thế |
deleted | Nháp đã bị xóa |
Trường invoice của payment (các trường) luôn phản ánh trạng thái hiện tại của hóa đơn, và mọi thay đổi được gửi qua IPN và webhook invoice.*:
"invoice": {
"requested": true,
"mode": "publish",
"status": "issued",
"id": "einv_01a0b90429f678e68ed7de882720a2b4",
"number": "8003",
"series": "1C26TAT",
"template": { "pattern": "1", "serial": "C26TAT" },
"lookup_code": "HTXAEM9P3AYXSM",
"tax_authority_code": null,
"pdf_url": "https://…",
"sync_state": "in_sync",
"issued_at": "2026-09-18T17:00:00.000Z",
"error": null,
"failed_step": null
}
Nếu payment được trả khi chưa kết nối nhà cung cấp nào ở môi trường đó, invoice.status là failed với id: null và lỗi "No e-invoice provider is connected in this environment".
Trang checkout chỉ cho người mua biết có hóa đơn hay không, và khi đã phát hành thì số hóa đơn và mã tra cứu.
Xem lại nháp trước khi ký
Với mode: "draft", hóa đơn dừng ở draft. Khi đó bạn có thể:
| Lời gọi | Chức năng |
|---|---|
PATCH /einvoices/{id} | Sửa buyer, items, payment_method, note |
POST /einvoices/{id}/publish | Gửi ký |
DELETE /einvoices/{id} | Xóa nháp |
Chỉ nháp mới sửa hoặc gửi ký được; trạng thái khác trả về 409 invalid_state. Ngay cả mode: "publish" cũng không phát hành trong một lời gọi: gửi ký luôn là một bước riêng, được xếp hàng.
Khi một bước lỗi
Timeout và sự cố của nhà cung cấp được thử lại tự động. Khi bị từ chối (ví dụ mã số thuế người mua không hợp lệ), hóa đơn dừng ở failed kèm thông báo của nhà cung cấp:
- Sửa theo thông báo. Nếu lỗi ở bước tạo nháp (
failed_step: "create_draft"), sửa người mua hoặc dòng hàng bằngPATCH /einvoices/{id}. POST /einvoices/{id}/retrychạy lại đúng bước đó.GET /einvoices/{id}/historyliệt kê mọi lần đổi trạng thái, cũ nhất trước, kèm phía thực hiện (source) và thời điểm.
{
"object": "list",
"data": [
{ "from": null, "to": "creating", "source": "hatang", "detail": { "mode": "publish", "template": "1/C26TAT" }, "at": "2026-09-18T16:59:58.120Z" },
{ "from": "draft", "to": "publishing", "source": "hatang", "detail": null, "at": "2026-09-18T16:59:59.010Z" }
]
}
Hóa đơn không gắn payment
POST /einvoices nhận cùng các trường như khối invoice, thêm:
| Trường | Kiểu | Bắt buộc | Mặc định | Ý nghĩa |
|---|---|---|---|---|
payment_id | chuỗi pay_… | Không | – | Gắn với một payment. Mỗi payment có tối đa một hóa đơn; hóa đơn thứ hai trả về 409 invalid_state |
transaction_id | chuỗi txn_… | Không | – | Gắn với một giao dịch ngân hàng nhận không qua đơn |
connection_id | chuỗi icn_… | Không | kết nối mặc định | Kết nối nhà cung cấp phát hành hóa đơn |
Schema OpenAPI: POST /einvoices
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/einvoices \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "publish",
"transaction_id": "txn_123",
"buyer": { "name": "Nguyễn Văn Khách" },
"items": [{ "name": "Cà phê hạt 1 kg", "quantity": 1, "unit_price": 91667, "vat_rate": 8 }]
}'
const { data: invoice, error } = await hatang.einvoices.create({
mode: "publish",
transaction_id: "txn_123",
buyer: { name: "Nguyễn Văn Khách" },
items: [{ name: "Cà phê hạt 1 kg", quantity: 1, unit_price: 91_667, vat_rate: 8 }],
});
invoice = requests.post("https://api.hatang.dev/einvoices", headers=HEADERS, json={
"mode": "publish",
"transaction_id": "txn_123",
"buyer": {"name": "Nguyễn Văn Khách"},
"items": [{"name": "Cà phê hạt 1 kg", "quantity": 1, "unit_price": 91667, "vat_rate": 8}],
}, timeout=10).json()
$invoice = json_decode($client->post('/einvoices', ['json' => [
'mode' => 'publish',
'transaction_id' => 'txn_123',
'buyer' => ['name' => 'Nguyễn Văn Khách'],
'items' => [['name' => 'Cà phê hạt 1 kg', 'quantity' => 1, 'unit_price' => 91667, 'vat_rate' => 8]],
]])->getBody(), true);
Phản hồi (201) là object hóa đơn (object: "einvoice") ở trạng thái creating, kèm buyer, items và totals đã tính (subtotal, vat, total). Theo dõi bằng GET /einvoices/{id} hoặc webhook invoice.*. GET /einvoices liệt kê hóa đơn, lọc theo status, payment_id hoặc sync_state.
Thay thế, hủy và xung đột
- Thay thế. Hóa đơn đã gửi cơ quan thuế (
issued,tax_accepted,tax_rejected) không thể hủy hay xóa qua Hạ Tầng.POST /einvoices/{id}/replacekèmreason,buyervàitemsđã sửa sẽ tạo hóa đơn thay thế: một bản nháp ghi rõ hóa đơn gốc, rồi gửi ký. Hóa đơn gốc chuyển sangreplacedkhi nhà cung cấp đã có hóa đơn thay thế. - Xóa nháp (hoặc hóa đơn lỗi) bằng
DELETE /einvoices/{id}. - Thay đổi trên trang nhà cung cấp được đồng bộ về Hạ Tầng: ký, mã của cơ quan thuế, hủy, thay thế, sửa và xóa nháp. Sự kiện của chúng có
source: "provider". Hóa đơn tạo trực tiếp trên trang nhà cung cấp chỉ được nhập khi kết nối bậtimport_external: true. - Xung đột. Nháp bị sửa ở cả Hạ Tầng và trang nhà cung cấp sẽ có
sync_state: "conflict"và sự kiệninvoice.conflict. Giải quyết bằngPOST /einvoices/{id}/resolvevới{ "keep": "hatang" }hoặc{ "keep": "provider" }. Trước khi giải quyết, không sửa hay gửi ký được. - Kiểm tra ngay.
POST /einvoices/{id}/synchỏi nhà cung cấp về một hóa đơn ngay lập tức.
Đối soát tiền vào với hóa đơn
Mọi giao dịch tiền vào tài khoản đều nên có hóa đơn, trừ khi không cần. Giao dịch được tính là đã có hóa đơn khi một hóa đơn còn hiệu lực (không lỗi, không bị xóa, hủy hay bị cơ quan thuế từ chối) trỏ tới nó, trực tiếp hoặc qua payment mà nó đã trả. Ngược lại là thiếu hóa đơn, trừ khi bạn đánh dấu không cần hóa đơn.
curl "https://api.hatang.dev/einvoices/reconciliation?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $HATANG_API_KEY"
from và to là ngày theo giờ Việt Nam (YYYY-MM-DD), cách nhau tối đa 400 ngày; mặc định là 30 ngày gần nhất. Phản hồi có totals (money_in, invoiced, missing, exempt, mỗi mục có count và amount), một dòng cho mỗi ngày, cùng danh sách giao dịch missing và exempt. Dashboard hiển thị cùng báo cáo tại Thanh toán → Đối soát hoá đơn.
Đánh dấu giao dịch không cần hóa đơn (hoàn tiền, chuyển giữa các tài khoản của chính bạn), hoặc bỏ đánh dấu:
curl https://api.hatang.dev/payments/transactions/txn_123/invoice-exempt \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Chuyển giữa các tài khoản của công ty" }'
curl -X DELETE https://api.hatang.dev/payments/transactions/txn_123/invoice-exempt \
-H "Authorization: Bearer $HATANG_API_KEY"
Chủ sở hữu và quản trị viên của doanh nghiệp có nhà cung cấp đang hoạt động còn nhận email hằng ngày lúc 08:00 (giờ Việt Nam) liệt kê tiền vào hôm trước còn thiếu hóa đơn, và email khi một hóa đơn bị lỗi, bị cơ quan thuế từ chối, bị xung đột, hoặc sau 1 giờ kể từ khi tạo vẫn chưa tới cơ quan thuế. Bản nháp bạn cố ý để lại (mode: "draft") không bị báo.
Các object và cách chúng liên kết
Khi một đơn hàng được thanh toán và xuất hóa đơn, có ba object tham gia: payment bạn tạo, transaction ngân hàng báo về, và hóa đơn điện tử (einvoice). Mỗi object trỏ tới các object kia bằng id, nên bạn bắt đầu từ object nào mà sự kiện gửi tới cũng được.
| Lưu vào đơn hàng | Lấy từ đâu | Dùng để |
|---|---|---|
id của payment (pay_…) | Phản hồi của POST /payments | GET /payments/{id}; là data.id trong mọi lần gọi IPN |
id của hóa đơn (einv_…) | invoice.id của payment khi hóa đơn đã có (null trước khi payment được trả), hoặc phản hồi của POST /einvoices | GET, PATCH, publish, retry, replace trên /einvoices/{id}; là data.id trong webhook invoice.* |
number, series, lookup_code | Hóa đơn, khi đã issued | Cho người mua biết hóa đơn của họ và nơi tra cứu |
reference | Hóa đơn, ngay từ đầu | Mã tham chiếu Hạ Tầng dùng ở nhà cung cấp; tìm hóa đơn trên trang của nhà cung cấp bằng mã này |
- Qua payment. Thêm khối
invoicevàoPOST /payments, hoặc gọiPOST /einvoiceskèmpayment_id. Mỗi payment có một hóa đơn đang hiệu lực; hóa đơn thay thế giữ nguyênpayment_id, và trườnginvoicecủa payment luôn là hóa đơn đang hiệu lực.GET /einvoices?payment_id=pay_…liệt kê tất cả. - Qua giao dịch, không có payment. Nếu bạn chỉ nhận webhook
transaction.received, gọiPOST /einvoiceskèmtransaction_idcủa giao dịch đó. - Sự kiện. Lần gọi IPN mang payment, trạng thái hóa đơn nằm ở
data.invoice. Webhookinvoice.*mang nguyên object hóa đơn, kèmsource(hatang, hoặcproviderkhi thay đổi đến từ trang của nhà cung cấp).
Yêu cầu xuất hóa đơn
Cùng một object, cùng quy tắc, được gửi ở bốn chỗ:
| Ở đâu | Nhận gì |
|---|---|
invoice trong POST /payments | Mọi trường bên dưới |
POST /einvoices | Mọi trường bên dưới, thêm payment_id, transaction_id, connection_id |
PATCH /einvoices/{id} (bản nháp) | buyer, items, payment_method, note; giá trị gửi lên thay hẳn giá trị cũ; đều không bắt buộc, chỉ gửi trường cần đổi |
POST /einvoices/{id}/replace | reason, buyer, items, payment_method, note |
Một yêu cầu đầy đủ: người mua là công ty, hai dòng hàng (một dòng tự nhập tiền thuế, một dòng không chịu thuế):
{
"mode": "publish",
"template": { "pattern": "1", "serial": "C26TAT" },
"buyer": {
"name": "Nguyen Van Khach",
"legal_name": "Cong ty TNHH Khach Hang",
"tax_code": "0109876543",
"address": "1 Trang Tien, Ha Noi",
"phone": "0901234567",
"buyer_code": "KH-0042"
},
"items": [
{ "name": "Coffee 1 kg", "code": "CF-1KG", "unit": "kg", "quantity": 1, "unit_price": 45455, "vat_rate": 10, "vat_amount": 4545 },
{ "name": "Delivery", "unit": "order", "quantity": 1, "unit_price": 20000, "vat_rate": -1 }
],
"payment_method": "CK",
"note": "Order #1002",
"send_email": true
}
Tổng của nó: tiền hàng 65.455, VAT 4.545, tổng cộng 70.000.
| Trường | Kiểu | Bắt buộc | Mặc định | Ý nghĩa |
|---|---|---|---|---|
mode | draft | publish | Không | default_mode của kết nối | draft dừng ở bản nháp, bạn xem lại rồi gọi POST /einvoices/{id}/publish; publish tạo nháp rồi gửi ký luôn |
template | object { pattern, serial } | Không | mẫu của kết nối | pattern: mẫu số, 1 đến 6 ký tự; serial: ký hiệu, đúng 6 ký tự |
buyer | người mua | Có | – | Cần name hoặc legal_name |
items | mảng dòng hàng, 1 đến 200 | Có | – | Các dòng hàng trên hóa đơn |
payment_method | chuỗi, tối đa 50 | Không | TM/CK | Hình thức thanh toán, in trên hóa đơn |
note | chuỗi, tối đa 500 | Không | – | Ghi chú, in trên hóa đơn |
send_email | boolean | Không | false | Gửi email hóa đơn đã phát hành cho người mua (tới buyer.email) |
Schema OpenAPI: POST /payments, POST /einvoices
Người mua
Cần name, legal_name hoặc cả hai. Với công ty, legal_name là tên công ty và name là người mua hàng; với cá nhân, chỉ cần name.
| Trường | Kiểu | Bắt buộc | Mặc định | Ý nghĩa |
|---|---|---|---|---|
name | chuỗi, tối đa 200 | Một trong hai | – | Người mua hàng, in ở ô "Họ tên người mua hàng" |
legal_name | chuỗi, tối đa 400 | Một trong hai | – | Tên công ty theo đăng ký, in ở ô "Tên đơn vị". Khách cá nhân để trống |
tax_code | 10 đến 14 chữ số hoặc dấu gạch | Không | – | Mã số thuế người mua, ví dụ 0109876543 hoặc 0109876543-001 |
address | chuỗi, tối đa 400 | Không | – | Địa chỉ người mua |
email | Không | – | Nơi send_email gửi hóa đơn tới | |
phone | chuỗi, tối đa 20 | Không | – | Số điện thoại người mua |
buyer_code | chuỗi, tối đa 50 | Không | – | Mã khách hàng của bạn, in trên hóa đơn |
Schema OpenAPI: POST /payments, POST /einvoices
Dòng hàng
| Trường | Kiểu | Bắt buộc | Mặc định | Ý nghĩa |
|---|---|---|---|---|
name | chuỗi, 1 đến 500 | Có | – | Tên hàng hóa, dịch vụ |
quantity | số lớn hơn 0 | Có | – | Được có số lẻ, ví dụ 1.5 kg |
unit_price | số, từ 0 | Có | – | Đơn giá trước VAT, tính bằng đồng. Được có số lẻ |
vat_rate | 10 | 8 | 5 | 0 | -1 | -2 | Không | 10 | -1: không chịu thuế (KCT); -2: không kê khai, tính nộp thuế (KKKNT) |
vat_amount | số đồng chẵn, từ 0 | Không | tính từ vat_rate | Tiền thuế của dòng, dùng đúng như gửi. Chỉ dùng khi vat_rate lớn hơn 0. Xem Tổng tiền và làm tròn VAT |
discount | số, từ 0 | Không | 0 | Chiết khấu cho cả dòng, tính bằng đồng trước VAT |
unit | chuỗi, tối đa 50 | Không | – | Đơn vị tính, ví dụ kg, gói |
code | chuỗi, tối đa 50 | Không | – | Mã hàng (SKU) của bạn |
Schema OpenAPI: POST /payments, POST /einvoices
Trong object hóa đơn, mỗi dòng trả về những gì bạn đã gửi, thêm ba số tiền: amount (thành tiền trước VAT), vat_amount và total.
Tổng tiền và làm tròn VAT
Đồng không có đơn vị nhỏ hơn, nên từng dòng được làm tròn riêng, rồi hóa đơn cộng các dòng đã làm tròn. Nhà cung cấp in đúng các số này.
| Số tiền | Cách tính |
|---|---|
amount của dòng | quantity × unit_price − discount, làm tròn tới đồng |
vat_amount của dòng | vat_amount nếu bạn gửi; nếu không thì amount × vat_rate / 100, làm tròn; bằng 0 khi vat_rate là 0, -1 hoặc -2 |
total của dòng | amount + vat_amount |
totals.subtotal / totals.vat / totals.total | Tổng amount / vat_amount / total của các dòng |
Số lẻ 0,5 làm tròn lên. Hạ Tầng không so tổng hóa đơn với amount của payment: bạn cần tự để hai số này bằng nhau.
Khi giá đã gồm VAT. Cửa hàng thường biết số tiền người mua trả (đã gồm VAT) rồi tính ngược ra giá trước thuế. Làm tròn giá đó rồi làm tròn tiền thuế có thể lệch một đồng. Ví dụ 50.000 đồng, thuế 10%:
unit_price | vat_amount | total | |
|---|---|---|---|
| Hạ Tầng tự tính VAT | 45.455 | 4.546 (45.455 × 10%, làm tròn) | 50.001 |
Gửi kèm vat_amount | 45.455 | 4.545 | 50.000 |
Vì vậy, với mỗi dòng, gửi vat_amount bằng phần còn lại của số tiền người mua trả sau khi đã làm tròn giá trước thuế:
// gross: số tiền người mua trả cho cả dòng, đã gồm VAT, tính bằng đồng.
function lineFromGross(name: string, quantity: number, gross: number, vatRate: 10 | 8 | 5) {
const unit_price = Math.round(gross / quantity / (1 + vatRate / 100));
const amount = Math.round(quantity * unit_price);
return { name, quantity, unit_price, vat_rate: vatRate, vat_amount: gross - amount };
}
lineFromGross("Coffee 1 kg", 1, 50_000, 10);
// { name: "Coffee 1 kg", quantity: 1, unit_price: 45455, vat_rate: 10, vat_amount: 4545 }
Quy tắc của vat_amount:
- Số đồng chẵn, từ 0.
4545.5bị từ chối với422 validation_error; Hạ Tầng không tự làm tròn. - Chỉ dùng cho dòng có
vat_ratelớn hơn 0; dòng0,-1hoặc-2gửi kèm sẽ bị từ chối với422 validation_error. - Hạ Tầng không đối chiếu với
vat_rate; chỉ nên lệch đúng phần làm tròn bạn cần. - Số này được gửi sang nhà cung cấp làm tiền thuế của dòng và được giữ nguyên khi đọc hóa đơn về, nên đồng bộ sau đó hay sửa trên trang nhà cung cấp đều không biến nó thành xung đột.
- Trên dashboard, ô Tiền thuế VAT trong Hoá đơn mới nhận cùng giá trị, viết
4545,4.545hay4,545đều được.
Object hóa đơn
Là phản hồi của POST /einvoices và GET /einvoices/{id}, và là data của mọi webhook invoice.*. Hóa đơn từ yêu cầu ở trên (chỉ dòng đầu), đã thanh toán và phát hành:
{
"object": "einvoice",
"id": "einv_01a178887ba6771caef28863e6f55879",
"environment": "production",
"connection_id": "icn_01a178887b357a9c8bc4ad098f6b8855",
"provider": "matbao",
"payment_id": "pay_01a178887b59783cb5f4f18f06071ed1",
"transaction_id": null,
"origin": "hatang",
"mode": "publish",
"status": "issued",
"sync_state": "in_sync",
"version": 1,
"buyer": {
"name": "Nguyen Van Khach",
"legal_name": "Cong ty TNHH Khach Hang",
"tax_code": "0109876543",
"address": "1 Trang Tien, Ha Noi",
},
"items": [
{ "name": "Coffee 1 kg", "unit": "kg", "quantity": 1, "unit_price": 45455, "vat_rate": 10, "amount": 45455, "vat_amount": 4545, "total": 50000 }
],
"totals": { "subtotal": 45455, "vat": 4545, "total": 50000 },
"currency": "VND",
"payment_method": "CK",
"note": null,
"template": { "pattern": "1", "serial": "C26TAT" },
"reference": "HT45MSKX4UY3AA",
"provider_invoice_id": "4/AUx/syzogcoBsIReMUk6…",
"number": "8001",
"series": "1C26TAT",
"lookup_code": "HT45MSKX4UY3AA",
"tax_authority_code": null,
"pdf_url": "https://…",
"issued_at": "2026-10-25T17:00:00.000Z",
"related_invoice_id": null,
"relation": null,
"conflict": null,
"error": null,
"failed_step": null,
"created_at": "2026-10-26T14:06:12.895Z",
"updated_at": "2026-10-26T14:06:13.004Z"
}
| Trường | Kiểu | Có thể null | Ý nghĩa |
|---|---|---|---|
object | chuỗi | Không | Luôn là einvoice |
id | chuỗi | Không | einv_… |
environment | development | production | Không | Môi trường của API key đã tạo hóa đơn |
connection_id | chuỗi | Không | Kết nối nhà cung cấp phát hành hóa đơn (icn_…) |
provider | chuỗi | Có | Nhà cung cấp, ví dụ matbao |
payment_id | chuỗi | Có | Payment của hóa đơn |
transaction_id | chuỗi | Có | Giao dịch ngân hàng của hóa đơn, khi không có payment |
origin | hatang | provider | Không | hatang khi tạo qua Hạ Tầng; provider khi được đưa về từ trang nhà cung cấp (import_external) |
mode | draft | publish | Không | Như đã yêu cầu |
status | chuỗi | Không | Trạng thái hiện tại: xem Sau khi thanh toán |
sync_state | in_sync | pending_push | conflict | Không | pending_push khi một thay đổi đang được gửi sang nhà cung cấp; conflict khi bản nháp bị sửa ở cả hai phía |
version | số nguyên | Không | Tăng một sau mỗi thay đổi qua Hạ Tầng |
buyer | người mua | Không | Như đã gửi; chỉ có các trường bạn đã gửi |
items | mảng dòng hàng | Không | Như đã gửi, mỗi dòng kèm amount, vat_amount và total đã tính |
totals | object { subtotal, vat, total } | Không | Tính bằng đồng |
currency | chuỗi | Không | VND |
payment_method, note | chuỗi | Có | Như đã gửi |
template | object { pattern, serial } | Không | Mẫu số và ký hiệu dùng để phát hành |
reference | chuỗi, tối đa 20 | Không | Mã tham chiếu Hạ Tầng dùng ở nhà cung cấp |
provider_invoice_id | chuỗi | Có | Id của chính nhà cung cấp, khi bản nháp đã có |
number | chuỗi | Có | Số hóa đơn, khi đã issued |
series | chuỗi | Có | Mẫu số và ký hiệu ghép lại, ví dụ 1C26TAT |
lookup_code | chuỗi | Có | Mã người mua nhập ở trang tra cứu của nhà cung cấp |
tax_authority_code | chuỗi | Có | Mã của cơ quan thuế, khi đã tax_accepted |
pdf_url | URL | Có | File PDF của nhà cung cấp, khi đã có |
issued_at | chuỗi ISO 8601 | Có | Ngày ghi trên hóa đơn |
related_invoice_id | chuỗi | Có | Ở hóa đơn thay thế: hóa đơn bị thay |
relation | replaces | adjusts | Có | Ở hóa đơn thay thế (replaces) hoặc điều chỉnh (adjusts) |
conflict | object | Có | Khi sync_state: "conflict": bản của nhà cung cấp, nằm trong content |
error | chuỗi | Có | Lý do lỗi |
failed_step | create_draft | update_draft | publish | delete_draft | Có | Bước bị lỗi |
created_at, updated_at | chuỗi ISO 8601 | Không |
Schema OpenAPI: GET /einvoices/{id}
Tóm tắt hóa đơn trên payment
Object payment giữ một bản tóm tắt hóa đơn của nó trong invoice, để handler IPN thấy hóa đơn mà không phải gọi thêm. Trường này là null khi không yêu cầu xuất hóa đơn và cũng chưa có hóa đơn nào.
| Trường | Kiểu | Có thể null | Ý nghĩa |
|---|---|---|---|
requested | boolean | Không | true khi payment được tạo kèm khối invoice |
mode | draft | publish | Có | Như đã yêu cầu |
status | chuỗi | Không | Như trên hóa đơn, thêm waiting_for_payment khi payment chưa được trả. failed với id: null khi đã trả mà chưa kết nối nhà cung cấp |
id | chuỗi | Có | Id einv_… của hóa đơn, null cho tới khi hóa đơn được tạo |
number, series, template, lookup_code, tax_authority_code, pdf_url, sync_state, issued_at, error, failed_step | như trên hóa đơn | Có | Như trên hóa đơn; null cho tới khi có hóa đơn |
Schema OpenAPI: GET /payments/{id}
Để đọc các dòng hàng và tổng tiền, lấy hóa đơn: GET /einvoices/{invoice.id}.
Lỗi
| Lỗi | Khi nào |
|---|---|
422 validation_error | Trường sai (người mua không có tên, mã số thuế sai, quá 200 dòng), hoặc mẫu chưa đăng ký trên tài khoản |
422 missing_required_field | Thiếu thông tin đăng nhập của nhà cung cấp |
422 invoice_provider_error | Nhà cung cấp từ chối request; thông báo nêu lý do |
409 invalid_state | Chưa kết nối nhà cung cấp, payment đã có hóa đơn, hóa đơn không ở trạng thái cho phép lời gọi này, hoặc cần giải quyết xung đột trước |
403 environment_mismatch | Nhà cung cấp không có ở môi trường này |
503 provider_unavailable | Nhà cung cấp không phản hồi; thử lại |
Bước tiếp theo
- Nhận kết quả: các lời gọi IPN mang thông tin hóa đơn.
- Webhook: mọi sự kiện
invoice.*. - Nhận giao dịch không cần tạo đơn.