# Money

URL: /docs/concepts/money

How the API represents prices and totals as integer minor units, how to display them, and how tax-inclusive and tax-exclusive prices are labelled.

Every price, total and discount in the API is a money object with an integer amount in minor units. There are no floats and no decimal strings to parse. This page explains the shape, how to display it and how to send amounts back.

## The money object [#the-money-object]

```json title="A money object"
{ "amount": 8900, "currency": "GBP", "formatted": "£89.00" }
```

| Field | Type | Meaning |
|---|---|---|
| `amount` | integer | The value in the currency's minor unit: pence for GBP, cents for EUR and USD. `8900` is £89.00. |
| `currency` | string | The ISO 4217 currency code, such as `GBP`. |
| `formatted` | string | The amount ready to show, formatted for the UK (`£89.00`). |

The same shape is used everywhere: product prices, cart lines, shipping options, discounts, tax, order totals and search results. A money field that has no value is `null`, never `0` and never a missing object. For example, a variant has no price when the store has not set one in its currency, and cart totals are `null` until the cart has been calculated.

Merchant CRM money custom fields use the same idea without `formatted`: `{ "amount": 250000, "currency": "GBP" }`.

## Never use floats [#never-use-floats]

Floating-point numbers cannot hold most decimal fractions exactly, so `0.1 + 0.2` is not `0.3`. Keep amounts as integers from the moment they arrive until you display them.

- **Show `formatted`.** It is correct for the currency and already rounded.
- **Do not add up totals yourself.** Promotions, shipping discounts and tax all change the numbers. Read `sub_total`, `discount_total`, `tax_total` and `total` from the cart or order.
- **Compare and sort on `amount`**, never on `formatted`.
- **Send integers back.** Query parameters that take money, such as `filter[price_min]` and `filter[price_max]`, take integer minor units. Send `2500` for £25.00, never `25` or `25.00`.

## Display amounts in another locale [#display-amounts-in-another-locale]

If your shop shows prices in a format other than the UK one, format `amount` yourself. Divide by the currency's number of minor units, which `Intl.NumberFormat` already knows:

```ts title="Format a money object for any locale"
type Money = { amount: number; currency: string; formatted: string }

export function formatMoney(money: Money, locale = 'en-GB'): string {
  const formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: money.currency })
  const digits = formatter.resolvedOptions().maximumFractionDigits ?? 2
  return formatter.format(money.amount / 10 ** digits)
}

formatMoney({ amount: 8900, currency: 'GBP', formatted: '£89.00' }, 'de-DE') // "89,00 £"
```

Division here is only for display. Never feed the result back into arithmetic or into a request.

## Currency [#currency]

Each store trades in its own currency. You can read it from:

- `currency` on `GET /v1/checkout/config`;
- `currency` on every money object;
- `meta.currency` on search responses.

Do not assume GBP. Read the currency from the response and pass it to your formatter.

## Percentages [#percentages]

Percentages that describe money are integers in basis points, so they never need floats either. `percent_bp: 1250` is 12.5%. You will see them on a variant's `pricing.saving` and in a bulk-price table's rows.

Tax rates in a tax breakdown are the exception: `percentage` is a string, such as `"20"`, meant for display.

## Tax-inclusive and tax-exclusive prices [#tax-inclusive-and-tax-exclusive-prices]

Stores set, per tax zone, whether prices are shown including or excluding tax. A cart tells you which applies through `tax_zone`:

```json title="tax_zone on a cart"
{
  "tax_zone": {
    "id": 1,
    "name": "UK",
    "price_display": "tax_inclusive",
    "prices_include_tax": true
  }
}
```

| Field | Values |
|---|---|
| `price_display` | `tax_inclusive` ("Including VAT") or `tax_exclusive` ("Excluding VAT") |
| `prices_include_tax` | The same, as a boolean |

The zone is the shipping address's, or the store's default zone before an address is set. `tax_zone` is `null` when the store has no tax zone.

This setting is for labelling. The amounts are already correct either way: `total` is always what the shopper pays, `tax_total` is the tax within it, and `tax_breakdown` lists each rate:

```json title="tax_breakdown on a cart"
[
  {
    "identifier": "vat-standard",
    "description": "VAT",
    "percentage": "20",
    "price": { "amount": 2050, "currency": "GBP", "formatted": "£20.50" }
  }
]
```

Use `price_display` to choose the label next to prices ("inc. VAT" or "ex. VAT"), and show `tax_total` as its own line when prices exclude tax.

<Cards>
  <Card title="Carts" href="/docs/concepts/carts" description="Lines, totals, discounts and the tax breakdown." />
  <Card title="Catalogue" href="/docs/concepts/catalogue" description="Prices, sale prices and price filters." />
</Cards>
