> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://api-docs.papertracc.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://api-docs.papertracc.com/_mcp/server.

# Overview

Invoice endpoints mounted at `/invoices`.

## Quantities and units

`quantity` on a line item accepts fractional values, and a line can carry a `unit`. What is accepted depends on whether the line is tied to an inventory item through `sku`.

| Line                                              | `quantity`                                 | `unit`                                                                    |
| ------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------- |
| No `sku`, or an item with no selling unit         | Any value above zero, to 4 decimal places. | The label you send, for example `hours`. Optional, at most 50 characters. |
| `sku` of an item sold `EACH`                      | Whole numbers only.                        | Always `EACH`. A `unit` on the request is ignored.                        |
| `sku` of an item sold by weight, volume or length | Any value above zero, to 4 decimal places. | Always the item's selling unit. A `unit` on the request is ignored.       |

Selling units are `EACH`, `G`, `KG`, `OZ`, `LB`, `ML`, `L`, `FL_OZ`, `CM`, `M`, `IN`, `FT` and `YD`. A `sku` that matches no inventory item is rejected with `404`.

```json
{
  "detail": "Site survey",
  "currency_code": "NGN",
  "quantity": 1.5,
  "unit": "hours",
  "rate": 20000
}
```

Line items in invoice responses return `quantity` as a number, `unit` when the line has one, and the `sku` of the item the line is tied to.

## Promotions and loyalty points

Send `incentives` on create or update to apply promotions and redeem loyalty points. You only name what to apply; every amount is calculated by the server.

```json
{
  "incentives": {
    "promotions": [
      { "promotion_id": "5b0e6c5e-3a55-4a0b-9a55-2f1f4f0c7d11", "code": "LAUNCH10" }
    ],
    "loyalty_points": 250
  }
}
```

| Field                       | Description                                                                                                  |
| --------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `promotions[].promotion_id` | The promotion to apply. Each promotion may appear once, and promotions that cannot be combined are rejected. |
| `promotions[].code`         | Required for code-based promotions.                                                                          |
| `promotions[].account_id`   | Account the discount posts to. Defaults to the workspace's Sales Returns & Allowances account.               |
| `loyalty_points`            | Points to redeem. The customer needs an active loyalty account with enough points.                           |

Each promotion and the loyalty redemption come back as an entry in `adjustments`, marked by `promotion_id`, `code_used` or `loyalty_points`, and each line reports its share of the discount in `discount`.

## Updating adjustments and incentives

On `PATCH /invoices/{id}` manual adjustments and incentives are replaced independently of each other.

| Field         | Omitted                                                                               | Sent                                                              |
| ------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `adjustments` | Manual adjustments are kept.                                                          | Replaces the manual adjustments. An empty array removes them all. |
| `incentives`  | Promotions and the loyalty redemption are kept, and recalculated if the items change. | Replaces them. An empty object removes them.                      |

```json
{
  "adjustments": []
}
```

## Payment terms

An invoice falls due either on a fixed `due_date` or on a payment term. Send `payment_term_id` on create or update to apply a term from `/configs/payment-terms`.

| Request           | Result                                                                                                       |
| ----------------- | ------------------------------------------------------------------------------------------------------------ |
| `payment_term_id` | The term is applied. It wins when `due_date` is also sent.                                                   |
| `due_date` only   | The invoice is due on that date and `payment_schedule` is null.                                              |
| Neither           | The customer's default term is used, then the workspace default term, then the workspace's default due days. |

When a term is applied, the response carries `payment_schedule` with each instalment's due date and amount and any early-payment discount, and `due_date` is the final due date. `is_overdue` follows the instalments: it is true once any instalment is past due and not fully covered by `total_paid`.

On `PATCH /invoices/{id}`, `payment_term_id` rebuilds the schedule from that term. A `due_date` different from the current one replaces the term with that fixed date and clears `payment_schedule`. Changing the invoice date or the total recalculates the schedule from the term already on the invoice.

```json
{
  "payment_term_id": "a4d2e9f1-6b3c-4e8a-b7d5-1c9f0e2a3b47"
}
```