# E-invoices

Ha Tang issues e-invoices (hóa đơn điện tử) under your own tax code, through your own account at the e-invoice provider. Add an `invoice` block to a payment and the invoice is created once the order is paid; or create one yourself for any payment or bank transfer. This page covers connecting the provider, the invoice lifecycle, fixing failures, and reconciling money in against invoices.

## Before you start

- A Mắt Bão e-invoice account with API access. The tax code, API user and password are under "Cấu hình API" in the Mắt Bão portal.
- At least one registered template and series (mẫu số and ký hiệu) on that account.
- An API key with `payments:write`. E-invoices use the Payments scopes and environments.

In `development`, connect Mắt Bão's demo sandbox account: invoices are real sandbox invoices but never reach the tax office.

## 1. Connect the provider

Do it once per environment, in the dashboard (**Payments settings → Invoice providers**) or through the API. First check the credentials and list the account's templates; nothing is saved:

**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 }
  ]
}
```

Then connect with the template you chose. The body:

| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
| `provider` | string | Yes | – | `matbao` (`GET /einvoices/providers` lists them) |
| `tax_code`, `username`, `password` | string | Yes for `matbao` | – | The seller's tax code and the provider API account; stored encrypted |
| `pattern` | string, 1 to 6 | Yes | – | Default template for new invoices; must be one of the account's |
| `serial` | string, exactly 6 | Yes | – | Default series, e.g. `C26TAT` |
| `default_mode` | `draft` \| `publish` | No | `draft` | What happens when a paid order asks for an invoice without `mode`: `draft` stops at the draft, `publish` also sends it for approval |
| `is_default` | boolean | No | `true` for the first provider | New invoices go to the default provider |
| `import_external` | boolean | No | `false` | Also bring in invoices created directly on the provider's site |
| `label` | string, up to 100 | No | – | Your name for the connection |

OpenAPI schema: [`POST /einvoices/connections`](https://hatang.dev/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" }'
```

The answer (`201`) is the connection, with a `callback_url`. Send that URL to Mắt Bão support so that changes made on their side reach Ha Tang at once; without it, Ha Tang checks an invoice still pending an hour after it was created every 15 minutes, for up to 7 days. A draft you keep on purpose (`mode: "draft"`) is not checked: call `POST /einvoices/{id}/sync` to bring in edits made on the provider's site. Credentials are stored encrypted. If the provider starts refusing them, the connection becomes `needs_reauth` and invoices pause until you save new ones with `PATCH /einvoices/connections/{id}`.

| Endpoint | Does |
|---|---|
| `GET /einvoices/providers` | Providers you can connect, with the fields each needs |
| `GET /einvoices/connections` / `GET /einvoices/connections/{id}` | Your connections |
| `PATCH /einvoices/connections/{id}` | Change the default mode, template, credentials, label |
| `POST /einvoices/connections/{id}/default` | Make it the default provider |
| `GET /einvoices/connections/{id}/templates` | The templates the account may issue on, read live |
| `POST /einvoices/connections/{id}/sync` | Pull changes from the provider now |
| `DELETE /einvoices/connections/{id}` | Unlink. Its invoices stay; the next provider becomes the default |

## 2. Add an invoice to a payment

Add an `invoice` block when you [create the payment](https://hatang.dev/docs/guides/payments/create-payment.md). Nothing is sent to the provider until the payment is `paid` or `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": "Order #1002",
    "ipn_url": "https://shop.example.com/hatang/ipn",
    "metadata": { "order_id": "1002" },
    "invoice": {
      "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"
      },
      "items": [{ "name": "September service plan", "unit": "plan", "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: "Order #1002",
  ipn_url: "https://shop.example.com/hatang/ipn",
  invoice: {
    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" },
    items: [{ name: "September service plan", unit: "plan", quantity: 1, unit_price: 250_000, vat_rate: 10 }],
    payment_method: "CK",
    send_email: true,
  },
});
```

**Python**

```python
payment = hatang.payments.create({
    "amount": 275000,
    "description": "Order #1002",
    "ipn_url": "https://shop.example.com/hatang/ipn",
    "metadata": {"order_id": "1002"},
    "invoice": {
        "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"},
        "items": [{"name": "September service plan", "unit": "plan", "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' => 'Order #1002',
        'ipn_url' => 'https://shop.example.com/hatang/ipn',
        'metadata' => ['order_id' => '1002'],
        'invoice' => [
            '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'],
            'items' => [['name' => 'September service plan', 'unit' => 'plan', 'quantity' => 1, 'unit_price' => 250000, 'vat_rate' => 10]],
            'payment_method' => 'CK',
            'send_email' => true,
        ],
    ],
])->getBody(), true);
```

| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
| `mode` | `draft` \| `publish` | No | the connection's `default_mode` | `draft` stops at the draft, which you review and send with `POST /einvoices/{id}/publish`; `publish` creates the draft and sends it for approval (signing) |
| `template` | object `{ pattern, serial }` | No | the connection's | `pattern`: mẫu số, 1 to 6 characters; `serial`: ký hiệu, exactly 6 |
| `buyer` | [buyer](#buyer) | Yes | – | Needs a `name` or a `legal_name` |
| `items` | array of [line items](#line-item), 1 to 200 | Yes | – | The lines on the invoice |
| `payment_method` | string, up to 50 | No | `TM/CK` | How it was paid, printed on the invoice |
| `note` | string, up to 500 | No | – | A note, printed on the invoice |
| `send_email` | boolean | No | `false` | Email the issued invoice to the buyer (at `buyer.email`) |

OpenAPI schema: [`POST /payments`](https://hatang.dev/docs/reference/api/post-payments), [`POST /einvoices`](https://hatang.dev/docs/reference/api/post-einvoices)

> **Caution: Totals**
>
> Make the invoice total (after VAT) equal the payment `amount`: here 250,000 + 10% = 275,000. Ha Tang does not check it. Each line is rounded to the đồng on its own; when your prices include VAT, give each line's `vat_amount` so the total lands on the đồng. See [Totals and VAT rounding](#totals).

## 3. What happens after payment {#after-payment}

Every invoice takes the same two steps, create the draft and send it for approval, each queued and retried on its own:

```mermaid
flowchart LR
    A[waiting_for_payment] -->|paid| 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` | Meaning |
|---|---|
| `waiting_for_payment` | Not paid yet; nothing sent (payment summary only) |
| `creating` | Paid; the draft is being created at the provider |
| `draft` | The draft exists. Stops here in `draft` mode |
| `publishing` | Sent for approval (being signed) |
| `issued` | Signed: it has a `number` and a `lookup_code`; waiting for the tax office |
| `tax_accepted` / `tax_rejected` | The tax office's answer (`tax_authority_code` when accepted) |
| `failed` | The provider refused a step: `error` says why and `failed_step` which step |
| `canceled` / `replaced` / `adjusted` | Changed afterwards, on the provider's site or by a replacement |
| `deleted` | A draft that was deleted |

The payment's `invoice` field ([its fields](#payment-invoice)) always shows where its invoice stands, and every change is sent as an IPN call and as `invoice.*` webhooks:

```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
}
```

If the payment is paid while no provider is connected in that environment, `invoice.status` is `failed` with `id: null` and the error "No e-invoice provider is connected in this environment".

The checkout page shows the payer only whether an invoice is coming and, once issued, its number and lookup code.

## Review drafts before they are signed

With `mode: "draft"`, the invoice stops at `draft`. You can then:

| Call | Does |
|---|---|
| `PATCH /einvoices/{id}` | Edit `buyer`, `items`, `payment_method`, `note` |
| `POST /einvoices/{id}/publish` | Send it for approval |
| `DELETE /einvoices/{id}` | Delete the draft |

Only a draft can be edited or sent for approval; anything else answers `409 invalid_state`. Even `mode: "publish"` never issues in one call: approval is always a separate, queued step.

## When a step fails

Timeouts and provider outages are retried automatically. A refusal (for example an invalid buyer tax code) stops the invoice at `failed` with the provider's message:

1. Fix what the message says. When it failed while creating the draft (`failed_step: "create_draft"`), correct the buyer or the items with `PATCH /einvoices/{id}`.
2. `POST /einvoices/{id}/retry` runs the same step again.
3. `GET /einvoices/{id}/history` lists every status change, oldest first, with which side made it (`source`) and when.

```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" }
  ]
}
```

## Invoices without a payment {#invoices-without-a-payment}

`POST /einvoices` takes the same fields as the `invoice` block, plus:

| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
| `payment_id` | string `pay_…` | No | – | Link it to a payment. A payment has at most one invoice; a second answers `409 invalid_state` |
| `transaction_id` | string `txn_…` | No | – | Link it to a bank transfer received [without an order](https://hatang.dev/docs/guides/payments/transactions.md) |
| `connection_id` | string `icn_…` | No | the default connection | The provider connection that issues it |

OpenAPI schema: [`POST /einvoices`](https://hatang.dev/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": "Nguyen Van Khach" },
    "items": [{ "name": "Coffee beans 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: "Nguyen Van Khach" },
  items: [{ name: "Coffee beans 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": "Nguyen Van Khach"},
    "items": [{"name": "Coffee beans 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' => 'Nguyen Van Khach'],
    'items' => [['name' => 'Coffee beans 1 kg', 'quantity' => 1, 'unit_price' => 91667, 'vat_rate' => 8]],
]])->getBody(), true);
```

The answer (`201`) is the [invoice object](#einvoice-object) (`object: "einvoice"`) in `creating`, with `buyer`, `items` and the computed `totals` (`subtotal`, `vat`, `total`). Follow it with `GET /einvoices/{id}` or the `invoice.*` webhooks. `GET /einvoices` lists invoices, filtered by `status`, `payment_id` or `sync_state`.

## Replace, cancel and conflicts

- **Replace.** An invoice sent to the tax office (`issued`, `tax_accepted`, `tax_rejected`) cannot be canceled or deleted through Ha Tang. `POST /einvoices/{id}/replace` with a `reason`, the corrected `buyer` and `items` creates a replacement: a draft that names the original, then sent for approval. The original becomes `replaced` once the provider has the replacement.
- **Delete** a draft (or a failed invoice) with `DELETE /einvoices/{id}`.
- **Changes on the provider's site** come back to Ha Tang: signing, the tax office's code, cancellation, replacement, draft edits and deletions. Their events carry `source: "provider"`. Invoices created directly on the provider's site are brought in only when the connection has `import_external: true`.
- **Conflicts.** A draft edited both in Ha Tang and on the provider's site gets `sync_state: "conflict"` and the `invoice.conflict` event. Settle it with `POST /einvoices/{id}/resolve` and `{ "keep": "hatang" }` or `{ "keep": "provider" }`. Editing or publishing is refused until then.
- **Check now.** `POST /einvoices/{id}/sync` asks the provider about one invoice right away.

## Reconcile money in against invoices

Every transfer into your accounts should end up with an invoice, unless it needs none. A transfer counts as *invoiced* when an invoice in force (not failed, deleted, canceled or tax-rejected) points at it, directly or through the payment it paid. Otherwise it is *missing*, unless you marked it *exempt*.

```bash
curl "https://api.hatang.dev/einvoices/reconciliation?from=2026-09-01&to=2026-09-30" \
  -H "Authorization: Bearer $HATANG_API_KEY"
```

`from` and `to` are days in Vietnam time (`YYYY-MM-DD`), at most 400 days apart; the default is the last 30 days. The answer has `totals` (`money_in`, `invoiced`, `missing`, `exempt`, each with `count` and `amount`), one row per day, and the `missing` and `exempt` transfers. The dashboard shows the same report under **Payments → Invoice reconciliation**.

Mark a transfer as needing no invoice (a refund, money between your own accounts), or undo it:

```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": "Transfer between our own accounts" }'

curl -X DELETE https://api.hatang.dev/payments/transactions/txn_123/invoice-exempt \
  -H "Authorization: Bearer $HATANG_API_KEY"
```

Owners and admins of a business with an active provider also get a daily email at 08:00 (Vietnam time) listing yesterday's money in that still has no invoice, and an email when an invoice fails, is rejected by the tax office, is in conflict, or has not reached the tax office an hour after it was created. A draft you keep on purpose (`mode: "draft"`) is not mailed about.

## Objects and how they link {#objects}

Three objects take part when an order is paid and invoiced: the **payment** you create, the **transaction** the bank reports, and the **e-invoice**. Each points at the others by id, so you can start from whichever one an event hands you.

```mermaid
flowchart LR
    O["Your order"] -->|"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/>(no payment)"| T
    I -->|"reference"| M["Invoice at the provider"]
```

| Keep in your order | Comes from | Use it to |
|---|---|---|
| Payment `id` (`pay_…`) | The `POST /payments` answer | `GET /payments/{id}`; it is `data.id` in every IPN call |
| Invoice `id` (`einv_…`) | The payment's `invoice.id` once the invoice exists (`null` before the payment is paid), or the `POST /einvoices` answer | `GET`, `PATCH`, `publish`, `retry`, `replace` on `/einvoices/{id}`; it is `data.id` in `invoice.*` webhooks |
| `number`, `series`, `lookup_code` | The invoice, once `issued` | Show the buyer their invoice and where to look it up |
| `reference` | The invoice, from the start | Ha Tang's reference for it at the provider; find the invoice on the provider's site by it |

- **Through a payment.** Put an `invoice` block in `POST /payments`, or call `POST /einvoices` with `payment_id`. A payment has one invoice in force; a replacement keeps the same `payment_id`, and the payment's `invoice` field shows the one that currently stands. `GET /einvoices?payment_id=pay_…` lists all of them.
- **Through a transfer, without a payment.** If you only receive `transaction.received` webhooks, call `POST /einvoices` with that transfer's `transaction_id`.
- **Events.** An IPN call carries the payment, with the invoice's state in `data.invoice`. An `invoice.*` webhook carries the whole invoice object, plus `source` (`hatang`, or `provider` when the change was made on the provider's site).

### The invoice request {#invoice-request}

The same object, with the same rules, is what you send in four places:

| Where | What it takes |
|---|---|
| `invoice` in `POST /payments` | Everything below |
| `POST /einvoices` | Everything below, plus `payment_id`, `transaction_id`, `connection_id` |
| `PATCH /einvoices/{id}` (a draft) | `buyer`, `items`, `payment_method`, `note`; what you send replaces what was there; all optional, send only what changes |
| `POST /einvoices/{id}/replace` | `reason`, `buyer`, `items`, `payment_method`, `note` |

A complete request, a company buyer with two lines (one with its VAT typed in, one not subject to VAT):

```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
}
```

Its totals: subtotal 65,455, VAT 4,545, total 70,000.

| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
| `mode` | `draft` \| `publish` | No | the connection's `default_mode` | `draft` stops at the draft, which you review and send with `POST /einvoices/{id}/publish`; `publish` creates the draft and sends it for approval (signing) |
| `template` | object `{ pattern, serial }` | No | the connection's | `pattern`: mẫu số, 1 to 6 characters; `serial`: ký hiệu, exactly 6 |
| `buyer` | [buyer](#buyer) | Yes | – | Needs a `name` or a `legal_name` |
| `items` | array of [line items](#line-item), 1 to 200 | Yes | – | The lines on the invoice |
| `payment_method` | string, up to 50 | No | `TM/CK` | How it was paid, printed on the invoice |
| `note` | string, up to 500 | No | – | A note, printed on the invoice |
| `send_email` | boolean | No | `false` | Email the issued invoice to the buyer (at `buyer.email`) |

OpenAPI schema: [`POST /payments`](https://hatang.dev/docs/reference/api/post-payments), [`POST /einvoices`](https://hatang.dev/docs/reference/api/post-einvoices)

#### Buyer {#buyer}

Give `name`, `legal_name` or both. For a company, `legal_name` is the company and `name` the person who bought; for a person, `name` alone.

| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
| `name` | string, up to 200 | One of the two | – | The person buying, printed as "Họ tên người mua hàng" (buyer's name) |
| `legal_name` | string, up to 400 | One of the two | – | The company's registered name, printed as "Tên đơn vị" (company name). Leave it out for a person |
| `tax_code` | 10 to 14 digits or dashes | No | – | The buyer's tax code (mã số thuế), e.g. `0109876543` or `0109876543-001` |
| `address` | string, up to 400 | No | – | The buyer's address |
| `email` | email | No | – | Where `send_email` sends the invoice |
| `phone` | string, up to 20 | No | – | The buyer's phone number |
| `buyer_code` | string, up to 50 | No | – | Your own customer code, printed on the invoice |

OpenAPI schema: [`POST /payments`](https://hatang.dev/docs/reference/api/post-payments), [`POST /einvoices`](https://hatang.dev/docs/reference/api/post-einvoices)

#### Line item {#line-item}

| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
| `name` | string, 1 to 500 | Yes | – | What was sold |
| `quantity` | number above 0 | Yes | – | Decimals allowed, e.g. `1.5` kg |
| `unit_price` | number, 0 or more | Yes | – | Price of one unit **before VAT**, in đồng. Decimals allowed |
| `vat_rate` | `10` \| `8` \| `5` \| `0` \| `-1` \| `-2` | No | `10` | `-1`: not subject to VAT (KCT); `-2`: not declared (KKKNT) |
| `vat_amount` | whole đồng, 0 or more | No | worked out from `vat_rate` | The line's VAT, used as given. Only with a `vat_rate` above 0. See [Totals and VAT rounding](#totals) |
| `discount` | number, 0 or more | No | `0` | Discount for the whole line, in đồng before VAT |
| `unit` | string, up to 50 | No | – | Unit, e.g. `kg`, `plan` |
| `code` | string, up to 50 | No | – | Your product code (SKU) |

OpenAPI schema: [`POST /payments`](https://hatang.dev/docs/reference/api/post-payments), [`POST /einvoices`](https://hatang.dev/docs/reference/api/post-einvoices)

In the invoice object every line comes back with what you sent plus three amounts: `amount` (before VAT), `vat_amount` and `total`.

### Totals and VAT rounding {#totals}

The đồng has no smaller unit, so every line is rounded on its own, and the invoice adds up the rounded lines. That is also what the provider prints.

| Amount | Worked out as |
|---|---|
| Line `amount` | `quantity × unit_price − discount`, rounded to the đồng |
| Line `vat_amount` | `vat_amount` when you give it; otherwise `amount × vat_rate / 100`, rounded; `0` when `vat_rate` is `0`, `-1` or `-2` |
| Line `total` | `amount + vat_amount` |
| `totals.subtotal` / `totals.vat` / `totals.total` | The sum of the lines' `amount` / `vat_amount` / `total` |

Halves round up. Ha Tang does not compare the invoice total with the payment `amount`: make them equal yourself.

**When prices include VAT.** Shops usually know what the buyer pays, VAT included, and work the price before VAT back from it. Rounding that price and then the VAT can land a đồng off. 50,000 đồng at 10%:

| | `unit_price` | `vat_amount` | `total` |
|---|---|---|---|
| VAT worked out by Ha Tang | 45,455 | 4,546 (45,455 × 10%, rounded) | **50,001** |
| VAT given as `vat_amount` | 45,455 | 4,545 | **50,000** |

So give each line's `vat_amount` as what is left of the price paid once the price before VAT is rounded:

```ts
// gross: what the buyer pays for the whole line, VAT included, in đồ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 }
```

Rules for `vat_amount`:

- Whole đồng, 0 or more. `4545.5` is refused with `422 validation_error`; nothing is rounded for you.
- Only on a line whose `vat_rate` is above 0; on a `0`, `-1` or `-2` line it is refused with `422 validation_error`.
- Ha Tang does not check it against `vat_rate`; keep it to the rounding difference you need.
- It is sent to the provider as the line's VAT, and kept when the invoice is read back from the provider, so a later sync or an edit on the provider's site does not turn it into a conflict.
- In the dashboard, the **VAT amount** field on **New invoice** takes the same value, written `4545`, `4.545` or `4,545`.

### The einvoice object {#einvoice-object}

What `POST /einvoices` and `GET /einvoices/{id}` answer, and the `data` of every `invoice.*` webhook. The invoice from the request above (first line only), paid and issued:

```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"
}
```

| Field | Type | Can be `null` | Meaning |
|---|---|---|---|
| `object` | string | No | Always `einvoice` |
| `id` | string | No | `einv_…` |
| `environment` | `development` \| `production` | No | The environment of the API key that created it |
| `connection_id` | string | No | The provider connection that issues it (`icn_…`) |
| `provider` | string | Yes | The provider, e.g. `matbao` |
| `payment_id` | string | Yes | The payment it is for |
| `transaction_id` | string | Yes | The bank transfer it is for, when there is no payment |
| `origin` | `hatang` \| `provider` | No | `hatang` when created through Ha Tang; `provider` when brought in from the provider's site (`import_external`) |
| `mode` | `draft` \| `publish` | No | As requested |
| `status` | string | No | Where it stands: see [What happens after payment](#after-payment) |
| `sync_state` | `in_sync` \| `pending_push` \| `conflict` | No | `pending_push` while a change is on its way to the provider; `conflict` when the draft was edited on both sides |
| `version` | integer | No | Goes up by one with every change made through Ha Tang |
| `buyer` | [buyer](#buyer) | No | As sent; only the fields you sent |
| `items` | array of [line items](#line-item) | No | As sent, each line with its computed `amount`, `vat_amount` and `total` |
| `totals` | object `{ subtotal, vat, total }` | No | In đồng |
| `currency` | string | No | `VND` |
| `payment_method`, `note` | string | Yes | As sent |
| `template` | object `{ pattern, serial }` | No | The template and series it is issued on |
| `reference` | string, up to 20 | No | Ha Tang's reference for it at the provider |
| `provider_invoice_id` | string | Yes | The provider's own id, once the draft exists |
| `number` | string | Yes | The invoice number, once `issued` |
| `series` | string | Yes | Template and series together, e.g. `1C26TAT` |
| `lookup_code` | string | Yes | What the buyer types on the provider's lookup page |
| `tax_authority_code` | string | Yes | The tax office's code, once `tax_accepted` |
| `pdf_url` | URL | Yes | The provider's PDF, once there is one |
| `issued_at` | ISO 8601 string | Yes | The date on the invoice |
| `related_invoice_id` | string | Yes | On a replacement: the invoice it replaces |
| `relation` | `replaces` \| `adjusts` | Yes | On a replacement (`replaces`) or an adjustment (`adjusts`) |
| `conflict` | object | Yes | With `sync_state: "conflict"`: the provider's version, under `content` |
| `error` | string | Yes | Why it failed |
| `failed_step` | `create_draft` \| `update_draft` \| `publish` \| `delete_draft` | Yes | The step that failed |
| `created_at`, `updated_at` | ISO 8601 string | No |  |

OpenAPI schema: [`GET /einvoices/{id}`](https://hatang.dev/docs/reference/api/get-einvoices-by-id)

### The payment's invoice summary {#payment-invoice}

The payment object keeps a short copy of its invoice in `invoice`, so an IPN handler sees the invoice without another call. It is `null` when no invoice was asked for and none exists.

| Field | Type | Can be `null` | Meaning |
|---|---|---|---|
| `requested` | boolean | No | `true` when the payment was created with an `invoice` block |
| `mode` | `draft` \| `publish` | Yes | As requested |
| `status` | string | No | As on the invoice, plus `waiting_for_payment` before the payment is paid. `failed` with `id: null` when it was paid while no provider was connected |
| `id` | string | Yes | The invoice's `einv_…` id, `null` until it exists |
| `number`, `series`, `template`, `lookup_code`, `tax_authority_code`, `pdf_url`, `sync_state`, `issued_at`, `error`, `failed_step` | as on the invoice | Yes | As on [the invoice](#einvoice-object); `null` until the invoice exists |

OpenAPI schema: [`GET /payments/{id}`](https://hatang.dev/docs/reference/api/get-payments-by-id)

To read the lines and totals, fetch the invoice: `GET /einvoices/{invoice.id}`.

## Errors

| Error | When |
|---|---|
| `422 validation_error` | A field is wrong (buyer without a name, bad tax code, more than 200 items), or the template is not registered on the account |
| `422 missing_required_field` | A provider credential is missing |
| `422 invoice_provider_error` | The provider refused the request; the message says why |
| `409 invalid_state` | No provider connected, the payment already has an invoice, the invoice is not in a state that allows this call, or a conflict must be resolved first |
| `403 environment_mismatch` | The provider is not available in this environment |
| `503 provider_unavailable` | The provider did not answer; try again |

## Next steps

- [Receive payment results](https://hatang.dev/docs/guides/payments/payment-results.md): the IPN calls that carry the invoice.
- [Webhooks](https://hatang.dev/docs/guides/payments/webhooks.md): every `invoice.*` event.
- [Receive transfers without orders](https://hatang.dev/docs/guides/payments/transactions.md).

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