# 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**

```bash
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": "..." }'
```

**Node.js**

```ts
const { data: check, error } = await hatang.einvoices.connections.check({
  provider: "matbao",
  tax_code: "0109876543",
  username: "api-user",
  password: process.env.MATBAO_PASSWORD!,
});
```

**Python**

```python
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()
```

**PHP**

```php
$check = json_decode($client->post('/einvoices/connections/check', ['json' => [
    'provider' => 'matbao', 'tax_code' => '0109876543', 'username' => 'api-user', 'password' => getenv('MATBAO_PASSWORD'),
]])->getBody(), true);
```

```json
{
  "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`](https://hatang.dev/vi/docs/reference/api/post-einvoices-connections)

```bash
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](https://hatang.dev/vi/docs/guides/payments/create-payment.md). 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**

```bash
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": "ketoan@khach.vn"
      },
      "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
    }
  }'
```

**Node.js**

```ts
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: "ketoan@khach.vn" },
    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,
  },
});
```

**Python**

```python
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",
                  "address": "1 Tràng Tiền, Hà Nội", "email": "ketoan@khach.vn"},
        "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")
```

**PHP**

```php
$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',
                        'address' => '1 Tràng Tiền, Hà Nội', 'email' => 'ketoan@khach.vn'],
            '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](#buyer) | Có | – | Cần `name` hoặc `legal_name` |
| `items` | mảng [dòng hàng](#line-item), 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`](https://hatang.dev/vi/docs/reference/api/post-payments), [`POST /einvoices`](https://hatang.dev/vi/docs/reference/api/post-einvoices)

> **Caution: Tổng tiền**
>
> 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](#totals).

## 3. Sau khi thanh toán {#after-payment}

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:

```mermaid
flowchart LR
    A[waiting_for_payment] -->|đã trả| B[creating]
    B --> C[draft]
    C -->|mode publish| D[publishing]
    D --> E[issued]
    E --> F[tax_accepted]
    E --> G[tax_rejected]
    B -.-> X[failed]
    D -.-> X
```

| `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](#payment-invoice)) 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.*`:

```json
"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:

1. 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ằng `PATCH /einvoices/{id}`.
2. `POST /einvoices/{id}/retry` chạy lại đúng bước đó.
3. `GET /einvoices/{id}/history` liệ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.

```json
{
  "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 {#invoices-without-a-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](https://hatang.dev/vi/docs/guides/payments/transactions.md) |
| `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`](https://hatang.dev/vi/docs/reference/api/post-einvoices)

**cURL**

```bash
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 }]
  }'
```

**Node.js**

```ts
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 }],
});
```

**Python**

```python
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()
```

**PHP**

```php
$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](#einvoice-object) (`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}/replace` kèm `reason`, `buyer` và `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 sang `replaced` khi 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ật `import_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ện `invoice.conflict`. Giải quyết bằng `POST /einvoices/{id}/resolve` vớ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}/sync` hỏ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*.

```bash
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:

```bash
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 {#objects}

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.

```mermaid
flowchart LR
    O["Đơn hàng của bạn"] -->|"metadata.order_id"| P["payment<br/>pay_…"]
    T["transaction<br/>txn_…"] -->|"payment_id"| P
    P -->|"invoice.id"| I["einvoice<br/>einv_…"]
    I -->|"payment_id"| P
    I -->|"transaction_id<br/>(không có payment)"| T
    I -->|"reference"| M["Hóa đơn ở nhà cung cấp"]
```

| 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 `invoice` vào `POST /payments`, hoặc gọi `POST /einvoices` kèm `payment_id`. Mỗi payment có một hóa đơn đang hiệu lực; hóa đơn thay thế giữ nguyên `payment_id`, và trường `invoice` củ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ọi `POST /einvoices` kèm `transaction_id` củ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`. Webhook `invoice.*` mang nguyên object hóa đơn, kèm `source` (`hatang`, hoặc `provider` khi thay đổi đến từ trang của nhà cung cấp).

### Yêu cầu xuất hóa đơn {#invoice-request}

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ế):

```json
{
  "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",
    "email": "accounting@example.com",
    "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](#buyer) | Có | – | Cần `name` hoặc `legal_name` |
| `items` | mảng [dòng hàng](#line-item), 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`](https://hatang.dev/vi/docs/reference/api/post-payments), [`POST /einvoices`](https://hatang.dev/vi/docs/reference/api/post-einvoices)

#### Người mua {#buyer}

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` | 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`](https://hatang.dev/vi/docs/reference/api/post-payments), [`POST /einvoices`](https://hatang.dev/vi/docs/reference/api/post-einvoices)

#### Dòng hàng {#line-item}

| 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](#totals) |
| `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`](https://hatang.dev/vi/docs/reference/api/post-payments), [`POST /einvoices`](https://hatang.dev/vi/docs/reference/api/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 {#totals}

Đồ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ế:

```ts
// 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.5` bị từ chối với `422 validation_error`; Hạ Tầng không tự làm tròn.
- Chỉ dùng cho dòng có `vat_rate` lớn hơn 0; dòng `0`, `-1` hoặc `-2` gửi kèm sẽ bị từ chối với `422 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.545` hay `4,545` đều được.

### Object hóa đơn {#einvoice-object}

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:

```json
{
  "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",
    "email": "accounting@example.com"
  },
  "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](#after-payment) |
| `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](#buyer) | Không | Như đã gửi; chỉ có các trường bạn đã gửi |
| `items` | mảng [dòng hàng](#line-item) | 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}`](https://hatang.dev/vi/docs/reference/api/get-einvoices-by-id)

### Tóm tắt hóa đơn trên payment {#payment-invoice}

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](#einvoice-object); `null` cho tới khi có hóa đơn |

Schema OpenAPI: [`GET /payments/{id}`](https://hatang.dev/vi/docs/reference/api/get-payments-by-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ả](https://hatang.dev/vi/docs/guides/payments/payment-results.md): các lời gọi IPN mang thông tin hóa đơn.
- [Webhook](https://hatang.dev/vi/docs/guides/payments/webhooks.md): mọi sự kiện `invoice.*`.
- [Nhận giao dịch không cần tạo đơn](https://hatang.dev/vi/docs/guides/payments/transactions.md).

Source: https://hatang.dev/vi/docs/guides/payments/einvoices
