---
title: "Upsert an order"
method: POST
path: "/public/v1/orders"
tags: ["Order"]
---

# Upsert an order

`POST /public/v1/orders`

Create or update a single sales order. Omit `id` to create a new order (Distru assigns the order number and id); pass an existing order's `id` to update it. An `id` that doesn't exist, or belongs to another company, returns a not-found error.

Updates are sparse at the top level: send only the fields you want to change — every field you omit (including `status`) keeps its current value. The `items` and `charges` collections work differently. Omit the whole `items` (or `charges`) field and its existing rows are left untouched. Send the field and it fully replaces that set: an existing row whose `id` you omit is deleted, an entry whose `id` matches an existing row updates it (omitted fields on that entry are kept from the existing row, so you can patch a single field by sending just its `id` and the change), and an entry whose `id` is new (or omitted — Distru then assigns one) is added. Sending `items` as an empty array removes every line, which an order cannot be left in, so it is rejected. Charges are full-replace on update — see the `charges` field for how existing charges, presets, and deletions are handled; on create, charge presets whose auto-apply tags match the order's products are added automatically. The whole upsert is atomic: if any part is rejected (a validation error, a disallowed status transition, an unfulfillable line) nothing is changed and the response is a 400 whose `errors` point at the offending field.

Setting `status` moves the order through its lifecycle and drives inventory. A PENDING or CANCELED order reserves nothing and touches no inventory; moving to PROCESSING commits sellable inventory — assigning a package or batch to a line item moves that quantity into a committed selling state and any unfulfilled line adds to the product's `reserved` quantity; READY_TO_SHIP, DELIVERING, DELIVERED, and COMPLETED additionally require every line item to be fulfilled. See the `status` field on the [order](#model-order) for the full per-status behavior and transition requirements.

Compliance: associating the order with a state transfer is one-system-only — send `metrc_transfer_id` OR `biotrack_id`, never both. Doing so builds the order from that outgoing Metrc/BioTrack transfer. Any order carrying package-tracked items must be associated with a compliance transfer before it can reach DELIVERING, DELIVERED, or COMPLETED.

Invoicing: set `upsert_invoice` to create or refresh this order's [invoice](#model-compactinvoice), and `email_invoice` to email it. The response returns the saved order with its recomputed `total`, line items (with cost fields), charges, invoices, and returns.

Required permission: `orders_permissions_create` to create, `orders_permissions_edit` (plus access to the order under team restrictions) to update.

## Request body

- object
  - `biotrack_id` string — The BioTrack manifest to associate with this order, building the order from that outgoing BioTrack transfer. Mutually exclusive with `metrc_transfer_id` — send at most one; an order can be linked to only one compliance transfer.
  - `blaze_payment_type` 'CASH' | 'CREDIT' | 'DEBIT' | 'COD' | 'ACH_TRANSFER' | 'CHEQUE' | 'OTHER' — The payment type for an order shipping to a Blaze-associated company. Required (and only meaningful) when the order's buyer company is mapped to a Blaze retailer through the Distru integration; leave it off otherwise.
  - `company_id` string — The buyer of this order, as a company relationship ID (the same id in each order's `company.id` and GET /public/v1/companies). Determines the customer, and drives pricing, default payment term (used to derive `due_datetime`), and `blaze_payment_type` requirements. Optional while the order stays PENDING, PROCESSING, or CANCELED, but required to move it to READY_TO_SHIP, DELIVERING, DELIVERED, or COMPLETED. Once a customer is set, you cannot clear it back to null on a later update.
  - `delivery_datetime` string — ISO8601 datetime the order was / will be delivered. Optional; null when the order has no delivery datetime set.
  - `due_datetime` string — The datetime by which the customer is expected to pay for this order. Optional: when omitted, it is derived from the customer's default payment term, then the company default order payment term, then falls back to the order date (COD).
  - `external_notes` string — This is a message that will be shown to the customer on order slips. This is the "Message to Customer" field in the Distru order form.
  - `upsert_invoice` boolean — When true, create an invoice for this order if it doesn't have one yet, or update the existing invoice with the order's latest changes.
  - `email_invoice` boolean — When true, email the order's invoice. No email is sent unless the order has an invoice (see `upsert_invoice`) and a recipient can be resolved from `email_invoice_addresses` or the buyer company relationship's invoice email.
  - `email_invoice_addresses` string — Comma-separated list of email addresses to send the invoice to when `email_invoice` is true. Takes precedence over the company relationship's invoice email. Invalid addresses are rejected.
  - `id` string — ID for this order. Omit it to create a new order — Distru assigns the ID. Provide an existing order's ID to update that order; an ID that doesn't exist returns a not-found error.
  - `order_datetime` string — ISO8601 datetime the order was placed. Required on create — omitting it there returns a validation error; on update, omit to leave the existing value unchanged. This is the field the list endpoint sorts (newest first) and filters on.
  - `charges` OrderChargeRequest[] — The order's complete set of charges and discounts — the extra lines (fees, discounts, taxes) added on top of the items. Each entry follows the [OrderChargeRequest](#model-orderchargerequest) shape. Replacement semantics: on update the charges you send become the order's entire set — any existing charge you don't include is deleted. Omit the `charges` field entirely to leave the current charges unchanged; send `[]` to remove them all. Each charge is matched to an existing one by `id` — a charge with a new or absent `id` is added, a charge with a known `id` is updated, and on a matched charge any field you omit keeps its current value — so sending just `{"id": "..."}` keeps that charge exactly as it is. You never need to resend a charge's full payload to preserve it; include its `id` and only the fields you want to change. Manual charges: provide `name`, `type`, `unit_type`, and the amount. For a PERCENT charge the amount is calculated from the order subtotal and any `price` you send is ignored; send `percent` instead. Preset charges: set `charge_preset_id` and leave `name` and `type` out — Distru fills them from the [charge preset](#model-chargepreset). If the preset allows inline edits you may also set `unit_type` and the amount (`price` or `percent`); otherwise send only `charge_preset_id`. Sending a field the preset controls returns a 400. `charge_preset_id` is fixed once a charge exists. Omitting `charge_preset_id` on an update keeps the charge's current preset, like any other omitted field. But you cannot change it: sending a different `charge_preset_id`, sending null to detach a preset, or adding a `charge_preset_id` to a charge that was manual all return a 400. To switch a charge to a different preset or to a manual charge, drop it (omit its `id`, which deletes it) and add a new charge. Auto-apply: on order create, presets whose auto-apply tags match the order's products are added automatically (see the preset's `auto_apply_tags_on_sales_orders` flag); a preset you also send explicitly is applied once. Updates never auto-apply: adding items later does not add new preset charges (existing PERCENT charges still recompute from the new subtotal; fixed-price ones are unchanged). The reverse also holds: an auto-applied charge is never auto-removed — deleting the line items whose product tags added it leaves the charge in place until you drop it yourself (omit its `id` from a `charges` update).
    - `charge_preset_id` string — The ID of the charge preset this charge is created from (see [GET /public/v1/charge-presets](#get-charge-presets)). When set, Distru fills `name`, `type`, and the amounts from the preset: sending `name`, `type`, or `tax_id` returns a 400, and `unit_type`/`percent`/`price` may only be sent when the preset allows inline edits — always as positive values, the preset's type determines whether the amount is added or subtracted. Omit for a manual charge. Fixed once the charge exists: it cannot be changed, cleared, or added to an existing manual charge — remove the charge and add a new one instead. An ID that doesn't exist for your company returns 400.
    - `id` string — ID for this order charge. If it matches an existing charge on this order that charge is updated (fields you omit keep their current value); otherwise a new charge is created with this ID. Omit it to have Distru assign the ID.
    - `name` string — The label for this line (e.g. "Delivery Fee"), shown on the order and its invoice. Required for a manual charge; must be omitted when `charge_preset_id` is set — the preset provides it.
    - `percent` number — The percentage applied for this line. Required when `unit_type` is PERCENT and must be null otherwise; the resulting amount is computed from the order subtotal. On a preset charge it may only be sent when the preset allows inline edits, as a positive value.
    - `price` number — The flat amount for this line. Applies when `unit_type` is PRICE. May be omitted for a PERCENT line, where Distru derives the amount from `percent`. On a preset charge it may only be sent when the preset allows inline edits, as a positive value.
    - `tax_id` string — The ID of the tax this line applies. When set, the charge is treated as a tax line: it appears with a nested `tax` object in the response and is included in tax totals. Providing it forces `type` to CHARGE (a tax can never be a discount), so `type` may be omitted for a tax line. Cannot be combined with `charge_preset_id`. Michigan operators are limited to at most one Michigan state tax line per order; this limit does not apply anywhere else. When updating an existing charge (sent with its `id`): omitting `tax_id` leaves its current tax as-is; sending null clears the tax and turns it back into a normal charge; sending the same id is a no-op; sending a different id re-points it to that tax.
    - `type` 'CHARGE' | 'DISCOUNT' — Whether this line adds to or subtracts from the order: CHARGE or DISCOUNT (SCREAMING_CASE). Required for a normal line; may be omitted when `tax_id` is set, which forces it to CHARGE; must be omitted when `charge_preset_id` is set — the preset provides it.
    - `unit_type` 'PERCENT' | 'PRICE' — How the line is measured: PERCENT (a percentage of the subtotal, set via `percent`) or PRICE (a flat amount, set via `price`). SCREAMING_CASE. Required for a manual charge; on a preset charge it comes from the preset and may only be sent when the preset allows inline edits.
  - `items` OrderItemRequest[] — The products being sold on this order, one entry per line. Required on create. On update it is optional: omit it to leave the order's existing lines untouched, or send it to fully replace the line set — an existing line whose `id` you omit is deleted, an entry whose `id` matches an existing line updates it (fields you omit on the entry keep their current value, so an `id`-only entry is a no-op), and an entry with a new or omitted `id` is added. An empty array is rejected because every order must keep at least one item. If the order is matched with a compliance transfer, its package-tracked lines each map one-to-one to a transferred package and cannot be deleted — omitting one is rejected. Each entry follows the [OrderItemRequest](#model-orderitemrequest) shape.
    - `batch_id` string — The ID of the batch this line item draws from; set it to fulfill a batch-tracked line, and the product is inferred from it (no `product_id` needed). To create an unfulfilled line instead, leave this empty and send `product_id` — the product's `reserved` quantity goes up without committing to a batch. Must be empty for product-tracked and package-tracked products.
    - `compliance_quantity` number — The compliance quantity for this item, expressed in the package's unit type; leave null when the item is not package-tracked (no `package_id`). Must be the full quantity currently in the package.
    - `id` string — ID for this order item. If it matches an existing line on this order that line is updated (fields you omit keep their current value); otherwise a new line is created with this ID. Omit it to have Distru assign the ID.
    - `is_sample` boolean — Marks this line as a sample rather than a normal sale. Defaults to false when omitted.
    - `location_id` string — The location this line item is fulfilled from, as a Distru location ID. Optional.
    - `note` string — A free-text note on this line item, up to 1024 characters. Optional; omit to leave an existing item's note unchanged, or send an empty string to clear it.
    - `package_id` string — The ID of the package this line item draws from; set it to fulfill a package-tracked line, and the product is inferred from it (no `product_id` needed). To create an unfulfilled line instead, leave this empty and send `product_id` — the product's `reserved` quantity goes up without committing to a package. Must be empty for product-tracked and batch-tracked products.
    - `price_base` number, required — Price per unit for this line item before any price tiers are applied, as a decimal (up to 9 decimal places). Required. Matching price tiers may adjust the price actually charged, so the line's resulting price in the response can differ from this value — control that with `price_tier_mode`.
    - `price_tier_mode` 'AUTO' | 'OVERRIDE' | 'NONE' — Controls how [price tiers](#model-pricetier) set this line's `price` (SCREAMING_CASE): AUTO lets Distru apply the best applicable tier automatically, OVERRIDE locks the line to the exact tier version in `price_tier_version_id`, and NONE disables price tiers so `price` stays equal to `price_base`. Defaults to AUTO on create; when updating an existing line (sent with its `id`), omit it to keep the line's current mode — except when sending `price_tier_version_id`, which always requires an explicit OVERRIDE in the same line, even if the line is already stored as OVERRIDE. Sending AUTO or NONE on an update also clears the line's existing version lock. Note that the stored mode moves on its own under AUTO: as soon as a tier matches, the line locks to that tier's current version and reads back as OVERRIDE (see `price_tier_mode` on the [sales order item](#model-salesorderitem)).
    - `price_tier_version_id` string — The price tier version to lock this line's pricing to. Only valid alongside `price_tier_mode: "OVERRIDE"` in the same line: required then, rejected with any other or omitted mode — so re-pointing an already-locked line to another version still means resending OVERRIDE next to the new id. Take the id from a tier's `current_version_id` (GET /public/v1/price-tiers) to apply the tier's latest state, or from another order item's `price_tier_version.id` to reuse the exact snapshot that priced it. The version must belong to one of your company's price tiers — an unknown or foreign id is rejected. Setting or changing this value (from null to a version, or from one version to another) requires the version's tier to currently be applicable to the line: its conditions (product/customer filters, minimum quantity, validity dates) are checked and a non-applicable tier is rejected. A line already locked to a version keeps its lock on later updates even if the tier has since stopped matching — only a change re-checks applicability.
    - `product_id` string — The ID of the product being sold. Required for product-tracked products, where `batch_id` and `package_id` must be left empty. For batch- and package-tracked products, `product_id` is inferred when you send `batch_id` or `package_id`; sending it on its own instead creates an unfulfilled line item — the order commits to the product without drawing from a specific batch or package yet, which adds to the product's `reserved` quantity while the order is PROCESSING. Set `batch_id` or `package_id` later to fulfill it. Every line item must include at least one of `product_id`, `batch_id`, or `package_id`.
    - `quantity` number, required — Quantity used on this order item, expressed in the product's unit type
  - `internal_notes` string — Free-form notes visible only inside Distru; never shown to the customer. Use `external_notes` for a customer-facing message.
  - `metrc_transfer_id` integer — The Metrc transfer to associate with this order, building the order from that outgoing Metrc transfer. This is Metrc's own integer transfer id, not a Distru ID. Mutually exclusive with `biotrack_id` — send at most one; an order can be linked to only one compliance transfer.
  - `location_id` string — The Distru location ID of the order's top-level location — used to filter orders and to associate the order with a compliance (Metrc) license, identifying which Metrc license the order takes place under. It is not the location sale quantities are drawn from; each order item sets that via its own `location_id`. Optional in general, but required when the order has any package-tracked items, where it must reference a location that has a compliance license and that license must match the license of every package-tracked item's location.
  - `billing_location_id` string — The billing location's ID (a Distru location ID). Optional.
  - `shipping_location_id` string — The shipping location's ID (a Distru location ID). Optional.
  - `owner_id` string — The Distru user that owns this order, as a user ID (the same id in the response's `owner.id`). Optional.
  - `status` 'PENDING' | 'PROCESSING' | 'READY_TO_SHIP' | 'DELIVERING' | 'DELIVERED' | 'COMPLETED' | 'CANCELED' — The status to set for this order, controlling where it sits in its lifecycle and how it affects inventory and compliance. Required on create; on update, omit to leave the current status unchanged. See the `status` field on the [order response](#model-orderresponse) for what each value means. Note that some transitions have requirements: moving to READY_TO_SHIP, DELIVERING, DELIVERED, or COMPLETED requires every line item to be fulfilled and a customer (`company_id`) to be set, and DELIVERING/DELIVERED/COMPLETED additionally require a compliance transfer when the order carries any package-tracked items.
  - `custom_data` object — A map of custom field IDs to their values. Use GET /public/v1/custom-fields?parent_object=order 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 order

## 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/eb775b7b6ee0/schema)
