# Order lines and tax

What Kustom validates on the order_lines array — the rules and limits on each field, tax amounts that have to reconcile across every line and against the order totals, and how to anonymize product names that would reveal special category data about the customer.

`order_lines` is the part of a Kustom order that fails most often, because it's the part
Kustom checks arithmetic on. Tax has to reconcile across every line and against the order
totals, and product names travel further than merchants expect — onto the confirmation page
and into payment method communications. This page collects the rules for both.

## The fields on an order line

Every line is validated on its own before the order's totals are checked. These are the rules
and limits for each field:

| Field | Required | Rules |
| --- | --- | --- |
| `type` | No | `physical` (the default), `digital`, `discount`, `shipping_fee`, `sales_tax`, `gift_card`, `store_credit` or `surcharge` |
| `reference` | No | Your own product id. Up to 255 characters |
| `name` | Yes | 1 to 255 characters. Shown to the customer, so see [anonymizing](#anonymizing-product-names-that-reveal-special-category-data) |
| `quantity` | Yes | A whole number, 0 or more |
| `quantity_unit` | No | 1 to 8 characters, such as `pcs` or `kg` |
| `unit_price` | Yes | Minor units, tax included, before any discount. At most 100000000 |
| `total_discount_amount` | No | Minor units, tax included. 0 or more |
| `total_amount` | Yes | `quantity × unit_price − total_discount_amount`. At most 100000000 |
| `tax_rate` | Yes | Two implicit decimals, from 0 to 10000. `2500` is 25% |
| `total_tax_amount` | Yes | Within ±1 of `total_amount − total_amount × 10000 / (10000 + tax_rate)`. Negative on a `discount` line |
| `merchant_data` | No | Up to 1024 characters of your own, returned on the order |
| `product_url`, `image_url` | No | Up to 1024 characters each |

## How Kustom validates tax on an order

Kustom checks the tax on every order it receives and rejects the order if your numbers don't
add up. Three fields are checked against each other:

| Field | Level | Must satisfy |
| --- | --- | --- |
| `tax_rate` | Order line | The rate applied to that line, as a percentage with two implicit decimals. `2500` is 25% |
| `total_tax_amount` | Order line | The tax contained in that line's `total_amount` |
| `order_tax_amount` | Order | The sum of every line's `total_tax_amount`, plus the `total_amount` of any `sales_tax` line |

A mismatch comes back as `400 BAD_VALUE` naming the exact field —
`order_lines[2].total_tax_amount`, `order_tax_amount`, `order_lines[0].tax_rate`. See
[Troubleshooting Checkout](/checkout/troubleshooting/) for how to read those
messages. When the numbers were right a moment ago and are wrong now, compare the last
successful update against the failing request: discounts, coupons, shipping fees and
gift cards are the usual culprits, because each of them adds or changes a line.

Amounts are always in minor units. `25000` is 250.00 SEK, never 25000 SEK.

Field-by-field definitions live in the [Checkout API reference](/api/checkout/).

## Recalculating tax when the shipping address changes

Sales tax often depends on where the goods are going, and at the moment you create the
order you may not know. So don't guess: create the order with a tax figure you can
defend — frequently zero, or a default `sales_tax` line — and correct it once the customer
supplies an address.

That correction happens through the `address_update` callback. Register it in
`merchant_urls`, and Kustom will `POST` you the current `order_amount`,
`order_tax_amount`, `order_lines`, `billing_address` and `shipping_address` whenever the
customer enters or edits an address. Reply within 10 seconds with the recalculated cart,
the valid shipping options or both. A late reply, a non-`200` or an invalid cart stops
the purchase and shows the customer an error.

The mechanics are on [Server-side callbacks](/checkout/callbacks/). What matters
here is what you change in the response:

- Recompute sales tax for the new address.
- Add or update the `sales_tax` line, if you use one.
- Add or update the `shipping_fee` line for the cost of delivery to that address.
- Re-derive `order_amount` and `order_tax_amount` so they still reconcile.

If the customer enters only one address, it's used as both the billing and the shipping
address. If your shipping selector lives inside the
checkout iframe, Kustom adds the `shipping_fee` line and moves the totals for you — unless you
register `shipping_option_update`, which hands that line to you. If it lives
on your own page, Kustom knows nothing about the choice — put the line and the new totals into
the cart yourself.

### Using SmartPost as a shipping option

SmartPost needs two things from the checkout: the pickup point address and the phone
number the PIN is sent to. Send the SmartPost address to Kustom as a shipping option
before the checkout loads, so Kustom can pass it to the payment method for risk
assessment, and use the phone number the customer enters in the checkout for the PIN.

Turn off separate shipping and billing addresses for these transactions. Otherwise the
customer is asked for a shipping address they have already chosen as a pickup point.

## Using a sales_tax order line

This is the tax-exclusive pattern US sales tax needs, not the European default. Everywhere
Kustom operates in Europe, tax is *included* in each line's `total_amount` and carried on the
line as `total_tax_amount` — the table above. Where you need control over rounding,
jurisdiction rules or how tax is presented, put the tax in its own order line of type
`sales_tax` and set `tax_rate` to `0` on the goods lines.

A `sales_tax` line is constrained: `quantity` must be `1`, and `tax_rate`, `total_tax_amount`
and `total_discount_amount` must all be `0`. Its `total_amount` is what counts toward
`order_tax_amount`, which is why the example below balances with every `total_tax_amount` at
zero.

```json
{
  "order_amount": 11900,
  "order_tax_amount": 1900,
  "order_lines": [
    {
      "type": "physical",
      "name": "Product 1",
      "quantity": 1,
      "unit_price": 10000,
      "tax_rate": 0,
      "total_amount": 10000,
      "total_tax_amount": 0
    },
    {
      "type": "sales_tax",
      "reference": "SALES_TAX",
      "name": "Sales Tax 19%",
      "quantity": 1,
      "unit_price": 1900,
      "tax_rate": 0,
      "total_amount": 1900,
      "total_tax_amount": 0
    }
  ]
}
```

The pattern is deliberate: the tax is a line item priced at the tax amount, and every
line's `total_tax_amount` is zero, so the only place tax appears is that one line. Mixing
the two styles in the same order is what produces `order_tax_amount` mismatches.

## Anonymizing product names that reveal special category data

`order_lines[].name` reaches Kustom, appears on the customer's confirmation page and turns
up in post-purchase communications from payment methods. For most catalogues that's
harmless. If your catalogue can reveal something about the customer that GDPR Article 9
treats as a special category, sending the real product name is a compliance problem — and
you're the data controller for what you send.

| Article 9 category | Product names that can reveal it | Typical merchants |
| --- | --- | --- |
| Health data | Prescription medication, diagnostic tests, mobility aids, incontinence products, hearing aids | Pharmacies, medical supply, health and wellness |
| Sex life or sexual orientation | Intimacy products, sexual health items, LGBTQ+ themed merchandise | Sexual wellness, adult retail |
| Religious or philosophical beliefs | Religious texts, prayer items, explicitly halal or kosher labelled supplements, faith-based courses | Religious bookshops, specialty food, online courses |
| Racial or ethnic origin | Products whose name alone reveals a specific heritage | Cultural goods, heritage food, specialist cosmetics |
| Political opinions | Memberships, merchandise or publications tied to a named party or movement | Political organizations, media, advocacy groups |
| Trade union membership | Union membership packages, union-branded merchandise | Unions, professional associations |
| Genetic data | DNA testing kits, genetic screening services | Health tech, consumer diagnostics |
| Biometric data | Fingerprint or facial recognition enrollment services | Security tech, access control |

If you're unsure whether a product qualifies, anonymize it. Over-anonymizing costs you a
little operational clarity. Under-anonymizing is a compliance failure.

### How to anonymize a line

Replace `name` with a generic label and keep your own product id in `reference`, so you lose
nothing you need for reconciliation, fulfillment or disputes.

Avoid — this exposes health data:

```json
{
  "name": "Sertraline 50mg Tablets - 28 pack",
  "reference": "100482",
  "quantity": 1,
  "unit_price": 18900,
  "total_amount": 18900,
  "tax_rate": 0,
  "total_tax_amount": 0
}
```

Send this instead:

```json
{
  "name": "Health product (anonymized)",
  "reference": "100482",
  "quantity": 1,
  "unit_price": 18900,
  "total_amount": 18900,
  "tax_rate": 0,
  "total_tax_amount": 0
}
```

In a mixed cart, anonymize only the sensitive lines. Ordinary products keep their real
names.

| Product category | Recommended `name` |
| --- | --- |
| Pharmacy, prescription or OTC medication | `Health product (anonymized)` |
| Mental health | `Health product (anonymized)` |
| Addiction support | `Health product (anonymized)` |
| Genetic testing or diagnostics | `Health product (anonymized)` |
| Sexual wellness | `Wellness product (anonymized)` |
| Disability or care aids | `Care product (anonymized)` |
| Religious or faith-based goods | `Product (anonymized)` |
| Ethnicity or cultural identity goods | `Product (anonymized)` |
| Political membership or publications | `Product (anonymized)` |
| Trade union membership or branded goods | `Product (anonymized)` |

Keep the word **(anonymized)** in the label. It tells your own operations team, and anyone
reviewing the order, that the label was chosen deliberately rather than being a data error. A
bare "product" or "item" reads as an integration bug.

### Implementing it, and living with it

Flag sensitive products in your catalogue — a boolean such as `is_sensitive` — and swap
the name in the code that builds the Kustom payload, not in your database. Keep the real
name in your own systems. You need it for fulfillment, returns and disputes. Then place a
test order in Playground and confirm the anonymized label is what appears on the
confirmation page and in Developer Logs.

Two consequences to plan for. Customers will see the generic label on their payment
confirmation, so say so somewhere they will read it — a line in your order confirmation
email such as "For your privacy, sensitive product names are not shown in payment
confirmations" is usually enough. And if a dispute arises, the evidence you submit through
the Kustom Portal is separate from the API payload: reference your own records, including
the real product name, in the evidence itself. Your product id in `reference` is what lets you
look it up.

## Related

- [Server-side callbacks](/checkout/callbacks/) — recalculate the cart when the
  shipping address changes.
- [Troubleshooting Checkout](/checkout/troubleshooting/) — decode a `BAD_VALUE`
  naming an order line field.
- [Saved payment methods](/checkout/saved-payment-methods/) — the token a recurring
  order is charged against.
- [Create an order](/checkout/integrate/create-order/) — where `order_lines` goes on the request.
- [Checkout API reference](/api/checkout/) — every order line field and its
  constraints.
