# Money and quantities

> How YoPrint encodes money and quantities, the move to decimal pairs, how to read and send amounts, and how to request exact values.

Web page: https://developer.yoprint.com/concepts/money-and-quantities/

YoPrint sends and takes money and quantities as exact values, not floating-point numbers. Different fields use different encodings, listed below. The exception is a Shippo rate request, which takes plain numbers. Before you read or send an amount, check its encoding here or on its reference page.

## At a glance

- **Read `dp` every time you read a decimal pair.** The scale is not fixed: `dp` can differ between fields and even between records of the same field.
- **Send `X-Yoprint-Decimal-Format: exact-v1` on every request.** This header ensures amounts that cannot be represented as exact JSON numbers return as strings.
- **Use a decimal type for arithmetic.** Never use native binary floating-point numbers for money or quantities.
- **Prefer decimal-pair fields over deprecated thousandths fields.** Where an older `calc_*` field has a decimal-pair replacement, read the replacement instead.
- **Check each field's reference entry for its encoding.** Fields use different encodings across answers and requests.

## The encodings

| Encoding                         | Looks like                   | Means                    | Used for                                                                                       |
| -------------------------------- | ---------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------- |
| **Decimal pair**                 | `{ "value": 1250, "dp": 2 }` | 12.50: `value` ÷ 10^`dp` | Newer amounts: line prices, costs and quantities, order and invoice totals                     |
| **Thousandths**                  | `12500`                      | 12.50: ÷ 1,000           | Older amounts: payments, customer balances, product prices, and the deprecated `calc_*` totals |
| **Percentage × 1000**            | `8250`                       | 8.25%: ÷ 1,000           | Tax rates and deposit percentages                                                              |
| **Ten-thousandths of a percent** | `82500`                      | 8.25%: ÷ 10,000          | The automatic sales tax lookup's rate                                                          |
| **Decimal string**               | `"24.00000000"`, `"12.40"`   | The number as written    | Stock levels, Shippo rates and weights, some report rows                                       |
| **Plain number**                 | `12.5`                       | The number as written    | Shippo rate requests only                                                                      |
| **Formatted string**             | `"$125.00"`                  | Text for people          | The payment webhook's `amount`: don't parse it                                                 |

**Every field, by encoding**

- **Decimal pair** (requests and answers): line item prices, costs, and quantities. Order totals (`order_item_cart_total`, `invoiced_total`, `order_balance_due`, `deposit_amount_due`, `shipment_cost_total`, margin, markup). Invoice totals (`total`, `cart_item_subtotal`, `tax_subtotal`, ...) and line items.
- **Thousandths** (requests and answers), older fields:
  - Order amounts: `paid_amount` and `calc_deposit_total`.
  - Customer balances: `unpaid_amount` and `wallet_fund`.
  - Product prices: `default_price`.
  - Payments and refunds: `amount`, sent and answered.
  - v1 filter amounts, and package label `charges`.
  - The **deprecated** `calc_*` totals on orders and invoices.
- **Percentage × 1000** (requests and answers): tax preset rates, payment term `deposit_percent`, a shipping method's `price_percent`, and an order's `deposit_total` in answers (sent as a pair with `dp` 3).
- **Ten-thousandths of a percent** (answers): the automatic sales tax lookup's `total` (the combined rate).
- **Decimal string, stock** (answers): stock levels (`stock_on_hand`, `stock_incoming`, `qty_need_ordering`), with 8 decimal places, or `"0"`.
- **Decimal string, money and weights** (answers): Shippo rates and package weights (`amount`, `weight`). Some report rows (purchase order report `calc_*`, tax report amounts).
- **Plain number** (requests): Shippo rate requests: parcel dimensions, parcel weights, and insurance amount (Shippo's own format, passed through).
- **Formatted string** (answers): the payment webhook's `amount`, in the team's currency. Read the order or invoice for exact amounts.

## Encodings in transition

YoPrint is moving amounts to decimal pairs:

**Diagram:** Each field's reference entry says which encoding it uses today.

1. **Before**: Amounts in thousandths, such as an order's `calc_total`
2. **Now**: Decimal pairs added. The old `calc_*` copies still come back, marked deprecated, so existing integrations keep working
3. **Later**: The deprecated copies are removed. Thousandths fields without a pair yet may change too

See [Deprecated fields](https://developer.yoprint.com/concepts/base-url-and-versions.md#deprecated-fields) for each old field and the one to read instead.

## Reading amounts

### Decimal pairs

**Diagram:** Read both parts every time: the same field can come back with a different dp.

1. **value**: `1250`: the digits, with no decimal point &
2. **dp**: `2`: how many of those digits come after the point =
3. **The amount**: 1250 ÷ 10² = **12.50**

A decimal pair consists of an integer `value` and a scale `dp`. The amount is `value` ÷ 10^`dp`:

- `{ "value": 1250, "dp": 2 }` is 1250 ÷ 10² = 12.50.
- `{ "value": 125, "dp": 1 }` is 125 ÷ 10¹ = 12.5.
- `{ "value": 3, "dp": 0 }` is 3 (a quantity of three).

When you read a pair, **never assume `dp`**. Answers keep the scale at which the amount is stored in YoPrint. The same field can return with different scales across different records, and an unset amount can return as `{ "value": 0, "dp": 0 }`. Always read `dp` directly from the object.

| Kind of field                   | Typical `dp` | Example                                  |
| ------------------------------- | ------------ | ---------------------------------------- |
| Order and invoice totals        | 4            | `{ "value": 1250000, "dp": 4 }` (125.00) |
| Line item unit prices and costs | 6            | `{ "value": 12500000, "dp": 6 }` (12.50) |
| Line item quantities            | 0            | `{ "value": 12, "dp": 0 }` (12 items)    |
| Unset amounts                   | 0            | `{ "value": 0, "dp": 0 }` (0.00)         |

### Thousandths

**Diagram:** Older fields count thousandths of the currency unit.

1. **The field**: `12500` ÷
2. **1,000**: The same for every thousandths field =
3. **The amount**: **12.50**

A thousandths field represents an amount multiplied by 1000. The amount is `value` ÷ 1000:

- `12500` is 12500 ÷ 1000 = 12.50.
- `125000` is 125000 ÷ 1000 = 125.00.

In answers, the value is usually a whole number. When an amount is finer than a thousandth, it returns as a fractional number: `1234.5` is 1234.5 ÷ 1000 = 1.2345. When you send `X-Yoprint-Decimal-Format: exact-v1`, any fractional thousandths value (or any value too large for a JSON number) returns as an exact decimal string of thousandths, such as `"1234.5"`.

### Percentages × 1000

A percentage × 1000 represents a percentage multiplied by 1000: `8250` is 8250 ÷ 1000 = 8.25%. With `X-Yoprint-Decimal-Format: exact-v1`, any value that is not a whole number returns as an exact string.

### Decimal strings

Stock levels return as strings with 8 decimal places (such as `"24.00000000"`), and Shippo rates return as decimal strings (such as `"12.40"`). Always parse them into your language's decimal type.

### Formatted strings

Payment webhooks include an `amount` field formatted as a currency string (such as `"$125.00"`). This string is intended for display to people, not for calculations. Do not parse it: currency symbols, digit separators, and decimal conventions vary by team currency. To get exact amounts, fetch the order or invoice from the API.

## Sending amounts

Most mistakes with money happen when sending it. Find the field you send in this table, and send it in that unit. Each example is 12.50 (or 8.25% for a rate).

| You send                                                                    | Fields                                              | Unit                                                        | Example                       |
| --------------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------- | ----------------------------- |
| Prices, costs and quantities on order, invoice and shipment lines           | `pricing.…total`, `quantity.total`                  | Decimal pair                                                | `{ "value": 1250, "dp": 2 }`  |
| A payment, a refund, or a card refund through the gateway                   | `amount`                                            | Thousandths                                                 | `12500`                       |
| Customer credit: add or take out, pay an order from it, move payments to it | `amount`                                            | Thousandths                                                 | `12500`                       |
| A payment link's allocations                                                | `total`                                             | A pair where `value` counts thousandths, whatever `dp` says | `{ "value": 12500, "dp": 3 }` |
| Product and variant prices                                                  | `default_price`, and `price` in `price_data`        | Thousandths                                                 | `12500`                       |
| Fee preset prices, a shipping method's minimum price                        | `price`, `min_price`                                | Thousandths                                                 | `12500`                       |
| Shipping box sizes                                                          | `length`, `width`, `height`                         | Thousandths of the unit                                     | `12500` is 12.5               |
| Tax preset rates, a payment term's deposit, a shipping method's markup      | the rate fields, `deposit_percent`, `price_percent` | Percentage × 1000                                           | `8250`                        |
| An order's deposit percentage                                               | `deposit_total`                                     | Percentage × 1000, in a pair with `dp` exactly `3`          | `{ "value": 8250, "dp": 3 }`  |
| Shippo rate requests                                                        | parcel sizes and weights, insurance `amount`        | Plain number, as Shippo takes it                            | `12.5`                        |

When a field isn't in this table, its reference page says its unit.

### Decimal pairs

When sending a decimal pair in a request body:

- `value` must be an integer, or a string of digits with an optional leading `-`. No decimal point and no leading zeros.
- `dp` must be an integer from 0 to 30.
- A plain number such as `12.5`, or a pair with a fractional number such as `{ "value": 12.5, "dp": 1 }`, is refused with HTTP `422`.
- YoPrint limits the maximum scale and value by field type:

| Kind of field                          | Most decimal places | Largest value              |
| -------------------------------------- | ------------------- | -------------------------- |
| Quantities                             | 8                   | 999,999,999,999.99999999   |
| Unit prices, costs, and rates          | 6                   | 999,999,999,999,999.999999 |
| Calculated amounts (discounts, totals) | 4                   | 999,999,999,999,999.9999   |

Sending more decimal places than allowed is refused with HTTP `422`, unless the trailing digits are zeros.

### Thousandths

Send a whole number of thousandths, with no decimal point: `25000` is 25.00. What happens to a number with decimals depends on the field:

- A payment or refund `amount`, and every customer credit `amount` (adding or taking out credit, paying an order from it, moving payments to it): refused with `422` `validation_failed`, and nothing is recorded. `error.fields.amount` says "The amount must be a whole number of thousandths of the currency unit, such as 12500 for 12.50."
- Product and variant prices: the request fails with HTTP `500`. Fix the number before you send it again.

The sign of a payment or refund `amount` is ignored: `-25000` also records 25.00.

### Request quirks

> **Caution: Two request fields break the rules above**
>
> - **An order's `deposit_total`**, on create and update, is a percentage × 1000 sent as a decimal pair with `dp` exactly `3`: a 50% deposit is `{ "value": 50000, "dp": 3 }`.
> - **A payment link's allocation `total`** looks like a decimal pair, but YoPrint reads `total.value` as thousandths whatever `dp` says. Send `{ "value": 12500, "dp": 3 }` for 12.50. `{ "value": 1250, "dp": 2 }` would be read as 1.25.

## Ask for exact responses

Send this header on every request:

```http
X-Yoprint-Decimal-Format: exact-v1
```

With this header:

- A decimal pair whose `value` is too large for a JSON number to hold exactly (beyond 9,007,199,254,740,991, which is 2^53 − 1) returns with `value` as a string of digits.
- A thousandths field that is not a whole number returns as an exact decimal string of thousandths, such as `"1234.5"`.
- A percentage × 1000 that is not a whole number returns as an exact decimal string.

Without it, a save of an order, an invoice, a shipment or their lines is refused with `422` `amount_out_of_range` when its answer would carry an amount the older number format can't hold exactly. Nothing is saved: send the header and try again.

Both v1 and v2 accept this header, and YoPrint echoes `X-Yoprint-Decimal-Format: exact-v1` in the response when it applied it. Always parse `value` and thousandths fields as either a number or a string. The client helpers on [Calling the API](https://developer.yoprint.com/concepts/calling-the-api.md) send this header on every request.

## Which order total to read

What each order total counts, and how the balance is worked out, is on [How the balance works](https://developer.yoprint.com/how-yoprint-works/invoices-and-payments.md#how-the-balance-works). Its fields are decimal pairs, except `paid_amount`, which is in thousandths.

> **Caution: Common traps**
>
> - **`calc_total`, `calc_subtotal`, `calc_tax`, and `calc_discount` on orders and quotes**: Do not read these deprecated fields. They count only what has been **invoiced** and remain `0` until the order has an invoice. To read the total value of an order's lines, use **`order_item_cart_total`**. To read invoiced totals, use **`invoiced_total`** and the other `invoiced_*` decimal pairs. See [Deprecated fields](https://developer.yoprint.com/concepts/base-url-and-versions.md#deprecated-fields).
> - **`deposit_total` vs `calc_deposit_total`**: In order answers, **`deposit_total`** is a deposit percentage × 1000 (`50000` is 50%), not a currency amount. The deposit amount is **`calc_deposit_total`** (in thousandths), and the unpaid deposit amount is **`deposit_amount_due`** (a decimal pair).

## Convert without rounding

Keep amounts as integers or strings until you display them. Do not convert to a binary floating-point number and back.

```javascript
// "4017.86" from { value: 40178600, dp: 4 }
function pairToString({ value, dp }) {
  const digits = String(value).replace('-', '').padStart(dp + 1, '0');
  const sign = String(value).startsWith('-') ? '-' : '';
  const whole = digits.slice(0, digits.length - dp);
  const fraction = digits.slice(digits.length - dp).replace(/0+$/, '');
  return sign + whole + (fraction ? '.' + fraction : '');
}

// "125" from 125000, and "1.2345" from 1234.5 or "1234.5" (thousandths)
function thousandthsToString(n) {
  const [whole, fraction = ''] = String(n).split('.');
  return pairToString({ value: whole + fraction, dp: 3 + fraction.length });
}
```

Use your language's decimal type for arithmetic: `decimal.Decimal` in Python, `BCMath` or `brick/math` in PHP, or a decimal library in JavaScript.

## Currency

Amounts are in the team's currency: an order takes the team's default currency when it's made. No numeric encoding carries the currency. Only the formatted display strings in webhooks include a currency symbol.
