---
title: "Upsert an invoice"
method: POST
path: "/public/v1/invoices"
tags: ["Invoice"]
---

# Upsert an invoice

`POST /public/v1/invoices`

Create or update a single invoice. This is one endpoint for both: omit `id` to create a new invoice (Distru assigns the ID and the human-readable invoice number), or pass an existing invoice's `id` to update it. Same URL, same request shape.

Every invoice bills an existing sales order, so `order_id` is required and each line item must reference an order item that belongs to that order. This endpoint bills existing order lines — it does **not** create, reserve, consume, or otherwise move inventory. Inventory is governed by the sales order and its fulfillment, not by invoicing. On save, Distru recalculates the invoice's charge amounts and `total` and recomputes its payment status from recorded payments; for an update that lowers the total below what has already been paid, the excess is reconciled into a customer credit automatically.

Updates are sparse: any top-level field you omit is left unchanged, and sending an explicit null clears that field. The `items` and `charges` collections are optional on update — omit either to leave the existing line items or charges untouched. When you DO send `items` or `charges`, that array is the complete set for that collection: any existing entry whose `id` you leave out is deleted, and passing `[]` clears them all. A line item or charge sent WITH an `id` is patched — merged onto the stored row, so you can change one field and omit the rest; one WITHOUT an `id` is a new entry. If any part of the request is rejected (a bad order item, a validation failure), the whole upsert fails and nothing is changed.

A voided invoice is frozen and cannot be edited through this endpoint — the request is rejected. An invoice is voided automatically when its sales order is canceled (and un-voided if that order later leaves the canceled status), so voiding is driven by the order, not set here.

Side effects reach other systems asynchronously. A successful 200 means the invoice was saved, not that downstream syncs finished: if the company is connected to QuickBooks Online the invoice is queued for sync there, and if the invoice's customer maps to a connected point-of-sale (Treez, Dutchie, or Blaze) it is queued for sync to that POS. Poll the relevant system to observe the synced result. Distru may also email the invoice PDF to the customer when the order is configured to do so.

Required permission: `invoices_permissions_create` to create a new invoice, or `invoices_permissions_edit` (plus access to the invoice under team restrictions) to update an existing one.

## Request body

- object
  - `id` string — ID for this invoice. Omit it to create a new invoice — Distru assigns the ID. Provide an existing invoice's ID to update that invoice; an ID that doesn't exist (or belongs to another company) returns a not-found error.
  - `order_id` string, required — The `id` of the sales order this invoice bills. Required. Determines the invoice's customer and the order items its line items may reference. The order must have a customer and a billing location, and must not be merged. Only set on create in practice — an invoice stays tied to the order it was created for.
  - `due_datetime` string, required — The datetime by which the customer should pay the invoice, as an ISO8601 datetime (e.g. `2026-08-30T00:00:00Z`). Required on create; on update, omit to leave it unchanged.
  - `invoice_datetime` string, required — The datetime the invoice is dated for, as an ISO8601 datetime (e.g. `2026-08-20T00:00:00Z`). Required on create; on update, omit to leave it unchanged. This is the date shown on the invoice and the key the list endpoint sorts and filters by.
  - `items` InvoiceItemRequest[] — The line items being billed, one entry per line, each following the [InvoiceItemRequest](#model-invoiceitemrequest) shape. Required on create (at least one line). Optional on update: omit the whole field to leave the existing line items unchanged. When sent, this array is the complete set of line items, so any existing item whose `id` you do not include is deleted. A line sent with an existing `id` is patched — merged onto the stored line, so you can change one field and omit the rest; a line WITHOUT an `id` is new.
    - `description` string — An optional free-text description for this billed line.
    - `id` string — ID for this invoice item. Omit it when creating a new item — Distru assigns one. Provide an existing item's ID to keep and patch that item: a line sent with an `id` is merged onto the stored line, so you can change one field and omit the rest. Because sending `items` is full-replace, any existing item whose ID you leave out of the request's `items` is deleted.
    - `order_item_id` string, required — The `id` of the sales order item this line bills. Required on a new line, and it must belong to the invoice's order; on a patch (a line sent with an `id`) omit it to keep the existing value. The product, batch or package, price, and cost are all taken from that order item — you cannot override them here.
    - `quantity` number, required — The quantity being billed on this line, as a decimal expressed in the product's unit type (up to 9 decimal places). May be less than the order item's quantity to bill only part of the line. Required on a new line; on a patch (a line sent with an `id`) omit it to keep the existing value.
  - `charges` InvoiceChargeRequest[] — Extra lines added on top of the items — fees, discounts, or taxes — each following the [InvoiceChargeRequest](#model-invoicechargerequest) shape. Optional; omit the whole field to leave the existing charges unchanged. When sent, this array is the complete set of charges, so any existing charge whose `id` you do not include is deleted, and an empty array clears all charges. A charge sent with an existing `id` is patched — merged onto the stored charge, so you can change one field and omit the rest.
    - `id` string — ID for this invoice charge. Omit it when creating a new charge — Distru assigns one. Provide an existing charge's ID to keep and patch that charge: a charge sent with an `id` is merged onto the stored charge, so you can change one field and omit the rest (its stored price is preserved when omitted). Because sending `charges` is full-replace, any existing charge whose ID you leave out is deleted.
    - `name` string — The label for this charge line, shown on the invoice.
    - `percent` number — The rate for a percentage-based line, as a percent (e.g. `8.25` means 8.25%), up to 4 decimal places. Provide this when `unit_type` is PERCENT. Distru computes the resulting amount from the invoice's items on save, so for a PERCENT line the `price` you send is ignored.
    - `price` number — The flat amount for a fixed-price line, up to 2 decimal places. Provide this when `unit_type` is PRICE. Ignored for PERCENT lines, where the amount is derived from `percent`.
    - `type` 'CHARGE' | 'DISCOUNT', required — Whether this line adds to or subtracts from the invoice total. SCREAMING_CASE: CHARGE (a fee added to the total) or DISCOUNT (subtracted from the total).
    - `unit_type` 'PERCENT' | 'PRICE', required — How this line's amount is expressed. SCREAMING_CASE: PERCENT (a percentage of the invoice's items, taken from `percent`) or PRICE (a flat amount, taken from `price`).
  - `billing_location_id` string — The `id` of the location to bill. Optional. Must be a location belonging to the order's customer; an ID that doesn't resolve to such a location is ignored.
  - `owner_id` string — The `id` of the Distru user who owns this invoice. Optional. Must be a user the caller is allowed to assign under their team restrictions.
  - `external_notes` string — Notes on this invoice that are visible to the customer. Optional.
  - `internal_notes` string — Notes on this invoice that are only visible internally. Optional.
  - `custom_data` object — A map of custom field IDs to their values. Use GET /public/v1/custom-fields?parent_object=invoice to retrieve available custom fields, their IDs, and their types. The value format depends on the field's type: a text field takes a string, a date field takes a full ISO8601 datetime, and a checkbox field takes an array of its selected options.

## Response `200`

A single invoice

## Other responses

- `400` — Invalid parameters
- `401` — Missing or invalid API token
- `403` — The API token lacks the required permission
- `404` — Not Found

---

[API](https://skmtc.dev/distru/apis/distru-api.md) · [All operations](https://skmtc.dev/distru/apis/distru-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/distru/distru-api/revisions/f8930b2c2924/schema)
