---
title: "Upsert a purchase order"
method: POST
path: "/public/v1/purchases"
tags: ["Purchase"]
---

# Upsert a purchase order

`POST /public/v1/purchases`

Upsert a single purchase order. To update an existing purchase order, pass in an existing purchase order ID in the id field. Updates are sparse: any 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 the order, so any existing entry whose `id` you do not include is deleted; send an empty `charges` array to clear all charges. Entries you do send are patched, not required in full: a charge or line item sent with an existing `id` is merged onto the stored row, so you can change one field and omit the rest. A line item WITHOUT an `id` is a new line and must declare its product via batch_id, package_id, or product_id. 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 purchase's products are added automatically. The order's line items must be either all package-tracked or all not package-tracked — a mix of the two is rejected. 

See the `status` field on the [purchase response](#model-purchase) for what each value means. Allowed transitions: PENDING, PROCESSING, and DELIVERING may move freely between one another and forward to PARTIALLY_RECEIVED or COMPLETED. Once a purchase reaches PARTIALLY_RECEIVED or COMPLETED it has received inventory and can no longer move back to PENDING, PROCESSING, or DELIVERING (it may still move between PARTIALLY_RECEIVED and COMPLETED). PARTIALLY_RECEIVED is not allowed for purchases that contain package-tracked items. 

For a PARTIALLY_RECEIVED purchase, set each line's `received_quantity` to the amount received so far. In a subsequent call you may decrease a line's `received_quantity`, or delete a line that has a positive `received_quantity`, as long as the previously-received quantity has not yet been consumed elsewhere in Distru (e.g. sold, transferred, or adjusted); otherwise the change is rejected. 

To match the purchase with an incoming compliance transfer, pass a top-level `metrc_transfer_id` or `biotrack_id`. This is only valid with `status` = COMPLETED, and requires the purchase's `location_id` to be on the license that received the transfer; the referenced incoming transfer must exist or the request is rejected. On each line item, identify the package it maps to with `metrc_package_id` (Metrc) or `biotrack_id` (BioTrack) and give its `compliance_quantity`. Once matched, a purchase is locked at COMPLETED and its transfer association cannot be changed. 

Required permission: `purchases_permissions_create` to create a new purchase order, `purchases_permissions_edit` (and access to the purchase under team restrictions) to update an existing purchase order.

## Request body

- object
  - `id` string — ID for this purchase order. Omit it to create a new purchase order — Distru assigns the ID. Provide an existing purchase order's ID to update it; an ID that doesn't exist returns a not-found error.
  - `description` string — A free-text description of the purchase order. Optional; send null to clear it.
  - `status` 'COMPLETED' | 'DELIVERING' | 'PENDING' | 'PARTIALLY_RECEIVED' | 'PROCESSING' — Where this purchase order sits in its lifecycle, which also governs when inventory is received. See the endpoint description for the allowed transitions and the `status` field on the purchase response for what each value means. Defaults to PENDING on create; on update, omit to leave it unchanged.
  - `metrc_transfer_id` integer — The ID of the incoming Metrc transfer to match this purchase with. When provided, `status` must be COMPLETED and each line item must identify its package via `metrc_package_id` and `compliance_quantity`. A purchase can match only one compliance transfer, so this cannot be sent together with `biotrack_id`. Once a purchase is matched, its status is locked at COMPLETED and the transfer association cannot be changed on a later update.
  - `biotrack_id` string — The ID of the incoming BioTrack transfer to match this purchase with. When provided, `status` must be COMPLETED and each line item must identify its package via `biotrack_id` and `compliance_quantity`. A purchase can match only one compliance transfer, so this cannot be sent together with `metrc_transfer_id`. Once a purchase is matched, its status is locked at COMPLETED and the transfer association cannot be changed on a later update.
  - `location_id` string — The ID of the location the purchased inventory is received into once the purchase reaches a received status (PARTIALLY_RECEIVED or COMPLETED). Also the default `location_id` for each line item that omits its own. Required on create; cannot be changed once it has been set, so on update omit it to leave it unchanged.
  - `billing_location_id` string — The ID of the location used as the billing address for this purchase order. Required on create; on update, omit to leave it unchanged.
  - `supplier_location_id` string — The ID of the supplier's location the purchased items ship from. Optional; omit to leave an existing value unchanged, or send null to clear it.
  - `company_id` string — The ID of the supplier (vendor) this purchase order is bought from. Required on create; cannot be changed once it has been set, so on update omit it to leave it unchanged.
  - `order_datetime` string — The datetime the purchase order was placed, as a full ISO8601 datetime in UTC (e.g. `2026-08-18T00:00:00Z`). Also the field the list endpoint sorts by. Required on create; on update, omit to leave it unchanged.
  - `due_datetime` string — The datetime by which the purchase order should be paid, as a full ISO8601 datetime in UTC (e.g. `2026-08-25T00:00:00Z`). Required on create; on update, omit to leave it unchanged.
  - `owner_id` string — The ID of the Distru user that owns this purchase order. Optional; send null to leave it unassigned.
  - `charges` PurchaseChargeRequest[] — The purchase order's complete set of charges and discounts — the extra lines (fees, discounts) added on top of the items. Each entry follows the [PurchaseChargeRequest](#model-purchasechargerequest) shape. Replacement semantics: on update the charges you send become the purchase'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 purchase 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 purchase create, presets whose auto-apply tags match the purchase's products are added automatically (see the preset's `auto_apply_tags_on_purchase_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` or `type` 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 purchase charge. Omit it when creating a new charge — Distru assigns one. Provide an existing charge's ID to update that charge.
    - `name` string — The name of this charge. Required for a manual charge; must be omitted when `charge_preset_id` is set — the preset provides it.
    - `percent` number — The percentage for this charge when `unit_type` is PERCENT, as a decimal (e.g. `10` means 10%). Required when `unit_type` is PERCENT; leave unset for PRICE charges. Applied against the order subtotal to derive the charge amount. On a preset charge it may only be sent when the preset allows inline edits, as a positive value.
    - `price` number — The flat charge amount when `unit_type` is PRICE, as a decimal in your company's currency. Required when `unit_type` is PRICE. For PERCENT charges leave it unset — the amount is computed from `percent` and the order subtotal. On a preset charge it may only be sent when the preset allows inline edits, as a positive value.
    - `type` 'CHARGE' | 'DISCOUNT' — Type of this line item. Required for a manual charge; must be omitted when `charge_preset_id` is set — the preset provides it.
    - `unit_type` 'PERCENT' | 'PRICE' — Determines if this line is tracked as a percentage or a flat charge. 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` PurchaseItemRequest[] — The products being purchased, one entry per line. Each entry follows the [PurchaseItemRequest](#model-purchaseitemrequest) 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 (subject to the received-quantity/consumption rules in the endpoint description). 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 and must declare its product via batch_id, package_id, or product_id. All lines must be either every one package-tracked or every one not package-tracked — a mix is rejected.
    - `batch_id` string — The ID of the batch to receive this line into (an existing batch). Provide it for batch-tracked products; the product is inferred from it, so `product_id` isn't needed. Must be left empty for product-tracked and package-tracked products.
    - `biotrack_id` string — The BioTrack package ID this line maps to within the matched incoming BioTrack transfer. Only used when the purchase is matched with a BioTrack transfer via the top-level `biotrack_id`; required on every line in that case. Cannot be combined with `metrc_package_id` on the same line.
    - `compliance_quantity` number — The full quantity in the matched compliance package, expressed in the package's unit type. Required for each line when matching the purchase with an incoming Metrc or BioTrack transfer; omit otherwise.
    - `id` string — ID for this purchase order item. Omit it when creating a new item — Distru assigns one. Provide an existing item's ID to update that item.
    - `location_id` string — The ID of the location this line's inventory is received into. Defaults to the purchase's `location_id` when omitted.
    - `metrc_package_id` integer — The Metrc package ID this line maps to within the matched incoming Metrc transfer. Only used when the purchase is matched with a Metrc transfer via the top-level `metrc_transfer_id`; required on every line in that case. This is Metrc's own numeric package id, not a Distru id. Cannot be combined with `biotrack_id` on the same line.
    - `price` number, required — The price per unit for this line, as a decimal in your company's currency (e.g. `12.50`). The line subtotal is `quantity` × `price`, with any charges applied on top.
    - `product_id` string — The ID of the product being purchased. Required for product-tracked and package-tracked products; for batch-tracked products it's inferred from `batch_id`, so you don't need to send it. Each line item must include `batch_id` or `product_id`. It must also be set when the line provides `metrc_package_id` or `biotrack_id` to match a compliance transfer package.
    - `quantity` number, required — The quantity ordered on this line, in the product's unit type, as a decimal (e.g. `10` or `10.5`). This is the ordered amount, not the amount received — for a PARTIALLY_RECEIVED purchase `received_quantity` tracks how much has arrived so far.
    - `received_quantity` number — The quantity received so far on this line, in the product's unit type. Only settable when the purchase status is PARTIALLY_RECEIVED (and the line is not package-tracked); must be between 0 and `quantity`. Omit for any other status — it is derived automatically. It may be decreased in a later call as long as the previously-received amount has not been consumed elsewhere in Distru.
  - `custom_data` object — A map of custom field IDs to their values. Use GET /public/v1/custom-fields?parent_object=purchase 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 purchase orders

## 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)
