{"openapi":"3.0.0","info":{"version":"1.0","title":"Distru API","license":{"name":"Proprietary","url":"https://www.distru.com/legal/terms-of-service"}},"paths":{"/public/v1/products/add-costs":{"post":{"description":"Add one or more costs to each of the given product-tracked products, and return the affected [products](#model-product) with their updated cost totals.\n\n`product_ids` is a non-empty list of product IDs. Every product must exist, be accessible to the authenticated company, and be product-tracked (a product tracked by batch or package is rejected). The cost lands on each product's active-status stock quantity. `location_ids` optionally scopes which locations' stock the cost applies to; omit it to apply across all locations.\n\nEach entry in `costs` records one cost against the selected records; the amount added to a record's cost basis is `cost_per_unit` × `quantity`. Every entry in the list is applied to every selected record, so N records and M cost entries create N × M cost entries. Fields:\n\n- `cost_type_id` (**required**): the cost type to apply, as its ID from GET /public/v1/cost-types. Must exist and be accessible to the authenticated company.\n- `quantity` (**required**): how many units of the cost type to apply, as a decimal. Must be greater than 0.\n- `cost_per_unit` (optional): the per-unit amount, as a decimal. When omitted, the cost type's own cost per unit is used. Must be omitted for cost types with a locked cost per unit (those that don't allow inline editing); sending it for such a type is rejected. It is only required when an inline-editable cost type has no cost per unit of its own.\n- `description` (optional): free-form text stored on the cost.\n\n\nWhen `distribute_by_quantity` is `true`, the total of each cost (`cost_per_unit` × `quantity`) is split across the selected records in proportion to each record's quantity, instead of applying the full cost to every record. Quantities are converted to a common unit before the split, so all selected records must share the same unit type category. Selecting a single record is a no-op (the whole cost lands on it). Defaults to `false` when omitted, applying the same cost in full to each selected record.\n\n\nThis is applied synchronously and atomically: a 200 means every cost has already been recorded and the response body reflects the updated records — there is nothing to poll. If any id, cost, or validation fails, the entire request is rejected and nothing is changed.\n\nAdding costs is additive, not an upsert. Each call records new cost entries and raises the recorded cost basis (COGS) of the selected inventory's stock; sending the same body twice applies the cost twice. Cost entries created here cannot be edited or removed through this endpoint. The effect is confined to Distru cost accounting — it does not push to, pull from, or alter Metrc or BioTrack.\n\n\nCommon errors (HTTP 400 unless noted):\n\n- The `*_ids` list is empty.\n- One or more ids don't exist or aren't accessible to the authenticated company.\n- A record isn't tracked by the endpoint's method (e.g. a product that isn't product-tracked, or a batch whose product isn't batch-tracked).\n- A record has no quantity to add a cost to.\n- `cost_type_id` is missing or unknown, or `quantity` is missing or not greater than 0.\n- `cost_per_unit` is set for a cost type with a locked cost per unit, or missing when required.\n- `distribute_by_quantity` is used across records with mixed unit type categories.\n- Cost Accounting module is not enabled, or the user lacks `costs_permissions_apply_to_inventory` (HTTP 403).\n\n\nRequired permission: `costs_permissions_apply_to_inventory`.\n","operationId":"DistruPublic.V1.CostController.add_product_costs","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddProductCostsRequest"}}},"description":"The products and costs to apply","required":true},"responses":{"200":{"description":"The affected products","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Products"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Add costs to products","tags":["Cost"]}},"/public/v1/companies/{id}":{"delete":{"description":"Deletes a company from your CRM. This is a soft delete: the company stops appearing in `GET /public/v1/companies` by default (pass the `deleted` filter as `include` or `only` to still see it) and this endpoint returns 404 for it, but the record is retained — `GET /public/v1/companies/{id}` keeps resolving it with a non-null `deleted_at`, and anything that already references the company (a sales order, purchase, invoice, or credit) keeps its reference and continues to render it. The delete cannot be undone through the API; recreating the company via upsert produces a new company with a new `id`. Responds 204 with no body on success, or 404 if no non-deleted company with that `id` exists on your account (including one that was already deleted or belongs to another account).\n\nThe delete cascades to the company's dependent CRM data, all in one atomic call: the products this company vendors and those products' batches are soft-deleted, and its [contacts](#model-contact) are soft-deleted. The company's credits are NOT deleted: they keep their applications to invoices and stay readable through the credit endpoints. Existing sales orders, purchases, and invoices for the company are also NOT deleted, and no reference ever blocks the delete. The one entry that can never be deleted is the company that represents your own business; attempting it returns a 400. No inventory is created, consumed, or released, and nothing is synced to Metrc or BioTrack. A successful delete records a `delete` entry in the company's activity log and notifies the relevant users.\n\nRequired permission: `companies_permissions_delete`.\n","operationId":"DistruPublic.V1.CompanyController.delete","parameters":[{"description":"ID of the company to delete, as returned by the list, fetch, and upsert endpoints. An ID that doesn't exist for your account (or was already deleted) returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a company","tags":["Company"]},"get":{"description":"Fetch one company from your CRM by its ID, including its underlying business record, owner, group, relationship type, payment term, custom fields, licenses, locations, and computed outstanding balance.\n\nReturns 404 if no company with that ID exists on your account. A soft-deleted company is still returned here, with a non-null `deleted_at`. This read is served from a replica and is eventually consistent — a create or update made through the API can take up to 1 second to be reflected.\n\nRequired permission: `companies_permissions_view`.\n","operationId":"DistruPublic.V1.CompanyController.show","parameters":[{"description":"ID of the company to fetch.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single company","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CompanyResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a company","tags":["Company"]}},"/public/v1/batches/add-costs":{"post":{"description":"Add one or more costs to each of the given batches, and return the affected [batches](#model-batch) with their updated cost totals.\n\n`batch_ids` is a non-empty list of batch IDs. Every batch must exist, be accessible to the authenticated company, and belong to a batch-tracked product. The cost lands on each batch's active-status stock quantity. `location_ids` optionally scopes which locations' stock the cost applies to; omit it to apply across all locations.\n\nEach entry in `costs` records one cost against the selected records; the amount added to a record's cost basis is `cost_per_unit` × `quantity`. Every entry in the list is applied to every selected record, so N records and M cost entries create N × M cost entries. Fields:\n\n- `cost_type_id` (**required**): the cost type to apply, as its ID from GET /public/v1/cost-types. Must exist and be accessible to the authenticated company.\n- `quantity` (**required**): how many units of the cost type to apply, as a decimal. Must be greater than 0.\n- `cost_per_unit` (optional): the per-unit amount, as a decimal. When omitted, the cost type's own cost per unit is used. Must be omitted for cost types with a locked cost per unit (those that don't allow inline editing); sending it for such a type is rejected. It is only required when an inline-editable cost type has no cost per unit of its own.\n- `description` (optional): free-form text stored on the cost.\n\n\nWhen `distribute_by_quantity` is `true`, the total of each cost (`cost_per_unit` × `quantity`) is split across the selected records in proportion to each record's quantity, instead of applying the full cost to every record. Quantities are converted to a common unit before the split, so all selected records must share the same unit type category. Selecting a single record is a no-op (the whole cost lands on it). Defaults to `false` when omitted, applying the same cost in full to each selected record.\n\n\nThis is applied synchronously and atomically: a 200 means every cost has already been recorded and the response body reflects the updated records — there is nothing to poll. If any id, cost, or validation fails, the entire request is rejected and nothing is changed.\n\nAdding costs is additive, not an upsert. Each call records new cost entries and raises the recorded cost basis (COGS) of the selected inventory's stock; sending the same body twice applies the cost twice. Cost entries created here cannot be edited or removed through this endpoint. The effect is confined to Distru cost accounting — it does not push to, pull from, or alter Metrc or BioTrack.\n\n\nCommon errors (HTTP 400 unless noted):\n\n- The `*_ids` list is empty.\n- One or more ids don't exist or aren't accessible to the authenticated company.\n- A record isn't tracked by the endpoint's method (e.g. a product that isn't product-tracked, or a batch whose product isn't batch-tracked).\n- A record has no quantity to add a cost to.\n- `cost_type_id` is missing or unknown, or `quantity` is missing or not greater than 0.\n- `cost_per_unit` is set for a cost type with a locked cost per unit, or missing when required.\n- `distribute_by_quantity` is used across records with mixed unit type categories.\n- Cost Accounting module is not enabled, or the user lacks `costs_permissions_apply_to_inventory` (HTTP 403).\n\n\nRequired permission: `costs_permissions_apply_to_inventory`.\n","operationId":"DistruPublic.V1.CostController.add_batch_costs","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddBatchCostsRequest"}}},"description":"The batches and costs to apply","required":true},"responses":{"200":{"description":"The affected batches","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Batches"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Add costs to batches","tags":["Cost"]}},"/public/v1/contacts":{"get":{"description":"List the contacts in your CRM, returned oldest-first by creation date. The response is a page envelope: `data` holds the contacts and `next_page` is a ready-to-call URL for the following page (null on the last page).\n\nThis endpoint is eventually consistent — a contact you just created or edited may take up to 1 second to appear or reflect its new values here.\n\nBy default only non-deleted contacts are returned; use the `deleted` filter to include or isolate soft-deleted ones. Results are further scoped to your company and to the contacts the authenticated user may see under their team restrictions, so two users at the same company can get different lists.\n\nRequired permission: `contacts_permissions_view`.\n","operationId":"DistruPublic.V1.ContactController.index","parameters":[{"description":"Restrict the result to specific contacts by ID (the same ID returned as each contact's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to contacts employed by specific companies by company ID (the same ID returned as each contact's `company.id`). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"company_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to contacts owned by any of these Distru users (each contact's `owner.id`). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to contacts by their creation datetime. The value is a comma-separated `start,end` pair of ISO8601 datetimes and either bound may be omitted: `2022-07-10T00:00:00Z,` returns contacts created on or after that instant, `,2022-07-10T00:00:00Z` returns those created on or before it, and `2022-07-10T00:00:00Z,2022-07-11T00:00:00Z` returns those created between the two (inclusive).","in":"query","name":"inserted_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Whether soft-deleted contacts are included. `no` (the default) returns only non-deleted contacts, `only` returns only soft-deleted ones, and `include` returns both. Soft-deleted contacts carry a non-null `deleted_at` in the response.","in":"query","name":"deleted","required":false,"schema":{"type":"string","enum":["no","include","only"],"default":"no"}},{"description":"Page selector, given as `page[number]`. One-based; defaults to page 1 when omitted. Page size is fixed (1000 contacts per page) and is not caller-configurable — follow the response's `next_page` URL to page through results.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter to contacts by the datetime they were last modified. Same comma-separated `start,end` ISO8601 format as `inserted_datetime`, with either bound optional: `,2022-07-10T00:00:00Z` returns contacts last updated on or before that instant.","in":"query","name":"updated_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by custom field values, as `custom_data[{id}]=value` where `{id}` is a custom field's numeric id. Repeat with different ids to filter on several fields at once; a record must match every one (AND). Matching is case-sensitive exact against the value stored on the record. The id must be a filterable custom field defined on this entity — use `GET /public/v1/custom-fields?parent_object=contact` to list the ids, their types, and which are filterable. A non-numeric id, an id not defined on this entity, or an id that isn't filterable returns a 400.","in":"query","name":"custom_data","required":false,"schema":{"type":"object"}}],"responses":{"200":{"description":"A list of contacts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Contacts"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get contacts","tags":["Contact"]},"post":{"description":"Upsert a single contact — one POST handles both create and update. Include an existing contact `id` to update that contact, or omit `id` to create a new one. The create path returns 201 and the update path returns 200; either way the response body is the full contact.\n\nUpdates are sparse: only the fields you send are changed, and any field you omit keeps its current value. `custom_data` is the exception in one direction — sending it replaces the contact's entire custom-field map (it is not merged key-by-key), so include every key you want to keep; omitting `custom_data` leaves the existing values untouched.\n\nA contact is a person record in your CRM. It is linked to a company relationship (its employer) via `company_id`, to an owning Distru user via `owner_id`, and carries a name/email/phone profile plus optional driver-license details that surface on an order's shipping manifest when this contact is named as the driver. Writing a contact does not move inventory and does not sync to Metrc or BioTrack; it does record an activity-log entry on the contact and notify the relevant users. New contacts appear in `GET /public/v1/contacts` within about a second (the list is eventually consistent).\n\nRequired permission: `contacts_permissions_create` to create a new contact, `contacts_permissions_edit` (plus access to the contact under your team restrictions) to update an existing one.\n","operationId":"DistruPublic.V1.ContactController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID of the contact. Provide it to update that contact — it must belong to your company, otherwise the request returns 404. Omit it to create a new contact."},"first_name":{"type":"string","description":"First name of the contact. Required on create; on update, omitting it leaves the current value unchanged. Must be 1-100 characters and cannot be the literal `you`/`You`. Combined with `last_name` to form the response `full_name`."},"last_name":{"type":"string","description":"Last name of the contact. When provided, must be 1-100 characters. Combined with `first_name` to form the response `full_name`."},"title":{"type":"string","description":"Job title of the contact."},"email":{"type":"string","description":"Email address of the contact. When provided, must contain an `@`."},"phone_number":{"type":"string","description":"Primary phone number of the contact."},"work_phone_number":{"type":"string","description":"Work phone number of the contact, returned as `work_phone_number`."},"description":{"type":"string","description":"Free-text note describing the contact."},"company_id":{"type":"string","description":"ID of the company relationship (the contact's employer) in your network that this contact belongs to. Must reference an existing company relationship you own. Returned as `company.id` in the response. Omit to create the contact without an employer; on update, omitting leaves the current employer unchanged."},"driver_license_number":{"type":"string","description":"Driver license number recorded on shipping manifests when this contact is assigned as the driver on an order shipment."},"driver_license_issuing_state":{"type":"string","description":"Two-letter US state or territory abbreviation (e.g. `CA`, `PR`) of the license's issuing state, recorded on shipping manifests when this contact is the driver. Must be one of the recognized US state/territory codes; any other value is rejected."},"owner_id":{"type":"string","description":"ID of the Distru user who owns this contact. Must be an existing user in your company that is assignable to you under your team restrictions. Omit on create to leave the contact without an owner; on update, omitting leaves the current owner unchanged."},"custom_data":{"type":"object","description":"A map of custom field IDs to their values. Use GET /public/v1/custom-fields?parent_object=contact to retrieve the available custom fields, their IDs, and their types; every key you send must be one of those IDs or the request is rejected. 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. Sending this field replaces the contact's entire custom-field map, so include every key you want to keep; omit it to leave existing custom values unchanged."}},"required":["first_name"]}}},"required":true},"responses":{"200":{"description":"An updated contact","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"201":{"description":"A new contact","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a contact","tags":["Contact"]}},"/public/v1/assemblies/{id}":{"delete":{"description":"Deletes an assembly. This is a hard delete: the assembly is permanently removed together with all of its outputs, inputs, costs, and bin links — it disappears from `GET /public/v1/assemblies`, `GET /public/v1/assemblies/{id}` returns 404 for it, and it cannot be recovered through the API. This is the same operation as `POST /public/v1/assemblies` with `action` `DELETE`. Responds 204 with no body on success, or 404 if no assembly with that `id` exists in your company (including one that belongs to another company). Metrc and non-compliance (NONE) licenses only; BioTrack is not supported.\n\nSome assemblies cannot be deleted; each of these is refused with a 400 and nothing is changed:\n\n• An assembly with any COMPLETED output — which includes every COMPLETED assembly (completing an assembly requires all of its outputs to be completed). The inventory a completed output produced stays; there is no way to un-complete or delete such an assembly through the API.\n• A Metrc processing job assembly whose job has been adjusted in Metrc, or that has completed outputs. Finish the processing job in Metrc instead — Distru deletes the assembly automatically about 30 minutes later.\n• A system-generated assembly (`creation_source` SALES_ORDER, SPLIT_PACKAGE, or LAB_TESTING) — only MANUALLY_CREATED assemblies can be deleted through the API.\n• A BioTrack assembly.\n• An assembly another request is mutating at that same moment (transient — retry).\n\n[Inventory](#model-inventory): the quantities the assembly's PENDING or COMPLETED inputs had claimed, and the product-level quantities its DRAFT inputs had reserved, are released back to available inventory. An input package this assembly had fully consumed (finished) is reactivated, and unfinished in Metrc as well.\n\nOther effects, all in one atomic call: tasks tied to the assembly are deleted; batches linked to it and files attached to it are detached but kept. If the assembly created a Metrc processing job (and that job was never adjusted in Metrc), the job is deleted in Metrc as a side effect after the request commits (that sync is eventual — observe it in Metrc, not in the 204).\n\nRequired permission: `assemblies_permissions_delete`.\n","operationId":"DistruPublic.V1.AssemblyController.delete","parameters":[{"description":"ID of the assembly to delete, as returned by the list, fetch, and upsert endpoints. An ID that doesn't exist for your company returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete an assembly","tags":["Assembly"]},"get":{"description":"Get a single assembly by ID, with its full outputs, inputs, and costs.\n\nLike the list endpoint, this reflects eventually consistent data — an assembly you just created or\nupdated may take up to a second to reflect its latest state here. Returns 404 if no assembly with\nthat ID exists in your company or your team restrictions hide it.\n\nRequired permission: `assemblies_permissions_view`.\n","operationId":"DistruPublic.V1.AssemblyController.show","parameters":[{"description":"The ID of the assembly to fetch.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single assembly","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AssemblyResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get an assembly","tags":["Assembly"]}},"/public/v1/test-results/{id}":{"delete":{"description":"Deletes a test result. This is a hard delete: the record is permanently removed — it disappears from `GET /public/v1/test-results`, `GET /public/v1/test-results/{id}` returns 404 for it, and it cannot be recovered through the API. Responds 204 with no body on success, or 404 if no test result with that `id` exists in your company (including one that belongs to another company or was already deleted).\n\nOnly Distru-only results can be deleted: a result synced from Metrc or BioTrack (compliance-tracked) is refused with a 400. Nothing else blocks the delete — a result can be deleted even while it is the primary result of its package or batch.\n\nEffects beyond the test result itself: the result is detached from the package or batch it was attached to (and from any child packages it propagated to), and its attached Certificate of Analysis (COA) file is permanently deleted with it. Where the result was the primary one, the affected packages/batches and their [inventory](#model-inventory) are left with no test result — potency stops showing for that inventory until another result is created or marked primary via the upsert endpoint; no result is auto-promoted. No inventory quantity is created, consumed, or released, and nothing is synced to Metrc or BioTrack.\n\nRequired permission: `products_permissions_edit`.\n","operationId":"DistruPublic.V1.TestResultController.delete","parameters":[{"description":"ID of the test result to delete, as returned by the list, fetch, and upsert endpoints. An ID that doesn't exist for your company returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a test result","tags":["TestResult"]},"get":{"description":"Get a single test result by its ID, in full: potency values, lab metadata, `additional_test_results`, `coa_url`, the `is_primary` flag, and the `package_id`/`batch_id` it is attached to. Returns 404 if no test result with that ID exists for the authenticated company.\n\nRequired permission: `products_permissions_view`.\n","operationId":"DistruPublic.V1.TestResultController.show","parameters":[{"description":"The ID of the test result to fetch, as returned by the list and upsert endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single test result","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestResultResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a test result","tags":["TestResult"]}},"/public/v1/reports/cultivation-transaction-history":{"get":{"description":"Read-only report. Returns one row per cultivation transaction over the reported date range, covering the full plant lifecycle: plant batch creations, adjustments and splits, growth phase changes, plant moves, destructions, additive applications, teardowns, harvests, waste, and packaging. Each row carries the transaction's date, strain, batch name, plant tag(s), product name, package label(s), type, the related entity (teardown or harvest) and its status, the signed amount and unit, and the transaction's description. Rows reflect the same plant events tracked in the state traceability system (Metrc); this endpoint only reads them and changes nothing.\n\nRows are sorted most-recent-first by transaction date. When no date filter is provided, the report defaults to the last 30 days. The `datetime`, `transaction_type`, `strain`, `plant_batch_ids`, and `license_ids` filters combine with AND — each narrows the result — while multiple values within `plant_batch_ids` or `license_ids` are OR'd. With no `license_ids` filter the report spans every license on your company.\n\nEvery value is returned as it appears in the report's CSV export, with numeric cells (amount, total cost) returned as strings with the currency and comma formatting stripped, so amounts match the rest of the API. Two columns are not normalized the way the rest of the public API is: `type` is a human-readable display string (e.g. `Move Plant(s)`), not a SCREAMING_CASE enum token, and the `transaction_type` filter accepts those same human-readable names; `related_entity_status` is the underlying teardown/harvest workflow status passed straight through (`PREPARING`, `PENDING`, or `COMPLETED`) rather than remapped through the public API's enum layer. The `total_cost` column is present only for API keys whose user can view costs; for everyone else the key is omitted from every row and from `meta.columns`. Report-level information (the resolved date range and the column definitions actually present) is returned under `meta`.\n\nRequired permission: `reports_permissions_cultivation_transaction_history` (and, for the `total_cost` column, permission to view costs).\n","operationId":"DistruPublic.V1.Report.CultivationTransactionHistoryController.index","parameters":[{"description":"Restricts rows to transactions whose date falls in the given range. Value is a comma-separated `after,before` pair of ISO8601 timestamps, both bounds inclusive. Either side may be left empty for an open-ended range: `2026-01-01T00:00:00Z,` returns everything on or after that instant, and `,2026-02-01T00:00:00Z` everything on or before it. Omit entirely to default to the last 30 days. The resolved range is echoed in `meta.date_range`.","in":"query","name":"datetime","required":false,"example":"?datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Restricts rows to a single cultivation transaction type. Accepts one of the human-readable type names (not a SCREAMING_CASE token) listed in `enum`. Omit to include every type. Each row's `type` field uses these same names; a Growth Phase Change row additionally appends the transition (e.g. `Growth Phase Change (Immature → Vegetative)`), but filter on the bare name shown here.","in":"query","name":"transaction_type","required":false,"schema":{"type":"string","enum":["Plant Batch Creation","Plant Batch Adjustment","Split Plant Batch","Destroy Plant","Move Plant(s)","Growth Phase Change","Additive Application","Package Plant Batch","Plant Added to Teardown","Harvest Created from Teardown","Record Waste (Harvest)","Create Package from Harvest"]}},{"description":"Restricts rows to an exact strain name — a whole-string, case-sensitive match, not a substring search. Omit to include all strains. Matches the value shown in each row's `strain` field.","in":"query","name":"strain","required":false,"example":"?strain=Blue Dream","schema":{"type":"string"}},{"description":"Restricts rows to transactions involving the given plant batches (plant groups), including the downstream teardown, harvest, waste, and packaging rows derived from them. Each value is a Distru plant batch ID; malformed IDs are ignored. Omit to include all batches. Multiple IDs are OR'd (a row tied to any listed batch is included) and this filter is AND'd with the others.","in":"query","name":"plant_batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restricts rows to transactions recorded under the given Distru license IDs. Each ID is matched against the licenses on your company; IDs outside your company are ignored. Omit (or leave empty) to include every license on your company. Multiple IDs are OR'd and this filter is AND'd with the others.","in":"query","name":"license_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Cultivation Transaction History report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CultivationTransactionHistoryReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Cultivation Transaction History report","tags":["CultivationTransactionHistory"]}},"/public/v1/packages":{"get":{"description":"Get packages sorted by their creation date and filtered by various attributes\n\nThis endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.\n\nRequired permission: `products_permissions_view`. Results are filtered to\nonly include packages the authenticated user can access under their team\nrestrictions.\n","operationId":"DistruPublic.V1.PackageController.index","parameters":[{"description":"Filter by creation datetime, given as an inclusive `after,before` range of ISO 8601 timestamps separated by a comma. Either bound may be left empty: `after,` keeps only packages created on or after `after`; `,before` only those created on or before `before`; `after,before` keeps those inside the closed range. A range with both bounds empty is rejected.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"1-based page number. Defaults to page 1 when omitted; must be greater than 0. Page size is fixed, so read the `next_page` URL in the response to fetch the following page rather than incrementing this yourself; `next_page` is null on the last page.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Keep only packages currently stored in any of these Distru location IDs (matches the `location.id` string in a package). Combined with the other filters by AND; multiple ids inside this filter are OR-ed. Ids that do not resolve to one of your locations simply match nothing.","in":"query","name":"location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose license has exactly this license number (exact string match, not a substring).","in":"query","name":"license_number","required":false,"example":"?license_number=1234567890","schema":{"type":"string"}},{"description":"Filter packages by package ID (the same string as each package's `id` in responses). Multiple ids are OR-ed. Values that do not decode to a known package id match no rows.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages of any of these product IDs (matches a package's `product_id`). Multiple ids are OR-ed.","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages in any of these Distru statuses (multiple values are OR-ed). Case-insensitive. If any value is not one of the statuses below, the filter matches no rows. Allowed values:\n• `ACTIVE` — active in both Distru and the compliance system\n• `ASSEMBLING` — fully consumed by a pending assembly\n• `DESTROYED` — destroyed (BioTrack only; Metrc packages never reach this)\n• `DISCONTINUED` — discontinued in Metrc\n• `FINISHED` — finished (quantity 0)\n• `ONHOLD` — on hold in Metrc (Metrc only)\n• `RETURNING` — tied to a Metrc return that has shipped\n• `SELLING` — assigned to a Distru sales order\n• `SOLD` — sold on a Metrc-enabled sales order\n• `TRANSFERRED` — transferred out of its Metrc license\n\nNote: the `status` field in responses is returned lowercase (e.g. `active`); this filter accepts either case. (The old `statuses` filter name still works but is deprecated.)\n","in":"query","name":"distru_statuses","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"When `true`, each package in the response also carries its cost fields (`total_cost_actual`, `total_cost_default`, `cost_per_unit_actual`, `cost_per_unit_default`). Omitted or `false` leaves those fields out of the response entirely. These cost fields are available only on this list endpoint; the single-package GET (`/packages/{id}`) never returns them, so it is not a superset of the list.","in":"query","name":"include_costs","required":false,"schema":{"type":"boolean"}},{"description":"Filter by last-modified datetime, given as an inclusive `after,before` range of ISO 8601 timestamps separated by a comma, with the same empty-bound rules as `inserted_datetime`.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=,2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by custom field values, as `custom_data[{id}]=value` where `{id}` is a custom field's numeric id. Repeat with different ids to filter on several fields at once; a record must match every one (AND). Matching is case-sensitive exact against the value stored on the record. The id must be a filterable custom field defined on this entity — use `GET /public/v1/custom-fields?parent_object=package` to list the ids, their types, and which are filterable. A non-numeric id, an id not defined on this entity, or an id that isn't filterable returns a 400.","in":"query","name":"custom_data","required":false,"schema":{"type":"object"}},{"description":"Case-insensitive substring match on the package's `compliance_label`.","in":"query","name":"compliance_label","required":false,"schema":{"type":"string"}},{"description":"Keep only packages whose `compliance_label` exactly matches (case-sensitive) any value in the list — send labels exactly as they appear in responses. Multiple values are OR-ed.","in":"query","name":"compliance_labels","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Case-insensitive substring match on the package's `batch_number`.","in":"query","name":"batch_number","required":false,"schema":{"type":"string"}},{"description":"Keep only packages whose `batch_number` exactly matches (case-sensitive) any value in the list — send batch numbers exactly as they appear in responses. Multiple values are OR-ed.","in":"query","name":"batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Case-insensitive substring match on the package's `compliance_product_name`.","in":"query","name":"compliance_product_name","required":false,"schema":{"type":"string"}},{"description":"Keep only packages whose `compliance_product_name` exactly matches (case-insensitive) any value in the list. Multiple values are OR-ed.","in":"query","name":"compliance_product_names","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose license is any of these Distru license IDs (matches a package's `license_id`). Multiple ids are OR-ed; ids that do not resolve match nothing.","in":"query","name":"license_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages stored in any of these bins (matches an id in a package's `bins[].id`). Multiple ids are OR-ed.","in":"query","name":"bin_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages owned by any of these users (matches a package's `owner.id`). Multiple ids are OR-ed.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages with any of these unit types (matches a package's `unit_type.id`). Multiple ids are OR-ed.","in":"query","name":"unit_type_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose product is in any of these categories (matches the package's `product.category.id`). Multiple ids are OR-ed.","in":"query","name":"product_category_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose product is in any of these subcategories (matches the package's `product.subcategory.id`). Multiple ids are OR-ed.","in":"query","name":"product_subcategory_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose product is in any of these groups (matches the package's `product.product_group.id`). Multiple ids are OR-ed.","in":"query","name":"product_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose product has any of these brands (matches the package's `product.brand.id`). Multiple ids are OR-ed.","in":"query","name":"product_brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose product is supplied by any of these vendors (matches the package's `product.vendor.id`; this is the company-relationship ID, not the raw company ID). Multiple ids are OR-ed.","in":"query","name":"product_vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose product has any of these strains (matches the package's `product.strain.id`). Multiple ids are OR-ed.","in":"query","name":"product_strain_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose product carries any of these tags (matches an id in the package's `product.tags[].id`). Multiple ids are OR-ed.","in":"query","name":"product_tag_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose product SKU exactly matches (case-insensitive) any value in the list (matches the package's `product.sku`). Multiple values are OR-ed.","in":"query","name":"product_skus","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only packages whose `lab_testing_state` exactly equals this value (case-insensitive).","in":"query","name":"lab_testing_state","required":false,"schema":{"type":"string"}},{"description":"Keep only packages whose `lab_testing_state` is any of these values (case-insensitive, OR-ed).","in":"query","name":"lab_testing_states","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by the package's `expiration_datetime`, given as an inclusive `after,before` range of ISO 8601 timestamps separated by a comma, with the same empty-bound rules as `inserted_datetime`.","in":"query","name":"expiration_datetime","required":false,"example":"?expiration_datetime=2026-01-01T00:00:00Z,2026-12-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the package's `finished_datetime`, an inclusive `after,before` range of ISO 8601 timestamps separated by a comma, same empty-bound rules as `inserted_datetime`.","in":"query","name":"finished_datetime","required":false,"example":"?finished_datetime=2026-01-01T00:00:00Z,","schema":{"type":"string"}},{"description":"Filter by the package's `inactivated_datetime`, an inclusive `after,before` range of ISO 8601 timestamps separated by a comma, same empty-bound rules as `inserted_datetime`.","in":"query","name":"inactivated_datetime","required":false,"example":"?inactivated_datetime=,2026-12-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the package's `harvest_date`, given as an inclusive `after,before` range of `YYYY-MM-DD` dates separated by a comma. Either bound may be empty; both empty is rejected.","in":"query","name":"harvest_date","required":false,"example":"?harvest_date=2026-01-01,2026-12-31","schema":{"type":"string"}},{"description":"Filter by the package's `packaged_date`, an inclusive `after,before` range of `YYYY-MM-DD` dates separated by a comma, same empty-bound rules as `harvest_date`.","in":"query","name":"packaged_date","required":false,"example":"?packaged_date=2026-01-01,","schema":{"type":"string"}},{"description":"Keep only test-sample (`true`) or non-test-sample (`false`) packages. Omit to match either.","in":"query","name":"is_test_sample","required":false,"schema":{"type":"boolean"}},{"description":"Keep only trade-sample (`true`) or non-trade-sample (`false`) packages. Omit to match either.","in":"query","name":"is_trade_sample","required":false,"schema":{"type":"boolean"}},{"description":"Keep only production-batch (`true`) or non-production-batch (`false`) packages. Omit to match either.","in":"query","name":"is_production_batch","required":false,"schema":{"type":"boolean"}},{"description":"Keep only packages that currently hold active quantity (`true`, `quantity_active > 0`) or none (`false`, `quantity_active = 0`). Omit to match either.","in":"query","name":"has_quantity_active","required":false,"schema":{"type":"boolean"}},{"description":"Keep only packages whose primary lab result has a file attached (`true`) or does not (`false`) — i.e. whether a downloadable COA exists. Omit to match either.","in":"query","name":"has_coa_attached","required":false,"schema":{"type":"boolean"}},{"description":"Keep only packages that contain remediated material (`true`) or do not (`false`). Omit to match either.","in":"query","name":"contains_remediated_material","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"A list of packages","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Packages"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get packages","tags":["Package"]}},"/public/v1/company-groups":{"get":{"description":"Lists the company groups owned by the authenticated company. Company groups are the labels you\nuse to organize your business relationships (your customers and vendors) — for example\n\"Wholesale\", \"Retail\", or a region — and price tiers can target or exclude companies by group.\n\nResults are scoped to your company only; groups belonging to other companies are never returned.\nThey come back sorted by creation time, oldest first, and are paginated: read `next_page` from the\nenvelope and request that URL to fetch the following page (it is null on the last page).\n\nRequired permission: `settings_permissions_company_relationship_groups`.\n","operationId":"DistruPublic.V1.CompanyGroupController.index","parameters":[{"description":"Restrict the result to specific company groups by ID (the same ID returned as each company group's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to company relationship groups by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to company relationship groups by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page to fetch, 1-based. Defaults to the first page when omitted. Example: `?page[number]=2`.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of company groups","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CompanyGroups"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get company groups","tags":["CompanyGroup"]},"post":{"description":"Creates or renames a single company group in one endpoint. Pass `id` to rename the matching\nexisting group; omit `id` to create a new one. A create responds 201, an update responds 200.\nCompany groups are the labels you use to organize your business relationships (customers and\nvendors); price tiers can target or exclude companies by group.\n\n`name` is required and must be unique within your company — reusing a name already taken by\nanother of your groups is rejected with a 400. On update, `name` fully replaces the stored name;\nthere are no other editable fields, so this endpoint only ever sets the group's name.\n\nRenaming a group does not change its ID, so any price tier that already targets or excludes this\ngroup keeps doing so under the new name. Creating a group does not attach it to any company\nrelationship — membership is managed separately.\n\nRequired permission: `settings_permissions_company_relationship_groups`.\n","operationId":"DistruPublic.V1.CompanyGroupController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID of the group to update. Present → the matching group owned by your company is renamed (404 if no such group exists for you). Absent → a new group is created."},"name":{"type":"string","description":"Display name of the company group. Required on both create and update. Leading/trailing whitespace is trimmed, and the result must be non-empty, at most 255 characters, and unique among your company's groups (a duplicate returns 400). Allowed characters are letters, digits, spaces, underscores, and `~#-$/|%&'().`; it may not contain two colons in sequence (`::`). Any other character is rejected with a 400. On update this replaces the existing name."}},"required":["name"]}}},"required":true},"responses":{"200":{"description":"The updated company group","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CompanyGroupFullResponse"}}}},"201":{"description":"The created company group","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CompanyGroupFullResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a company group","tags":["CompanyGroup"]}},"/public/v1/tags/{id}":{"delete":{"description":"Permanently deletes the tag. This is a hard delete — the tag is gone for good and cannot be recovered;\nto reuse the name later you must create a fresh tag.\n\nDeleting a tag also detaches it from everything it was applied to: it is removed from every\n[product](#model-product) and every tax that carried it. Those products and taxes are not otherwise\nchanged — only the tag label disappears from them. No inventory and no compliance system (Metrc/BioTrack)\nis affected.\n\nReturns 404 when no tag with that ID exists for the authenticated company.\n\nAny authenticated API key for the company may manage tags; no additional settings permission is required.\n","operationId":"DistruPublic.V1.TagController.delete","parameters":[{"description":"ID of the tag to delete.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a tag","tags":["Tag"]},"get":{"description":"Fetches a single tag by its ID. Returns 404 when no tag with that ID exists for the authenticated\ncompany — IDs from another company are never visible.\n\nAny authenticated API key for the company may read tags; no additional settings permission is required.\n","operationId":"DistruPublic.V1.TagController.show","parameters":[{"description":"ID of the tag to fetch, as returned by the list and upsert endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single tag","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TagResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a tag","tags":["Tag"]}},"/public/v1/assemblies/split_package":{"post":{"description":"Split a single Metrc source package into multiple output packages.\n\nCreates one assembly containing every input and output. On success each output package is\nqueued for creation in Metrc and synced asynchronously — the Metrc package identifiers are\nnot present in the immediate response, and the source and output packages are briefly flagged\nas syncing.\n\nMetrc licenses only. Up to 300 outputs per request. The whole split is applied atomically:\nif any output is rejected, none are created.\n\nRequired permission: `assemblies_permissions_create`.\n","operationId":"DistruPublic.V1.AssemblyController.split_package","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SplitPackageRequest"}}},"description":"The source package and outputs to create","required":true},"responses":{"201":{"description":"The created assembly","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AssemblyResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Split a package","tags":["Assembly"]}},"/public/v1/bins":{"get":{"description":"List the bins belonging to the authenticated company, sorted alphabetically by name (A→Z, case-insensitive).\n\nA bin is a named sub-location used by bin inventory tracking — packages, batches, plants, plant groups and assembly outputs are associated to a bin to record where within a location they physically sit. Bins only exist for companies that have bin inventory tracking enabled, so this list is empty for companies that do not.\n\nResults are paginated; follow `next_page` in the response envelope to page forward, or stop when it is null.\n\nRequired permission: `settings_permissions_bins`.\n","operationId":"DistruPublic.V1.BinController.index","parameters":[{"description":"Restrict the result to specific bins by ID (the same ID returned as each bin's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Case-insensitive substring filter on the bin name — returns every bin whose name contains this text anywhere. Omit to return all bins. Example: `?search=cooler` matches \"Walk-in Cooler\" and \"Cooler 2\".","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Filter to bins by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to bins by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page number to fetch, as `page[number]`. Must be a positive integer; defaults to 1 when omitted (a zero or negative value is rejected with a 400). Each page returns up to 500 bins; when more remain, the response envelope's `next_page` holds the URL for the following page and is null on the last page. Example: `?page[number]=2`.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of bins","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Bins"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get bins","tags":["Bin"]},"post":{"description":"Create or update a single bin. Pass `id` to update the matching bin; omit `id` to create a new one. A create responds 201 with the new bin; an update responds 200 with the updated bin. Both responses carry the full bin, including its server-assigned `id` and timestamps.\n\nA bin is a named sub-location used by bin inventory tracking — packages, batches, plants, plant groups and assembly outputs are associated to a bin to record where within a location they physically sit. This endpoint only names and renames the bin itself; it never moves, creates, or consumes inventory, and it does not change which packages, batches or plants are associated to a bin. Renaming a bin is reflected everywhere that bin is already referenced.\n\nNames are unique per company, case-insensitively — `Cooler` and `cooler` collide — and cannot contain commas. A create or update that would collide with an existing name, or that includes a comma, is rejected with a 400 and a human-readable message.\n\nBins only exist for companies that have bin inventory tracking enabled; if it is disabled the request is rejected. Updates are scoped to your company — an `id` for a bin you do not own is treated as not found.\n\nRequired permission: `settings_permissions_bins`.\n","operationId":"DistruPublic.V1.BinController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The bin's ID. Provide it to update that bin (only `name` can change); omit it to create a new bin. An ID for a bin outside your company returns 404."},"name":{"type":"string","description":"The bin's name. Required on both create and update. Must be unique within your company (compared case-insensitively) and cannot contain a comma; violating either returns a 400. This is the only editable field on update."}},"required":["name"]}}},"required":true},"responses":{"200":{"description":"The updated bin","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BinResponse"}}}},"201":{"description":"The created bin","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BinResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a bin","tags":["Bin"]}},"/public/v1/product-groups/{id}":{"delete":{"description":"Permanently deletes the product group. This is a hard delete: the record is removed and cannot\nbe recovered. Returns 404 if no group with that ID belongs to the company.\n\nDeleting a group ripples to everything that referenced it:\n• Products in this group are un-grouped — their group becomes empty. The products themselves are\nnot deleted.\n• The group is dropped from any menus that featured it as a collection.\n• Any price tier that targeted or excluded this group has that condition removed, which changes\nwhich products those price tiers apply to.\n\nRequired permission: `settings_permissions_product_groups`.\n","operationId":"DistruPublic.V1.ProductGroupController.delete","parameters":[{"description":"The product group's Distru ID, as returned in the `id` field of any product group response.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a product group","tags":["ProductGroup"]},"get":{"description":"Fetches a single product group by its ID, scoped to the authenticated company. Returns 404 if\nno group with that ID belongs to the company.\n\nRequired permission: `settings_permissions_product_groups`.\n","operationId":"DistruPublic.V1.ProductGroupController.show","parameters":[{"description":"The product group's Distru ID, as returned in the `id` field of any product group response.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single product group","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductGroupResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a product group","tags":["ProductGroup"]}},"/public/v1/taxes":{"get":{"description":"List the taxes configured for the authenticated company. A tax is a named, reusable tax rate\n(percentage) that gets applied to [order](#model-order) and [invoice](#model-invoice) line\ntotals. Use this to enumerate the taxes you can reference elsewhere and to read their current\nrates, codes, and QuickBooks Online mappings.\n\nResults are ordered oldest-first (by creation time) and paginated. Soft-deleted taxes are never\nreturned. This endpoint is read-only — taxes cannot be created, edited, or deleted through the\npublic API — so it has no effect on inventory, compliance, or any other entity.\n\nRequired permission: `settings_permissions_taxes`.\n","operationId":"DistruPublic.V1.TaxController.index","parameters":[{"description":"Restrict the result to specific taxes by ID (the same ID returned as each tax's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to taxes by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to taxes by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page to fetch, passed as `page[number]`. 1-based; defaults to 1 when omitted. Must be greater than 0 — a value of 0 or below is rejected with a 400. Each page holds up to 500 taxes (page size is fixed and cannot be changed). When more pages exist the response envelope's `next_page` holds the ready-to-call URL for the following page, otherwise `next_page` is null. A page past the end returns an empty `data` array with a null `next_page`.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of taxes","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Taxes"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get taxes","tags":["Tax"]}},"/public/v1/payments/{id}":{"get":{"description":"Get a single payment by its ID.\n\nThe response carries the same shape as a list entry, plus `quickbooks_deposit_account_name` (the human-readable name of the QuickBooks Online deposit account, resolved only here — the list endpoint omits it). Like the list endpoint, exactly one of `invoice` / `purchase` is populated depending on `payment_type`, and `status` is POSTED or VOIDED.\n\nReturns 404 if no payment with that ID exists within your company. Voided payments are returned normally with `status` VOIDED; soft-deleted payments return 404.\n\nRequired permission: `payments_permissions_view`.\n","operationId":"DistruPublic.V1.PaymentController.show","parameters":[{"description":"The payment's Distru ID, as returned in the `id` field of the list endpoint.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single payment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PaymentResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a payment","tags":["Payment"]}},"/public/v1/reports/inventory-valuation":{"get":{"description":"Returns one row per product with its current on-hand position — active, assembling, reserved, available, incoming, and pending-output quantities — alongside the product's descriptive attributes (SKU, vendor, brand, unit type, category, subcategory, group, owner), its unit cost and price, its inventory alert thresholds, and its active inventory value. This is a read-only snapshot computed at request time; it never changes inventory, and does not touch compliance systems (Metrc/BioTrack). Use it to reconcile stock and value a company's holdings, not to drive stock movements.\n\nQuantities are derived, not stored verbatim:\n• `available_quantity` is `active_quantity` minus `reserved_quantity`, and can be negative when reservations exceed on-hand stock.\n• `incoming_quantity` sums quantities on open (not-yet-received) purchases; `pending_output_quantity` sums the output still owed by open assemblies.\n• Active and assembling quantities count only inventory that is itself active. When `location_ids` and/or `user_ids` are supplied, the counted stock is narrowed to those locations and users (see those params).\n\nThe active value is `active_quantity` × unit price by default. Pass `calculation_method=cost` to value it by unit cost instead; this also renames the value column (`active_value_price` becomes `active_value_cost`). A product with no unit cost set is valued at 0 under `cost` (unit price is always set, so `price` never hits this case).\n\nEvery value is returned as it appears in the report's CSV export, with numeric cells returned as strings (currency and comma formatting stripped, matching the rest of the API); a few identifier-like values with significant leading zeros keep their display string. Any Product custom fields configured for the company are appended as extra keys on each row (one per custom field), so the row shape varies by company. Report-level information (the resolved report date and column definitions) is returned under `meta`. All matching products are returned; there is no pagination. Rows come back ordered by `available_quantity`, highest first.\n\nRequired permission: `reports_permissions_inventory_valuation`.\n","operationId":"DistruPublic.V1.Report.InventoryValuationController.index","parameters":[{"description":"Keeps only products that match the term. Matching is case-insensitive: a product is kept when the term appears anywhere in its name or SKU, or when the term is a close fuzzy match on the name (so minor misspellings of the name still match). Omit (or pass empty) to return every product.","in":"query","name":"search","required":false,"example":"?search=Blue%20Dream","schema":{"type":"string"}},{"description":"How to value active inventory. `price` (the default when omitted) values each product at `active_quantity` × unit price; `cost` values it at `active_quantity` × unit cost and renames the value column from `active_value_price` to `active_value_cost`. A product with no unit cost set is valued at 0 under `cost`.","in":"query","name":"calculation_method","required":false,"schema":{"type":"string","enum":["cost","price"]}},{"description":"Narrows the active/assembling/reserved/incoming/pending-output stock counted to these locations. Values are Distru location IDs. Combined with `user_ids` as a union — stock counts if it sits at any listed location OR belongs to any listed user. Omit (or pass empty) to count stock across all locations.","in":"query","name":"location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Narrows the counted stock to inventory belonging to these users. Values are Distru user IDs. Combined with `location_ids` as a union (see that param). Omit (or pass empty) to count stock across all users.","in":"query","name":"user_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keeps only products supplied by these vendors. Values are Distru vendor IDs (the vendor company relationship). Omit (or pass empty) to include every vendor.","in":"query","name":"vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keeps only products under these brands. Values are Distru brand IDs (the brand company relationship). Omit (or pass empty) to include every brand.","in":"query","name":"brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Inventory Valuation report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/InventoryValuationReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Inventory Valuation report","tags":["InventoryValuation"]}},"/public/v1/bins/{id}":{"delete":{"description":"Permanently delete a bin. This is a hard delete — the bin is gone for good and cannot be recovered; there is no soft-delete or undo.\n\nDeleting a bin also detaches it from everything associated to it: any packages, batches, plants, plant groups and assembly outputs that referenced this bin are unbinned. Those records, and their inventory, are not deleted — they simply no longer point to any bin. Nothing about inventory quantities, packages, or compliance changes; only the bin association is removed.\n\nReturns 404 if no bin with that ID belongs to your company (the lookup is company-scoped), and 204 with no body on success.\n\nBins only exist for companies that have bin inventory tracking enabled; if it is disabled the request is rejected.\n\nRequired permission: `settings_permissions_bins`.\n","operationId":"DistruPublic.V1.BinController.delete","parameters":[{"description":"The bin's ID.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a bin","tags":["Bin"]},"get":{"description":"Fetch a single bin by ID.\n\nReturns 404 if no bin with that ID belongs to the authenticated company — the lookup is scoped to your company, so an ID from another company is indistinguishable from one that does not exist.\n\nRequired permission: `settings_permissions_bins`.\n","operationId":"DistruPublic.V1.BinController.show","parameters":[{"description":"The bin's ID.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single bin","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BinResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a bin","tags":["Bin"]}},"/public/v1/invoices":{"get":{"description":"List the company's invoices, newest first (sorted by invoice date, descending), with the given filters applied. All filters combine with AND; each returned invoice matches every filter you pass. This is a read-only operation with no side effects.\n\nResults are paginated. The response carries a `next_page` URL only while more pages remain; follow it to walk the full result set. Page size is fixed at 500 invoices.\n\nThis endpoint returns eventually consistent data: a just-created or just-updated invoice may take up to about 1 second to appear or reflect its latest values here.\n\nRequired permission: `invoices_permissions_view`. Results are further narrowed to only the invoices the authenticated user is allowed to see under their team restrictions, so two API keys on the same company can see different subsets.\n","operationId":"DistruPublic.V1.InvoiceController.index","parameters":[{"description":"Restrict the result to specific invoices by ID (the same ID returned as each invoice's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by the invoice's due datetime. A comma-separated `min,max` range of ISO8601 UTC datetimes; both bounds are inclusive and either may be omitted. `2022-07-10T00:00:00Z,` keeps invoices due on or after that instant, `,2022-07-10T00:00:00Z` keeps those due on or before it, and passing both bounds keeps invoices due within the range.","in":"query","name":"due_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the datetime the invoice was created in Distru. A comma-separated `min,max` range of ISO8601 UTC datetimes; both bounds are inclusive and either may be omitted (see `due_datetime` for the range semantics).","in":"query","name":"inserted_datetime","required":false,"example":"2022-07-10T00:00:00Z,2022-07-11T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the invoice datetime (the date the invoice is dated for, which is also the sort key). A comma-separated `min,max` range of ISO8601 UTC datetimes; both bounds are inclusive and either may be omitted (see `due_datetime` for the range semantics).","in":"query","name":"invoice_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Filter to invoices whose invoice number contains this value. This is a case-insensitive substring match, not an exact match — `001` matches `INV-0012` and `10015`.","in":"query","name":"invoice_number","required":false,"example":"001","schema":{"type":"string"}},{"description":"Filter by payment status. Repeat the bracketed key to pass several statuses; an invoice matches when its status equals any value you supply (OR across values). Values are SCREAMING_CASE:\n• `NOT_PAID` — no payments recorded yet\n• `PARTIALLY_PAID` — some, but not all, of the total has been paid\n• `FULLY_PAID` — paid in full\n• `OVER_PAID` — recorded payments exceed the total. Rare and effectively legacy: new overpayments are converted into a customer credit instead of moving an invoice into this status.","in":"query","name":"statuses","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["NOT_PAID","PARTIALLY_PAID","FULLY_PAID","OVER_PAID"],"type":"string"}}},{"description":"Filter to invoices billing any of these sales orders. Repeat the bracketed key to pass several IDs; an invoice matches when its order's `id` equals any value you supply (OR across values). Empty list is no filter. At most 200 IDs.","in":"query","name":"order_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices whose sales order has any of these human-readable order numbers (the SO number shown in the UI, not the order's `id`). Case-insensitive exact match per value. Repeat the bracketed key per value; empty list is no filter. At most 200 values.","in":"query","name":"order_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices whose sales order has any of these statuses (OR across values). SCREAMING_CASE: `PENDING`, `PROCESSING`, `READY_TO_SHIP`, `DELIVERING`, `DELIVERED`, `COMPLETED`, `CANCELED`. Empty list is no filter. At most 200 values. This is the order's status, distinct from the invoice's payment `statuses`.","in":"query","name":"order_statuses","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"}}},{"description":"Filter to invoices whose order bills any of these customers (the order's customer, returned as each invoice's `company.id`). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"company_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices whose customer belongs to any of these company relationship groups. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"company_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices owned by any of these Distru users (matching each invoice's `owner.id`). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices whose invoice number is any of these values. Case-sensitive exact match per value (send numbers exactly as they appear in responses) — unlike `invoice_number`, which is a case-insensitive substring match. Repeat the bracketed key per value; empty list is no filter. At most 200 values.","in":"query","name":"invoice_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by whether the invoice is voided. `true` keeps only voided invoices, `false` keeps only non-voided ones; omit for both. An invoice is voided automatically when its sales order is canceled. See also `voided_datetime`.","in":"query","name":"is_voided","required":false,"schema":{"type":"boolean"}},{"description":"Filter by the invoice `total` (the value returned as each invoice's `total`, including line items, charges, discounts, and taxes). Inclusive range written as `min,max`; either side may be omitted. `100,` keeps invoices totaling 100 or more, `,500` keeps those totaling 500 or less, and `100,500` keeps those in between.","in":"query","name":"total","required":false,"example":"100,500","schema":{"type":"string"}},{"description":"Filter by the invoice's outstanding balance (`remaining_amount` — the `total` minus recorded payments; negative when over-paid). Inclusive range written as `min,max`; either side may be omitted (see `total` for the range semantics). `0,0` keeps fully-paid invoices; `0.01,` keeps invoices with a positive balance still owed.","in":"query","name":"remaining_amount","required":false,"example":"0.01,","schema":{"type":"string"}},{"description":"The 1-based page number to fetch. Defaults to page 1 when omitted. Page size is fixed at 500 invoices; use the response's `next_page` link rather than guessing the last page. Must be greater than 0.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter by the datetime the invoice was last modified in Distru. A comma-separated `min,max` range of ISO8601 UTC datetimes; both bounds are inclusive and either may be omitted (see `due_datetime` for the range semantics).","in":"query","name":"updated_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the datetime the invoice was voided. A comma-separated `min,max` range of ISO8601 UTC datetimes; both bounds are inclusive and either may be omitted (see `due_datetime` for the range semantics). Non-voided invoices (null `voided_datetime`) never match a bound. See also `is_voided`.","in":"query","name":"voided_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Filter to invoices that bill a line item drawn from any of these batches (matching the billed order item's batch). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item drawn from any of these packages (matching the billed order item's package). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"package_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose package carries any of these compliance labels (the package's Metrc/BioTrack label). Case-sensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"package_compliance_labels","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose package has any of these batch numbers (the batch number stored on the package). Case-sensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"package_batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose batch has any of these batch numbers (matching the billed order item's batch). Case-sensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"batch_batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item of any of these products (matching the billed order item's product). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose product belongs to any of these product categories. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_category_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose product belongs to any of these product subcategories. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_subcategory_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose product belongs to any of these product groups. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose product has any of these brands. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose product has any of these vendors (the product's supplier company relationship). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose product has any of these strains. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_strain_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose product carries any of these tags. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_tag_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices that bill a line item whose product has any of these SKUs. Case-insensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_skus","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by custom field values, as `custom_data[{id}]=value` where `{id}` is a custom field's numeric id. Repeat with different ids to filter on several fields at once; a record must match every one (AND). Matching is case-sensitive exact against the value stored on the record. The id must be a filterable custom field defined on this entity — use `GET /public/v1/custom-fields?parent_object=invoice` to list the ids, their types, and which are filterable. A non-numeric id, an id not defined on this entity, or an id that isn't filterable returns a 400.","in":"query","name":"custom_data","required":false,"schema":{"type":"object"}}],"responses":{"200":{"description":"A list of invoices","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Invoices"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get invoices","tags":["Invoice"]},"post":{"description":"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.\n\nEvery 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.\n\nUpdates 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.\n\nA 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.\n\nSide 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.\n\nRequired 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.\n","operationId":"DistruPublic.V1.InvoiceController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"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":{"items":{"$ref":"#/components/schemas/InvoiceItemRequest"},"type":"array","description":"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."},"charges":{"items":{"$ref":"#/components/schemas/InvoiceChargeRequest"},"type":"array","description":"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."},"billing_location_id":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"Notes on this invoice that are visible to the customer. Optional."},"internal_notes":{"type":"string","description":"Notes on this invoice that are only visible internally. Optional."},"custom_data":{"type":"object","description":"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."}},"required":["order_id","due_datetime","invoice_datetime"]}}},"required":true},"responses":{"200":{"description":"A single invoice","content":{"*/*":{"schema":{"$ref":"#/components/schemas/InvoiceResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert an invoice","tags":["Invoice"]}},"/public/v1/invoices/{id}/payments":{"post":{"description":"Record a payment against an invoice. On success the invoice's `paid_amount`, `remaining_amount`, and payment `status` are recomputed to reflect the new payment.\n\nThe recorded amount is capped at the invoice's outstanding balance: if you send more than what remains, a payment for the remaining balance is recorded and the excess is turned into a customer credit for the account (it does not push the invoice into an over-paid status). A payment cannot be filed against an invoice that is already fully paid, or against a voided invoice — both are rejected. Returns 404 when the invoice does not exist in the caller's company.\n\nIf the company is connected to QuickBooks Online there are two extra behaviors. First, Distru synchronously pulls the latest payment and credit data from QuickBooks Online before recording, which can briefly fail with a \"try again in a few seconds\" error while that refresh runs — retry the request in that case. Second, the payment (or its invoice) is only queued for sync to QuickBooks Online once its dependencies are already synced there; otherwise the sync is deferred. A 200 is therefore not confirmation the payment reached QuickBooks Online — the response's `quickbooks_sync_enqueued` flag tells you whether a sync was queued, and you should poll QuickBooks Online to observe the final result. The payment may also be pushed to LeafLink when that integration is configured for the customer.\n\nRequired permission: `invoices_permissions_receive_payment`. The authenticated user must also be allowed to view invoices under their team restrictions.\n","operationId":"DistruPublic.V1.InvoiceController.insert_payment","parameters":[{"description":"The `id` of the invoice to record the payment against — the `id` returned by the list and show invoice endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"payment_method_id":{"type":"string","description":"The `id` of the payment method to record this payment under. Required. Retrieve valid IDs from GET /public/v1/payment-methods."},"amount":{"type":"number","format":"decimal","description":"The payment amount. Required. Rounded to 2 decimal places. Any portion exceeding the invoice's outstanding balance is not applied to the invoice — it becomes a customer credit instead."},"payment_datetime":{"type":"string","description":"The datetime the payment was made, as an ISO8601 datetime (e.g. `2026-08-20T00:00:00Z`). Required."},"description":{"type":"string","description":"A free-text description of the payment. Required."},"quickbooks_deposit_account_id":{"type":"string","description":"QuickBooks Online deposit account ID. Mutually exclusive with `quickbooks_deposit_account_name` — sending both is rejected. If your company is integrated with QuickBooks Online, exactly one of this or `quickbooks_deposit_account_name` must be provided; ignored if you are not integrated. The referenced account must be of type \"Bank\" or \"Other Current Asset\"."},"quickbooks_deposit_account_name":{"type":"string","description":"QuickBooks Online deposit account name. Mutually exclusive with `quickbooks_deposit_account_id` — sending both is rejected. If your company is integrated with QuickBooks Online, exactly one of this or `quickbooks_deposit_account_id` must be provided; ignored if you are not integrated. The referenced account must be of type \"Bank\" or \"Other Current Asset\"."}},"required":["payment_method_id","amount","payment_datetime","description"]}}},"required":true},"responses":{"200":{"description":"A single payment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PaymentResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Insert a payment for an invoice","tags":["Invoice"]}},"/public/v1/drivers":{"get":{"description":"Lists the drivers belonging to the authenticated company, oldest first (ordered by creation\ntime ascending). Soft-deleted drivers are excluded, and results never cross company\nboundaries.\n\nDrivers only exist for companies whose compliance type is METRC or BIOTRACK; a company with\nany other compliance type simply returns an empty list here.\n\nResults are paginated. The response `next_page` holds the URL of the following page, or null on\nthe last page.\n\nRequired permission: `settings_permissions_drivers`.\n","operationId":"DistruPublic.V1.DriverController.index","parameters":[{"description":"Restrict the result to specific drivers by ID (the same ID returned as each driver's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to drivers by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to drivers by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page to fetch, 1-based. Defaults to page 1 when omitted; the page size is fixed at 500. Must be greater than 0. Example: `?page[number]=2`.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of drivers","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Drivers"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get drivers","tags":["Driver"]},"post":{"description":"Creates or updates a single driver in one call. Omit `id` to create a new driver; pass the\n`id` of an existing driver to update it. Responds 201 on create and 200 on update, with\nthe full saved driver as the body.\n\nOnly companies whose compliance type is METRC or BIOTRACK can manage drivers. A request from a\ncompany with any other compliance type is rejected with a 400 and a single human-readable error\nmessage — no driver is created or changed.\n\nWhich fields are required depends on the company's compliance type, and the requirement is\nenforced only when CREATING (no `id`). On update, every field is optional: this is a sparse\nupdate, so only the fields you send are changed and any field you omit keeps its stored value.\nSending an empty string for an optional contact or license field (`email`, `us_state`,\n`driver_license`, `phone_number`, `occupational_license_number`) stores null rather than an\nempty string; leading and trailing whitespace on those fields is trimmed.\n• METRC create requires: `first_name`, `last_name`, `phone_number`, `driver_license`,\n  `occupational_license_number`. The BIOTRACK-only fields (`email`, `us_state`, `birth_date`,\n  `hire_date`) are ignored for METRC companies.\n• BIOTRACK create requires: `first_name`, `last_name`, `birth_date`, `email`, `driver_license`,\n  `us_state`, `hire_date`. The METRC-only fields (`phone_number`, `occupational_license_number`)\n  are ignored for BIOTRACK companies.\n\nCompliance side effects: for a BIOTRACK company, a successful save queues an asynchronous push\nto BioTrack — a 200/201 confirms the driver was stored in Distru, not that BioTrack accepted it,\nso poll `GET /public/v1/drivers/{id}` to observe the stored record. The save is rejected up\nfront with a 400 (and nothing is stored) when your company or user BioTrack credentials are\nmissing or lack permission for this operation; once the push is queued, a later rejection by\nBioTrack does not undo the Distru save. For a METRC company, the driver is stored for use when\nbuilding Metrc transfer templates and transfers and is not pushed to Metrc on save.\n\nRequired permission: `settings_permissions_drivers`.\n","operationId":"DistruPublic.V1.DriverController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID of the driver to update, as returned by the list, fetch, and upsert endpoints. Omit to create a new driver. When present but not matching a driver in your company, the request fails."},"first_name":{"type":"string","description":"The driver's first name. Required on create for every compliance type. On update, omit to leave unchanged."},"last_name":{"type":"string","description":"The driver's last name. Required on create for every compliance type. On update, omit to leave unchanged."},"email":{"type":"string","description":"The driver's email. Required on create for BIOTRACK companies; ignored for METRC companies (stored null). On update, omit to leave unchanged."},"phone_number":{"type":"string","description":"The driver's phone number. Required on create for METRC companies; ignored for BIOTRACK companies (stored null). May contain only digits, parentheses, `+`, `-`, and spaces (e.g. `+1 (415) 555-0100`). On update, omit to leave unchanged."},"driver_license":{"type":"string","description":"The driver's license number. Required on create for both METRC and BIOTRACK companies. On update, omit to leave unchanged."},"us_state":{"type":"string","description":"The US state that issued the driver's license, as a free-form string (e.g. `CA`). Required on create for BIOTRACK companies; ignored for METRC companies (stored null). On update, omit to leave unchanged."},"birth_date":{"type":"string","description":"The driver's date of birth as an ISO-8601 calendar date, `YYYY-MM-DD` (e.g. `1990-05-15`), no time component. Required on create for BIOTRACK companies; ignored for METRC companies (stored null). On update, omit to leave unchanged."},"hire_date":{"type":"string","description":"The date the driver was hired as an ISO-8601 calendar date, `YYYY-MM-DD` (e.g. `2023-01-09`), no time component. Required on create for BIOTRACK companies; ignored for METRC companies (stored null). On update, omit to leave unchanged."},"occupational_license_number":{"type":"string","description":"The driver's occupational license number. Required on create for METRC companies; ignored for BIOTRACK companies (stored null). On update, omit to leave unchanged."}}}}}},"responses":{"200":{"description":"The updated driver","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DriverResponse"}}}},"201":{"description":"The created driver","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DriverResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a driver","tags":["Driver"]}},"/public/v1/vehicles/{id}":{"delete":{"description":"Deletes a vehicle. This is a soft delete: the vehicle stops appearing in the list and fetch endpoints, its `id` returns 404, and it can no longer be updated through the upsert endpoint — but the record is retained, so anything that already references it is unaffected. The delete cannot be undone through the API; recreating the vehicle via upsert produces a new vehicle with a new `id`. Responds 204 with no body on success, or 404 if no vehicle with that `id` exists in your company (including one that was already deleted or belongs to another company).\n\nA vehicle can be deleted at any time regardless of what references it: transfers, shipping manifests on orders, purchases, and stock transfers that already name the vehicle keep their reference and continue to render it. No inventory is created, consumed, or released.\n\nFor a company with the BioTrack compliance integration enabled, a successful delete also queues an asynchronous removal of the vehicle in BioTrack — a 204 means the vehicle was deleted in Distru, not that BioTrack has processed the removal. That delete is rejected up front with a 400 (and the vehicle is kept) when your company or user BioTrack credentials are missing or lack permission for this operation; once queued, a later rejection by BioTrack does not restore the Distru record. Companies without BioTrack (Metrc or no compliance system) have no such sync and can always delete a vehicle.\n\nRequired permission: `settings_permissions_vehicles`.\n","operationId":"DistruPublic.V1.VehicleController.delete","parameters":[{"description":"ID of the vehicle to delete, as returned by the list, fetch, and upsert endpoints. An ID that doesn't exist for your company returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a vehicle","tags":["Vehicle"]},"get":{"description":"Fetch a single vehicle by its id. Scoped to the API key's company — an id belonging to another company, or a soft-deleted vehicle, returns 404, as does an unknown id.\n\nRequired permission: `settings_permissions_vehicles`.\n","operationId":"DistruPublic.V1.VehicleController.show","parameters":[{"description":"The vehicle's id, as returned by the list or upsert endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single vehicle","content":{"*/*":{"schema":{"$ref":"#/components/schemas/VehicleResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a vehicle","tags":["Vehicle"]}},"/public/v1/purchases/{id}":{"delete":{"description":"Deletes a purchase. This is a hard delete: the purchase is permanently removed together with its line items, charges, and payments — it disappears from `GET /public/v1/purchases`, `GET /public/v1/purchases/{id}` returns 404 for it, and it cannot be recovered through the API. Responds 204 with no body on success, or 404 if no purchase with that `id` exists in your company (including one that belongs to another company or was already deleted).\n\nA purchase that has been matched to a compliance transfer cannot be deleted: the delete is refused with a 400 for a purchase carrying a `metrc_transfer_id` or a `biotrack_id`. Transfer matching is permanent, so such a purchase can never be deleted through the API.\n\nDeleting a received purchase (PARTIALLY_RECEIVED or COMPLETED) pulls its received quantities back out of [inventory](#model-inventory), and the recorded unit costs for those lines are removed. That reversal only succeeds while the received inventory is still untouched: if any line's received quantity has already been used elsewhere in Distru — sold, transferred, adjusted, or consumed in an assembly — the delete is refused with a 400 and nothing is changed. Purchases in PENDING, PROCESSING, or DELIVERING never affected inventory, so they delete without any inventory movement.\n\nOther effects, all in one atomic call: tasks tied to the purchase are deleted, and if your company is integrated with QuickBooks Online the linked bill and its payments are scheduled for deletion there too (that sync is eventual — observe it in QuickBooks Online, not in the 204). Files attached to the purchase are detached but kept. Nothing is synced to Metrc or BioTrack.\n\nRequired permission: `purchases_permissions_delete` (plus access to the purchase under team restrictions).\n","operationId":"DistruPublic.V1.PurchaseController.delete","parameters":[{"description":"ID of the purchase to delete, as returned by the list, fetch, and upsert endpoints. An ID that doesn't exist for your company returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a purchase","tags":["Purchase"]},"get":{"description":"Get a single purchase by its ID, returned as the full [purchase](#model-purchase) shape (line items, charges, active payments, and custom data). Draft purchases and purchases outside your company are not found (404).\n\nRequired permission: `purchases_permissions_view`, plus access to the purchase under the authenticated user's team restrictions.\n","operationId":"DistruPublic.V1.PurchaseController.show","parameters":[{"description":"Purchase ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single purchase","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PurchaseResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a purchase","tags":["Purchase"]}},"/public/v1/credits/{id}/cancel":{"post":{"description":"Cancel (void) a credit. A canceled credit keeps its record and history and still appears in\nthis API, but its `status` becomes CANCELED and its remaining balance can no longer be applied\nto invoices. The response returns the credit with its updated status.\n\nBy default the credit's existing applications to invoices (its credit uses) are left in place.\nSet `should_delete_credit_uses` to true to also remove those applications, returning the used\namounts to the affected invoices and payments.\n\nOverpayment credits (created from an invoice overpayment or a QuickBooks Online payment) cannot\nbe canceled — void the associated payment instead, which returns an error here. Canceling an\nalready-canceled credit is a no-op that returns the credit unchanged.\n\nIf your account syncs credits with QuickBooks Online, the cancellation is pushed to QuickBooks\nOnline in the background; a 200 confirms the cancel in Distru, not that the QuickBooks Online\nside has finished. Credits do not touch inventory or state compliance (Metrc / BioTrack).\n\nRequired permission: `credits_permissions_edit`. The authenticated user must also have access to\nthe credit under their team restrictions.\n","operationId":"DistruPublic.V1.CreditController.cancel","parameters":[{"description":"The credit's ID (as returned in the `id` field of a credit).","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelCredit"}}},"description":"Cancel options (optional body)"},"responses":{"200":{"description":"The canceled credit","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CreditResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Cancel a credit","tags":["Credit"]}},"/public/v1/test-results/{id}/pdf":{"get":{"description":"Download the Certificate of Analysis (COA) PDF for a single test result — its attached lab result file. Returns 404 when the test result has no attached lab result file.\n\nThe same permissions that govern downloading this PDF in the Distru web app apply to this endpoint.\n\nBy default the response body is the binary PDF with content type `application/pdf`. Pass `?format=url` to instead receive a JSON envelope `{\"data\": {\"url\": ..., \"expires_datetime\": ...}}` containing a temporary signed download URL.\n\nAll PDF download endpoints share a single, combined rate limit for your account: 20 requests per minute and 1000 per day in total across every PDF endpoint (not per endpoint). Requests over the limit return 429 with a Retry-After header, and only successful downloads count toward it.\n","operationId":"DistruPublic.V1.TestResultController.download_pdf","parameters":[{"description":"The ID of the test result whose Certificate of Analysis (COA) PDF you want. This is the same `id` returned by the list and get endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Controls the response body. Omit to receive the raw PDF bytes (Content-Type: application/pdf). Pass `url` to instead receive a JSON object holding a short-lived signed download URL you can hand to a browser or fetch later. Only the literal value `url` is accepted.","in":"query","name":"format","required":false,"schema":{"type":"string","enum":["url"]}}],"responses":{"200":{"description":"By default the binary PDF file (Content-Type: application/pdf). When ?format=url is passed, a JSON object with a temporary signed download URL (see PdfDownloadUrl schema).","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/PdfDownloadUrl"}}}},"400":{"description":"Invalid parameters","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No lab result file is attached to this test result","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests — the account's shared PDF download rate limit (20/minute, 1000/day across all PDF endpoints) was exceeded. Retry after the period in the Retry-After header.","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Download a test result's Certificate of Analysis (COA) PDF","tags":["TestResult"]}},"/public/v1/taxes/{id}":{"get":{"description":"Fetch a single tax by its ID. A tax is a named, reusable tax rate (percentage) applied to\n[order](#model-order) and [invoice](#model-invoice) line totals; use this to resolve a tax id\nseen on those records into its current rate, code, and QuickBooks Online mappings.\n\nReturns 404 when no tax with that ID exists for the authenticated company, or when the tax has\nbeen deleted. This endpoint is read-only and touches no other data.\n\nRequired permission: `settings_permissions_taxes`.\n","operationId":"DistruPublic.V1.TaxController.show","parameters":[{"description":"The tax's ID, as returned in the `id` field of a tax response.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single tax","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TaxResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a tax","tags":["Tax"]}},"/public/v1/adjustments":{"get":{"description":"List stock adjustments for your company, oldest first (by creation time), with optional filters.\n\nA stock adjustment is a manual change to on-hand inventory that isn't a sale, purchase, or transfer — for example recording waste, theft, damage, a physical recount, or a reconciliation with the state compliance system.\n\nNarrow the result with the `product_ids`, `batch_ids`, and `location_ids` filters (matching each adjustment's `product_id` / `batch_id` / `location_id`) and with the `inserted_datetime`, `completion_datetime`, and `updated_datetime` windows. When several filters are supplied an adjustment must satisfy all of them (AND).\n\nResults are always scoped to the company the API key belongs to. The response is a paginated envelope: `data` holds the page of adjustments and `next_page` is the URL of the following page, or null on the last page.\n\nThis endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.\n\nRequired permission: `products_permissions_view`.\n","operationId":"DistruPublic.V1.StockAdjustmentController.index","parameters":[{"description":"Restrict the result to specific stock adjustments by ID (the same ID returned as each stock adjustment's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by when the adjustment was created in Distru (its `inserted_datetime`). Inclusive ISO8601 range `after,before` separated by a comma; either bound may be omitted — `after,` keeps adjustments created on or after `after`, `,before` those on or before `before`, and `after,before` those inside the closed range. A range with both bounds empty is rejected.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the adjustment's effective date — the `completion_datetime` in the response. Inclusive ISO8601 range `after,before` separated by a comma; either bound may be omitted. A range with both bounds empty is rejected.","in":"query","name":"completion_datetime","required":false,"example":"?completion_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by when the adjustment was last modified in Distru (its `updated_datetime`). Same inclusive ISO8601 `after,before` comma range format as `inserted_datetime`, with either bound optional.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Restrict to adjustments made against specific batches by batch ID (as returned in each adjustment's `batch_id`). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to adjustments made at specific locations by location ID (as returned in each adjustment's `location_id`). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to adjustments of specific products by product ID (as returned in each adjustment's `product_id`). Repeat the bracketed key once per ID; matches ANY. A malformed ID is rejected with a 400; an unknown-but-well-formed ID matches nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"1-based page number. Defaults to page 1 when omitted; must be greater than 0. Page size is fixed, so follow the `next_page` URL in the response to fetch the following page rather than incrementing this yourself; `next_page` is null on the last page.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of stock adjustments","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockAdjustments"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get adjustments","tags":["StockAdjustment"]},"post":{"description":"Record a stock adjustment — a manual change to on-hand inventory that isn't a sale, purchase, or transfer (for example waste, theft, damage, a recount, or a reconciliation with the state compliance system).\n\nIdentify what to adjust with exactly one of `product_id`, `batch_id`, or `package_id`, matching how the product's inventory is tracked (product-, batch-, or package-tracked). Supplying more than one, or none, is rejected.\n\nThere are two adjustment modes, selected by whether you pass `package_id`:\n\n• Standard adjustment (`product_id` or `batch_id`): set `quantity` in the product's unit type and a source `location_id`. A positive `quantity` adds on-hand inventory at that location (recorded as a gain), a negative `quantity` removes it (recorded as a loss); when `reason` is `waste` the quantity moves the stock into a waste state and must be negative. No compliance system is touched, so the change is visible in Distru inventory immediately.\n\n• Compliance adjustment (`package_id`): set `compliance_quantity` in the package's unit type and omit `location_id` — the package's current location is used automatically. The package's tracked quantity is updated and the change is pushed to the connected state traceability system (Metrc or BioTrack) synchronously, as part of this request. A 200 means the compliance system accepted the change; if it rejects it, you get a 400 carrying the compliance error message as a single human-readable string. `reason` must be one the compliance system accepts for package adjustments.\n\nThe whole operation is atomic: if any validation fails or the compliance sync is rejected, nothing is persisted — no inventory moves and the package is left untouched. A compliance adjustment additionally requires the target package to be in an adjustable state — it is rejected if the package is on hold, destroyed, discontinued, inactive, syncing, transferred, finished, scheduled for destruction, assigned to an order, or has unresolved compliance audit discrepancies, and a negative adjustment cannot exceed the package's currently available quantity. For Metrc finished-good packages, an increase also cannot push the quantity above what was received or created for that package (less what has already been used), and count-based packages may be required to stay whole numbers.\n\nStock adjustments are immutable: once created they cannot be edited or deleted through this API.\n\nRequired permission: `products_permissions_adjust_inventory`. Setting `unit_cost` additionally requires cost accounting to be enabled for the company and permission to apply costs on quantity adjustments.\n","operationId":"DistruPublic.V1.StockAdjustmentController.insert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"product_id":{"type":"string","description":"ID of the product to adjust. Provide only when the product is product-tracked. Supply exactly one of `product_id`, `batch_id`, or `package_id`; supplying more than one, or none, is rejected. Choosing `product_id` or `batch_id` makes this a standard (non-compliance) adjustment."},"batch_id":{"type":"string","description":"ID of the batch to adjust. Provide only when the batch's product is batch-tracked. Supply exactly one of `product_id`, `batch_id`, or `package_id`. Choosing `product_id` or `batch_id` makes this a standard (non-compliance) adjustment."},"package_id":{"type":"string","description":"ID of the package to adjust. Provide only when the product is package-tracked. Presence of `package_id` switches this into a compliance adjustment: use `compliance_quantity` instead of `quantity`, omit `location_id`, and the change is synced to Metrc or BioTrack. Supply exactly one of `product_id`, `batch_id`, or `package_id`."},"quantity":{"type":"number","description":"Amount to adjust stock by, expressed in the product's unit type, as a decimal string (e.g. `\"10\"` adds ten, `\"-4\"` removes four). Required for standard adjustments (`product_id`/`batch_id`) and must be omitted when `package_id` is set (use `compliance_quantity` instead). Positive adds on-hand inventory, negative removes it. Must be negative when `reason` is `waste`. Must be greater than -1000000000."},"compliance_quantity":{"type":"number","description":"Amount to adjust stock by, expressed in the package's unit type, as a decimal string. Required when `package_id` is set (a compliance adjustment) and must be omitted otherwise (use `quantity` instead). Same sign convention as `quantity`: positive adds, negative removes, and a negative amount cannot exceed the package's currently available quantity. In the response, this value is echoed back as `compliance_quantity` (package unit type) while `quantity` carries the same change converted into the product's unit type."},"description":{"type":"string","description":"Free-text note explaining the adjustment. Required for compliance adjustments (`package_id` set); optional for standard adjustments. Max length 800 characters for standard adjustments and 250 characters for compliance adjustments."},"unit_cost":{"type":"number","description":"Cost per unit applied to the added inventory, as a decimal string. Allowed only for companies with cost accounting enabled and for callers with permission to apply costs on quantity adjustments. Must be omitted when the adjustment quantity is negative (a removal can't be costed). Required for a positive adjustment when the company setting 'Require Cost on Intake and Quantity Adjustments' is on. The value you send is reflected back in the response as `unit_cost` and drives `total_cost`."},"completion_datetime":{"type":"string","description":"Effective date/time of the adjustment — surfaced as `completion_datetime` in the response — as an ISO8601 datetime, e.g. `2022-07-10T00:00:00Z`. Required for both standard and compliance adjustments."},"reason":{"type":"string","description":"Reason for the adjustment. Required. For standard adjustments, one of the lowercase, case-sensitive values `waste`, `stolen`, `damaged`, `fire`, `write-off`, `expired`, `lab-testing`, `revaluation`, or `other`; only `waste` changes behavior — it forces `quantity` to be negative and moves the stock into a waste state rather than a plain loss. For compliance adjustments, must be a reason the connected state compliance system (Metrc or BioTrack) accepts for package adjustments; an unrecognized reason is rejected together with the list of valid reasons. Echoed back verbatim as `reason` in the response."},"location_id":{"type":"string","description":"ID of the source location the adjustment applies to. Required for standard adjustments. Must be omitted for compliance adjustments (`package_id` set) — the package's current location is used automatically, so setting it is rejected."}}}}}},"responses":{"200":{"description":"The stock adjustment was inserted successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockAdjustmentResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Insert a stock adjustment","tags":["StockAdjustment"]}},"/public/v1/menus":{"get":{"description":"List menus for the authenticated company, ordered oldest-first by creation time.\n\nA menu is a shareable product catalog and price list you send to customers. It controls who can view the catalog (`visibility`), which weekday delivery windows and minimum-order rules apply at checkout, and the status new orders receive when a customer checks out through it. Each returned menu also carries `product_count` — the number of active products currently on it — and `url`, its primary public link (null when the menu has no primary URL).\n\nResults are paginated: the response wraps the menus in `data` and sets `next_page` to the URL of the following page, or null on the last page. Use `active`, `visibility`, and the `inserted_datetime` / `updated_datetime` windows to narrow the list; omit them all to return every menu in the company regardless of state. When several filters are supplied a menu must satisfy all of them (AND).\n\nRequired permission: `products_permissions_view`.\n","operationId":"DistruPublic.V1.MenuController.index","parameters":[{"description":"Restrict the result to specific menus by ID (the same ID returned as each menu's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by active state. `?active=true` returns only active menus, `?active=false` only inactive ones. Omit to return menus regardless of active state. Any other value is rejected. DEPRECATED comma form `?active=true,false` still works but is equivalent to omitting the filter and will be removed in a future version.","in":"query","name":"active","required":false,"schema":{"type":"boolean"}},{"description":"Filter by creation datetime (the menu's `inserted_datetime`). Comma-separated `from,to` range in ISO-8601 UTC; both bounds inclusive and either side may be omitted. `2022-07-10T00:00:00Z,` returns menus created on or after that instant, `,2022-07-10T00:00:00Z` those created on or before it. Omit to apply no creation-time filter.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-01T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by last-modified datetime (the menu's `updated_datetime`). Same comma-separated `from,to` ISO-8601 range format as `inserted_datetime`, with either bound optional.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=,2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by menu visibility. Repeat the bracketed key with SCREAMING_CASE values: `PUBLIC` (viewable by anyone, no login required), `PRIVATE` (only logged-in users from the menu's own company), `PASSCODE_PROTECTED` (that company's users plus anyone holding the passcode). A subset returns only menus with those visibilities; listing all three, or omitting the param, applies no filter. Any unrecognized value is rejected. At most 200 values may be given.","in":"query","name":"visibilities","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PUBLIC","PRIVATE","PASSCODE_PROTECTED"],"type":"string"}}},{"description":"DEPRECATED — use `visibilities[]` instead; the comma form will be removed in a future version. Comma-separated SCREAMING_CASE list of `PUBLIC`, `PRIVATE`, `PASSCODE_PROTECTED`. Same matching as `visibilities[]`: a subset filters to those visibilities, listing all three (or omitting) applies no filter. Any unrecognized value is rejected.","in":"query","name":"visibility","required":false,"example":"?visibility=PUBLIC,PASSCODE_PROTECTED","schema":{"type":"string"}},{"description":"Page selector using a 1-based page number. Defaults to page 1 when omitted; must be greater than 0. Follow the response's `next_page` URL to walk subsequent pages. Example: `?page[number]=2`.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"Menus index","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Menus"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get menus","tags":["Menu"]}},"/public/v1/reports/sales-order-history":{"get":{"description":"Returns one row per sales order — its dates, customer, status, owner, and monetary totals (paid, outstanding, subtotal, taxes, discounts, charges, returns, and grand total). This is a read-only report: it never creates, changes, or deletes orders, inventory, payments, or compliance records, and has no effect on Metrc or BioTrack. Use it to reconcile order value and payment state across a date window; do not use it to fetch a single order's line items (use the Orders endpoints for that).\n\nWhen no `order_datetime` filter is supplied the report defaults to orders whose order date falls in the last 30 days. All other date filters (`delivery_datetime`, `due_datetime`, `created_datetime`, `updated_datetime`) have no default — omitting them applies no bound on that field. Multiple filters combine with AND; repeated values within one array filter combine with OR.\n\nThis endpoint is not paginated: it returns every order matching the filters in a single response, and there is no `next_page` link. A wide date window can therefore produce a large payload, so narrow the date filters to bound the result set. Unlike the report's own UI export, the response carries only order rows — there is no trailing \"Total\" summary row.\n\nEvery cell is returned as it appears in the report's CSV export. Values that look numeric (the monetary columns) are returned as strings (currency and comma formatting stripped) so they match the rest of the API; everything else stays a string too. A value whose numeric part has a leading zero (for example an order number like `\"0042\"`) keeps its full display string so the leading zeros are not lost. Date cells are display-formatted strings in the resolved format, not ISO8601, and each date is returned twice — once in the company's timezone and once in UTC.\n\nCompanies on a compliance integration receive two extra columns — a manifest number (labeled per the active system, Metrc or BioTrack) and the shipped-from license — appended after the standard columns. Any Order custom fields configured for the company are appended after those, keyed by the slugified custom field label. The `meta` object echoes the resolved human-readable date range and the full ordered list of column definitions (each with its response `key` and display `label`), so an integrator can map dynamic keys without hardcoding them.\n\nRequires the \"view the sales order history report\" permission on the API key.\n","operationId":"DistruPublic.V1.Report.SalesOrderHistoryController.index","parameters":[{"description":"Filter to orders whose status is any of the given values (OR). Omit for no status filter. SCREAMING_CASE; one of PENDING, PROCESSING, READY_TO_SHIP, DELIVERING, DELIVERED, COMPLETED, CANCELED.","in":"query","name":"status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"}}},{"description":"Filter to orders whose payment status is any of the given values (OR). Omit for no payment filter. SCREAMING_CASE; one of NOT_PAID, PARTIALLY_PAID, FULLY_PAID, OVER_PAID.","in":"query","name":"payment_status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["NOT_PAID","PARTIALLY_PAID","FULLY_PAID","OVER_PAID"],"type":"string"}}},{"description":"Filter to orders created through any of the given sources (OR). Omit for no source filter. SCREAMING_CASE; one of LEAFLINK, EXTERNAL_BUYER, INTERNAL_USER, API.","in":"query","name":"order_source","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["LEAFLINK","EXTERNAL_BUYER","INTERNAL_USER","API"],"type":"string"}}},{"description":"Filter by order date, as a comma-separated `after,before` pair of ISO8601 UTC timestamps. Either side may be blank for an open-ended bound: `2026-01-01T00:00:00Z,` matches on/after Jan 1, `,2026-02-01T00:00:00Z` matches before Feb 1. When this param is omitted the report falls back to the last 30 days of order dates; supplying it overrides that default.","in":"query","name":"order_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by delivery date, as a comma-separated `after,before` pair of ISO8601 UTC timestamps; either side may be blank for an open-ended bound. No default when omitted.","in":"query","name":"delivery_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by due date, as a comma-separated `after,before` pair of ISO8601 UTC timestamps; either side may be blank for an open-ended bound. No default when omitted.","in":"query","name":"due_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the order's creation timestamp, as a comma-separated `after,before` pair of ISO8601 UTC timestamps; either side may be blank for an open-ended bound. No default when omitted.","in":"query","name":"created_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the order's last-modified timestamp, as a comma-separated `after,before` pair of ISO8601 UTC timestamps; either side may be blank for an open-ended bound. No default when omitted.","in":"query","name":"updated_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the order grand total, as a comma-separated `min,max` numeric range (inclusive). Either side may be blank for an open-ended bound (e.g. `100,` for totals >= 100).","in":"query","name":"total","required":false,"example":"100,500","schema":{"type":"string"}},{"description":"Filter by whether the order is matched to a compliance (Metrc/BioTrack) transfer. `true` returns only matched orders, `false` only unmatched; omit to include both.","in":"query","name":"matched_with_compliance_transfer","required":false,"example":true,"schema":{"type":"boolean"}},{"description":"Case-insensitive substring match against the order number, the customer name, and the LeafLink short ID (any one matching returns the order). Omit for no text search.","in":"query","name":"search","required":false,"example":"SO-1024","schema":{"type":"string"}},{"description":"Filter to orders for any of these customers, identified by their company relationship IDs (OR).","in":"query","name":"company_relationship_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders whose customer belongs to any of these customer groups, by group ID (OR).","in":"query","name":"company_relationship_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders shipped from any of these licenses, by license ID (OR).","in":"query","name":"shipped_from_license_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders placed against any of these menus, by menu ID (OR).","in":"query","name":"menu_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders containing a product of any of these brands, by brand ID (OR).","in":"query","name":"brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders owned by any of these users, by user ID (OR).","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders created by any of these users, by user ID (OR).","in":"query","name":"creator_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders containing any of these batches, by batch ID (OR).","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders containing any of these products, by product ID (OR). Unlike the other ID filters, these are the same id returned as a product's `id` elsewhere in the API.","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Sales Order History report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SalesOrderHistoryReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Sales Order History report","tags":["SalesOrderHistory"]}},"/public/v1/payments/{id}/void":{"post":{"description":"Void a payment by its ID. A voided payment is retained rather than deleted — its record still resolves and it keeps appearing in the list and show endpoints with `status` VOIDED. The response returns the payment in its post-void state.\n\nThis one endpoint voids both kinds of payment; it dispatches on `payment_type`:\n• INVOICE — reverses the money applied to the [invoice](#model-compactinvoice), recomputes the invoice's payment status, and cancels both the credits this payment generated from an overpayment and the credits applied toward it. Requires the `invoices_permissions_receive_payment` permission.\n• PURCHASE — reverses the money applied to the [purchase](#model-paymentpurchase) and recomputes the purchase's payment status. Requires the `purchases_permissions_make_payments` permission.\n\nVoiding an already-voided payment returns 400. If the company is connected to QuickBooks Online, the void is queued to sync there (a 200 is not confirmation it reached QuickBooks Online — poll QuickBooks Online to observe the result); an invoice payment is also pushed to LeafLink when that integration is configured for the customer. The change is eventually consistent — it can take up to 1 second to reflect in the list and show endpoints.\n","operationId":"DistruPublic.V1.PaymentController.void","parameters":[{"description":"The ID of the payment to void, as returned in the `id` field of the list and show payment endpoints. An ID that doesn't exist for your company returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The voided payment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PaymentResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Void a payment","tags":["Payment"]}},"/public/v1/metrc/tags/{id}":{"get":{"description":"Get a single Metrc tag by its Distru ID — the `id` value returned in the tags list (a Distru resource ID, not the raw tag label). Read-only and scoped to the company that owns the API key; returns 404 if no tag with that ID belongs to your company.\n","operationId":"DistruPublic.V1.MetrcController.show","parameters":[{"description":"The Distru ID of the tag, as returned in the `id` field of the tags list (not the Metrc tag label).","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single Metrc tag","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MetrcTagResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a Metrc tag","tags":["Metrc"]}},"/public/v1/reports/sales-by-product":{"get":{"description":"Returns one row per product with its quantity sold and total sales over the reported date range, alongside the product's descriptive attributes (SKU, unit type, category, subcategory, group, vendor, owner, unit cost, sale price, wholesale price, shipped-from license, and UPC). Quantity sold and total sales are aggregated from sales order items over the date range and are net of returns, so a product with more returns than sales can report negative figures. This is a read-only aggregate report; it does not touch inventory, compliance (Metrc/BioTrack), or any order records.\n\nUse this to pull period sales performance broken down by product (for example a monthly or quarterly sell-through summary). It is not the endpoint for line-item detail — it collapses every matching order item into a single per-product total, so you cannot see which orders contributed. Filters combine with AND (a row must satisfy all supplied filters); within a single multi-value filter the values combine with OR.\n\nDate range: when `order_datetime` is omitted the report covers the last 30 days, resolved in the requesting user's company timezone. Canceled orders are excluded unless the `status` filter explicitly lists `CANCELED`.\n\nResponse shape: each row is returned as it appears in the report's CSV export. Cells that look numeric (money, quantities) are returned as strings (currency and percent formatting stripped) so they match the rest of the API; everything else is a string too. An optional product attribute the product doesn't have comes back as `null` (the one exception is `sku`, which is always a string and is empty rather than null when unset). Because every numeric cell is a string, an all-digit identifier such as a numeric `sku` or `upc` comes back as a string, and one that carries a significant leading zero keeps its full display string so the zero isn't lost. Any Product custom fields configured for the company are appended as extra columns keyed by the field's slug, so rows for such companies carry additional keys beyond the documented ones. Report-level information (the resolved human-readable date range and the ordered column definitions) is returned under `meta`.\n\nRequires the \"view the sales by product report\" permission.\n","operationId":"DistruPublic.V1.Report.SalesByProductController.index","parameters":[{"description":"Include only sales orders whose status is one of the supplied values. Values are SCREAMING_CASE and combine with OR. Allowed: PENDING, PROCESSING, READY_TO_SHIP, DELIVERING, DELIVERED, COMPLETED, CANCELED. When omitted, every status except CANCELED is included; CANCELED orders are counted only when you list CANCELED here.","in":"query","name":"status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"}}},{"description":"Restrict to orders whose order date falls in this range. Format is two ISO8601 datetimes separated by a comma, `after,before` (inclusive bounds). Leave one side empty to make the range open-ended: `2026-01-01T00:00:00Z,` matches everything on or after that instant, and `,2026-02-01T00:00:00Z` everything up to it; supplying neither side (a lone comma) is rejected. When the whole parameter is omitted, the report covers the last 30 days resolved in the company's timezone.","in":"query","name":"order_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Case-insensitive substring match against the product name or SKU; only products matching are included in the report.","in":"query","name":"search","required":false,"example":"?search=blue+dream","schema":{"type":"string"}},{"description":"Include only sales to these customers, given as public customer (company relationship) IDs. Values combine with OR. Combine with `exclude_customer_ids` to include-then-exclude.","in":"query","name":"customer_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Exclude sales to these customers, given as public customer (company relationship) IDs. Applied after `customer_ids`, so a customer listed in both is excluded. Values combine with OR.","in":"query","name":"exclude_customer_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Include only orders owned by these sales reps, given as public user IDs. Values combine with OR. When omitted, orders from every sales rep in the company are included (no owner restriction).","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Include only order items whose shipped-from location is one of these, given as public location IDs. Values combine with OR. This filter and `user_ids` are evaluated as a single OR: an order item is kept when its shipped-from location matches `location_ids` OR its handling user matches `user_ids`. When both `location_ids` and `user_ids` are omitted the report spans every location and user in the company (no location/user restriction); supplying only one of the two turns off the other's default, so passing just `location_ids` restricts the report purely by location.","in":"query","name":"location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Include only order items handled by these users, given as public user IDs. Values combine with OR. Evaluated together with `location_ids` as a single OR: an order item is kept when its handling user matches `user_ids` OR its shipped-from location matches `location_ids`. When both are omitted the report spans every user and location in the company; supplying just `user_ids` turns off the location default and restricts the report purely by user.","in":"query","name":"user_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Include only order items shipped from these licenses, given as public license IDs. Values combine with OR.","in":"query","name":"shipped_from_license_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Sales By Product report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SalesByProductReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Sales By Product report","tags":["SalesByProduct"]}},"/public/v1/assemblies/{id}/pdf":{"get":{"description":"Download the rendered work order PDF for a single assembly — the same printable production sheet\nlisting its outputs, inputs, and costs that Distru generates in the UI. This is a read-only\noperation: it neither changes the assembly nor touches inventory or compliance.\n\nThe same permissions that govern downloading this PDF in the Distru web app apply to this endpoint.\n\nBy default the response body is the binary PDF with content type `application/pdf`. Pass `?format=url` to instead receive a JSON envelope `{\"data\": {\"url\": ..., \"expires_datetime\": ...}}` containing a temporary signed download URL.\n\nAll PDF download endpoints share a single, combined rate limit for your account: 20 requests per minute and 1000 per day in total across every PDF endpoint (not per endpoint). Requests over the limit return 429 with a Retry-After header, and only successful downloads count toward it.\n","operationId":"DistruPublic.V1.AssemblyController.download_pdf","parameters":[{"description":"The ID of the assembly whose work order PDF you want.","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Omit to receive the rendered PDF bytes directly (Content-Type `application/pdf`). Pass `url` to instead receive a JSON object holding a short-lived signed download URL (see the [PdfDownloadUrl](#model-pdfdownloadurl) response); use this when you would rather hand the link off than stream the bytes yourself.","in":"query","name":"format","required":false,"schema":{"type":"string","enum":["url"]}}],"responses":{"200":{"description":"By default the binary PDF file (Content-Type: application/pdf). When ?format=url is passed, a JSON object with a temporary signed download URL (see PdfDownloadUrl schema).","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/PdfDownloadUrl"}}}},"400":{"description":"Invalid parameters","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests — the account's shared PDF download rate limit (20/minute, 1000/day across all PDF endpoints) was exceeded. Retry after the period in the Retry-After header.","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Download a work order PDF","tags":["Assembly"]}},"/public/v1/companies":{"get":{"description":"List the companies in your CRM (each entry is your relationship with another company), ordered oldest-first by creation date. Filter by name or legal business name, business category, relationship type, group, owner, license number, US state or city, QuickBooks Online / LeafLink links, outstanding balance, custom fields, creation and last-modified datetimes, and whether soft-deleted entries are included.\n\nThis is a read-only listing served from a replica, so it returns eventually consistent data — a create or update made through the API can take up to 1 second to appear here.\n\nRequired permission: `companies_permissions_view`. Results are scoped to your own account and further limited to the companies the authenticated user can see under their team restrictions, so this may return fewer entries than exist on the account.\n","operationId":"DistruPublic.V1.CompanyController.index","parameters":[{"description":"Restrict the result to specific companies by ID (the same ID returned as each company's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by the company's business category, matching exactly one value verbatim. Note these values are Title Case, not the SCREAMING_CASE used by other enums in this API — the same casing the `category` response field returns.","in":"query","name":"category","required":false,"schema":{"type":"string","enum":["Other","Cultivator","Delivery","Dispensary","Distributor","Lab","Manufacturer","Microbusiness","Retail"]}},{"description":"Filter by the datetime each company was created. A comma-separated, inclusive ISO8601 range written as `after,before`; omit either side to leave that bound open. `?inserted_datetime=2022-07-10T00:00:00Z,` returns entries created on or after that instant, `?inserted_datetime=,2022-07-10T00:00:00Z` returns entries created on or before it, and supplying both bounds restricts to the range between them.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Controls whether soft-deleted companies are returned. `no` (the default when omitted) returns only non-deleted entries, `only` returns only soft-deleted entries, and `include` returns both. A soft-deleted entry keeps a non-null `deleted_at` in the response.","in":"query","name":"deleted","required":false,"schema":{"type":"string","enum":["no","include","only"],"default":"no"}},{"description":"Page to return, as `page[number]=N` (1-based). Defaults to page 1 when omitted; the page size is fixed at 5000. When more entries remain, the response's `next_page` field holds the URL for the next page; it is null on the last page. `number` must be greater than 0.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter by the datetime each company was last modified. Same comma-separated, inclusive `after,before` ISO8601 range format as `inserted_datetime`. `?updated_datetime=,2022-07-10T00:00:00Z` returns entries last modified on or before that instant.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=,2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by company name, matching a case-insensitive substring of each company's display `name`. `?name=acme` matches “Acme Dispensary”. For exact, multi-value matching use `names` instead.","in":"query","name":"name","required":false,"example":"?name=acme","schema":{"type":"string"}},{"description":"Restrict to companies whose display `name` exactly matches one of the given values (case-insensitive). Repeat the bracketed key once per name. An empty list is treated as no filter. At most 200 names may be given.","in":"query","name":"names","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by the company's registered legal business name (distinct from its display `name`), matching a case-insensitive substring of each company's `legal_business_name`.","in":"query","name":"legal_business_name","required":false,"example":"?legal_business_name=acme%20llc","schema":{"type":"string"}},{"description":"Filter to companies that hold a license with this exact number, matched against the license numbers under each company's `licenses`. Case-insensitive and surrounding whitespace is ignored.","in":"query","name":"license_number","required":false,"example":"?license_number=C11-0000123-LIC","schema":{"type":"string"}},{"description":"Restrict to companies assigned any of these relationship types, matched against each company's `relationship_type.id`. Repeat the bracketed key once per ID. Unknown IDs match nothing; an empty list is treated as no filter. At most 200 IDs.","in":"query","name":"relationship_type_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to companies in any of these groups, matched against each company's `group.id`. Repeat the bracketed key once per ID. Unknown IDs match nothing; an empty list is treated as no filter. At most 200 IDs.","in":"query","name":"company_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to companies owned by any of these Distru users, matched against each company's `owner_id`. Repeat the bracketed key once per ID. Unknown IDs match nothing; an empty list is treated as no filter. At most 200 IDs.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to companies that have a location in any of these US states, given as two-letter state codes and matched against the `state` of each company's `locations`. Repeat the bracketed key once per code. At most 200 codes may be given.","in":"query","name":"states","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["AK","AL","AR","AZ","CA","CO","CT","DC","DE","FL","GA","GU","HI","IA","ID","IL","IN","KS","KY","LA","MA","MD","ME","MI","MN","MO","MS","MT","NC","ND","NE","NH","NJ","NM","NV","NY","OH","OK","OR","PA","PR","RI","SC","SD","TN","TX","UT","VA","VI","VT","WA","WI","WV","WY"],"type":"string"}}},{"description":"Restrict to companies that have a location whose city matches this case-insensitive substring, matched against the `city` of each company's `locations`.","in":"query","name":"city","required":false,"example":"?city=beverly","schema":{"type":"string"}},{"description":"Restrict to companies linked to any of these LeafLink customer IDs, matched exactly against each company's `leaflink_customer_id`. Repeat the bracketed key once per ID. An empty list is treated as no filter. At most 200 IDs.","in":"query","name":"leaflink_customer_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"integer"}}},{"description":"Restrict to companies linked to any of these QuickBooks Online customer IDs, matched exactly against each company's `qb_customer_id`. Repeat the bracketed key once per ID. An empty list is treated as no filter. At most 200 IDs.","in":"query","name":"qb_customer_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to companies linked to any of these QuickBooks Online vendor IDs, matched exactly against each company's `qb_vendor_id`. Repeat the bracketed key once per ID. An empty list is treated as no filter. At most 200 IDs.","in":"query","name":"qb_vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"When `true`, returns only companies whose computed `outstanding_balance` is greater than 0 (they owe you money). When `false`, returns only companies with an outstanding balance of 0 or less, including those with a credit balance. Omit to include all.","in":"query","name":"has_outstanding_balance","required":false,"schema":{"type":"boolean"}},{"description":"Filter by each company's computed `outstanding_balance`. A comma-separated, inclusive `min,max` decimal range; omit either side to leave that bound open. `?outstanding_balance=100,` returns companies owing at least 100, `?outstanding_balance=,500` at most 500, and both bounds restricts to the range between them.","in":"query","name":"outstanding_balance","required":false,"example":"?outstanding_balance=100,500","schema":{"type":"string"}},{"description":"Filter by custom field values, as `custom_data[{id}]=value` where `{id}` is a custom field's numeric id. Repeat with different ids to filter on several fields at once; a record must match every one (AND). Matching is case-sensitive exact against the value stored on the record. The id must be a filterable custom field defined on this entity — use `GET /public/v1/custom-fields?parent_object=company` to list the ids, their types, and which are filterable. A non-numeric id, an id not defined on this entity, or an id that isn't filterable returns a 400.","in":"query","name":"custom_data","required":false,"schema":{"type":"object"}}],"responses":{"200":{"description":"A list of companies","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Companies"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get companies","tags":["Company"]},"post":{"description":"Create or update one company in your CRM through a single endpoint. Omit `id` to create; pass an existing company `id` to update that entry. The response is the same object returned by `GET /public/v1/companies/{id}`, with status 201 on create and 200 on update.\n\nA company here is your relationship with another business plus the underlying business record. The two are written together: `name`, `category`, `legal_business_name`, `default_email`, `phone_number`, `website`, `default_sales_order_notes`, and `default_purchase_order_notes` describe the business itself, while the emails, `owner_id`, `group_id`, `relationship_type_id`, `default_payment_term_id`, `outstanding_balance_threshold`, and `custom_data` describe your relationship with it. On create both records are created in one call; on update both are amended in place — no second company is created.\n\nUpdates are sparse: only the fields you send change, and any field you omit keeps its current value. Sending `custom_data`, however, replaces the entire custom-field map — any custom field not present in the map you send is cleared, so send the full set you want to keep. Required custom fields are enforced on both create and update, so a create or update that leaves a required custom field unset is rejected. Renaming a company also updates its public menu URLs, so previously shared menu links for that company change.\n\nThis endpoint does not touch inventory and has no state-traceability (Metrc/BioTrack) effect — companies are CRM records only. A few response fields are read-only and cannot be set here: the computed `outstanding_balance`, `leaflink_brand_id`, and the nested `licenses` and `locations`.\n\nYou can link this company to your QuickBooks Online and LeafLink records through `qb_customer_id`, `qb_vendor_id`, and `leaflink_customer_id`. These run the same checks and side effects as the in-app mapping screens: the id must exist in your synced QuickBooks Online / LeafLink data, each may be linked to only one company on your account, and linking a `leaflink_customer_id` reassigns that customer's existing LeafLink orders to this company. Send an explicit `null` to unlink; omit the field to leave the current link unchanged. The link runs in the same transaction as the rest of the upsert, so if it is rejected the whole request is rolled back and nothing is persisted.\n\nValidation is all-or-nothing: if any field is rejected the whole upsert fails and nothing is persisted; errors come back as a `400` with an `errors` array. Referencing an `id` that does not exist on your account (or that your team restrictions hide) returns `404`.\n\nRequired permission: `companies_permissions_create` to create; `companies_permissions_edit`, plus access to the company under team restrictions, to update. Setting `qb_customer_id` or `qb_vendor_id` additionally requires `settings_permissions_quickbooks`; setting `leaflink_customer_id` additionally requires `companies_permissions_update_leaflink_data`.\n","operationId":"DistruPublic.V1.CompanyController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID of the company to update. When present, that company is updated and its business record amended in place; when absent, a new company and its underlying business record are created together. Must reference a company on your account that you can access under team restrictions, otherwise the request returns 404."},"name":{"type":"string","description":"Display name of the company. Required on create; left as-is when omitted on update. Must be at least one character and cannot contain special characters. Must be unique among the active companies on your account, on both create and update — a duplicate name is rejected. Renaming an existing company also rewrites its public menu URLs, so previously shared menu links for that company change."},"category":{"type":"string","description":"Business category for the company. Must be one of a fixed set of values, returned verbatim in the response's `category` field (note these are Title Case, not the SCREAMING_CASE used by other enums in this API): `Dispensary`, `Delivery`, `Cultivator`, `Manufacturer`, `Distributor`, `Microbusiness`, `Lab`, `Retail`, or `Other`. Any other value is rejected. Optional; left as-is when omitted on update."},"legal_business_name":{"type":"string","description":"The company's registered legal business name, distinct from its display `name`. Left as-is when omitted on update."},"default_email":{"type":"string","description":"Primary email address for the company. Must be a valid email address when provided. Left as-is when omitted on update."},"phone_number":{"type":"string","description":"Phone number for the company. Free-text; not format-validated. Left as-is when omitted on update."},"website":{"type":"string","description":"Website URL for the company. Left as-is when omitted on update."},"default_sales_order_notes":{"type":"string","description":"Notes pre-filled onto new sales orders created for this company. Left as-is when omitted on update."},"default_purchase_order_notes":{"type":"string","description":"Notes pre-filled onto new purchase orders created for this company. Left as-is when omitted on update."},"invoice_email":{"type":"string","description":"Email address that invoices for this company are sent to. Left as-is when omitted on update."},"sales_order_email":{"type":"string","description":"Email address that sales orders for this company are sent to. Left as-is when omitted on update."},"purchase_order_email":{"type":"string","description":"Email address that purchase orders for this company are sent to. Left as-is when omitted on update."},"order_shipment_email":{"type":"string","description":"Email address that order shipment notifications for this company are sent to. Left as-is when omitted on update."},"relationship_type_id":{"type":"string","description":"ID of the relationship type to assign to this company (surfaces as the `relationship_type` object in responses). Left as-is when omitted on update."},"group_id":{"type":"string","description":"ID of the group to assign to this company (surfaces as the `group` object in responses). Left as-is when omitted on update."},"owner_id":{"type":"string","description":"ID of the user who owns this company. Must be a user visible to the authenticated user under their team restrictions, otherwise the request is rejected. Left as-is when omitted on update."},"default_payment_term_id":{"type":"string","description":"ID of the payment term applied by default to this company. Use `GET /public/v1/payment-terms` to look up available payment term IDs. Left as-is when omitted on update."},"outstanding_balance_threshold":{"type":"integer","description":"Outstanding-balance (unpaid invoice total) at which Distru starts showing warnings and sending alerts for this company. A positive integer in your account's currency major unit (e.g. whole dollars), compared directly against the company's outstanding balance. When null, the account-wide default threshold applies; when set, it supersedes that default. Left as-is when omitted on update."},"qb_customer_id":{"type":"string","description":"Links this company to a QuickBooks Online customer. Must be a customer id present in your synced QuickBooks Online data, and each customer may be linked to only one company on your account. Left as-is when omitted; send null to unlink. Requires the `settings_permissions_quickbooks` permission."},"qb_vendor_id":{"type":"string","description":"Links this company to a QuickBooks Online vendor. Must be a vendor id present in your synced QuickBooks Online data, and each vendor may be linked to only one company on your account. Left as-is when omitted; send null to unlink. Requires the `settings_permissions_quickbooks` permission."},"leaflink_customer_id":{"type":"integer","description":"Links this company to a LeafLink customer. Must be a customer id present in your synced LeafLink data, and each customer may be linked to only one company on your account. Linking also reassigns that customer's existing LeafLink orders to this company. Cannot be set on a company that represents your own business. Left as-is when omitted; send null to unlink (existing orders keep their current company). Requires the `companies_permissions_update_leaflink_data` permission."},"custom_data":{"type":"object","description":"A map of custom field IDs to their values. Use `GET /public/v1/custom-fields?parent_object=company` 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. This replaces the whole custom-field map — any field you omit from the map is cleared, so send every value you want to keep. Fields configured as required must be present with a value or the request is rejected."}}}}}},"responses":{"200":{"description":"An updated company relationship","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CompanyResponse"}}}},"201":{"description":"A new company relationship","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CompanyResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a company","tags":["Company"]}},"/public/v1/reports/inventory-transaction-history":{"get":{"description":"Returns the Inventory Transaction History report as JSON: one row per inventory transaction (a single recorded movement of stock) falling within the reported date range, ordered newest-first by transaction date. Each row carries the transaction's date, product, package/batch identifiers, Metrc production batch number, transaction type, the related entity that caused the movement (order, purchase, return, assembly, teardown, breakdown, stock transfer, or stock adjustment) with that entity's status and customer/vendor, the signed amount and unit type, the package's potency figures (THC/CBD), and the transaction's total cost.\n\nThis is a read-only report and changes nothing. It reflects inventory movements already recorded by orders, purchases, assemblies, teardowns, breakdowns, returns, stock transfers, stock adjustments, and Metrc/BioTrack compliance syncs — so a row appears here only after the operation that moved the stock. Use it to reconcile or audit stock movement over a period; it is not the way to look up a single package's current balance. The entire matching result set is returned in one response — there is no pagination — so scope every request with the date range and the entity filters below to keep the payload manageable (a broad range can span hundreds of thousands of rows).\n\nWhen no `datetime` filter is provided, the report defaults to the last 30 days: from the start of the day 30 days ago through the end of today, in the company's timezone. At most one entity filter takes effect per request — when several are sent, the report applies `package_id` first, then `batch_ids`, then `product_ids`, and ignores the rest.\n\nEvery value mirrors the report's CSV export: numeric cells are returned as strings (currency and comma formatting stripped, matching the rest of the API), everything else stays a string, and identifier-like values whose number carries a significant leading zero keep their display string. Companies on the BioTrack compliance integration do not receive the `metrc_unit_name` or `metrc_production_batch_number` columns at all. The resolved date range and the column definitions are returned under `meta`.\n\nRequired permission: `reports_permissions_inventory_transaction_history`.\n","operationId":"DistruPublic.V1.Report.InventoryTransactionHistoryController.index","parameters":[{"description":"Include only transactions whose date falls in this range, as two comma-separated         ISO8601 timestamps `after,before` (both bounds inclusive). Either side may be left         empty to make the range open-ended (`2026-01-01T00:00:00Z,` for everything from that         instant on). When omitted entirely the report defaults to the last 30 days — from         the start of the day 30 days ago through the end of today, in the company's timezone.","in":"query","name":"datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Include only transactions for these products, given as Distru product IDs. Takes         effect only when neither `package_id` nor `batch_ids` is supplied (`package_id` wins,         then `batch_ids`, then `product_ids`). Omit to include every product in range.","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Include only transactions for these batches, given as Distru batch IDs. Applies only         when `package_id` is absent, and takes precedence over `product_ids` when both are         sent. Omit to include every batch in range.","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Include only transactions for this single package, given as a Distru package ID.         This is the highest-priority entity filter: when present, `batch_ids` and         `product_ids` are ignored. Omit to leave package filtering off.","in":"query","name":"package_id","required":false,"example":"?package_id=3fa85f64-5717-4562-b3fc-2c963f66afa6","schema":{"type":"string"}}],"responses":{"200":{"description":"The Inventory Transaction History report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/InventoryTransactionHistoryReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Inventory Transaction History report","tags":["InventoryTransactionHistory"]}},"/public/v1/reports/order-fulfillment":{"get":{"description":"Returns one row per product sold over the reported date range, pivoted across the matching sales orders: alongside the product's group, category, and subcategory, each row carries one dynamic column per order (keyed by the slugified order number, e.g. `so_1042`) holding the net quantity of that product on that order, plus the product's total units, unit price, and total value. This is a read-only reporting endpoint — it never changes orders, inventory, or compliance state.\n\nWhen no `order_datetime` filter is provided, the report defaults to the last 30 days: from the beginning of the day 30 days ago through the end of today, in the company's timezone. Canceled and merged orders are always excluded from this report, so filtering `status` to CANCELED returns no rows.\n\nEvery value is returned as it appears in the report's CSV export, with numeric cells returned as strings (currency and comma formatting stripped) so they match the rest of the API. Because the per-order columns are dynamic, the exact set of keys on each row and in `meta.columns` depends on which orders match the filters; a product that was not on a given order leaves that order's cell as an empty string (`\"\"`). Report-level information (the resolved human-readable date range and the full column list) is returned under `meta`.\n\nRequired permission: `reports_permissions_order_fulfillment`.\n","operationId":"DistruPublic.V1.Report.OrderFulfillmentController.index","parameters":[{"description":"Restricts the report to orders whose order date falls in this range, given as two         ISO8601 timestamps separated by a comma: `after,before` (both bounds inclusive).         Either side may be left empty to leave that bound open-ended —         `2026-01-01T00:00:00Z,` keeps only the lower bound and leaves the upper bound         open, while `,2026-02-01T00:00:00Z` keeps only the upper bound and leaves the lower         bound open. Both sides empty (`order_datetime=,`) is rejected as invalid. Omit the         parameter entirely to get the default window: the last 30 days, from the start of         the day 30 days ago through the end of today, in the company's timezone.","in":"query","name":"order_datetime","required":false,"example":"?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Restricts to orders in any of the given statuses (values are OR'd together).         SCREAMING_CASE. Canceled and merged orders are excluded from this report regardless         of this filter, so CANCELED yields no rows.","in":"query","name":"status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"}}},{"description":"Case-insensitive substring match on the order number, the customer's company         name, or the order's LeafLink short ID; an order is included when the text appears         in any of the three. Omit to apply no text filter.","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Restricts to orders for the given customers, identified by their company         relationship IDs (the ID of the buyer on the order). Multiple IDs are OR'd.","in":"query","name":"company_relationship_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restricts the report to these products, given as product IDs. Omit to include         every product sold in the range.","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restricts to order items fulfilled from these locations, identified by their         location IDs. Combined with `user_ids` using OR: an order item matches when its         location OR its user is in the respective list. Providing only `location_ids`         restricts to those locations; omitting both `location_ids` and `user_ids` applies no         location/user restriction.","in":"query","name":"location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restricts to order items assigned to these users, identified by their user         IDs. Combined with `location_ids` using OR (see `location_ids`). Providing only         `user_ids` restricts to those users; omitting both applies no location/user         restriction.","in":"query","name":"user_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restricts to orders owned by these sales reps, identified by their user IDs.         Multiple IDs are OR'd; omit to include orders from every owner.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Order Fulfillment report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OrderFulfillmentReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Order Fulfillment report","tags":["OrderFulfillment"]}},"/public/v1/reports/sales-by-company":{"get":{"description":"Returns one row per customer with its last order date, order count, total received (payments applied to the customer's orders) and total sales (order totals net of returns) over the reported date range. A customer only appears when it has at least one order matching every active filter inside the range — customers with no qualifying order in the window are omitted entirely, so an empty `data` array is a valid result.\n\nThis is a read-only report and has no side effects. It does not touch inventory, compliance (Metrc/BioTrack), or any other entity — it only aggregates existing orders, payments and returns.\n\nDate handling:\n• When no `order_datetime` filter is provided, the range defaults to the last 30 days (from the start of the day 30 days ago through the end of today).\n• The range and all dates are resolved in the API user's own timezone, so day boundaries follow that timezone rather than UTC.\n\nStatus handling:\n• When `status` is omitted, every status except `CANCELED` is counted.\n• Canceled orders are only counted when the `status` filter explicitly includes `CANCELED`.\n\nRow ordering:\n• The report does not accept a sort parameter. By default rows are returned by `total_sales` descending (highest-selling customer first).\n• When `search` is supplied, rows are instead ordered by how closely the customer's name matches the search term.\n\nEvery value is returned as it appears in the report's CSV export, with numeric cells returned as strings (currency and percent formatting stripped), matching the rest of the API. Any custom fields configured on the customer relationship are appended as extra columns keyed by their slugified label; those keys are per-company and are also listed in `meta.columns`. Report-level information (the resolved date range and column definitions) is returned under `meta`.\n\nRequires the \"view the sales by company report\" permission on the API key.\n","operationId":"DistruPublic.V1.Report.SalesByCompanyController.index","parameters":[{"description":"Restricts which orders are counted toward each customer's metrics (order count and totals) to the given statuses. Repeat the key once per value. When omitted, every status except `CANCELED` is counted; include `CANCELED` explicitly to count canceled orders. SCREAMING_CASE enum values only.","in":"query","name":"status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"}}},{"description":"Bounds which orders are counted to those whose order date falls in this range (inclusive on both ends). A comma-separated pair of ISO8601 datetimes, `after,before`. Either side may be left blank for an open-ended bound (e.g. `2026-01-01T00:00:00Z,` for from-only, or `,2026-02-01T00:00:00Z` for until-only). When omitted entirely, defaults to the last 30 days. Bounds are evaluated in the API user's timezone.","in":"query","name":"order_datetime","required":false,"example":"?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Case-insensitive substring match on the customer's company name or legal business name; results are ordered by closeness of match. Omit to return all customers.","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Restricts to customers belonging to any one of the given customer group IDs (OR across the list). Each is a Distru ID. Repeat the key once per value, e.g. `?company_relationship_group_ids[]=<id1>&company_relationship_group_ids[]=<id2>`. Omit to apply no group filter.","in":"query","name":"company_relationship_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restricts the counted orders to those whose owner (assigned sales rep) is any one of the given user IDs (OR across the list). Each is a Distru ID. Note this filters the orders, not the customer's assigned owner. Repeat the key once per value. Omit to apply no owner filter.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Sales By Company report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SalesByCompanyReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Sales By Company report","tags":["SalesByCompany"]}},"/public/v1/reports/harvest-outputs":{"get":{"description":"Returns one row per line item of every harvest assembly whose creation date falls in the reported date range. Each assembly expands into its inputs (the harvested material consumed), its outputs (the products produced, with their batch and package numbers), and its cost line items — the `cost_input_output` column identifies which. Every row carries the assembly's date, number, and status, plus the harvest name, strain, location, product, product category, quantity, and unit type. Rows are grouped by assembly (ordered by assembly date, then number), and within each assembly the outputs come first, then inputs, then costs. When no `datetime` filter is provided, the report defaults to the last 7 days.\n\nThis is a read-only report; nothing is created, and no inventory or compliance state changes when you call it. The entire report is returned in one response — there is no pagination, so a wide date range can return a large payload.\n\nThe filters narrow which assemblies appear, not which line items. An assembly is included only if it has an input matching every input filter (`harvest_name`, `strain`, `location_id`) AND an output matching every output filter (`output_product_name`, `output_product_category_id`); once an assembly qualifies, all of its input, output, and cost rows are returned — including rows that don't themselves match the filter (e.g. filtering by `output_product_name` still returns that assembly's inputs and costs).\n\nEvery value is returned as it appears in the report's CSV export, with numeric cells returned as strings (currency and comma formatting stripped) so they match the rest of the API; a value with a significant leading zero (an identifier such as a batch or package number) keeps its display string so the zero isn't lost. The cost columns (`unit_cost_actual`, `unit_cost_default`, `total_cost_actual`, `total_cost_default`, `cost_type`, `cost_type_description`) are omitted entirely — the keys are absent, not null — for users without permission to view costs, which is a separate permission from the report permission below. Report-level information (the resolved date range and column definitions) is returned under `meta`.\n\nRequired permission: `reports_permissions_harvest_outputs`.\n","operationId":"DistruPublic.V1.Report.HarvestOutputsController.index","parameters":[{"description":"Keep only assemblies whose creation date falls in this range. Two ISO8601 timestamps separated by a comma: `after,before`. Interpreted in the requesting user's timezone. Either side may be left empty to make the range open-ended (`,2026-02-01T00:00:00Z` = up to that date; `2026-01-01T00:00:00Z,` = from that date on). Defaults to the last 7 days when omitted.","in":"query","name":"datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Keep only assemblies with this status. `PENDING` = not yet completed; `COMPLETED` = completed. Omit to return assemblies of any status. SCREAMING_CASE.","in":"query","name":"status","required":false,"example":"COMPLETED","schema":{"type":"string","enum":["PENDING","COMPLETED"]}},{"description":"Keep only assemblies that have an input whose harvest name contains this text (case-insensitive substring). Combined with the other input filters using AND.","in":"query","name":"harvest_name","required":false,"schema":{"type":"string"}},{"description":"Keep only assemblies that have an input whose harvest strain contains this text (case-insensitive substring). Combined with the other input filters using AND.","in":"query","name":"strain","required":false,"schema":{"type":"string"}},{"description":"Keep only assemblies that have an input at this location. A single Distru location ID; exact match against the input's location. Combined with the other input filters using AND.","in":"query","name":"location_id","required":false,"schema":{"type":"string"}},{"description":"Keep only assemblies that have an output whose product name contains this text (case-insensitive substring). Combined with `output_product_category_id` using AND.","in":"query","name":"output_product_name","required":false,"schema":{"type":"string"}},{"description":"Keep only assemblies that have an output whose product belongs to this category. A single Distru product-category ID; exact match. Combined with `output_product_name` using AND.","in":"query","name":"output_product_category_id","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"The Harvest Outputs report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/HarvestOutputsReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Harvest Outputs report","tags":["HarvestOutputs"]}},"/public/v1/tags":{"get":{"description":"Lists every tag belonging to the authenticated company. Tags are the reusable labels you attach to\nproducts and taxes to categorize, group, and filter them.\n\nResults are ordered by creation time, oldest first, and paginated. Narrow the set with the\n`inserted_datetime` and `updated_datetime` range filters; otherwise it returns the full set for the\ncompany, one page at a time. Follow `next_page` in the response until it is null to walk the whole list.\n\nAny authenticated API key for the company may read tags; no additional settings permission is required.\n","operationId":"DistruPublic.V1.TagController.index","parameters":[{"description":"Restrict the result to specific tags by ID (the same ID returned as each tag's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to tags by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to tags by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page to fetch, as `page[number]`. 1-based; defaults to `1` when omitted. Must be greater than 0. Each page holds up to 500 tags (page size is fixed and cannot be changed). A page past the end returns an empty `data` array with a null `next_page`.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of tags","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Tags"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get tags","tags":["Tag"]},"post":{"description":"Creates a new tag or renames an existing one. Pass `id` to update the tag with that ID; omit `id` to\ncreate a new tag. A create returns 201, an update returns 200; both return the saved tag.\n\nA tag is only a label. Creating or renaming one does not attach it to anything, and touches no inventory\nand no compliance system (Metrc/BioTrack) — these are Distru-internal labels, unrelated to Metrc's\nphysical package tags. Renaming a tag keeps it attached to every [product](#model-product) and tax it was\nalready on, so those records simply reflect the new name.\n\n`name` must be unique within the company, compared case-insensitively — \"Indica\" and \"indica\" collide, and\na duplicate is rejected with a validation error. Updating a tag to a name already used by another tag is\nlikewise rejected.\n\nAny authenticated API key for the company may manage tags; no additional settings permission is required.\n","operationId":"DistruPublic.V1.TagController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID of the tag to update. When present, the tag with this ID is renamed and the response is 200. When omitted, a new tag is created and the response is 201. A non-existent ID returns 404."},"name":{"type":"string","description":"The tag's display name. Required. Up to 255 characters. Must be unique within the company, case-insensitively. Allowed characters are letters, digits, spaces, underscores, and `~#-$/|%&'().` — other symbols and emoji are rejected — and it may not contain two colons (`::`) in a row. On update, the value fully replaces the previous name."}},"required":["name"]}}},"required":true},"responses":{"200":{"description":"The updated tag","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TagResponse"}}}},"201":{"description":"The created tag","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TagResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a tag","tags":["Tag"]}},"/public/v1/unit-types/{id}":{"get":{"description":"Get a single unit type by ID. Returns 404 if no unit type with that ID exists in your company.\nInactive unit types are still returned. This endpoint is read-only.\n\nRemember the two shapes a unit type can take: a locked, built-in standard unit carries a\n`category` and `qty_per_si_unit`; a custom unit you defined returns null for both.\n","operationId":"DistruPublic.V1.UnitTypeController.show","parameters":[{"description":"ID of the unit type to fetch, as returned in the `id` field of the list endpoint.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single unit type","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UnitTypeFullResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a unit type","tags":["UnitType"]}},"/public/v1/cost-types/{id}":{"delete":{"description":"Soft-delete a cost type. After deletion it is excluded from the list and fetch endpoints and can\nno longer be applied to new records, but the row itself is retained. Returns 204 on success, or\n404 if no cost type with that ID exists in your company — a cost type that was already deleted\nalso reads as 404.\n\nDeleting a cost type does not remove or alter costs already applied from it: those applied cost\nrecords snapshot their own amount and quantity, so they remain intact and unchanged. No inventory\nis affected and nothing is synced to Metrc or BioTrack. Because name uniqueness only considers\nactive, non-deleted cost types, deleting a cost type frees its name to be reused by a new one.\n\nRequired permission: `costs_permissions_manage_cost_types`.\n","operationId":"DistruPublic.V1.CostTypeController.delete","parameters":[{"description":"ID of the cost type to delete.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a cost type","tags":["CostType"]},"get":{"description":"Get a single cost type by ID. Returns 404 if no cost type with that ID exists in your company or\nif it has been soft-deleted. Inactive (but not deleted) cost types are still returned.\n\nRequired permission: `costs_permissions_manage_cost_types`.\n","operationId":"DistruPublic.V1.CostTypeController.show","parameters":[{"description":"ID of the cost type to fetch.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single cost type","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CostTypeResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a cost type","tags":["CostType"]}},"/public/v1/adjustments/{id}":{"get":{"description":"Get a single stock adjustment by its ID. Returns 404 if no adjustment with that ID exists for your company. Like the list endpoint, this reads eventually consistent data — a just-created adjustment may take up to 1 second to appear.\n\nRequired permission: `products_permissions_view`.\n","operationId":"DistruPublic.V1.StockAdjustmentController.show","parameters":[{"description":"ID of the stock adjustment to fetch.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single stock adjustment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockAdjustmentResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a stock adjustment","tags":["StockAdjustment"]}},"/public/v1/reports/sales-order-tax":{"get":{"description":"Returns the total tax collected across sales orders, aggregated by tax. Each row in `data` is one unique combination of tax name and tax rate, with `total_tax` summing every matching order's charge of that tax. This is a read-only reporting endpoint — it does not create, modify, or sync anything, and has no effect on inventory, compliance, or the orders themselves.\n\nWhich orders are counted is controlled entirely by the query filters below. Only charges flagged as taxes are summed; non-tax charges (fees, discounts) are excluded. Passing `tax_ids` narrows the sum to those specific taxes.\n\n• The `order_datetime` filter scopes the report by order date and is the only filter with a default: when omitted, the report covers the last 30 days, computed in the authenticated user's timezone. Every other filter is additive and applies no default — omit it to leave that dimension unconstrained.\n• Rows are grouped independently by tax name and by tax rate, so the same tax name can appear on multiple rows if it was charged at different rates within the range.\n\nNumeric cells are returned as strings (currency and percent formatting stripped) so they match the rest of the API: `tax_rate` is a percentage value (e.g. `\"27\"` means 27%) and `total_tax` is a currency amount in the company's currency. `meta` carries the resolved, human-readable date range and the report's column definitions.\n\nRequired permission: `reports_permissions_sales_order_tax`. Company admins are always authorized regardless of this permission.\n","operationId":"DistruPublic.V1.Report.SalesOrderTaxController.index","parameters":[{"description":"Restrict the report to orders in any of the given statuses (SCREAMING_CASE). Repeat the key to pass several; an order matches if its status is any one of them. Omit to include every status.","in":"query","name":"status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"}}},{"description":"Restrict the report to orders with any of the given payment statuses (SCREAMING_CASE). Matches on any value listed. Omit to include all payment statuses.","in":"query","name":"payment_status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["NOT_PAID","PARTIALLY_PAID","FULLY_PAID","OVER_PAID"],"type":"string"}}},{"description":"Restrict the report to orders created through any of the given sources (SCREAMING_CASE): LEAFLINK, EXTERNAL_BUYER, INTERNAL_USER, or API. Matches on any value listed. Omit to include all sources.","in":"query","name":"order_source","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["LEAFLINK","EXTERNAL_BUYER","INTERNAL_USER","API"],"type":"string"}}},{"description":"Restrict the report to orders whose order date falls in this range. Format is two comma-separated ISO8601 UTC datetimes, `after,before`. Either side may be left empty for an open-ended range (`2026-01-01T00:00:00Z,` = on or after that instant; `,2026-02-01T00:00:00Z` = up to that instant). This is the only filter with a default: when the whole param is omitted, the report covers the last 30 days in the authenticated user's timezone.","in":"query","name":"order_datetime","required":false,"example":"?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Restrict the report to orders whose delivery date falls in this range. Same `after,before` comma-separated ISO8601 UTC format as `order_datetime`; either side may be empty. No default — omit to leave delivery date unconstrained.","in":"query","name":"delivery_datetime","required":false,"example":"?delivery_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Restrict the report to orders whose due date falls in this range. Same `after,before` comma-separated ISO8601 UTC format as `order_datetime`; either side may be empty. No default.","in":"query","name":"due_datetime","required":false,"example":"?due_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Restrict the report to orders created (in Distru) within this range. Same `after,before` comma-separated ISO8601 UTC format as `order_datetime`; either side may be empty. No default. This is when the order record was created, independent of its order date.","in":"query","name":"created_datetime","required":false,"example":"?created_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Restrict the report to orders last modified within this range. Same `after,before` comma-separated ISO8601 UTC format as `order_datetime`; either side may be empty. No default.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Restrict the report to orders whose grand total falls within a range, given as two comma-separated decimal amounts `min,max` in the company's currency. Compared inclusively.","in":"query","name":"total","required":false,"example":"?total=100,500","schema":{"type":"string"}},{"description":"Restrict to orders by whether they are linked to a compliance (Metrc/BioTrack) transfer. `true` keeps only matched orders, `false` only unmatched. Omit to include both.","in":"query","name":"matched_with_compliance_transfer","required":false,"schema":{"type":"boolean"}},{"description":"Free-text search restricting the report to orders whose order number, customer name, or LeafLink short ID contains the given text (case-insensitive substring). Omit for no text filter.","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Restrict the report to orders for any of the given customers, identified by their Distru company-relationship (customer) IDs. These are the IDs returned by the customers endpoint. Repeat the key to pass several; matches any. Omit to include all customers.","in":"query","name":"company_relationship_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the report to orders whose customer belongs to any of the given customer-group IDs. Repeat the key to pass several; matches any. Omit for no group filter.","in":"query","name":"company_relationship_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the report to orders shipped from any of the given license IDs. Repeat the key to pass several; matches any. Omit for no license filter.","in":"query","name":"shipped_from_license_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the report to orders placed against any of the given menu IDs. Repeat the key to pass several; matches any. Omit for no menu filter.","in":"query","name":"menu_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the report to orders containing a product of any of the given brand IDs. Repeat the key to pass several; matches any. Omit for no brand filter.","in":"query","name":"brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the report to orders owned by any of the given user IDs. Repeat the key to pass several; matches any. Omit for no owner filter.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the report to orders created by any of the given user IDs. Repeat the key to pass several; matches any. Omit for no creator filter.","in":"query","name":"creator_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the report to orders containing any of the given batch IDs. Repeat the key to pass several; matches any. Omit for no batch filter.","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the report to orders containing any of the given product IDs. Repeat the key to pass several; matches any. Omit for no product filter.","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the summed tax charges to the given tax IDs. Unlike the order filters, this narrows which taxes contribute to `total_tax` rather than which orders are counted. Repeat the key to pass several; matches any. Omit to sum every tax.","in":"query","name":"tax_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Sales Order Tax report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SalesOrderTaxReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Sales Order Tax report","tags":["SalesOrderTax"]}},"/public/v1/contacts/{id}":{"delete":{"description":"Deletes a contact from your CRM. This is a soft delete: the contact stops appearing in `GET /public/v1/contacts` by default (pass the `deleted` filter as `include` or `only` to still see it) and this endpoint returns 404 for it, but the record is retained — `GET /public/v1/contacts/{id}` keeps resolving it with a non-null `deleted_at`, and anything that already references the contact (an order's sales-rep assignment, a shipping manifest that names it as driver or contact) keeps its reference and continues to render it. The delete cannot be undone through the API; recreating the contact via upsert produces a new contact with a new `id`. Responds 204 with no body on success, or 404 if no non-deleted contact with that `id` exists in your company (including one that was already deleted or belongs to another company).\n\nA contact can be deleted at any time — no reference blocks it, regardless of the orders, shipping manifests, or company relationships that use it. No inventory is created, consumed, or released, and nothing is synced to Metrc or BioTrack. A successful delete records a `delete` entry in the contact's activity log and notifies the relevant users.\n\nRequired permission: `contacts_permissions_delete`, plus access to the contact under your team restrictions — a contact the authenticated user cannot access returns 403.\n","operationId":"DistruPublic.V1.ContactController.delete","parameters":[{"description":"ID of the contact to delete, as returned by the list, fetch, and upsert endpoints. An ID that doesn't exist for your company (or was already deleted) returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a contact","tags":["Contact"]},"get":{"description":"Fetch a single contact by its ID, including its profile, employer, owner, and custom-field values. Returns 404 if no such contact exists in your company or the authenticated user cannot see it under their team restrictions. Like the list endpoint, reads are eventually consistent — a just-written change may take up to 1 second to reflect here.\n\nRequired permission: `contacts_permissions_view`.\n","operationId":"DistruPublic.V1.ContactController.show","parameters":[{"description":"ID of the contact to fetch. Must belong to your company; returns 404 otherwise.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single contact","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a contact","tags":["Contact"]}},"/public/v1/batches":{"get":{"description":"List batches, sorted oldest-first by creation date and filtered by the query parameters below. Only batches of batch-tracked products are returned; batches belonging to products on any other inventory-tracking method are never listed here.\n\nResults are paginated. The response wraps the batches in `data` and returns a `next_page` URL; follow it to page through results, and stop when `next_page` is null.\n\nBy default each batch is returned without cost data. Pass `include_costs=true` to enrich every batch that currently holds positive on-hand quantity with its cost and quantity totals (`total_cost_actual`, `total_cost_default`, `cost_per_unit_actual`, `cost_per_unit_default`); batches with no on-hand stock omit those fields even when the flag is set. The single-batch `GET /public/v1/batches/{id}` endpoint always includes these totals, so use it when you need cost for one batch.\n\nThis endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.\n\nRequired permission: `products_permissions_view`. Results are additionally scoped to only the batches the authenticated user can access under their team restrictions, so two API keys at the same company may see different subsets.\n","operationId":"DistruPublic.V1.BatchController.index","parameters":[{"description":"Restrict the result to specific batches by ID (the same ID returned as each batch's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only batches belonging to any of these products (matches each batch's `product_id`). Repeat the bracketed key once per id; multiple ids are OR-ed.","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Case-insensitive substring match on the batch's `batch_number`. `batch_number` is a user-set label and is not guaranteed unique, so this may return more than one batch.","in":"query","name":"batch_number","required":false,"example":"?batch_number=LOT-124","schema":{"type":"string"}},{"description":"Return only batches whose `batch_number` exactly matches (case-sensitive) any value in the list — send batch numbers exactly as they appear in responses. Repeat the bracketed key once per value.","in":"query","name":"batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only batches whose product is in any of these categories (matches the batch's `product.category.id`). Multiple ids are OR-ed.","in":"query","name":"product_category_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only batches whose product is in any of these subcategories (matches the batch's `product.subcategory.id`). Multiple ids are OR-ed.","in":"query","name":"product_subcategory_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only batches whose product is in any of these groups (matches the batch's `product.product_group.id`). Multiple ids are OR-ed.","in":"query","name":"product_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only batches whose product has any of these brands (matches the batch's `product.brand.id`). Multiple ids are OR-ed.","in":"query","name":"product_brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only batches whose product is supplied by any of these vendors (matches the batch's `product.vendor.id`; this is the company-relationship ID, not the raw company ID). Multiple ids are OR-ed.","in":"query","name":"product_vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only batches whose product has any of these strains (matches the batch's `product.strain.id`). Multiple ids are OR-ed.","in":"query","name":"product_strain_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only batches whose product carries any of these tags (matches an id in the batch's `product.tags[].id`). Multiple ids are OR-ed.","in":"query","name":"product_tag_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only batches whose product SKU exactly matches (case-insensitive) any value in the list (matches the batch's `product.sku`). Multiple values are OR-ed.","in":"query","name":"product_skus","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only batches that currently hold active quantity (`true`) or none (`false`). Omit to match either.","in":"query","name":"has_quantity_active","required":false,"schema":{"type":"boolean"}},{"description":"Filter by batch creation time as a comma-separated ISO 8601 range `start,end` (inclusive). Omit either side to leave that bound open: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Restrict to batches owned by any of these Distru users (each batch's `owner_id`). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Controls whether soft-deleted batches are included. `no` (the default) returns only non-deleted batches, `only` returns only soft-deleted batches, `include` returns both. SCREAMING_CASE is not used here — pass the lowercase value.","in":"query","name":"deleted","required":false,"schema":{"type":"string","enum":["no","include","only"],"default":"no"}},{"description":"When `true`, each returned batch is enriched with its cost and on-hand quantity totals — `total_cost_actual`, `total_cost_default`, `cost_per_unit_actual`, and `cost_per_unit_default`. Defaults to `false` when omitted, in which case those four fields are left off the batch objects entirely (keeping the listing lighter). Even when `true`, the fields appear only for batches that currently hold positive on-hand quantity; a batch with no stock omits them. To always get cost for a single batch regardless of stock, use `GET /public/v1/batches/{id}`.","in":"query","name":"include_costs","required":false,"schema":{"type":"boolean","default":false}},{"description":"Page to fetch, as `page[number]=N` (1-based; defaults to page 1 when omitted). Page size is fixed by the server, so paginate by following the response's `next_page` URL rather than computing offsets yourself.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter by the batch's most-recent modification time as a comma-separated ISO 8601 range `start,end` (inclusive). Omit either side to leave that bound open, e.g. `,2022-07-10T00:00:00Z` matches batches last modified on or before that instant.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=,2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by custom field values, as `custom_data[{id}]=value` where `{id}` is a custom field's numeric id. Repeat with different ids to filter on several fields at once; a record must match every one (AND). Matching is case-sensitive exact against the value stored on the record. The id must be a filterable custom field defined on this entity — use `GET /public/v1/custom-fields?parent_object=batch` to list the ids, their types, and which are filterable. A non-numeric id, an id not defined on this entity, or an id that isn't filterable returns a 400.","in":"query","name":"custom_data","required":false,"schema":{"type":"object"}}],"responses":{"200":{"description":"A list of batches","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Batches"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get batches","tags":["Batch"]},"post":{"description":"Create or update a single batch. Omit `id` to create a new batch; pass the `id` of an existing batch to update it. Either way the whole operation is atomic — if any field is rejected, nothing is written and the batch is left unchanged.\n\nA batch is Distru's lot for a batch-tracked product: it is the unit that inventory quantity, cost, test results, and (optionally) bins hang off of. You can only create a batch under a product whose inventory-tracking method is batch-tracked; pointing `product_id` at any other product is rejected. Products that track inventory as compliance packages are managed through their package endpoints, not here.\n\nOn create, the batch's inventory records are initialized so quantity and cost can begin accumulating against it (a brand-new batch starts with no on-hand quantity — receive a [purchase](#model-purchase), run an assembly, or adjust stock to add inventory). The write is recorded to the batch's activity history. This endpoint does not push to or pull from any state traceability system (Metrc / BioTrack); a 200 reflects the Distru batch record only.\n\nUpdate is a targeted patch, not a full replace: only the fields you send are changed, and any field you omit keeps its current value. `name` and `product_id` are effectively fixed after creation — `name` is ignored on update, and a batch cannot be moved to a different product. See each field below for its create-time default and its null-vs-omit behavior (notably `bin_ids`).\n\nRequired permission: `products_permissions_create` to create, `products_permissions_edit` to update.\n","operationId":"DistruPublic.V1.BatchController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The Distru batch ID to update. Omit to create a new batch. When present, it must reference a batch in your company or the request is rejected."},"product_id":{"type":"string","description":"The Distru product ID this batch belongs to. Required on create, and the product must be a batch-tracked product in your company (pointing at a product tracked any other way is rejected). Immutable once the batch exists — on update, omit it or resend the batch's current product; sending a different product id is rejected rather than silently ignored, and a batch can never be moved to another product."},"name":{"type":"string","description":"The batch's short internal name. If omitted on create, Distru auto-assigns the next sequential name for the product (e.g. `B1`, then `B2`). Ignored on update — an existing batch's name cannot be changed here. This is distinct from `batch_number`, the user-facing lot label."},"batch_number":{"type":"string","description":"The user-facing lot / batch number label (e.g. `LOT-1241291`). Free-form and not required to be unique. Nullable; leave omitted or null if the batch has no external lot number."},"thc":{"type":"string","description":"A free-form THC label for the batch, as displayed in Distru (e.g. \"18.5%\"). This is a static value stored on the batch record; it does not set or derive from any lab result — the batch's primary test result tracks potency separately."},"cbd":{"type":"string","description":"A free-form CBD label for the batch, as displayed in Distru (e.g. \"0.3%\"). This is a static value stored on the batch record; it does not set or derive from any lab result — the batch's primary test result tracks potency separately."},"expiration_datetime":{"type":"string","description":"When the batch expires, as an ISO 8601 datetime (e.g. `2026-01-31T00:00:00Z`). Nullable; omit or send null if the batch has no expiration."},"harvest_datetime":{"type":"string","description":"When the batch's material was harvested, as an ISO 8601 datetime (e.g. `2025-09-15T00:00:00Z`). Nullable and optional; surfaced in the response as `harvest_datetime`."},"manufactured_datetime":{"type":"string","description":"When the batch was manufactured, as an ISO 8601 datetime (e.g. `2025-09-20T00:00:00Z`). If omitted on create, defaults to the time the batch is created. Surfaced in the response as `manufactured_datetime`."},"owner_id":{"type":"string","description":"The Distru user ID of the batch's designated owner. Must be an active user in your company that the authenticated user is allowed to assign to, otherwise the request is rejected. Nullable; omit or send null for no owner."},"description":{"type":"string","description":"Free-form notes about the batch. Nullable and optional."},"custom_data":{"type":"object","description":"A map of custom field IDs to their values. Use GET /public/v1/custom-fields?parent_object=batch 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."},"bin_ids":{"items":{"type":"string"},"type":"array","description":"The IDs of the bins this batch is stored in. Behaviour: omit `bin_ids` to leave the batch's bins unchanged; pass `null` or an empty array to clear all bins; pass a non-empty array to replace the batch's bins with exactly those. Ignored unless bin inventory tracking is enabled for your company.\n"}}}}}},"responses":{"200":{"description":"A single batch","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchFullResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Create or update a batch","tags":["Batch"]}},"/public/v1/reports/inventory-assets":{"get":{"description":"Read-only report. Returns one row per on-hand inventory asset — a product held at a location, and when the product is batch- or package-tracked, split further by batch or by package. Each row carries the asset's descriptive attributes (product, SKU, vendor, owner, unit type, category, subcategory, license, location, package number, batch number, expiration date, harvest date, tracking method), its `active`, `assembling`, and `selling` quantities, its unit price, and its actual and default unit and total costs.\n\nQuantities and costs are a point-in-time snapshot. Omit `datetime` to report the position as of now, or pass a past `datetime` to reconstruct the position at that instant. A `datetime` earlier than the earliest supported snapshot (before `2023-08-03T11:00:00Z`) is rejected — positions are only reconstructable from the instant the point-in-time inventory history begins. The snapshot counts only inventory that was in an `active`, `selling`, or `assembling` state at that instant; and it drops assets whose product was deleted or whose batch was deleted or whose package was inactivated as of the snapshot, so a deletion made after the snapshot instant still shows in a past-dated report. This is a computed position, not a live table — it does not create, reserve, consume, or otherwise change inventory, and it pushes nothing to Metrc or BioTrack.\n\nPass `style=granular` to expand each asset into the individual cost inputs that produced it: each asset's `Final` row is followed by one `Input` row per traced cost component. This mode adds the `final_input`, `cost_origin`, and `cost_quantity` columns, and requires permission to view cost details — the request is rejected without it. In the default `collapsed` style each asset is a single row and those three columns are absent.\n\nThe four cost columns (`unit_cost_actual`, `unit_cost_default`, `total_cost_actual`, `total_cost_default`) are present only for callers with permission to view costs; without it they are dropped from every row and from `meta.columns`. `location_id` narrows the report to a single location.\n\nEvery value is returned as it appears in the report's CSV export, with numeric cells returned as strings (currency and comma formatting stripped, matching the rest of the API) and enum cells (`tracking_method`) normalized to their SCREAMING_CASE tokens. Report-level information — the resolved snapshot date and the exact column set for this request — is returned under `meta`.\n\nRequired permission: `reports_permissions_inventory_assets` (granular style additionally requires permission to view cost details).\n","operationId":"DistruPublic.V1.Report.InventoryAssetsController.index","parameters":[{"description":"Row granularity. `collapsed` (the default when omitted) returns one row per asset. `granular` follows each asset's `Final` row with one `Input` row per traced cost component and adds the `final_input`, `cost_origin`, and `cost_quantity` columns; it requires permission to view cost details and is rejected without it. Note these two values are lowercase, unlike the SCREAMING_CASE enums elsewhere in the API.","in":"query","name":"style","required":false,"schema":{"type":"string","enum":["collapsed","granular"]}},{"description":"Narrow the report to a single location by its Distru location ID. Omit to include every location the caller's company can see.","in":"query","name":"location_id","required":false,"example":"?location_id=a1b2c3d4-0000-0000-0000-000000000000","schema":{"type":"string"}},{"description":"Point-in-time snapshot as an ISO8601 UTC datetime. Defaults to now when omitted; the report then reconstructs on-hand quantities and costs as of this instant. A value earlier than the earliest supported snapshot (before 2023-08-03T11:00:00Z) is rejected.","in":"query","name":"datetime","required":false,"example":"?datetime=2026-07-01T00:00:00Z","schema":{"type":"string"}}],"responses":{"200":{"description":"The Inventory Assets report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/InventoryAssetsReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Inventory Assets report","tags":["InventoryAssets"]}},"/public/v1/purchases/{id}/pdf":{"get":{"description":"Download the rendered PDF for a single purchase order.\n\nThe same permissions that govern downloading this PDF in the Distru web app apply to this endpoint.\n\nBy default the response body is the binary PDF with content type `application/pdf`. Pass `?format=url` to instead receive a JSON envelope `{\"data\": {\"url\": ..., \"expires_datetime\": ...}}` containing a temporary signed download URL.\n\nAll PDF download endpoints share a single, combined rate limit for your account: 20 requests per minute and 1000 per day in total across every PDF endpoint (not per endpoint). Requests over the limit return 429 with a Retry-After header, and only successful downloads count toward it.\n","operationId":"DistruPublic.V1.PurchaseController.download_pdf","parameters":[{"description":"The ID of the purchase order to render. Draft purchases and purchases outside your company are treated as not found.","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Pass `url` to receive a signed download URL instead of the binary PDF","in":"query","name":"format","required":false,"schema":{"type":"string","enum":["url"]}}],"responses":{"200":{"description":"By default the binary PDF file (Content-Type: application/pdf). When ?format=url is passed, a JSON object with a temporary signed download URL (see PdfDownloadUrl schema).","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/PdfDownloadUrl"}}}},"400":{"description":"Invalid parameters","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests — the account's shared PDF download rate limit (20/minute, 1000/day across all PDF endpoints) was exceeded. Retry after the period in the Retry-After header.","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Download a purchase order PDF","tags":["Purchase"]}},"/public/v1/purchases/{id}/payments":{"post":{"description":"Record a payment made against a purchase order. Each call adds one payment; there is no upsert here, so calling it again adds another payment rather than editing an existing one. The response is the created payment.\n\nThe payment rolls up into the purchase: the purchase's `paid` total increases by this amount and its `payment_status` is recomputed (NOT_PAID → PARTIALLY_PAID → FULLY_PAID, or OVER_PAID if payments exceed the total). Fetch the purchase again to observe the new totals. If your company is integrated with QuickBooks Online, the payment is also recorded against the linked bill using the deposit account you supply.\n\nA 404 is returned if the purchase does not exist or is not in your company.\n\nRequired permission: `purchases_permissions_make_payments`. The authenticated user must also be allowed to view purchases under their team restrictions.","operationId":"DistruPublic.V1.PurchaseController.insert_payment","parameters":[{"description":"The purchase's ID — the `id` returned by the list and show purchase endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"payment_method_id":{"type":"string","description":"ID of the payment method this payment was made with (e.g. cash, check, ACH). Must reference an existing payment method in your company."},"amount":{"type":"number","format":"decimal","description":"Amount of the payment in the purchase's currency, rounded to 2 decimal places. Adds to the purchase's `paid` total; exceeding the purchase total leaves it OVER_PAID."},"payment_datetime":{"type":"string","description":"The datetime the payment was made, as a full ISO8601 datetime (e.g. `2026-08-18T00:00:00Z`)."},"description":{"type":"string","description":"A free-text note describing the payment."},"quickbooks_deposit_account_id":{"type":"string","description":"QuickBooks Online deposit account ID. Cannot include both this and quickbooks_deposit_account_name. If your company is integrated with QuickBooks Online, either this or quickbooks_deposit_account_name must be provided. Account type must be \"Bank\" or \"Credit Card\""},"quickbooks_deposit_account_name":{"type":"string","description":"QuickBooks Online deposit account name. Cannot include both this and quickbooks_deposit_account_id. If your company is integrated with QuickBooks Online, either this or quickbooks_deposit_account_id must be provided. Account type must be \"Bank\" or \"Credit Card\""}},"required":["payment_method_id","amount","payment_datetime","description"]}}},"required":true},"responses":{"200":{"description":"A single payment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PaymentResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Insert a payment for a purchase","tags":["Purchase"]}},"/public/v1/packages/add-costs":{"post":{"description":"Add one or more costs to each of the given packages, and return the affected [packages](#model-package) with their updated cost totals.\n\n`package_ids` is a non-empty list of package IDs; every package must exist and be accessible to the authenticated company. Packages carry their own location, so this endpoint does not accept `location_ids`. Unlike batches and products, the cost lands on the package's full current quantity regardless of its status (active, selling, assembling, etc.).\n\nEach entry in `costs` records one cost against the selected records; the amount added to a record's cost basis is `cost_per_unit` × `quantity`. Every entry in the list is applied to every selected record, so N records and M cost entries create N × M cost entries. Fields:\n\n- `cost_type_id` (**required**): the cost type to apply, as its ID from GET /public/v1/cost-types. Must exist and be accessible to the authenticated company.\n- `quantity` (**required**): how many units of the cost type to apply, as a decimal. Must be greater than 0.\n- `cost_per_unit` (optional): the per-unit amount, as a decimal. When omitted, the cost type's own cost per unit is used. Must be omitted for cost types with a locked cost per unit (those that don't allow inline editing); sending it for such a type is rejected. It is only required when an inline-editable cost type has no cost per unit of its own.\n- `description` (optional): free-form text stored on the cost.\n\n\nWhen `distribute_by_quantity` is `true`, the total of each cost (`cost_per_unit` × `quantity`) is split across the selected records in proportion to each record's quantity, instead of applying the full cost to every record. Quantities are converted to a common unit before the split, so all selected records must share the same unit type category. Selecting a single record is a no-op (the whole cost lands on it). Defaults to `false` when omitted, applying the same cost in full to each selected record.\n\n\nThis is applied synchronously and atomically: a 200 means every cost has already been recorded and the response body reflects the updated records — there is nothing to poll. If any id, cost, or validation fails, the entire request is rejected and nothing is changed.\n\nAdding costs is additive, not an upsert. Each call records new cost entries and raises the recorded cost basis (COGS) of the selected inventory's stock; sending the same body twice applies the cost twice. Cost entries created here cannot be edited or removed through this endpoint. The effect is confined to Distru cost accounting — it does not push to, pull from, or alter Metrc or BioTrack.\n\n\nCommon errors (HTTP 400 unless noted):\n\n- The `*_ids` list is empty.\n- One or more ids don't exist or aren't accessible to the authenticated company.\n- A record isn't tracked by the endpoint's method (e.g. a product that isn't product-tracked, or a batch whose product isn't batch-tracked).\n- A record has no quantity to add a cost to.\n- `cost_type_id` is missing or unknown, or `quantity` is missing or not greater than 0.\n- `cost_per_unit` is set for a cost type with a locked cost per unit, or missing when required.\n- `distribute_by_quantity` is used across records with mixed unit type categories.\n- Cost Accounting module is not enabled, or the user lacks `costs_permissions_apply_to_inventory` (HTTP 403).\n\n\nRequired permission: `costs_permissions_apply_to_inventory`.\n","operationId":"DistruPublic.V1.CostController.add_package_costs","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddPackageCostsRequest"}}},"description":"The packages and costs to apply","required":true},"responses":{"200":{"description":"The affected packages","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Packages"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Add costs to packages","tags":["Cost"]}},"/public/v1/products/{id}":{"delete":{"description":"Deletes a product from your catalog. This is a soft delete: the product stops appearing in `GET /public/v1/products` by default (pass the `deleted` filter as `include` or `only` to still see it) and this endpoint returns 404 for it, but the record is retained — `GET /public/v1/products/{id}` keeps resolving it with a non-null `deleted_at`. The delete cannot be undone through the API; recreating the product via upsert produces a new product with a new `id`. Responds 204 with no body on success, or 404 if no non-deleted product with that `id` exists on your account (including one that was already deleted or belongs to another account).\n\nA product can only be deleted while nothing depends on it. The delete is refused with a 400 when the product: holds any active inventory (every package, batch, or product-level quantity must be zero first), appears on any sales order or purchase, has any returns, appears on any inventory transfer, has any product requests, or is used as an input or output of any assembly. The product only becomes deletable once no such record references it. In practice this means a product that has been used can rarely be deleted — to retire it, set `is_inactive` to `true` via `POST /public/v1/products` instead. An inactive product is hidden from normal use but keeps its history, and can be reactivated at any time.\n\nThe delete cascades, all in one atomic call: the product's [batches](#model-batch) are soft-deleted, its own [bill of materials](#model-billofmaterials) is deleted, and the product is removed as an input from every other product's bill of materials (a bill left with no inputs is deleted too). The product is also removed from menus, and if it is linked to a LeafLink product that link is removed. No inventory is created, consumed, or released, and nothing is synced to Metrc or BioTrack. A successful delete records a `delete` entry in the product's activity log and notifies the relevant users.\n\nRequired permission: `products_permissions_delete` (plus access to the product under team restrictions).\n","operationId":"DistruPublic.V1.ProductController.delete","parameters":[{"description":"ID of the product to delete, as returned by the list, fetch, and upsert endpoints. An ID that doesn't exist for your account (or was already deleted) returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a product","tags":["Product"]},"get":{"description":"Get a single product by ID. Unlike the list endpoint, the response always\nincludes the product's `bill_of_materials` (or null if it has none).\n\nReturns 404 if no product with that ID exists in your company, or if the\nauthenticated user cannot see it under their team restrictions — the two\ncases are indistinguishable from the response.\n\nRequired permission: `products_permissions_view`.\n","operationId":"DistruPublic.V1.ProductController.show","parameters":[{"description":"Product ID.","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"When true, the product carries `packages_with_active_quantity_by_location` — its packages that hold active quantity, grouped by location (empty when the product is not package-tracked). Defaults to false, in which case the field is omitted.","in":"query","name":"include_packages_with_active_quantity_by_location","required":false,"schema":{"type":"boolean","default":false}},{"description":"When true, the product carries `batches_with_active_quantity_by_location` — its batches that hold active quantity, grouped by location, each batch carrying its active quantity at that location (empty when the product is not batch-tracked). Defaults to false, in which case the field is omitted.","in":"query","name":"include_batches_with_active_quantity_by_location","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"A single product","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a product","tags":["Product"]}},"/public/v1/custom-fields":{"get":{"description":"List every custom field definition owned by the authenticated company.\n\nA custom field is a user-defined attribute attached to one entity type (its `parent_object`) — for example an extra `Batch #` text field on orders or a `Lab` dropdown on packages. Once defined, the field becomes available on every record of that entity type, and callers populate it per-record through that entity's `custom_data` map on its own create/update endpoint (the numeric `id` returned here is the key used inside `custom_data`).\n\nPass `parent_object` to return only the fields for one entity type; omit it to return the company's fields across all entity types. This is a read-only settings lookup and changes nothing in inventory or compliance.\n\nRequired permission: `settings_permissions_custom_fields`.\n","operationId":"DistruPublic.V1.CustomFieldController.index","parameters":[{"description":"Return only the fields attached to this entity type. Exact match against the parent object identifier; an unknown value returns an empty list. One of: `assembly`, `batch`, `company`, `contact`, `invoice`, `order`, `package`, `product`, `purchase`, `request`, `return`, `shipment`, `stock_transfer`, `task`. Omit to return the company's custom fields across all entity types. Example: `?parent_object=order`","in":"query","name":"parent_object","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of custom field definitions","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomFieldDefinitions"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"List custom field definitions","tags":["CustomField"]},"post":{"description":"Create a new custom field on one entity type (`parent_object`).\n\nOnce created, the field is immediately available on every record of that entity type. Callers then set a per-record value by putting `\"<id>\": <value>` into that record's `custom_data` map on its own create/update endpoint, where `<id>` is the numeric id returned here. Creating a field does not touch existing records — they simply carry no value for it until one is written — and has no effect on inventory, Metrc, or BioTrack.\n\nThis endpoint only creates. It never updates: there is no upsert here, and `field_type`, `parent_object`, and `filterable` are fixed at creation and cannot be changed afterward. To edit a field's `name`, `description`, `required` flag, or option list later, use `POST /public/v1/custom-fields/{id}`.\n\nField-type rules the request must satisfy:\n• `dropdown` and `checkbox` fields require a non-empty `field_options` list (the selectable values); each option must be unique, non-empty, and free of commas.\n• `text` and `date` fields must not carry `field_options`, and are always non-filterable regardless of what `filterable` is sent.\n\nRequired permission: `settings_permissions_custom_fields`.\n","operationId":"DistruPublic.V1.CustomFieldController.insert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Display name of the field, e.g. `Lab Name`. Required. Must be unique among the fields on the same `parent_object` for this company; uniqueness is case-insensitive, so `Lab` and `lab` collide. At most 70 characters. May not use a reserved name (such as `category`, `strain`, or `owner_id`) or contain certain special characters."},"description":{"type":"string","description":"Optional free-text note describing the field's purpose. At most 100 characters. Omit or send null for no description."},"parent_object":{"type":"string","description":"The entity type this field is attached to. Required and immutable after creation. One of: `assembly`, `batch`, `company`, `contact`, `invoice`, `order`, `package`, `product`, `purchase`, `request`, `return`, `shipment`, `stock_transfer`, `task`."},"field_type":{"enum":["checkbox","date","dropdown","text"],"type":"string","description":"The kind of value this field stores. Required and immutable after creation. One of: `text` (free text), `date` (a calendar date), `dropdown` (a single choice from `field_options`), `checkbox` (one or more choices from `field_options`). `dropdown` and `checkbox` require `field_options`; `text` and `date` must not have them."},"filterable":{"type":"boolean","description":"Whether records of this entity type can be filtered by this field's value. Defaults to false when omitted. Applies only to `dropdown` and `checkbox` fields; for `text` and `date` it is forced to false no matter what is sent. Immutable after creation, so this is the only chance to enable it."},"required":{"type":"boolean","description":"Whether a value for this field must be supplied when a record of this entity type is saved in the Distru app. Defaults to false when omitted. Editable later via the update endpoint."},"field_options":{"items":{"type":"string"},"type":"array","description":"The selectable values for `dropdown` and `checkbox` fields, e.g. `[\"Lab A\", \"Lab B\"]`. Required and non-empty for those two types; each value must be unique, non-empty, at most 255 characters, and contain no commas. Must be omitted (or empty) for `text` and `date` fields."},"disabled_field_options":{"items":{"type":"string"},"type":"array","description":"The subset of `field_options` to turn off. A disabled option can no longer be selected on new or edited records, but it stays in `field_options` so records already holding the value keep displaying it. Every value must also be present in `field_options`. Applies only to `dropdown` and `checkbox` fields; must be omitted (or empty) for `text` and `date`. Defaults to empty when omitted."}},"required":["name","parent_object","field_type"]}}},"required":true},"responses":{"201":{"description":"Custom field created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomFieldDefinitionResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Create a custom field","tags":["CustomField"]}},"/public/v1/product-pos-mappings/{id}":{"delete":{"description":"Permanently remove a single product POS mapping by its id. The row is hard-deleted, not soft-deleted, so it will no longer appear in list or get responses and the id cannot be reused.\n\nThis removes only the link between the Distru product and the external POS product. It does not change the product, the connected retailer, or anything in the external POS catalog; it just stops Distru from treating them as the same product going forward. To repoint a product at a different POS product, delete the mapping and upsert a new one (or upsert over the existing one).\n\nReturns 204 on success, or 404 if no mapping with that id belongs to your company.\n\nRequired permission: `products_permissions_edit`.\n","operationId":"DistruPublic.V1.ProductPosMappingController.delete","parameters":[{"description":"ID of the product POS mapping to delete, as returned in the `id` field of a mapping. Must belong to a product in your company, or the request returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a product POS mapping","tags":["ProductPosMapping"]},"get":{"description":"Fetch a single product POS mapping by its id. The response includes the linked Distru product, the POS type, and only the POS-specific fields for that type (Blaze, Dutchie, or Treez).\n\nReturns 404 if no mapping with that id belongs to a product in your company.\n\nRequired permission: `products_permissions_view`.\n","operationId":"DistruPublic.V1.ProductPosMappingController.show","parameters":[{"description":"ID of the product POS mapping, as returned in the `id` field of a mapping. Must belong to a product in your company, or the request returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single product POS mapping","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductPosMappingResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a product POS mapping","tags":["ProductPosMapping"]}},"/public/v1/products":{"get":{"description":"Returns a paginated list of products, oldest first (ascending by creation\ntime). Soft-deleted products are excluded unless you opt into them with the\n`deleted` filter. The response is a `{data, next_page}` envelope; follow\n`next_page` (null on the last page) to walk every page.\n\nThis endpoint is eventually consistent: a create or update made through the\nAPI can take up to about one second to appear or change here, so a product\nyou just wrote may briefly be missing or stale in the list.\n\nRequired permission: `products_permissions_view`. Results are additionally\nlimited to the products the authenticated user can see under their team\nrestrictions, so two API keys at the same company can return different sets.\n","operationId":"DistruPublic.V1.ProductController.index","parameters":[{"description":"Case-insensitive substring match on the product's name. Omit to match products of any name.","in":"query","name":"name","required":false,"schema":{"type":"string"}},{"description":"Case-insensitive substring match on the product's SKU. Omit to match products of any SKU.","in":"query","name":"sku","required":false,"schema":{"type":"string"}},{"description":"Case-insensitive substring match on the product's UPC. Omit to match products of any UPC.","in":"query","name":"upc","required":false,"schema":{"type":"string"}},{"description":"Restrict to products whose name exactly matches (case-insensitive) any value in the list. Repeat the bracketed key once per value.","in":"query","name":"names","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products whose SKU exactly matches (case-insensitive) any value in the list. Repeat the bracketed key once per value.","in":"query","name":"skus","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products whose UPC exactly matches (case-insensitive) any value in the list. Repeat the bracketed key once per value.","in":"query","name":"upcs","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Distru menu IDs; a product is returned if it belongs to any one of them (OR). Repeat the bracketed key once per value. IDs that don't resolve to a menu in your company are ignored; if that leaves no valid menu ID, `data` comes back empty rather than unfiltered. At most 200 IDs may be given.","in":"query","name":"menu_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"DEPRECATED — use `menu_ids[]` instead; the comma form will be removed in a future version. Comma-separated Distru menu IDs; a product is returned if it belongs to any one of them (OR). Tokens that don't resolve to a menu in your company are ignored; if that leaves no valid menu ID, `data` comes back empty rather than unfiltered.","in":"query","name":"menu_id","required":false,"example":"?menu_id=550e8400-e29b-41d4-a716-446655440000,6ba7b810-9dad-11d1-80b4-00c04fd430c8","schema":{"type":"string"}},{"description":"Case-insensitive substring match on the name of a menu the product belongs to. When combined with `menu_ids`, both must hold (AND).","in":"query","name":"menu_name","required":false,"schema":{"type":"string"}},{"description":"Filter by product creation time. A comma-separated `after,before` pair of ISO8601 UTC datetimes, both bounds inclusive. Leave either side empty to make that bound open-ended: `2022-07-10T00:00:00Z,` returns products created at or after that instant, `,2022-07-10T00:00:00Z` returns those created at or before it.","in":"query","name":"inserted_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Whether to include soft-deleted products. `no` (the default) returns only non-deleted products, `only` returns only soft-deleted ones, `include` returns both. Note these values are lower-case, unlike response enums.","in":"query","name":"deleted","required":false,"schema":{"type":"string","enum":["no","include","only"],"default":"no"}},{"description":"Restrict the result to specific products by ID (the same ID returned as each product's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products in any of these categories (the same ID returned as each product's `category.id`). Repeat the bracketed key once per ID. IDs that don't resolve match nothing.","in":"query","name":"category_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products in any of these subcategories (the same ID returned as each product's `subcategory.id`). Repeat the bracketed key once per ID.","in":"query","name":"subcategory_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products in any of these groups (the same ID returned as each product's `product_group.id`). Repeat the bracketed key once per ID.","in":"query","name":"product_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products with any of these brands (the same ID returned as each product's `brand.id`). Repeat the bracketed key once per ID.","in":"query","name":"brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products with any of these strains (the same ID returned as each product's `strain.id`). Repeat the bracketed key once per ID.","in":"query","name":"strain_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products carrying any of these tags (the same ID returned in each product's `tags[].id`). Repeat the bracketed key once per ID.","in":"query","name":"tag_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products supplied by any of these vendors (the same ID returned as each product's `vendor.id`). This is the company-relationship ID, not the raw company ID. Repeat the bracketed key once per ID.","in":"query","name":"vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products owned by any of these users (the same ID returned as each product's `owner.id`). Repeat the bracketed key once per ID.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products with any of these unit types (the same ID returned as each product's `unit_type.id`). Repeat the bracketed key once per ID.","in":"query","name":"unit_type_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to products whose strain is of any of these types. Repeat the bracketed key once per value. SCREAMING_CASE.","in":"query","name":"strain_types","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["INDICA","INDICA_DOMINANT","SATIVA","SATIVA_DOMINANT","HYBRID","HIGH_CBD"],"type":"string"}}},{"description":"Restrict to products linked to any of these LeafLink product IDs (the integer `leaflink_product_id` returned on each product). Repeat the bracketed key once per ID.","in":"query","name":"leaflink_product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"integer"}}},{"description":"Restrict to products with this inventory tracking method (the same value returned as each product's `inventory_tracking_method`). SCREAMING_CASE.","in":"query","name":"inventory_tracking_method","required":false,"schema":{"type":"string","enum":["PACKAGE","BATCH","PRODUCT"]}},{"description":"Restrict to products with this menu visibility (the same value returned as each product's `menu_visibility`). SCREAMING_CASE.","in":"query","name":"menu_visibility","required":false,"schema":{"type":"string","enum":["DO_NOT_INCLUDE","INCLUDE_IN_ALL","INCLUDE_IN_SELECT"]}},{"description":"Restrict to active (`true`) or inactive (`false`) products. Omit to match products regardless of active state.","in":"query","name":"is_active","required":false,"schema":{"type":"boolean"}},{"description":"Restrict to featured (`true`) or non-featured (`false`) products. Omit to match products regardless of featured state.","in":"query","name":"is_featured","required":false,"schema":{"type":"boolean"}},{"description":"Restrict to products that currently hold active inventory quantity (`true`) or none (`false`). Omit to match products regardless of active quantity.","in":"query","name":"has_quantity_active","required":false,"schema":{"type":"boolean"}},{"description":"Page to fetch, 1-indexed. Defaults to page 1 when omitted. `page[number]` must be greater than 0.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter by the time a product was last modified. Same `after,before` comma-separated ISO8601 UTC range format as `inserted_datetime`, both bounds inclusive, either side optional.","in":"query","name":"updated_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"When true, each product carries its `bill_of_materials` object (or null if it has none). Defaults to false, in which case `bill_of_materials` is omitted entirely to keep the list light.","in":"query","name":"include_bill_of_materials","required":false,"schema":{"type":"boolean","default":false}},{"description":"When true, each product carries `packages_with_active_quantity_by_location` — its packages that hold active quantity, grouped by location (empty for products that are not package-tracked). Defaults to false, in which case the field is omitted. Because this runs extra per-product queries, setting it (or `include_batches_with_active_quantity_by_location`) shrinks the page size to 50.","in":"query","name":"include_packages_with_active_quantity_by_location","required":false,"schema":{"type":"boolean","default":false}},{"description":"When true, each product carries `batches_with_active_quantity_by_location` — its batches that hold active quantity, grouped by location, each batch carrying its active quantity at that location (empty for products that are not batch-tracked). Defaults to false, in which case the field is omitted. Because this runs extra per-product queries, setting it (or `include_packages_with_active_quantity_by_location`) shrinks the page size to 50.","in":"query","name":"include_batches_with_active_quantity_by_location","required":false,"schema":{"type":"boolean","default":false}},{"description":"Filter by custom field values, as `custom_data[{id}]=value` where `{id}` is a custom field's numeric id. Repeat with different ids to filter on several fields at once; a record must match every one (AND). Matching is case-sensitive exact against the value stored on the record. The id must be a filterable custom field defined on this entity — use `GET /public/v1/custom-fields?parent_object=product` to list the ids, their types, and which are filterable. A non-numeric id, an id not defined on this entity, or an id that isn't filterable returns a 400.","in":"query","name":"custom_data","required":false,"schema":{"type":"object"}}],"responses":{"200":{"description":"A list of products","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Products"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get products","tags":["Product"]},"post":{"description":"Creates or updates a single product catalog entry through one endpoint. Omit `id` to create a new product (Distru assigns and returns its ID); pass an existing product's `id` to update it. An `id` that isn't valid, or one that doesn't belong to your company, returns a not-found error. On create you must supply at minimum `name`, `sku`, `category_id`, `vendor_id`, `unit_type_id`, `unit_price`, and `inventory_tracking_method`; your company settings can make more fields mandatory (for example a required description or a required product photo). `name` and `sku` must each be unique within your company.\n\nUpdates are sparse: only the fields you send are changed, and any field you omit keeps its stored value. The list- and map-valued fields replace wholesale *when you send them*, but omitting the field leaves the current value untouched. `tags` replaces the product's entire tag set when sent — any tag you leave out of the list is removed, and an empty array clears them all; omit `tags` to leave the current tags unchanged. `custom_data` likewise replaces the entire custom-field map when sent, and is left untouched when omitted. Menu placement is driven by `menu_visibility`: omit it to leave the product's current menu placement untouched; send DO_NOT_INCLUDE to remove it from all menus, INCLUDE_IN_ALL to place it on every menu, or INCLUDE_IN_SELECT with `menus` to set the exact set (that list replaces the product's full menu set — omit a menu to remove it). `menus` may only be sent alongside `menu_visibility`: it is required with INCLUDE_IN_SELECT and ignored with the other two modes, so sending `menus` without `menu_visibility`, or INCLUDE_IN_SELECT without `menus`, is rejected with a 400.\n\nSome attributes are locked once established. `inventory_tracking_method` cannot be changed after it is first set — the only allowed transition is PRODUCT to BATCH. `unit_type_id` cannot be changed once the product has any inventory. Attempting either returns a validation error.\n\nThis endpoint writes only the product's catalog record. It does NOT create, move, reserve, or release inventory (add packages or batches separately), and it does NOT itself push the product to Metrc or BioTrack — products are local catalog data and only their inventory syncs to state traceability. For BioTrack companies the `category_id` you choose must be compatible with `inventory_tracking_method` (a PACKAGE-tracked product needs a category tied to a BioTrack inventory type; a non-package one needs a category that is not). Changing `menu_visibility` or `menus` changes which DistruCommerce menus and the Order Tracker surface the product. If the product is linked to a connected sales channel (LeafLink, or a POS such as Blaze/Dutchie/Treez), catalog and price edits made here propagate to that channel. The product's [bill_of_materials](#model-billofmaterials) is readable in the response but is NOT settable here — manage it through its own endpoints.\n\nRequired permission: `products_permissions_create` to create a new product, or `products_permissions_edit` (plus access to the product under team restrictions) to update an existing one.","operationId":"DistruPublic.V1.ProductController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID for this product. Omit it to create a new product — Distru assigns the ID. Provide an existing product's ID to update that product; an ID that doesn't exist returns a not-found error."},"inventory_tracking_method":{"enum":["PACKAGE","BATCH","PRODUCT"],"type":"string","description":"How this product's inventory is tracked. Required on create. Once set it cannot be changed, with the single exception that a PRODUCT-tracked product may later be switched to BATCH. SCREAMING_CASE, one of:• PACKAGE: Inventory is defined by individual packages (the tracking method used for state-compliance/Metrc packages).• PRODUCT: Not grouped in any manner. Inventory is a simple running quantity you add to or remove from as you transact.• BATCH: Grouped into batches that share common traits such as expiration dates and test results."},"sku":{"type":"string","description":"Stock Keeping Unit (SKU). Required on create and must be unique within your company."},"name":{"type":"string","description":"Product name. Required on create and must be unique within your company. Cannot contain the characters `:`, `[`, or `]`, or other Metrc-disallowed special characters."},"vendor_id":{"type":"string","description":"ID of the company relationship for the vendor company that supplies this product. This is the company-relationship ID, not the raw company ID. Required on create."},"category_id":{"type":"string","description":"ID of the product's category. Required on create. For BioTrack companies this must be compatible with `inventory_tracking_method`: a PACKAGE-tracked product needs a category tied to a BioTrack inventory type, a non-package one needs a category that is not."},"external_name":{"type":"string","description":"Customer-facing name shown on DistruCommerce menus and the Order Tracker. Falls back to `name` when left blank or omitted, so the response's `external_name` is never null."},"unit_type_id":{"type":"string","description":"ID of the product's unit type. Required on create. Cannot be changed once the product has any inventory."},"unit_price":{"type":"number","description":"Sale price of the product per unit, as a decimal. Required on create. Must be non-negative (>= 0)."},"description":{"type":"string","description":"Plain-text description of the product. If you send `description` without `description_markdown`, the markdown description is overwritten with this plain text. If you send `description_markdown` without `description`, the request is rejected — the two must be provided together (or neither). Some companies require a description; those companies reject a create/update that omits both."},"description_markdown":{"type":"string","description":"Markdown-formatted description. Must be provided together with `description`. Only italic, bold, strikethrough, and links are supported by the display — other markdown may render unpredictably."},"upc":{"type":"string","description":"Universal Product Code (UPC) for this product"},"subcategory_id":{"type":"string","description":"ID of the product's subcategory. When provided it must be a child of the category given in `category_id`."},"group_id":{"type":"string","description":"ID of the product's group."},"brand_id":{"type":"string","description":"ID of the company relationship for the brand company associated with this product. This is the company-relationship ID, not the raw company ID."},"quantity_available_threshold_min":{"type":"number","description":"Minimum quantity you want to keep in stock. When the product's available quantity dips below this number it is automatically included in scheduled Low Inventory Reports. Optional; omit to set no low threshold. When both thresholds are set, `quantity_available_threshold_max` must be greater than this value."},"quantity_available_threshold_max":{"type":"number","description":"Maximum quantity you want to keep in stock. When the product's available quantity exceeds this number it is automatically included in scheduled Inventory Reports. Optional; omit to set no high threshold. When both thresholds are set, this must be greater than `quantity_available_threshold_min`."},"units_per_case":{"type":"number","description":"Number of units in a case of the product. When provided must be greater than 0."},"unit_cost":{"type":"number","description":"Cost of the product per unit, as a decimal. When provided must be non-negative (>= 0)."},"msrp":{"type":"number","description":"The Manufacturer's Suggested Retail Price (MSRP) of the product per unit. If you have POS integrations enabled in Distru, this may be synced to your POS"},"wholesale_unit_price":{"type":"number","description":"The wholesale price of the product per unit. When provided must be non-negative (>= 0)."},"treez_wholesale_price":{"type":"number","description":"The Treez wholesale price of the product per unit, as a decimal. When your company has the Treez wholesale price setting enabled, this value is used as the default price on order items for this product. Omit to leave an existing value unchanged; send null to clear it."},"is_featured":{"type":"boolean","description":"Whether the product is featured. Featured products are shown at the top of menus. Defaults to false when omitted on create."},"strain_id":{"type":"string","description":"ID of the strain associated with the product."},"owner_id":{"type":"string","description":"ID of the user considered the owner of the product. Must be a user the authenticated caller is allowed to assign as owner under their team restrictions."},"leaflink_product_id":{"type":"integer","description":"The LeafLink product ID this product is linked to, used to match LeafLink orders to this product. Must be unique within your account — sending a value already linked to another product is rejected. Omit to leave an existing link unchanged; send null to unlink."},"is_inactive":{"type":"boolean","description":"Whether the product is inactive (hidden from normal use). Defaults to false when omitted on create. Can be flipped back to active at any time."},"total_cannabinoid_unit":{"type":"string","description":"Unit for this product's THC/CBD content. One of PERCENT or MG (SCREAMING_CASE). Required whenever `total_thc` or `total_cbd` is provided."},"total_thc":{"type":"string","description":"THC content of the product, expressed in the unit given by `total_cannabinoid_unit` (which must be provided alongside it). Must be non-negative (>= 0); when `total_cannabinoid_unit` is PERCENT it cannot exceed 100."},"total_cbd":{"type":"string","description":"CBD content of the product, expressed in the unit given by `total_cannabinoid_unit` (which must be provided alongside it). Must be non-negative (>= 0); when `total_cannabinoid_unit` is PERCENT it cannot exceed 100."},"menu_visibility":{"enum":["DO_NOT_INCLUDE","INCLUDE_IN_ALL","INCLUDE_IN_SELECT"],"type":"string","description":"Controls which menus (if any) the product appears on. Defaults to DO_NOT_INCLUDE when omitted on create; on update, omit it to leave the product's current menu placement untouched. SCREAMING_CASE, one of:• DO_NOT_INCLUDE: The product appears on no menus; any `menus` you send are ignored.• INCLUDE_IN_ALL: The product appears on every menu in your company; `menus` is ignored.• INCLUDE_IN_SELECT: The product appears only on the menus listed in `menus`, which fully replaces its current menu set. `menus` is required in this mode.\nRequired whenever `menus` is provided — sending `menus` without `menu_visibility` is rejected."},"unit_net_weight":{"type":"number","description":"Net weight/volume of the product per unit. When provided must be greater than 0 and requires `unit_net_weight_and_serving_size_unit_type_id` to be set."},"unit_serving_size":{"type":"number","description":"Serving size of the product per unit. When provided must be greater than 0, cannot exceed `unit_net_weight`, and requires `unit_net_weight_and_serving_size_unit_type_id` to be set."},"unit_net_weight_and_serving_size_unit_type_id":{"type":"string","description":"ID of the unit type that `unit_net_weight` and `unit_serving_size` are measured in. Required whenever either of those is set. Applies only to count-based products; leave null otherwise. Once set, changing the product away from a count/'Unit' category is rejected."},"gross_weight":{"type":"number","description":"Gross weight of the product. When provided must be greater than 0 and must be set together with `gross_weight_unit_type_id` (both present or both absent)."},"gross_weight_unit_type_id":{"type":"string","description":"ID of the weight unit type `gross_weight` is measured in. Must be a weight-based unit type that Metrc supports, and must be set together with `gross_weight` (both present or both absent)."},"tags":{"items":{"type":"string"},"type":"array","description":"Tag IDs to associate with the product. When sent, the list replaces the product's complete tag set — any tag left out is removed, and an empty array clears all tags. Omit `tags` on update to leave the current tags unchanged."},"menus":{"items":{"type":"string"},"type":"array","description":"Menu IDs the product should appear on. Only used when `menu_visibility` is INCLUDE_IN_SELECT — required in that mode, ignored with DO_NOT_INCLUDE and INCLUDE_IN_ALL. When applied, this list becomes the product's complete menu set, so omit a menu from the list to remove the product from it. Must be sent together with `menu_visibility`: sending `menus` without `menu_visibility` is rejected, as is INCLUDE_IN_SELECT without `menus`. Omitting `menu_visibility` entirely leaves the current menu placement untouched."},"custom_data":{"type":"object","description":"A map of custom field IDs to their values. Use GET /public/v1/custom-fields?parent_object=product 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. On update, omit `custom_data` to leave the stored map unchanged; when sent it replaces the entire map, so include every field you want to keep."}}}}}},"responses":{"200":{"description":"A single product","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a product","tags":["Product"]}},"/public/v1/products/{id}/images":{"post":{"description":"Replaces the full set of images on a product. Distru downloads each URL you provide server-side at request time, re-hosts the bytes, and attaches them to the product; the URLs themselves are not stored, so the source images may change or disappear afterward without affecting the product. This is a full replacement, not an append: the product's existing images are removed and replaced by exactly what you send. Send an empty array to remove all images.\n\nThe order of the array is the display order — the first URL becomes the product's primary image. At most 4 URLs are accepted, and each must resolve to a JPEG, PNG, or GIF. The operation is all-or-nothing: if any URL fails to download or isn't a supported image type, no images are changed and the response is a 400 with a single human-readable error message.\n\nThis affects only the product's images; it does not touch inventory or push anything to Metrc or BioTrack. On success the response is 204 with no body — re-fetch the product to see the new image URLs.\n\nRequired permission: `products_permissions_edit` (plus access to the product under team restrictions).","operationId":"DistruPublic.V1.ProductController.upsert_images","parameters":[{"description":"Product ID.","in":"path","name":"id","required":true,"example":"550e8400-e29b-41d4-a716-446655440000","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array"}}},"description":"URLs of the images to attach, in display order (first = primary image). At most 4 URLs, each pointing to a JPEG, PNG, or GIF. An empty array removes every image from the product."},"responses":{"204":{"description":"Images updated successfully"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Associate images with a product","tags":["Product"]}},"/public/v1/reports/purchases-by-company":{"get":{"description":"Returns the Purchases By Company report: one row per vendor (a company relationship where your company buys from a related company) summarizing purchasing activity over a date range. Each row carries the vendor's most recent purchase date, the number of purchases counted, and the total amount spent.\n\nWhich vendors appear:\n• Only vendors with at least one non-draft purchase matching the filters (the date range plus any `owner_ids` filter) are included; a vendor with no qualifying purchases in the range is omitted entirely.\n• Draft purchases are never counted, and purchases you lack permission to view (team visibility) are excluded from the counts and totals.\n\nBehaviors worth noting:\n• `last_purchase_date` is the vendor's most recent non-draft purchase across all time — it is NOT restricted to the reported date range, so it can fall outside the range you filtered on.\n• `purchase_order_count` and `total_purchases` are computed only over the purchases inside the date range (and matching the `owner_ids` filter).\n• When no date filter is provided, the report defaults to the last 30 days. All date bounds are interpreted in your company's timezone.\n\nEvery value is returned as it appears in the report's CSV export: numeric cells are returned as strings (currency and comma formatting stripped, matching the rest of the API) and date cells stay formatted strings. Any CompanyRelationship custom fields configured for your company are appended as extra columns on each row and listed in `meta.columns`. The resolved date range and the report's column definitions are returned under `meta`.\n\nRequired permission: `reports_permissions_purchases_by_company`.\n","operationId":"DistruPublic.V1.Report.PurchasesByCompanyController.index","parameters":[{"description":"Restrict the counted purchases to those whose order date falls in this range. The value is two comma-separated ISO8601 timestamps, `after,before`, and the range is inclusive on both ends. Either side may be left empty for an open-ended range: `2026-01-01T00:00:00Z,` counts everything from that instant onward and `,2026-02-01T00:00:00Z` everything up to it (both sides empty is rejected). Bounds are evaluated in your company's timezone. When omitted, the report covers the last 30 days. This only bounds the aggregated `purchase_order_count` and `total_purchases` — `last_purchase_date` always reflects the vendor's most recent non-draft purchase regardless of this filter.\n","in":"query","name":"order_datetime","required":false,"example":"?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Case-insensitive substring match on the vendor's related company name or its legal business name; only vendors that contain the term in either are returned. Omit to list all vendors.\n","in":"query","name":"search","required":false,"example":"?search=green","schema":{"type":"string"}},{"description":"Filter to vendors belonging to any of these vendor group ids (match-any / OR). Each id is a Distru vendor group id. Repeat the bracketed key once per value. Omit or send an empty list to apply no group filter.\n","in":"query","name":"company_relationship_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter the counted purchases to those owned by any of these purchase owners / sales reps (match-any / OR). Each id is a Distru user id. Because this filters the purchases themselves, it also narrows which vendors appear: a vendor is listed only if it has non-draft purchases in the range owned by one of these reps, and its `purchase_order_count` and `total_purchases` reflect only those purchases. Repeat the bracketed key once per value. Omit or send an empty list to include all owners.\n","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Purchases By Company report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PurchasesByCompanyReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Purchases By Company report","tags":["PurchasesByCompany"]}},"/public/v1/product-pos-mappings":{"get":{"description":"List the links between your Distru products and their matching products in an external point-of-sale (POS) catalog — Blaze, Dutchie, or Treez. Each row is one product-to-POS-product link; a single Distru product can appear more than once when it is mapped in several POS systems or to several retailers.\n\nFiltering:\n• Pass no filter to return every POS mapping for your company, across all POS systems and retailers.\n• `product_id` returns every mapping for one Distru product.\n• `blaze_retailer_id`, `dutchie_retailer_id`, or `treez_retailer_id` returns every mapping for one connected retailer of that POS.\n\nAt most one of the id selectors (`product_id`, `blaze_retailer_id`, `dutchie_retailer_id`, `treez_retailer_id`) may be supplied per request — sending more than one returns 400. Each id selector is an exact match on the stored id, not a search. The `inserted_datetime` and `updated_datetime` ranges are separate and may be added on top of any (or no) id selector to narrow the result to a creation/last-updated window. The full matching set is returned in one response.\n\nRequired permission: `products_permissions_view`.\n","operationId":"DistruPublic.V1.ProductPosMappingController.index","parameters":[{"description":"Return only mappings for this Distru product, given as its ID. Exact match. Cannot be combined with any retailer filter.","in":"query","name":"product_id","required":false,"example":"?product_id=123e4567-e89b-12d3-a456-426614174000","schema":{"type":"string"}},{"description":"Return only Blaze mappings for this connected Blaze retailer, given as its Distru ID (the `blaze_retailer_id` seen on a mapping). Exact match. Cannot be combined with another filter.","in":"query","name":"blaze_retailer_id","required":false,"example":"?blaze_retailer_id=456e7890-e89b-12d3-a456-426614174000","schema":{"type":"string"}},{"description":"Return only Dutchie mappings for this connected Dutchie retailer, given as its Distru ID (the `dutchie_retailer_id` seen on a mapping). Exact match. Cannot be combined with another filter.","in":"query","name":"dutchie_retailer_id","required":false,"example":"?dutchie_retailer_id=789e0123-e89b-12d3-a456-426614174000","schema":{"type":"string"}},{"description":"Return only Treez mappings for this connected Treez retailer, given as its Distru retailer id (an integer). Exact match. Cannot be combined with another filter.","in":"query","name":"treez_retailer_id","required":false,"schema":{"type":"integer"}},{"description":"Filter to product POS mappings by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it. May be combined with an id selector.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to product POS mappings by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range. May be combined with an id selector.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductPosMappingsResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"List product POS mappings","tags":["ProductPosMapping"]},"post":{"description":"Create or update the link between one of your Distru products and its matching product in a single external POS catalog (Blaze, Dutchie, or Treez). This link is what lets Distru recognize the same product on both sides when it reconciles catalogs, menus, and inventory with that retailer, so mapping a product is a prerequisite for that retailer's POS integration to act on it.\n\nYou do not send a POS type. It is derived from which POS fields you supply, and you must supply exactly one complete pair:\n• Blaze: `blaze_product_id` + `blaze_retailer_id` (optionally `blaze_asset_id`).\n• Dutchie: `dutchie_product_id` + `dutchie_retailer_id`.\n• Treez: `treez_product_id` + `treez_retailer_id` (optionally `treez_photo_url`).\n\nSupplying no complete pair, or fields belonging to more than one POS, returns 400 — a mapping targets one POS only.\n\nCreate vs. update is decided for you: the request matches an existing mapping by product plus the retailer of the supplied POS. A match is updated in place and returns 200; no match creates a new mapping and returns 201. Because the match key is stable, sending the same body twice is idempotent and never produces a duplicate.\n\nBefore the mapping is written, several things are checked and any failure returns 400 with a human-readable message:\n• The `product_id` must be a product in your company.\n• The supplied retailer must be a POS retailer connected to your company.\n• The supplied POS product must already exist in Distru's synced copy of that retailer's catalog — you cannot map to a product Distru has not yet pulled from the POS.\n• For Blaze only, the product's category must already have a Blaze category mapping configured for that retailer.\n\nUniqueness is enforced on both sides: a given POS product can back only one Distru product per retailer, and a Distru product can have only one mapping per retailer. Violating either returns 400.\n\nOn update, only the fields you send are changed — the request is sparse. The `product_id` plus the complete POS pair must always be present (they form the match key and are re-validated), but the optional fields are treated per-field: omit `blaze_asset_id` or `treez_photo_url` to keep its current value, or send it as `null` to clear it. Repointing to a different POS product for the same retailer is a normal update: send the new `..._product_id` with the same `product_id` and retailer id.\n\nThis endpoint only maintains the link record — it does not itself move inventory or push the product to the POS; it makes the product eligible for that retailer's sync.\n\nRequired permission: `products_permissions_edit`.\n","operationId":"DistruPublic.V1.ProductPosMappingController.upsert","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertProductPosMapping"}}},"description":"POS mapping data","required":true},"responses":{"200":{"description":"Updated existing mapping","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductPosMappingResponse"}}}},"201":{"description":"Created new mapping","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductPosMappingResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Create or update a product POS mapping","tags":["ProductPosMapping"]}},"/public/v1/payment-terms":{"get":{"description":"Get the payment terms configured for your company. A payment term is the agreed timeframe a\ncustomer has to pay — for example \"Net 30\" means payment is due 30 days after the order date.\n\nUse it to resolve the payment term applied to an order or invoice, or to pick a valid term id\nfor an integration that sets one. Each term carries:\n• `name` — the term's display label (e.g. \"Net 30\"), unique within your company.\n• `days` — a non-negative whole number of days added to compute the due date. `0` means due the\n  same day (immediately). The due datetime is the order's start date plus `days`, set to\n  `time_of_day`. The start date is normally the order date, but is the delivery date if your\n  company is configured to date its due dates from delivery.\n• `time_of_day` — the local clock time the term becomes due on the due date, as `HH:MM:SS` on a\n  24-hour clock (e.g. `\"17:00:00\"` = 5 PM). It is interpreted in your company's configured time\n  zone, not UTC.\n• `locked` — `true` marks a built-in system term whose length (`days`) is fixed and which cannot\n  be deleted; only its `time_of_day` is changeable. `false` marks a fully editable, user-created\n  term. Independently of `locked`, any term (locked or not) currently chosen as the company's\n  default order or default purchase payment term cannot be deleted while it holds that role — but\n  this endpoint does not expose which term is a default.\n\nReturns every payment term for your company in a single response — this endpoint is not paginated,\nso `next_page` is always null and there are no filter or page parameters. Order of the returned\nterms is not guaranteed.\n\nNote: This endpoint returns eventually consistent data — a term you just created, edited, or\ndeleted elsewhere may take up to 1 second to appear or disappear here.\n\nRequired permission: `settings_permissions_payment_terms`.\n","operationId":"DistruPublic.V1.PaymentTermController.index","responses":{"200":{"description":"A list of payment terms","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PaymentTerms"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get payment terms","tags":["PaymentTerm"]}},"/public/v1/test-results":{"get":{"description":"List test results for the authenticated company, most recently created last (ordered by creation time, then id). Each result is returned in full, including its potency values, lab metadata, `additional_test_results`, `coa_url`, `is_primary` flag, and the `package_id`/`batch_id` it is attached to.\n\nResults are paginated; follow the `next_page` URL in the envelope to walk the full set. Reads are eventually consistent — a test result you just created or updated may take up to 1 second to appear or reflect its latest values here.\n\nRequired permission: `products_permissions_view`.\n","operationId":"DistruPublic.V1.TestResultController.index","parameters":[{"description":"Restrict the result to specific test results by ID (the same ID returned as each test result's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the result to test results attached to these packages by ID (the package's ID, as returned in each result's `package_id`). A result is attached to exactly one package or one batch, so this matches only package-attached results. This filter looks at direct attachment only: if a package *inherits* a test result from its assembly inputs, that package is not considered here — only packages the result is directly attached to are matched. Repeat the bracketed key once per ID. Unknown IDs match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"package_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the result to test results attached to these batches by ID (the batch's ID, as returned in each result's `batch_id`). A result is attached to exactly one package or one batch, so this matches only batch-attached results. This filter looks at direct attachment only: if a batch *inherits* a test result from its assembly inputs, that batch is not considered here — only batches the result is directly attached to are matched. Repeat the bracketed key once per ID. Unknown IDs match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the result to test results whose attached package or batch belongs to one of these products by ID. This filter looks at direct attachment only: it matches on the product of the package or batch the result is directly attached to, so if a package/batch *inherits* a test result from its assembly inputs, that product is not considered here. Repeat the bracketed key once per ID. Unknown IDs match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the result to test results with these Metrc lab-result identifiers — Metrc's own integer IDs (the `metrc_id` in the response), not Distru IDs. Matches a result if its Metrc ID equals any listed value. Only results synced from Metrc carry one. Repeat the bracketed key once per ID. An empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"metrc_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"integer"}}},{"description":"Filter to test results whose creation time falls in a range. Value is a comma-separated pair of ISO8601 UTC datetimes, `<start>,<end>`; either side may be left empty for an open-ended bound. Both bounds are inclusive. Example: `?inserted_datetime=,2022-07-10T00:00:00Z` returns everything created on or before that instant; `?inserted_datetime=2022-07-01T00:00:00Z,2022-07-10T00:00:00Z` bounds both ends.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=,2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to test results whose last-modified time falls in a range. Value is a comma-separated pair of ISO8601 UTC datetimes, `<start>,<end>`; either side may be left empty for an open-ended bound. Both bounds are inclusive. Example: `?updated_datetime=,2022-07-10T00:00:00Z` returns everything modified on or before that instant; `?updated_datetime=2022-07-01T00:00:00Z,2022-07-10T00:00:00Z` bounds both ends.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=,2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Page number to fetch, as `page[number]=N` (1-based; must be greater than 0). Defaults to page 1 when omitted. Page size is fixed at 5000, so prefer following the envelope's `next_page` URL over incrementing this by hand.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of test results","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestResults"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get test results","tags":["TestResult"]},"post":{"description":"Create or update a single test result (lab result / Certificate of Analysis) for one package or batch. Omit `id` to create; pass an existing test result's `id` to update it. Same URL and request shape for both.\n\nOn create, attach the result to exactly one material by sending either `package_id` or `batch_id` (never both, and at least one is required). On update, send neither — the attachment is fixed once created, and passing `package_id` or `batch_id` on an update is rejected. Only non-compliance-tracked test results can be updated through this endpoint; results synced from Metrc or BioTrack are read-only here and any update attempt is rejected.\n\nUpdates are sparse: only the fields you send are changed, and any field you omit keeps its stored value. To clear an optional potency or lab field back to null, send it explicitly as `null`. Potency percentage fields accept at most 4 decimal places and must be between 0 and 100.\n\nResults created or updated here are Distru-only records — this endpoint never pushes to Metrc or BioTrack. Compliance-tracked results only ever arrive by syncing from those systems, which is why they are read-only here.\n\nEffects beyond the test result row:\n• Creating a result attaches it to the given package or batch and propagates it down to that material's child packages where applicable.\n• `is_primary: true` makes this the primary result for its package/batch: it unsets whatever result was previously primary there, propagates to child packages, and repoints those materials' inventory/stock records at this result (so it becomes the potency shown for that inventory). A material always keeps exactly one primary — the first result created for a material becomes primary automatically even if you send `is_primary: false`.\n• You cannot flip an existing primary result from `is_primary: true` to `false` directly; instead mark a different result on the same package/batch as primary, which demotes this one automatically.\n\nRequired permission: `products_permissions_edit`.\n","operationId":"DistruPublic.V1.TestResultController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The test result's ID. Omit to create a new result; include an existing result's ID to update it (an ID that doesn't exist for your company returns 404). Only non-compliance-tracked results can be updated — updating one that is synced from Metrc or BioTrack is rejected. When present, `package_id` and `batch_id` must be omitted."},"package_id":{"type":"string","description":"The ID of the package this result is attached to. Provide exactly one of `package_id` or `batch_id` on create; sending both, or neither, is rejected. Must be omitted on update (the attachment cannot be moved)."},"batch_id":{"type":"string","description":"The ID of the batch this result is attached to. Provide exactly one of `package_id` or `batch_id` on create; sending both, or neither, is rejected. Must be omitted on update. Batch-attached results cannot be compliance-tracked."},"additional_test_results":{"type":"object","description":"Required on create; on update, omit to leave the stored object unchanged. A key/value object of extra lab fields beyond the built-in THC/CBD ones (e.g. terpenes, pesticides, moisture). Which keys are valid depends on the attached material's product-category test result settings; an unrecognized key is rejected, and keys valid overall but outside that material's configured set are dropped from responses. Each value must be a valid decimal within that field's configured decimal-place, digit, and range limits (percentage fields 0–100). Null values are ignored. When you do send it on update it replaces the entire object, not merged per-key — include every entry you want to keep; send `{}` to clear them all. See [here](#additionaltestresult) for the valid options."},"mg_per_unit_type":{"enum":["mg/g","mg/mL"],"type":"string","description":"Required on create; on update, omit to leave unchanged. The unit the `*_mg_per_unit` fields are expressed in. One of `mg/g` or `mg/mL`."},"is_primary":{"type":"boolean","description":"Required on create; on update, omit to leave unchanged. Whether this is the primary result for its package/batch. Setting `true` makes it primary — it demotes any other primary on the same material, propagates to child packages, and repoints that material's inventory/stock potency at this result. You cannot change an existing primary result from `true` to `false` directly; mark a different result on the same package/batch as primary instead, which demotes this one automatically. Note: the first result created for a material always becomes primary regardless of this flag."},"release_date":{"type":"string","description":"The lab's release date for this result, as an ISO8601 date (`YYYY-MM-DD`). Optional; omitting it on update leaves it unchanged. Send `null` to clear it."},"lab_license_number":{"type":"string","description":"The license number of the testing lab. Optional; omitting it on update leaves it unchanged. Send `null` to clear it."},"lab_name":{"type":"string","description":"The name of the testing lab. Optional; omitting it on update leaves it unchanged. Send `null` to clear it."},"name":{"type":"string","description":"Required on create; on update, omit to leave unchanged. A display name for this result (250 characters or fewer)."},"thc_percentage":{"type":"number","format":"decimal","description":"THC as a percentage of the material (0–100, at most 4 decimal places). Optional; omitting it on update leaves it unchanged. Send `null` to clear it."},"total_thc_percentage":{"type":"number","format":"decimal","description":"Total THC as a percentage of the material, accounting for THCA conversion (0–100, at most 4 decimal places). Optional; omitting it on update leaves it unchanged. Send `null` to clear it."},"thc_mg_per_unit":{"type":"number","format":"decimal","description":"THC content per unit, expressed in the unit named by `mg_per_unit_type`. Must be 0 or greater; unlike the percentage fields it has no upper bound and no decimal-place limit. Optional; omitting it on update leaves it unchanged. Send `null` to clear it."},"total_thc_mg_per_unit":{"type":"number","format":"decimal","description":"Total THC content per unit (accounting for THCA conversion), expressed in the unit named by `mg_per_unit_type`. Must be 0 or greater; no upper bound and no decimal-place limit. Optional; omitting it on update leaves it unchanged. Send `null` to clear it."},"cbd_percentage":{"type":"number","format":"decimal","description":"CBD as a percentage of the material (0–100, at most 4 decimal places). Optional; omitting it on update leaves it unchanged. Send `null` to clear it."},"total_cbd_percentage":{"type":"number","format":"decimal","description":"Total CBD as a percentage of the material, accounting for CBDA conversion (0–100, at most 4 decimal places). Optional; omitting it on update leaves it unchanged. Send `null` to clear it."},"cbd_mg_per_unit":{"type":"number","format":"decimal","description":"CBD content per unit, expressed in the unit named by `mg_per_unit_type`. Must be 0 or greater; unlike the percentage fields it has no upper bound and no decimal-place limit. Optional; omitting it on update leaves it unchanged. Send `null` to clear it."},"total_cbd_mg_per_unit":{"type":"number","format":"decimal","description":"Total CBD content per unit (accounting for CBDA conversion), expressed in the unit named by `mg_per_unit_type`. Must be 0 or greater; no upper bound and no decimal-place limit. Optional; omitting it on update leaves it unchanged. Send `null` to clear it."}}}}}},"responses":{"200":{"description":"A single test result","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestResultResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a test result","tags":["TestResult"]}},"/public/v1/product-groups":{"get":{"description":"Lists the authenticated company's product groups, oldest first (ordered by creation time\nascending). Product groups are the categories a product can belong to: each product references\nat most one group, and groups are also used as targeting or exclusion filters on price tiers.\n\nResults are paginated, up to 500 groups per page. When more rows exist the response includes a\n`next_page` URL to fetch the following page; `next_page` is null on the last page. Filter the list\nby the `inserted_datetime` and `updated_datetime` ranges.\n\nRequired permission: `settings_permissions_product_groups`.\n","operationId":"DistruPublic.V1.ProductGroupController.index","parameters":[{"description":"Restrict the result to specific product groups by ID (the same ID returned as each group's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to product groups by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to product groups by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page number to fetch, starting at 1. Omit to get the first page. Must be greater than 0. Follow the `next_page` URL in the response to page forward.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of product groups","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductGroups"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get product groups","tags":["ProductGroup"]},"post":{"description":"Creates or updates a single product group. Pass `id` to update the matching group; omit `id`\nto create a new one. A create returns 201, an update returns 200.\n\n`name` is the only editable field and is required on both create and update; the update\nreplaces the group's name. A group's name must be unique within the company, compared\ncase-insensitively (`Flower` and `flower` collide) — a duplicate is rejected with a 400. The\nname may contain only letters, digits, spaces, underscores, and the characters\n`~ # - $ / | % & ' ( ) .`; any other special character, and two colons in a row (`::`), are\nrejected with a 400.\n\nAssigning products to a group is not done here: a product's group is set on the product itself,\nnot on this endpoint. This endpoint only defines the group's identity.\n\nRequired permission: `settings_permissions_product_groups`.\n","operationId":"DistruPublic.V1.ProductGroupController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The Distru ID of the product group to update, as returned in a product group response. Present updates that group; absent creates a new one."},"name":{"type":"string","description":"The group's display name. Required on both create and update. Must be unique within the company, compared case-insensitively (`Flower` and `flower` collide). May contain only letters, digits, spaces, underscores, and the characters `~ # - $ / | % & ' ( ) .`; any other special character, and two colons in a row (`::`), are rejected with a 400."}},"required":["name"]}}},"required":true},"responses":{"200":{"description":"The updated product group","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductGroupResponse"}}}},"201":{"description":"The created product group","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductGroupResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a product group","tags":["ProductGroup"]}},"/public/v1/payments":{"get":{"description":"Get a paginated list of your company's payments, newest first (ordered by creation time, then id).\n\nA payment is a single record of money moving, and comes in one of two kinds, reported in `payment_type`:\n• INVOICE — money received from a customer against an [invoice](#model-compactinvoice). The `invoice` field is populated and `purchase` is null.\n• PURCHASE — money paid to a vendor against a [purchase](#model-paymentpurchase). The `purchase` field is populated and `invoice` is null.\n\nExactly one of `invoice` / `purchase` is populated on any payment; the other is always null. Only invoice payments carry `credit_uses` and `overpayment_credits` (both null on purchase payments).\n\nEach payment has a `status` of POSTED or VOIDED. A voided payment is retained rather than deleted, so historical references to it still resolve. By default this endpoint returns both POSTED and VOIDED payments; pass `payment_status` to narrow. Soft-deleted payments are never returned.\n\nNarrow the result with `company_ids` (the customer/vendor), `invoice_ids` (INVOICE payments only), `purchase_ids` (PURCHASE payments only), `payment_method_ids`, `amount` range, `payment_number`, `payment_type`, `payment_status`, and the `inserted_datetime` / `payment_datetime` / `updated_datetime` windows. When several filters are supplied a payment must satisfy all of them (AND).\n\nResults are eventually consistent: a newly created, updated, or voided payment can take up to 1 second to appear here or to reflect its latest state.\n\nRequired permission: `payments_permissions_view`.\n","operationId":"DistruPublic.V1.PaymentController.index","parameters":[{"description":"Restrict the result to specific payments by ID (the same ID returned as each payment's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by the payment `amount` as an inclusive `min,max` decimal range separated by a comma. Either bound may be omitted: `100,` keeps payments of 100 or more, `,500` those of 500 or less, `100,500` those in between. A range with both bounds empty is rejected.","in":"query","name":"amount","required":false,"example":"?amount=100,500","schema":{"type":"string"}},{"description":"Restrict to payments tied to specific companies by company ID (the same ID returned as each payment's `company.id` — the customer for INVOICE payments, the vendor for PURCHASE payments). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"company_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by when the payment was created in Distru. Accepts a comma-separated `after,before` ISO8601 datetime range; either side may be omitted for an open-ended bound. `2022-07-10T00:00:00Z,` returns payments created at or after that instant, `,2022-07-10T00:00:00Z` returns those created at or before it, and giving both bounds a closed range.","in":"query","name":"inserted_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Restrict to INVOICE payments applied to any of the given invoices by invoice ID (the same ID returned under each payment's `invoice.id`). Repeat the bracketed key once per ID; matches ANY. PURCHASE payments never match. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"invoice_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to payments made with specific payment methods by payment method ID (the same ID returned under each payment's `payment_method.id`). Repeat the bracketed key once per ID; matches ANY. Payments fully paid with credits (which have no payment method) never match. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"payment_method_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to PURCHASE payments applied to any of the given purchases by purchase ID (the same ID returned under each payment's `purchase.id`). Repeat the bracketed key once per ID; matches ANY. INVOICE payments never match. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"purchase_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"1-based page number to return; defaults to page 1 when omitted. Must be greater than 0. Each page holds up to 1000 payments; follow the `next_page` URL in the response to fetch the next page (it is null on the last page).","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter by the payment's `payment_datetime` (the datetime the payment was recorded as made, not when it was created in Distru). Same comma-separated `after,before` ISO8601 range format as `inserted_datetime`, with either bound optional.","in":"query","name":"payment_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Filter to payments whose `payment_number` contains this value (case-insensitive substring match, not an exact match).","in":"query","name":"payment_number","required":false,"schema":{"type":"string"}},{"description":"Filter by payment status. POSTED returns only non-voided payments; VOIDED returns only voided ones. Omit to return both POSTED and VOIDED payments.","in":"query","name":"payment_status","required":false,"schema":{"type":"string","enum":["POSTED","VOIDED"]}},{"description":"Filter by what the payment is tied to. INVOICE returns money received from customers against invoices; PURCHASE returns money paid to vendors against purchases. Omit to return both types.","in":"query","name":"payment_type","required":false,"schema":{"type":"string","enum":["INVOICE","PURCHASE"]}},{"description":"Filter by when the payment was last modified in Distru. Same comma-separated `after,before` ISO8601 range format as `inserted_datetime`, with either bound optional.","in":"query","name":"updated_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}}],"responses":{"200":{"description":"A list of payments","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Payments"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get payments","tags":["Payment"]}},"/public/v1/strains/{id}":{"delete":{"description":"Permanently delete a strain. This is a HARD delete — the strain row is removed outright (strains have no archived or soft-deleted state), so it immediately disappears from the list and fetch endpoints and its `id` stops resolving. Deletion cannot be undone; recreating the strain via upsert produces a new strain with a new `id`.\n\nDeleting a strain never deletes the products classified under it: those products remain intact, but their strain association is cleared, so they read back with a null strain. The strain is also removed from any menus that grouped products by it. No inventory is created, consumed, or released, and nothing is synced to Metrc or BioTrack.\n\nA strain can be deleted at any time, with one exception: when your company runs BioTrack compliance and the strain is attached to any package-tracked product, the request is rejected with a 400 — BioTrack requires every package-tracked product to keep a strain. This check also counts inactive or deleted products, since they could be reactivated later; reassign those products to another strain first. Companies on Metrc or with no compliance system can delete a strain regardless of its product associations. Returns 204 on success, or 404 if no strain with that `id` exists in your company.\n\nRequired permission: `settings_permissions_strains`.\n","operationId":"DistruPublic.V1.StrainController.delete","parameters":[{"description":"ID of the strain to delete, as returned in the `id` field of the list or upsert response. An ID that doesn't exist for your company returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a strain","tags":["Strain"]},"get":{"description":"Fetch one strain by its `id`. Returns the same reference record found in the list endpoint — its name and genetic classification — scoped to your company. A strain from another company, or one that does not exist, returns 404 (the two cases are indistinguishable, so you cannot use this to probe for strains you can't see).\n\nRequired permission: `settings_permissions_strains`.\n","operationId":"DistruPublic.V1.StrainController.show","parameters":[{"description":"The Distru strain id, as returned in the `id` field of the list or upsert response (e.g. `\"01H...\"`). Not a Metrc or other external identifier.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single strain","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StrainResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a strain","tags":["Strain"]}},"/public/v1/batches/{id}":{"delete":{"description":"Deletes a batch. This is a soft delete: the batch stops appearing in `GET /public/v1/batches` by default (pass the `deleted` filter as `include` or `only` to still see it) and this endpoint returns 404 for it, but the record is retained — `GET /public/v1/batches/{id}` keeps resolving it with a non-null `deleted_at`. The delete cannot be undone through the API; recreating the batch via upsert produces a new batch with a new `id`. Responds 204 with no body on success, or 404 if no non-deleted batch with that `id` exists in your company (including one that was already deleted or belongs to another company).\n\nA batch can only be deleted while nothing depends on it. The delete is refused with a 400 when the batch: appears on any sales order or purchase line item, has any returns, appears on any inventory transfer, or is used as an input or output of any assembly. The batch only becomes deletable once no such record references it. Only batches of batch-tracked products can be deleted here — a batch of a product on any other inventory-tracking method is refused with a 400.\n\nA batch may still hold on-hand quantity when it is deleted; that remaining quantity is not adjusted away, but it immediately stops counting as active inventory (it drops out of the batch's and its product's `quantity_active` and `quantity_active_by_location` fields, and of the [inventory](#model-inventory) endpoint). No inventory is created, consumed, or released, and nothing is synced to Metrc or BioTrack. A successful delete records a `delete` entry in the batch's activity log and notifies the relevant users.\n\nRequired permission: `products_permissions_delete`.\n","operationId":"DistruPublic.V1.BatchController.delete","parameters":[{"description":"ID of the batch to delete, as returned by the list, fetch, and upsert endpoints. An ID that doesn't exist for your company (or was already deleted) returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a batch","tags":["Batch"]},"get":{"description":"Fetch one batch by its Distru batch ID, including its cost and on-hand quantity totals, custom field values, primary test result, and (when your company has bin tracking enabled) its bins. Returns 404 if no batch with that id exists in your company, or the authenticated user cannot access it under their team restrictions.\n\nRequired permission: `products_permissions_view`.\n","operationId":"DistruPublic.V1.BatchController.show","parameters":[{"description":"The Distru batch ID to fetch.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single batch","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchFullResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a batch","tags":["Batch"]}},"/public/v1/packages/{id}/primary-test-result/pdf":{"get":{"description":"Download the Certificate of Analysis (COA) PDF for a single package, built from the package's primary lab result. Returns 404 when the package has no primary lab result with an attached file.\n\nThe same permissions that govern downloading this PDF in the Distru web app apply to this endpoint.\n\nBy default the response body is the binary PDF with content type `application/pdf`. Pass `?format=url` to instead receive a JSON envelope `{\"data\": {\"url\": ..., \"expires_datetime\": ...}}` containing a temporary signed download URL.\n\nAll PDF download endpoints share a single, combined rate limit for your account: 20 requests per minute and 1000 per day in total across every PDF endpoint (not per endpoint). Requests over the limit return 429 with a Retry-After header, and only successful downloads count toward it.\n","operationId":"DistruPublic.V1.PackageController.download_primary_test_result_pdf","parameters":[{"description":"The package's ID (the same `id` string returned by `GET /public/v1/packages`).","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Omit to receive the binary PDF (`Content-Type: application/pdf`). Pass `url` to instead receive a JSON body with a short-lived signed download URL (see the [PdfDownloadUrl](#model-pdfdownloadurl) schema). `url` is the only accepted value.","in":"query","name":"format","required":false,"schema":{"type":"string","enum":["url"]}}],"responses":{"200":{"description":"By default the binary PDF file (Content-Type: application/pdf). When ?format=url is passed, a JSON object with a temporary signed download URL (see PdfDownloadUrl schema).","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/PdfDownloadUrl"}}}},"400":{"description":"Invalid parameters","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No test results are attached to this package","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests — the account's shared PDF download rate limit (20/minute, 1000/day across all PDF endpoints) was exceeded. Retry after the period in the Retry-After header.","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Download a package's Certificate of Analysis (COA) PDF","tags":["Package"]}},"/public/v1/reports/purchase-order-history":{"get":{"description":"Read-only report that returns one row per [purchase](#model-purchase), each carrying the purchase's order and due dates, vendor, status, owner, and monetary totals (amount paid and purchase total). This is a reporting endpoint: it never changes inventory, compliance state, or any purchase — calling it has no side effects.\n\nScope and filtering:\n\n• Only purchases visible to the API key's company are included; there are no cross-company rows.\n• DRAFT purchases are always excluded — a purchase appears here only once it has left draft, so the `status` of every row is one of PENDING, PROCESSING, DELIVERING, PARTIALLY_RECEIVED, or COMPLETED.\n• When `order_datetime` is omitted the report defaults to the most recent 30-day range of purchase (order) dates (from the start of the day 30 days ago through the end of today, in the company's timezone). Because of this default the report always carries an order-date bound — there is no way to fetch every purchase regardless of order date. The other date filters (`due_datetime`, `created_datetime`, `updated_datetime`) have no default — omit one and it simply is not applied.\n• Different filters combine with AND (a row must satisfy every filter you supply), while multiple values inside a single list filter combine with OR. All date-range bounds are inclusive.\n\nRow shape:\n\n• Every value is returned as it appears in the report's CSV export. Monetary cells (`paid`, `amount`) are returned as strings (currency and comma formatting stripped) so they match the rest of the API; dates as ISO8601 timestamps in the company's timezone; everything else as strings.\n• The column set is dynamic. Companies on a compliance integration get an extra manifest-number column (`metrc_manifest_number` for Metrc, `biotrack_manifest_number` for BioTrack), and every Purchase custom field configured for the company is appended as an extra column keyed by its slugified label. Because of this, read `meta.columns` to discover the exact keys present rather than hardcoding them.\n• `meta` also returns the resolved human-readable date range the report covers (after the 30-day default is applied).\n\nRequired permission: `reports_permissions_purchase_order_history`.\n","operationId":"DistruPublic.V1.Report.PurchaseOrderHistoryController.index","parameters":[{"description":"Keep only purchases whose status is one of the supplied values (OR within the list). Values are SCREAMING_CASE: PENDING, PROCESSING, DELIVERING, PARTIALLY_RECEIVED, COMPLETED. DRAFT is not accepted and never returned — draft purchases are excluded from this report regardless of filters. Omit to include every non-draft status.","in":"query","name":"status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["COMPLETED","DELIVERING","PENDING","PARTIALLY_RECEIVED","PROCESSING"],"type":"string"}}},{"description":"Keep only purchases whose order (purchase) date falls in the given range. Format is a comma-separated ISO8601 datetime pair `after,before`; both bounds are inclusive. Leave either side empty for an open-ended bound (e.g. `2026-01-01T00:00:00Z,` for everything from that date onward), but a bare comma with both sides empty (`,`) is rejected — omit the param entirely to apply no bound. This is the one date filter with a default: when omitted, the report covers the most recent 30-day range of order dates.","in":"query","name":"order_datetime","required":false,"example":"?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Keep only purchases whose due date falls in the given range. Comma-separated ISO8601 datetime pair `after,before`; both bounds inclusive, either side may be empty for an open-ended bound (a bare `,` is rejected). No default — omit to skip this filter.","in":"query","name":"due_datetime","required":false,"example":"?due_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Keep only purchases created in the given range, by the record's creation timestamp. Comma-separated ISO8601 datetime pair `after,before`; both bounds inclusive, either side may be empty (a bare `,` is rejected). No default — omit to skip this filter.","in":"query","name":"created_datetime","required":false,"example":"?created_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Keep only purchases last modified in the given range, by the record's last-updated timestamp. Comma-separated ISO8601 datetime pair `after,before`; both bounds inclusive, either side may be empty (a bare `,` is rejected). No default — omit to skip this filter.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Keep only purchases whose total is within the given range. Comma-separated `min,max` numeric pair; both bounds inclusive. Leave either side empty for an open-ended bound (e.g. `100,` for 100 and up). Compares against the same purchase total returned in each row's `amount`.","in":"query","name":"total","required":false,"example":"?total=100,500","schema":{"type":"string"}},{"description":"Keep only purchases whose paid amount is within the given range. Comma-separated `min,max` numeric pair; both bounds inclusive, either side may be empty for an open-ended bound. Matches on the sum of the purchase's non-voided payments — the same value returned in each row's `paid` — so a purchase with no payments counts as 0.","in":"query","name":"paid","required":false,"example":"?paid=0,250","schema":{"type":"string"}},{"description":"Keep only purchases whose purchase number contains the search term. Case-insensitive partial (substring) match on the purchase number, with matches ordered by closeness to the term. Omit to skip.","in":"query","name":"search","required":false,"example":"?search=PO-1024","schema":{"type":"string"}},{"description":"Keep only purchases from the given vendors, identified by Distru company-relationship ID (OR within the list). Omit to include every vendor.","in":"query","name":"company_relationship_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only purchases received into the given warehouse locations, by Distru location ID (OR within the list). Omit to include every location.","in":"query","name":"location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only purchases owned by the given users, by Distru user ID (OR within the list). Omit to include every owner.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only purchases created by the given users, by Distru user ID (OR within the list). Omit to include every creator.","in":"query","name":"creator_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only purchases that include one of the given batches, by Distru batch ID (OR within the list).","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only purchases that include one of the given products, by Distru product ID (OR within the list).","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Purchase Order History report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PurchaseOrderHistoryReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Purchase Order History report","tags":["PurchaseOrderHistory"]}},"/public/v1/reports/sales-by-user":{"get":{"description":"Returns the Sales By User leaderboard: one row per user (sales rep) with their leaderboard rank, order count, pre-tax sales, and total sales over the reported date range. Rows are always ranked by total sales and returned in that order, with the top seller at rank 1 — this ordering is fixed and cannot be changed by the caller.\n\nThis is a read-only aggregate of the orders already in your account; it creates nothing and changes no inventory, compliance, or order state. Each order is attributed to the user set as its sales rep, and the money columns are net of returns — a return lowers that rep's pre-tax and total sales. Both money columns reflect the same set of orders; `total_sales` includes tax while `sales_pre_tax` excludes it.\n\nScope of what is counted:\n• Date range — orders are included by their order date. When `order_datetime` is omitted, the report covers the last 30 days (from the start of the day 30 days ago through the end of today), resolved in the account's time zone.\n• Status — canceled orders are excluded by default. To count them, pass `CANCELED` in the `status` filter; passing `status` at all replaces the default set entirely, so list every status you want counted.\n• Users — every sales rep with at least one qualifying order appears; deactivated users are omitted. Use `user_ids` to restrict the leaderboard to specific reps.\n\nResponse values mirror the report's CSV export with numeric cells returned as strings, so `leaderboard_rank`, `order_count`, and the two money columns come back as strings (currency and comma formatting stripped), matching the rest of the API. Report-level context — the resolved human-readable date range and the column definitions — is returned under `meta`.\n\nRequires the \"view the sales by user report\" permission on the API key's user.\n","operationId":"DistruPublic.V1.Report.SalesByUserController.index","parameters":[{"description":"Restricts the report to orders in these statuses (SCREAMING_CASE). Repeat the key to pass several. When omitted, every status except CANCELED is counted; passing this parameter replaces that default entirely, so include CANCELED here if you want canceled orders in the totals, and list every status you want counted.","in":"query","name":"status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"}}},{"description":"Restricts the report to orders whose order date falls in this range, given as two comma-separated ISO8601 timestamps, `after,before`. Either side may be left empty for an open-ended range (`,2026-02-01T00:00:00Z` is everything up to that instant; `2026-01-01T00:00:00Z,` is everything from that instant on). When omitted entirely, the report covers the last 30 days (start of the day 30 days ago through end of today), resolved in the account's time zone.","in":"query","name":"order_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Restricts the report to orders whose order number, customer name, or LeafLink short id matches this term (case-insensitive, partial match) before the per-user totals are computed. Omit to include all orders in range.","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Restricts the leaderboard to these users (sales reps), by their Distru user id. Repeat the key to pass several. When omitted, every sales rep with a qualifying order appears. Deactivated users are never included, even if listed here.","in":"query","name":"user_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Sales By User report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SalesByUserReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Sales By User report","tags":["SalesByUser"]}},"/public/v1/payment-methods/{id}":{"get":{"description":"Fetch a single [payment method](#model-payment_method) by its ID. Use this to resolve the method a\npayment references, or to read one method's current `name`, `type`, `active` flag, and QuickBooks\nOnline mapping (`qb_payment_method_id`).\n\nReturns 404 if no payment method with that ID exists within your company — the lookup is scoped to\nyour company, so an ID belonging to another company reads as not found. A soft-deleted method is\nstill returned here (with its `deleted_at` set); deletion hides a method from new use but does not\nremove it.\n\nRequired permission: `settings_permissions_payment_methods`.\n","operationId":"DistruPublic.V1.PaymentMethodController.show","parameters":[{"description":"The payment method's ID — the id string returned in the `id` field of the list and fetch responses. Required. An ID that belongs to another company, or does not exist, returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single payment method","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PaymentMethodResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a payment method","tags":["PaymentMethod"]}},"/public/v1/credits/{id}":{"delete":{"description":"Soft-delete a credit. The credit is marked as deleted and stops appearing in this API and the\nDistru UI, but the record is retained rather than being permanently removed, so its history is\npreserved. Returns 204 with no body on success.\n\nA credit cannot be deleted once it has been used (has at least one active application to an\ninvoice). Overpayment credits (created from an invoice overpayment or a QuickBooks Online\npayment) cannot be deleted unless they have already been canceled — void the associated payment\ninstead. Either case returns an error.\n\nIf your account syncs credits with QuickBooks Online, the deletion is also pushed to QuickBooks\nOnline in the background; a 204 confirms the delete in Distru, not that the QuickBooks Online\nside has finished.\n\nRequired permission: `credits_permissions_delete`. The authenticated user must also have access\nto the credit under their team restrictions.\n","operationId":"DistruPublic.V1.CreditController.delete","parameters":[{"description":"The credit's ID (as returned in the `id` field of a credit).","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a credit","tags":["Credit"]},"get":{"description":"Get a single credit by its ID, scoped to your company. The response carries the credit's live\ncomputed `status` and `remaining_balance` alongside its stored fields. Returns 404 if no such\ncredit exists for your company or it has been soft-deleted.\n\nRequired permission: `credits_permissions_view`. The authenticated user must also have access to\nthe requested credit under their team restrictions.\n","operationId":"DistruPublic.V1.CreditController.show","parameters":[{"description":"The credit's ID (as returned in the `id` field of a credit).","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single credit","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CreditResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a credit","tags":["Credit"]}},"/public/v1/price-tiers":{"get":{"description":"List the company's price tiers, newest first (by creation time). A price tier lowers the price\nof a matching sales order item — it applies to an order item only when every one of the tier's\npopulated conditions is met. Soft-deleted tiers are excluded. Tiers outside their active window\nare still listed; the window only governs whether a tier applies to orders, not whether it\nappears here.\n\nFilter with `company_relationship_id` (a customer condition), `product_ids` (a product\ncondition), or `search` (substring on the internal name). When several filters are supplied a\ntier must satisfy all of them (AND).\n\nResults are paginated. Walk pages with `page[number]`; the response's `next_page` holds the URL\nof the following page, or null on the last page.\n\nRequired permission: `settings_permissions_price_tiers`.\n","operationId":"DistruPublic.V1.PriceTierController.index","parameters":[{"description":"Restrict the result to specific price tiers by ID (the same ID returned as each price tier's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only tiers that include this customer in a customer condition. The ID of a company relationship (customer). Matches a tier whose \"one of these customers\" condition lists this id exactly; tiers with no customer condition, or that only exclude this customer, are not returned.","in":"query","name":"company_relationship_id","required":false,"schema":{"type":"string"}},{"description":"Return only tiers that explicitly list any of these products in their product condition (the same products returned under a tier's `conditions.one_of_products`). Repeat the bracketed key once per product ID; matches ANY. Tiers with no product condition — all-products tiers, or those scoped by category/brand/group — are not returned, nor are tiers that only exclude these products. A malformed ID is rejected with a 400; an unknown-but-well-formed ID matches nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Case-insensitive substring match on the tier's internal name. Does not search external_name.","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Filter to price tiers by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to price tiers by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Restrict to price tiers owned by any of these Distru users (each tier's `owner.id`). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Page to return via `page[number]` (1-based, must be greater than 0). Defaults to the first page.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of price tiers","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PriceTiers"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get price tiers","tags":["PriceTier"]},"post":{"description":"Create a price tier when `id` is absent, update it in place when present. Every write records a\nnew immutable version and points the tier's `current_version_id` at it, so order items priced\nby an earlier version keep their frozen pricing (see `price_tier_version` on the\n[sales order item](#model-salesorderitem)).\n\nUpdates are sparse: only the fields you send change, and anything you omit is left as-is. This\napplies to `conditions` too — send only the conditions you want to change, and send an empty\narray (or null) for a `one_of_*`/`not_one_of_*` list to clear that one. Omitting `menu_ids` leaves the\ntier's current menus untouched; send the full list to replace them. When you send the\n`min_quantity` or `total_thc_percentage_range` object, include every sub-field — a present\nsub-field may be null, but a missing one is rejected. Send the object as null to clear it.\n\n`conditions` decides whether the tier can apply to a sales order item — every populated\ncondition must be met together (AND), or the tier is not applicable. Within a single \"one of\"\nlist any listed match qualifies; a \"not one of\" list disqualifies any listed match;\n`min_quantity` is compared against the combined quantity of the order's matching items; and\n`total_thc_percentage_range` applies only where selling by potency is enabled. A create must\nresolve to at least one condition, otherwise it is rejected with a 400. Creating a tier also\nrequires your company to be set up for price tiers; if it is not, the create fails with a 400\nand a message to contact Distru support.\n\nSaving a tier also refreshes buyer-facing and statewide marketplace price discovery, which\nhappens asynchronously — a 200/201 reflects the saved tier itself, not yet its propagation to\nevery menu or marketplace surface that shows it. When a promo-enabled tier newly becomes\neligible for a menu, that menu gets a fresh promo entry.\n\nPromo card fields default sensibly on create: omit `menu_promo_card_type` and it becomes `TEXT`,\nand a `TEXT` card fills `menu_promo_card_background_hex` and `menu_promo_card_text_hex` with\ndefault colors when you omit them. Send `menu_promo_card_type: \"IMAGE\"` for an image card, where\nthe hex colors are optional. `menu_promo_enabled` (default false) controls whether the card shows\non menus.\n\nRequired permission: `settings_permissions_price_tiers`.\n","operationId":"DistruPublic.V1.PriceTierController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Price tier ID to update. Omit to create a new tier; when present, that tier is updated in place and a new immutable version is recorded."},"name":{"type":"string","description":"Internal name of the tier, up to 255 characters. Required."},"external_name":{"type":"string","description":"Buyer-facing name shown on menus and marketplace surfaces, up to 255 characters. When omitted or null, buyer-facing surfaces fall back to `name`."},"price_or_percent":{"enum":["PRICE","PERCENT"],"type":"string","description":"Whether the discount is a fixed amount (`PRICE`, uses `price`) or a percentage (`PERCENT`, uses `percent`). SCREAMING_CASE. A tier carries exactly one — setting one clears the other. Required."},"price":{"type":"string","description":"Discount amount, as a decimal string with up to 2 decimals (e.g. \"10.00\"). Its effect depends on `is_flat`: when `is_flat` is false it is subtracted from the order item's base price; when `is_flat` is true it replaces the base price outright. Must be zero or greater. Required when `price_or_percent` is `PRICE`; leave unset for `PERCENT`, which clears it automatically."},"percent":{"type":"integer","description":"Discount percentage off the base price, an integer 0-100. Required when `price_or_percent` is `PERCENT`; switching to `PRICE` clears it automatically."},"is_flat":{"type":"boolean","description":"Whether `price` is a flat replacement price (true) rather than an amount subtracted from the base price (false). Defaults to false. Can only be true with `price_or_percent` `PRICE` — true with `PERCENT` is rejected."},"valid_from_datetime":{"type":"string","description":"ISO8601 UTC start of the active window (e.g. \"2026-01-01T00:00:00Z\"). Null or omitted means no start bound. Outside the active window the tier is still listed but never applies to orders."},"valid_until_datetime":{"type":"string","description":"ISO8601 UTC end of the active window (e.g. \"2026-12-31T23:59:59Z\"). Null or omitted means no end bound (never expires)."},"menu_mode":{"enum":["ALL","NONE","SPECIFIC"],"type":"string","description":"Which menus the tier appears on: `ALL` every menu, `NONE` no menus, `SPECIFIC` only the menus in `menu_ids`. Required, SCREAMING_CASE. Setting `ALL` or `NONE` clears any specific menu selection."},"menu_ids":{"items":{"type":"string"},"type":"array","description":"IDs of the menus the tier appears on, used only when `menu_mode` is `SPECIFIC` (ignored for `ALL`/`NONE`, whose selection is cleared). Omit to leave the tier's current menus unchanged; send the full list to replace them — a sent list is the complete set, so menus not in it are removed."},"menu_promo_enabled":{"type":"boolean","description":"Whether a promo card for this tier shows on menus. Defaults to false. When true, `menu_promo_card_type` is required, and a `TEXT` card additionally requires both hex colors."},"menu_promo_card_type":{"enum":["TEXT","IMAGE"],"type":"string","description":"Promo card style, SCREAMING_CASE. Defaults to `TEXT` when omitted on create. A `TEXT` card requires both hex colors (filled with defaults if omitted on create); an `IMAGE` card does not use the hex colors. `TEXT` is the standard style."},"menu_promo_card_background_hex":{"type":"string","description":"Promo card background color as a hex string (e.g. \"#9F60FF\"). Required for a `TEXT` card — defaults to a standard color when omitted on create; ignored for `IMAGE`."},"menu_promo_card_text_hex":{"type":"string","description":"Promo card text color as a hex string (e.g. \"#0D1D23\"). Required for a `TEXT` card — defaults to a standard color when omitted on create; ignored for `IMAGE`."},"menu_promo_card_emoji":{"type":"string","description":"Emoji shown on the promo card. Optional; null for none."},"owner_id":{"type":"string","description":"ID of the user that owns this tier. Must be a user you are allowed to assign (within your team-visibility scope); an unassignable id is rejected. Send null to clear ownership; omit to leave it unchanged."},"conditions":{"$ref":"#/components/schemas/PriceTierConditionsInput"}},"required":["name","price_or_percent","menu_mode"]}}},"required":true},"responses":{"200":{"description":"The updated price tier","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PriceTierResponse"}}}},"201":{"description":"The created price tier","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PriceTierResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a price tier","tags":["PriceTier"]}},"/public/v1/vehicles":{"get":{"description":"List the authenticated company's vehicles, oldest first (by creation time, ascending). Only vehicles owned by the API key's company are returned; soft-deleted vehicles are omitted. Narrow the result with the `inserted_datetime` and `updated_datetime` range filters, or omit them to return the full set, paginated.\n\nResults are paged; follow `next_page` in the response envelope until it is null to walk every page.\n\nRequired permission: `settings_permissions_vehicles`.\n","operationId":"DistruPublic.V1.VehicleController.index","parameters":[{"description":"Restrict the result to specific vehicles by ID (the same ID returned as each vehicle's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to vehicles by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to vehicles by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"1-based page number to fetch. Defaults to page 1 when omitted; must be greater than 0. Example: `?page[number]=2`.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of vehicles","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Vehicles"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get vehicles","tags":["Vehicle"]},"post":{"description":"Create or update a vehicle. This is a single upsert: omit `id` to create a new vehicle, or pass the `id` of an existing vehicle owned by your company to update it in place. Passing an `id` that does not exist, is soft-deleted, or belongs to another company returns 404 — this endpoint never creates a vehicle at a caller-chosen id.\n\nOn create, `make`, `model` and `license_plate_number` are required. Updates are sparse: only the fields you send are changed, and any field you omit keeps its current value. There is no way to clear a field back to null through this endpoint — sending a field always overwrites it with the value you provide, and omitting it leaves the stored value intact. `make`, `model` and `license_plate_number` can never be blank.\n\nIf your company has the BioTrack compliance integration enabled, this endpoint also pushes the vehicle to the state traceability system (BioTrack) as part of the same request. That sync is synchronous, so no polling is needed: a 200 means BioTrack accepted the vehicle too, and if BioTrack rejects it the whole call fails and nothing is saved (create and BioTrack sync commit or roll back together). For BioTrack-enabled companies the following fields become required in addition to the three above: `year`, `color`, `vin`, `license_plate_state` and `description`. Companies without BioTrack are not synced anywhere and only need `make`, `model` and `license_plate_number`.\n\nOn validation failure (missing required field, BioTrack rejection) the response is a 400 whose body is `{ \"errors\": [...] }`, each entry carrying a human-readable message pointing at the offending body field.\n\nVehicles are a settings-level record referenced by transfers and manifests; upserting one does not touch inventory, orders, or purchases.\n\nRequired permission: `settings_permissions_vehicles`.\n","operationId":"DistruPublic.V1.VehicleController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID of the vehicle to update. Omit to create a new vehicle; when present it must identify a vehicle owned by your company."},"make":{"type":"string","description":"Manufacturer of the vehicle (e.g. \"Ford\"). Required on create and can never be cleared; on update, omit to leave unchanged."},"model":{"type":"string","description":"Model of the vehicle (e.g. \"Transit\"). Required on create and can never be cleared; on update, omit to leave unchanged."},"year":{"type":"string","description":"Model year of the vehicle, sent as a free-form string (e.g. \"2021\") — it is stored verbatim and never validated or coerced to a number. Optional for companies without BioTrack, but required when your company has BioTrack enabled. Omit to leave unchanged on update."},"color":{"type":"string","description":"Color of the vehicle (e.g. \"White\"). Optional, but required when your company has BioTrack enabled. Omit to leave unchanged on update."},"license_plate_number":{"type":"string","description":"License plate number. Required on create and can never be cleared; on update, omit to leave unchanged."},"license_plate_state":{"type":"string","description":"State the license plate is registered in, as a two-letter US state code (e.g. \"CA\"); must be one of the recognized US state/territory codes. Optional for companies without BioTrack, but required when your company has BioTrack enabled. Omit to leave unchanged on update."},"vin":{"type":"string","description":"Vehicle identification number (VIN). Optional, but required when your company has BioTrack enabled. Omit to leave unchanged on update."},"description":{"type":"string","description":"Free-text name or description for the vehicle. Optional, but required when your company has BioTrack enabled. Omit to leave unchanged on update."}}}}}},"responses":{"200":{"description":"The created or updated vehicle","content":{"*/*":{"schema":{"$ref":"#/components/schemas/VehicleResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Create or update a vehicle","tags":["Vehicle"]}},"/public/v1/packages/move":{"post":{"description":"Move a list of packages to a destination location within a single license.\n\n`package_ids` is a non-empty list of at most 300 package IDs; they must all resolve and all belong to the same license. All packages must be active and have a positive active quantity. `location_id` is the destination Distru location and must belong to that same license. Optionally pass `metrc_location_id` (Metrc's own location id) to also move the packages to a Metrc location.\n\nEach package's on-hand inventory is relocated to `location_id` via an internal stock transfer (one per distinct source location), so the move is reflected in inventory ledgers at both the source and destination. Packages already stored in `location_id` are left untouched.\n\nThis operation is atomic: if any package or location is invalid the entire request is rejected and nothing is moved. The error message is a single human-readable string, not a per-package structured error.\n\nNot supported for BioTrack licenses. For Metrc licenses, if `metrc_location_id` is passed, then Metrc is updated asynchronously: a successful response means the moves were applied in Distru and a pending Metrc activity was created for each package. The Metrc path additionally requires that your state has Metrc locations enabled and a Metrc key with permission to move packages.\n\nRequired permission: `products_permissions_adjust_inventory`.\n","operationId":"DistruPublic.V1.PackageController.move","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MovePackagesRequest"}}},"description":"The packages to move","required":true},"responses":{"200":{"description":"The moved packages","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Packages"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Move packages","tags":["Package"]}},"/public/v1/credits":{"get":{"description":"List store credits held by your customers, newest first (by creation datetime, then id). A\ncredit is an amount a customer can apply toward what they owe; each row carries its live\ncomputed `status` and `remaining_balance`.\n\nOnly credits belonging to your company are returned; soft-deleted credits are excluded. Filter\nby customer (`company_ids`), by the invoices a credit has been applied to (`invoice_ids`), by\n`amount` range, by `source`, `status`, `credit_number`, or the creation/last-modified windows.\nAll filters below are combined with AND — a credit must match every filter you send.\n\nThis endpoint returns eventually consistent data: a credit you just created, updated, canceled,\nor deleted (and any change to its `status`/`remaining_balance`) can take up to 1 second to be\nreflected here.\n\nRequired permission: `credits_permissions_view`. Only credits the authenticated user can access\nunder their team restrictions are returned.\n","operationId":"DistruPublic.V1.CreditController.index","parameters":[{"description":"Restrict the result to specific credits by ID (the same ID returned as each credit's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by the credit's current `amount` as an inclusive `min,max` decimal range separated by a comma. Either bound may be omitted: `100,` keeps credits of 100 or more, `,500` those of 500 or less, `100,500` those in between. A range with both bounds empty is rejected.","in":"query","name":"amount","required":false,"example":"?amount=100,500","schema":{"type":"string"}},{"description":"Restrict to credits held by specific customers by company ID (the same ID returned as each credit's `company.id`). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"company_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Case-insensitive substring match on the credit number (partial matches count; e.g. `100` matches `CR-1001`).","in":"query","name":"credit_number","required":false,"example":"?credit_number=CR-100","schema":{"type":"string"}},{"description":"Restrict to credits that have been applied to any of the given invoices by invoice ID (a credit is applied to an invoice through its credit uses; the applied invoice is visible under each credit's `credit_uses[].payment.invoice.id`). Repeat the bracketed key once per ID; matches ANY. Credits never applied to one of these invoices are excluded. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"invoice_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to credits owned by any of these Distru users (each credit's `owner.id`). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by creation datetime as an ISO8601 `after,before` range separated by a comma. Either bound may be left empty for an open-ended range: `2022-07-10T00:00:00Z,` means on or after that instant, `,2022-07-10T00:00:00Z` means on or before it. Both sides empty is rejected.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-01T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page number to fetch. Defaults to `1`; must be greater than 0.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter by how the credit was created (SCREAMING_CASE): USER (added by hand), RETURN (generated from a return), INVOICE_PAYMENT (an invoice overpayment in Distru), QB_PAYMENT / QB_CREDIT_MEMO (originated in QuickBooks Online). Exact match.","in":"query","name":"source","required":false,"schema":{"type":"string","enum":["INVOICE_PAYMENT","QB_CREDIT_MEMO","QB_PAYMENT","RETURN","USER"]}},{"description":"Filter by the credit's live computed status (SCREAMING_CASE): ACTIVE (has a remaining balance still to spend), REDEEMED (fully applied, nothing left), CANCELED (voided; its balance can no longer be applied). Exact match.","in":"query","name":"status","required":false,"schema":{"type":"string","enum":["ACTIVE","CANCELED","REDEEMED"]}},{"description":"Filter by last-modified datetime as an ISO8601 `after,before` range, same comma format as `inserted_datetime`. Either bound may be empty for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=,2022-07-10T00:00:00Z","schema":{"type":"string"}}],"responses":{"200":{"description":"A list of credits","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Credits"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get credits","tags":["Credit"]},"post":{"description":"Create a new credit or update an existing one through a single endpoint.\n\nOmit `id` to create a new credit; include the `id` of an existing credit to update it. Updates\nare sparse: only the fields you send are changed, and any field you omit keeps its current\nvalue. On create, the `credit_number` and `original_amount` are assigned automatically —\n`original_amount` is frozen to the create-time `amount` and never changes afterward.\n\nCredits created through the API are always manually-created (`USER` source) credits — the same\nas a credit you would add by hand in the Distru UI. Credits generated automatically (from a\nreturn, an invoice overpayment, or QuickBooks Online) cannot be created here, and only\n`owner_id`, `external_note` and `internal_note` can be updated on them — their amount, customer\nand QuickBooks Online item cannot be set through the API. On a credit memo created in QuickBooks\nOnline (`QB_CREDIT_MEMO` source) `owner_id` is the only updatable field: its notes live in\nQuickBooks Online and are re-imported from there on every sync.\n\nConstraints: on update the customer (`company_id`) cannot be changed. `amount` must be greater\nthan 0 and, on update, cannot be set below the amount already applied to invoices by this credit\n(its used amount). Once a credit has an owner it can be reassigned but not removed.\n\nCredits do not touch inventory or state compliance (Metrc / BioTrack). They do interact with\nQuickBooks Online: if your account syncs credits with QuickBooks Online, updating an existing\ncredit first pulls the latest credit and payment state from QuickBooks Online (so a stale local\namount can be rejected), and any create or update is then pushed to QuickBooks Online in the\nbackground. A 200/201 confirms the credit was saved in Distru, not that it has finished syncing\nto QuickBooks Online — re-fetch the credit and read `qb_sync_status` to observe the sync result.\n\nRequired permission: `credits_permissions_create` to create, `credits_permissions_edit` to\nupdate. Updating also requires access to the credit under the authenticated user's team\nrestrictions, and an `owner_id` they can assign under those same restrictions.\n","operationId":"DistruPublic.V1.CreditController.upsert","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertCredit"}}},"description":"Credit data","required":true},"responses":{"200":{"description":"The updated credit","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CreditResponse"}}}},"201":{"description":"The created credit","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CreditResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Create or update a credit","tags":["Credit"]}},"/public/v1/metrc/items":{"get":{"description":"Get the Metrc items — the item definitions Distru caches from Metrc — across your licenses, filtered by various attributes. Results are ordered by item name ascending and scoped to the company that owns the API key.\n\nEach row mirrors an item as it exists in Metrc (name, product category, strain, unit of measure, quantity type), plus Distru's `unit_type` resolved by matching the item's Metrc unit of measure to one of your company's unit types by name (null when no unit type matches that name), and the owning `license`. Items deleted in Metrc are included by default — use `is_deleted` to return only live or only deleted items.\n\nThis is a read-only cache: it does not create, edit, or delete items in Metrc. Data is eventually consistent — changes synced from Metrc can take up to ~1 second to appear here.\n","operationId":"DistruPublic.V1.MetrcController.list_items","parameters":[{"description":"Filter to items belonging to this license, given as a Distru license resource ID (as returned by the licenses endpoint — not a Metrc or state license number). Omit to return items across all of the company's licenses; a license ID that doesn't belong to your company simply returns no items.","in":"query","name":"license_id","required":false,"example":"b1f4c2a0-9c3e-4d2b-8f1a-2e5c6d7a8b9c","schema":{"type":"string"}},{"description":"Filter to items whose Metrc name contains this value (case-insensitive substring match against the item's `name`).","in":"query","name":"search","required":false,"example":"blue dream","schema":{"type":"string"}},{"description":"Filter by the item's quantity category, matched against its Metrc quantity type: `COUNT` (count-based), `VOLUME` (volume-based), or `WEIGHT` (weight-based). Omit to return all categories.","in":"query","name":"unit_type_category","required":false,"example":"WEIGHT","schema":{"type":"string","enum":["COUNT","VOLUME","WEIGHT"]}},{"description":"Filter to items whose Metrc product category name exactly matches one of the given values — an item matches if its `product_category_name` equals any listed value (OR semantics). Case-sensitive: values must match the Metrc category name verbatim. An empty list is ignored (returns items unfiltered by this parameter).","in":"query","name":"category_names","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by whether the item is deleted in Metrc. `true` returns only deleted items; `false` returns only live items. Omit to return both.","in":"query","name":"is_deleted","required":false,"schema":{"type":"boolean"}},{"description":"Filter to items with these Metrc item identifiers — Metrc's own integer IDs (the `metrc_id` in the response), not Distru IDs. Matches an item if its Metrc ID equals any of the listed values (OR semantics). An empty list is ignored (returns items unfiltered by this parameter).","in":"query","name":"metrc_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"integer"}}},{"description":"Filter by when the item was first cached in Distru. Accepts a comma-separated `from,to` range (ISO-8601 UTC); either side may be omitted, e.g. `2022-07-10T00:00:00Z,` returns items cached on or after that time.","in":"query","name":"inserted_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Filter by when the item's cache was last updated in Distru. Accepts a comma-separated `from,to` range (ISO-8601 UTC); either side may be omitted, e.g. `,2022-07-10T00:00:00Z` returns items last updated on or before that time.","in":"query","name":"updated_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"1-based page number, passed as `page[number]`. Defaults to `1` when omitted and must be greater than 0. Each page holds up to 200 items; when more remain, the response's `next_page` holds the URL for the following page.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of Metrc items","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MetrcItems"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get Metrc items","tags":["Metrc"]}},"/public/v1/packages/{id}":{"post":{"description":"Update the Distru-tracked fields of an existing package. Packages cannot be created through the API, only updated.\n\nThese are Distru-side attributes only — this endpoint does not push anything to Metrc or BioTrack, does not move inventory, and does not change the package's quantity or status. To change a package's location use `POST /public/v1/packages/move`; to finish it use `POST /public/v1/packages/finish`.\n\n`is_inactive` flips the package between active and inactive. Inactivating hides the package from active inventory in Distru and excludes its quantity from its product's active-quantity figures; it does not change the package's quantity, `distru_status`, or anything in the compliance system, and it can be reversed at any time by sending `false`. The flip runs in the same transaction as the rest of the update, so if it is rejected the whole request is rolled back and nothing is persisted.\n\nSupports sparse updates: only the fields included in the request body are changed; omitted fields are left untouched. A field sent explicitly as `null` clears it (see `bin_ids` for the array-clearing rule). The path `id` is the package's ID from `GET /public/v1/packages`; an unknown id returns 404.\n\nRequired permission: `products_permissions_edit`.\n","operationId":"DistruPublic.V1.PackageController.update","parameters":[{"description":"The package's ID (the `id` string returned by `GET /public/v1/packages`).","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"batch_number":{"type":"string","description":"The package's batch number. Sparse: omit to leave unchanged; send `null` to clear."},"expiration_datetime":{"type":"string","description":"The package's expiration datetime, ISO 8601 (e.g. `2026-12-31T00:00:00Z`). Sparse: omit to leave unchanged; send `null` to clear. Read back on the package as `expiration_datetime`."},"description":{"type":"string","description":"Free-form text describing the package. Sparse: omit to leave unchanged; send `null` to clear."},"harvest_date":{"type":"string","description":"The package's harvest date, `YYYY-MM-DD` (e.g. `2026-08-01`). Sparse: omit to leave unchanged; send `null` to clear."},"is_inactive":{"type":"boolean","description":"Whether the package is inactive. Send `true` to inactivate, `false` to reactivate; a package already in the requested state is left unchanged. Sparse: omit to leave unchanged; `null` is rejected. Rejected with a 400 while the package is syncing with the compliance system. Read back on the package as `inactivated_datetime` (null while active)."},"custom_data":{"type":"object","description":"A map of custom field IDs to their values. Use GET /public/v1/custom-fields?parent_object=package 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. Sparse at the top level only: omit `custom_data` to leave the package's custom fields unchanged, but when you send it the map replaces the package's entire custom field data, so include every field you want to keep."},"bin_ids":{"items":{"type":"string"},"type":"array","description":"The IDs of the bins this package is stored in. Behaviour: omit `bin_ids` to leave the package's bins unchanged; pass `null` or an empty array to clear all bins; pass a non-empty array to replace the package's bins with exactly those. Ignored unless bin inventory tracking is enabled for your company.\n"}}}}}},"responses":{"200":{"description":"The updated package","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PackageFullResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Update a package","tags":["Package"]}},"/public/v1/invoices/{id}":{"delete":{"description":"Deletes an invoice. This is a hard delete: the invoice is permanently removed together with its line items, charges, and payments — it disappears from `GET /public/v1/invoices`, `GET /public/v1/invoices/{id}` returns 404 for it, and it cannot be recovered through the API. Responds 204 with no body on success, or 404 if no invoice with that `id` exists in your company (including one that belongs to another company or was already deleted). A VOIDED invoice can be deleted too. Any custom validation rule your company has configured for invoice deletion can refuse the delete with a 400, in which case nothing is changed.\n\nEffects on payments and credits, all in one atomic call: every payment recorded on the invoice (voided ones included) is deleted with it. [Credit](#model-credit) balance that was applied to the invoice through those payments is released back onto the credits, and credits that were generated by overpaying this invoice are canceled.\n\nThe [order](#model-order) the invoice bills is untouched: it keeps its status and line items, and its invoiced and paid totals simply no longer include this invoice, so the order can be invoiced again. If your company is integrated with QuickBooks Online, the linked QuickBooks Online invoice and its payments are scheduled for deletion there too (that sync is eventual — observe it in QuickBooks Online, not in the 204). Files attached to the invoice are detached but kept. Nothing is synced to Metrc or BioTrack.\n\nRequired permission: `invoices_permissions_delete` (plus access to the invoice under team restrictions).\n","operationId":"DistruPublic.V1.InvoiceController.delete","parameters":[{"description":"ID of the invoice to delete, as returned by the list, fetch, and upsert endpoints. An ID that doesn't exist for your company returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete an invoice","tags":["Invoice"]},"get":{"description":"Fetch a single invoice by its ID, including its line items, charges, non-voided payments, and custom field values. Read-only, with no side effects. Returns 404 when no invoice with that ID exists in the caller's company. Required permission: `invoices_permissions_view`. The authenticated user must also have access to the requested invoice under their team restrictions, otherwise the request is rejected even though the invoice exists.","operationId":"DistruPublic.V1.InvoiceController.show","parameters":[{"description":"The invoice's ID — the `id` returned by the list and show invoice endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"An invoice","content":{"*/*":{"schema":{"$ref":"#/components/schemas/InvoiceResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get an invoice","tags":["Invoice"]}},"/public/v1/unit-types":{"get":{"description":"List unit types for the authenticated company. A unit type is a company-scoped unit of measure\n(e.g. Gram, Pound, Milliliter, or a custom unit you define) used to express quantities throughout\nDistru — on products, inventory levels, [cost type](#model-costtype) rates, and order and purchase\nline items. This endpoint is read-only; unit types are managed in the Distru web app.\n\nThere are two kinds of unit type in the response:\n• Distru's built-in standard units (Gram, Pound, Liter, etc.) are `locked` (cannot be renamed or\ndeleted) and always carry a `category` (COUNT, VOLUME, or WEIGHT) and a `qty_per_si_unit`\nconversion factor.\n• Custom units you create are unlocked and have both `category` and `qty_per_si_unit` set to null —\nthey are treated as opaque labels with no physical conversion.\n\nResults are scoped to your company and ordered oldest-first by creation time. Both active and\ninactive unit types are returned. The response is paginated — follow the `next_page` URL to page\nthrough the full set.\n","operationId":"DistruPublic.V1.UnitTypeController.index","parameters":[{"description":"Restrict the result to specific unit types by ID (the same ID returned as each unit type's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to unit types by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to unit types by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page selection. `page[number]` is 1-based and must be greater than 0; defaults to page 1 when omitted. Page size is fixed by the server and cannot be set from the request, so paging is controlled solely through `page[number]`. Results are ordered oldest-first by creation time — follow the `next_page` URL in the response to fetch the next page, which is null on the final page.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of unit types","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UnitTypes"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get unit types","tags":["UnitType"]}},"/public/v1/metrc/tags":{"get":{"description":"Get the Metrc tags — the unique, state-issued compliance identifiers (RFID labels) provisioned to your licenses — filtered by various attributes. Results are ordered by tag label ascending and scoped to the company that owns the API key.\n\nThis is a read-only view of tags already synced into Distru from Metrc; it does not order, provision, or reserve tags. Each tag is either a `PACKAGE` tag (retail/wholesale package labels) or a `PLANT` tag, and reports via `is_assigned` whether it has been attached to a package or plant yet.\n\nData is eventually consistent — a tag newly synced from Metrc can take up to ~1 second to appear here.\n","operationId":"DistruPublic.V1.MetrcController.index","parameters":[{"description":"Restrict the result to specific tags by ID (the same ID returned as each tag's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by tag kind. `PACKAGE` covers retail/wholesale package labels; `PLANT` covers plant tags. Omit to return both kinds.","in":"query","name":"kind","required":false,"example":"PACKAGE","schema":{"type":"string","enum":["PACKAGE","PLANT"]}},{"description":"Filter to tags provisioned to this license, given as a Distru license resource ID (as returned by the licenses endpoint — not a Metrc or state license number). Omit to return tags across all of the company's licenses; a license ID that doesn't belong to your company simply returns no tags.","in":"query","name":"license_id","required":false,"example":"b1f4c2a0-9c3e-4d2b-8f1a-2e5c6d7a8b9c","schema":{"type":"string"}},{"description":"Filter by whether the tag is currently attached to a package or plant. `true` returns only assigned tags; `false` returns only unassigned (still-available) tags. Omit to return both.","in":"query","name":"is_assigned","required":false,"schema":{"type":"boolean"}},{"description":"Filter to the single tag whose full 24-character label equals this value exactly. Labels are uppercase hex (`A-F`, `0-9`). Unlike `search`, this value is matched verbatim and is not uppercased or stripped, so pass the exact uppercase label; a lowercase or partial value returns nothing. Use `search` instead for substring or fuzzy matching.","in":"query","name":"tag","required":false,"example":"1A4FF0100000022000004999","schema":{"type":"string"}},{"description":"Filter to tags whose label contains this value (case-insensitive substring match). The value is uppercased and stripped of any character outside `A-F`/`0-9` before matching, so only its hex portion is used; a value that reduces to 24 characters is matched as an exact full label. If nothing remains after stripping, the filter is ignored.","in":"query","name":"search","required":false,"example":"0004999","schema":{"type":"string"}},{"description":"Filter by when the tag was created in Distru. Accepts a comma-separated `from,to` range (ISO-8601 UTC); either side may be omitted, e.g. `2022-07-10T00:00:00Z,` returns tags created on or after that time.","in":"query","name":"inserted_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Filter by when the tag was last modified in Distru. Accepts a comma-separated `from,to` range (ISO-8601 UTC); either side may be omitted, e.g. `,2022-07-10T00:00:00Z` returns tags last modified on or before that time.","in":"query","name":"updated_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"1-based page number, passed as `page[number]`. Defaults to `1` when omitted and must be greater than 0. Each page holds up to 5000 tags; when more remain, the response's `next_page` holds the URL for the following page.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of Metrc tags","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MetrcTags"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get Metrc tags","tags":["Metrc"]}},"/public/v1/orders/{id}/test-results/pdf":{"get":{"description":"Download the combined Certificate of Analysis (COA) PDF for a single sales order, built from the primary lab result of each line item's batch or package. Returns 404 when the order has no attached lab results.\n\nThe same permissions that govern downloading this PDF in the Distru web app apply to this endpoint.\n\nBy default the response body is the binary PDF with content type `application/pdf`. Pass `?format=url` to instead receive a JSON envelope `{\"data\": {\"url\": ..., \"expires_datetime\": ...}}` containing a temporary signed download URL.\n\nAll PDF download endpoints share a single, combined rate limit for your account: 20 requests per minute and 1000 per day in total across every PDF endpoint (not per endpoint). Requests over the limit return 429 with a Retry-After header, and only successful downloads count toward it.\n","operationId":"DistruPublic.V1.OrderController.download_test_results_pdf","parameters":[{"description":"ID for an order","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Pass `url` to receive a signed download URL instead of the binary PDF","in":"query","name":"format","required":false,"schema":{"type":"string","enum":["url"]}}],"responses":{"200":{"description":"By default the binary PDF file (Content-Type: application/pdf). When ?format=url is passed, a JSON object with a temporary signed download URL (see PdfDownloadUrl schema).","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/PdfDownloadUrl"}}}},"400":{"description":"Invalid parameters","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No test results are attached to this order","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests — the account's shared PDF download rate limit (20/minute, 1000/day across all PDF endpoints) was exceeded. Retry after the period in the Retry-After header.","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Download a sales order's Certificate of Analysis (COA) PDF","tags":["Order"]}},"/public/v1/custom-fields/{id}":{"delete":{"description":"Deletes a custom field definition and every value ever stored in it. This is a hard delete and it cannot be undone: the definition is removed permanently, and the field's value is erased from the `custom_data` of every record of the field's entity type across your whole company — an order field is scrubbed from every order, a package field from every package, and so on. The erasure happens synchronously in this request, so a 204 means the values are already gone; for a field carrying values on many thousands of records the request can take a while to respond. The field's column also disappears from that entity's table view in the Distru app. Recreating a field with the same name later produces a new field with a new numeric id and no values.\n\nA custom field can always be deleted — no record, value, or setting blocks it, including fields marked `required`. Responds 204 with no body on success, or 404 if no field with that id belongs to your company (a field owned by another company is indistinguishable from one that does not exist). No inventory is created, consumed, or released, and nothing is synced to Metrc or BioTrack.\n\nRequired permission: `settings_permissions_custom_fields`.\n","operationId":"DistruPublic.V1.CustomFieldController.delete","parameters":[{"description":"The numeric id of the custom field to delete, as returned by the list endpoint and used as the key inside each record's `custom_data`. An id that doesn't exist for your company returns 404. Example: `482`","in":"path","name":"id","required":true,"schema":{"type":"integer"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a custom field","tags":["CustomField"]},"get":{"description":"Get one custom field definition by its numeric id, scoped to the authenticated company.\n\nUse this to resolve a field's current type, options, and settings — for example before writing a value into an entity's `custom_data`, where this same id is the map key. Returns 404 when no field with that id belongs to the company (a field owned by another company is indistinguishable from one that does not exist). Read-only; touches nothing in inventory or compliance.\n\nRequired permission: `settings_permissions_custom_fields`.\n","operationId":"DistruPublic.V1.CustomFieldController.show","parameters":[{"description":"The numeric id of the custom field, as returned by the list endpoint and used as the key inside each record's `custom_data`. Example: `482`","in":"path","name":"id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"A single custom field definition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomFieldDefinitionResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a custom field definition","tags":["CustomField"]},"post":{"description":"Update an existing custom field. Only `name`, `description`, `required`, and `field_options` are editable here; `field_type`, `parent_object`, and `filterable` are fixed at creation. This endpoint does not accept those three fields — any value sent for them is silently ignored, not applied.\n\nEach body field is sparse: omit a field to leave it unchanged. For `name`, `description`, and `required`, sending an explicit null is treated the same as omitting it — the field keeps its current value, so an existing `description` cannot be blanked back to empty through this endpoint. Only the fields you send with a real value are applied, so a request that sends just `name` renames the field and leaves its options and settings intact.\n\n`field_options` is a full replacement of the option list, and editing it ripples through the whole company's data:\n• An option present before but absent from the new list is deleted. Deleting an option scrubs that value out of every record of this entity type — for a `dropdown` the record's selection is cleared, for a `checkbox` that one value is removed from the record's selected set — and removes it from any saved filters that referenced it (a filter left with no values is dropped). This runs across all matching records for the company and can be a large change.\n• An option that stays in the list is preserved along with the records that selected it.\n• A new value in the list is added as a selectable option.\n• Renaming an option (by editing its text while keeping its position) carries the new name into every record and saved filter that used the old value.\nOmitting `field_options` entirely leaves the current options untouched. This field applies only to `dropdown` and `checkbox` fields.\n\nThis is a settings/metadata change only — it does not affect inventory, Metrc, or BioTrack. Returns 404 when no field with that id belongs to the company.\n\nRequired permission: `settings_permissions_custom_fields`.\n","operationId":"DistruPublic.V1.CustomFieldController.update","parameters":[{"description":"The numeric id of the custom field to update, as returned by the list endpoint. Example: `482`","in":"path","name":"id","required":true,"schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"New display name. Omit (or send null) to leave unchanged. Must stay unique among the fields on the same entity type for this company; uniqueness is case-insensitive, so `Lab` and `lab` collide. At most 70 characters, not a reserved name, and free of certain special characters. Renaming also updates the field's column label in the Distru app."},"description":{"type":"string","description":"New description. Omit (or send null) to leave unchanged — null does not clear an existing description. At most 100 characters."},"required":{"type":"boolean","description":"New value for whether a value must be supplied when a record is saved. Omit to leave unchanged."},"field_options":{"items":{"type":"string"},"type":"array","description":"The complete new option list for a `dropdown` or `checkbox` field, e.g. `[\"Lab A\", \"Lab C\"]`. This is a full replace, not a merge: any current option missing from this list is deleted and scrubbed from every record and saved filter that used it (see the endpoint description). Each value must be unique, non-empty, at most 255 characters, and contain no commas. Omit the field to leave the current options unchanged; send the complete list to change them. Do not send null. Ignored for `text` and `date` fields."},"disabled_field_options":{"items":{"type":"string"},"type":"array","description":"The complete new set of turned-off options for a `dropdown` or `checkbox` field. This is a full replace: an option listed here is disabled, and any option not listed is re-enabled. A disabled option can no longer be selected on new or edited records but stays in `field_options` so records already holding it keep displaying it. Every value must be one of the field's options (the new `field_options` when you also send that, otherwise the current ones); an unknown value is rejected. Omit to leave the current disabled set unchanged; send `[]` to re-enable everything. Ignored for `text` and `date` fields."}}}}}},"responses":{"200":{"description":"Custom field updated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomFieldDefinitionResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Update a custom field","tags":["CustomField"]}},"/public/v1/product-categories":{"get":{"description":"List the product categories belonging to the authenticated company, oldest-first by creation\ntime. Soft-deleted categories are excluded, and results are scoped to your company only. Each\nentry also carries its subcategories.\n\nResults are paged: the response returns a fixed-size page plus a `next_page` URL, which is null\non the last page.\n\nRequired permission: `settings_permissions_product_categories`.\n","operationId":"DistruPublic.V1.ProductCategoryController.index","parameters":[{"description":"Restrict the result to specific product categories by ID (the same ID returned as each category's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to product categories by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to product categories by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page selector (1-based). Request a page with `?page[number]=1`; follow the `next_page` URL in the response to walk subsequent pages. Omitting it returns the first page.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of product categories","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategories"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get product categories","tags":["ProductCategory"]},"post":{"description":"Create or update a single product category. Include the `id` of an existing category to update\nit in place; omit `id` to create a new one. A create returns 201, an update returns 200. Updates\nare sparse — only the fields you send change, and omitted fields keep their current value.\n\n`name` must be unique within your company. A name currently held by a soft-deleted category is\nalso rejected, so restore or rename that deleted category first. Names are trimmed of surrounding\nwhitespace and may not contain special characters.\n\n`official_product_category_id` maps the category to Distru's system-defined official category\nlist and is required when creating. Once a category maps to a non-null official category that\nmapping is locked: on update, omit it or resend the same value — sending a different value is\nrejected. The one exception is a category that is currently unmapped (null), which can happen\nfor categories created before this mapping existed: you may set its mapping on an update, but\nnot change it afterward. This mapping also drives how the category is synced to your connected\npoint-of-sale integrations (Blaze, Dutchie, Treez), so treat it as permanent once chosen.\n\nSubcategories are not editable through this endpoint; manage them with the product subcategory\nendpoints. Existing products keep their category assignment.\n\nRequired permission: `settings_permissions_product_categories`.\n","operationId":"DistruPublic.V1.ProductCategoryController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID of the category to update. When present, that category is updated in place; when absent, a new category is created."},"name":{"type":"string","description":"The category's display name. Must be unique within your company (a name held by a soft-deleted category is also rejected). Trimmed of surrounding whitespace; special characters are not allowed. Required when creating; on update, omit to leave the current name unchanged."},"official_product_category_id":{"type":"string","description":"ID of the official (Distru system-defined) category this maps to; list valid IDs with `GET /public/v1/official-product-categories`. Required when creating. Once a category holds a non-null mapping it is locked — on update omit it or resend the same value, since a different value is rejected. A category that is currently unmapped (null) is the one exception: you may set its mapping on an update, but not change it afterward. This mapping drives how the category syncs to your point-of-sale integrations (Blaze, Dutchie, Treez). This is an external-facing string ID, not a numeric one."}},"required":["name","official_product_category_id"]}}},"required":true},"responses":{"200":{"description":"The updated product category","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}},"201":{"description":"The created product category","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a product category","tags":["ProductCategory"]}},"/public/v1/reports/plant-lifecycle":{"get":{"description":"Returns one row per plant batch (plant group) whose creation date falls in the reported range, summarizing its lifecycle: the batch name, creation date, strain, and the counts of plants started, promoted to vegetative, destroyed, and harvested. Each row also carries the promotion-to-veg date, first and last harvest dates, the durations spent as a batch, from veg to last harvest, and over the total lifecycle (in days), and the names of the harvests the batch produced. Use this to reconcile cultivation throughput and (with the cost permission) per-stage plant costs against what the state traceability system recorded.\n\nScope of what is included — this report is Metrc-only. A batch appears only when all of the following hold: it sits on an active Metrc license, and that license's facility is one that can track vegetative plants. BioTrack and non-compliance batches are never returned, and neither are batches on licenses whose facility skips the vegetative stage (there plants move straight from immature to flowering, so the veg-based metrics here would be meaningless). The report is not paginated — every matching batch is returned in a single response.\n\nDate range and filters — the `datetime` filter matches the batch creation (planted) date. When it is omitted, the report defaults to batches created in the last 30 days. An optional `strain` filter narrows to batches whose Metrc strain name contains the given text (case-insensitive substring).\n\nReading the values — every cell is returned as it appears in the report's CSV export: date cells are `MM/DD/YYYY` strings, and count and cost cells are numeric strings (the raw amount with any currency formatting stripped, e.g. `\"1234.56\"`), matching the rest of the API where all amounts are strings. A leading-zero numeric value is kept as its display string so significant zeros are not lost. Several fields are null when the batch has not reached the relevant stage: `promoted_to_veg_date`, `days_veg_to_last_harvest`, the harvest dates, and `harvest_name_s` are null until a batch is promoted to veg or harvested. `days_as_batch` counts from creation to the veg-promotion date, or to today when the batch has not yet been promoted (so it keeps growing for still-un-promoted batches), and is therefore always present.\n\nCost columns — `total_cost_batch_stage`, `total_cost_veg_to_last_harvest`, `destroyed_plant_cost`, and `total_lifecycle_cost` require permission to view costs. For a caller without that permission these keys are omitted from every row entirely (not returned as null), and the corresponding column definitions are dropped from `meta.columns`. When present, each cost can still be null for a batch that has no recorded cost, and `total_lifecycle_cost` is the sum of the other three (null only when all three are null).\n\nReport-level information (the resolved human-readable date range and the ordered column definitions) is returned under `meta`.\n\nRequired permission: `reports_permissions_plant_lifecycle`.\n","operationId":"DistruPublic.V1.Report.PlantLifecycleController.index","parameters":[{"description":"Restricts the report to plant batches whose creation (planted) date falls in this range. The value is a comma-separated ISO8601 range formatted as `<start>,<end>`, and both bounds are inclusive. Leave either side empty for an open-ended range — `,2026-02-01T00:00:00Z` returns every batch created on or before that instant, and `2026-01-01T00:00:00Z,` every batch created on or after it. When omitted, the report defaults to batches created in the last 30 days.","in":"query","name":"datetime","required":false,"example":"?datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Restricts the report to batches whose strain name contains this text (case-insensitive substring match against the batch's Metrc strain name). Omit or leave empty to include every strain.","in":"query","name":"strain","required":false,"example":"?strain=Blue","schema":{"type":"string"}}],"responses":{"200":{"description":"The Plant Lifecycle report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PlantLifecycleReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Plant Lifecycle report","tags":["PlantLifecycle"]}},"/public/v1/invoices/{id}/pdf":{"get":{"description":"Download the rendered PDF for a single invoice — the same document a user downloads from the Distru UI. This is a read-only operation: it moves no inventory, changes no invoice data, and triggers no compliance or accounting sync.\n\nThe same permissions that govern downloading this PDF in the Distru web app apply to this endpoint.\n\nBy default the response body is the binary PDF with content type `application/pdf`. Pass `?format=url` to instead receive a JSON envelope `{\"data\": {\"url\": ..., \"expires_datetime\": ...}}` containing a temporary signed download URL.\n\nAll PDF download endpoints share a single, combined rate limit for your account: 20 requests per minute and 1000 per day in total across every PDF endpoint (not per endpoint). Requests over the limit return 429 with a Retry-After header, and only successful downloads count toward it.\n","operationId":"DistruPublic.V1.InvoiceController.download_pdf","parameters":[{"description":"The invoice's ID — the `id` returned by the list and show invoice endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Controls the response shape. Omit it (the default) to receive the binary PDF directly (`Content-Type: application/pdf`). Pass the value `url` to instead receive a JSON body holding a short-lived signed download URL (see the [PdfDownloadUrl](#model-pdfdownloadurl) response); useful when you want to hand the download off to a browser or store the link rather than stream the bytes.","in":"query","name":"format","required":false,"schema":{"type":"string","enum":["url"]}}],"responses":{"200":{"description":"By default the binary PDF file (Content-Type: application/pdf). When ?format=url is passed, a JSON object with a temporary signed download URL (see PdfDownloadUrl schema).","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/PdfDownloadUrl"}}}},"400":{"description":"Invalid parameters","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests — the account's shared PDF download rate limit (20/minute, 1000/day across all PDF endpoints) was exceeded. Retry after the period in the Retry-After header.","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Download an invoice PDF","tags":["Invoice"]}},"/public/v1/cost-types":{"get":{"description":"List cost types for the authenticated company. A cost type is a reusable, company-scoped\ncategory of cost accounting figure (e.g. freight, labor) with a default per-unit amount and a\nunit of measure. Cost types are configuration, not transactions — they are the templates you\ndraw from when applying a cost to a plant, a package or batch, an assembly or breakdown output,\na purchase item, or a product. Each applied cost snapshots its own amount and quantity at apply\ntime, so a cost type's values here only seed future applications and never restate historical\ncosts.\n\nResults are scoped to your company and ordered oldest-first by creation time. Both active and\ninactive cost types are returned; soft-deleted ones are excluded. The response is paginated —\nfollow the `next_page` URL to page through the full set.\n\nRequired permission: `costs_permissions_manage_cost_types`.\n","operationId":"DistruPublic.V1.CostTypeController.index","parameters":[{"description":"Restrict the result to specific cost types by ID (the same ID returned as each cost type's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to cost types by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to cost types by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page selection. `page[number]` is 1-based and must be greater than 0; defaults to page 1 when omitted. Results are ordered oldest-first by creation time — follow the `next_page` URL in the response to fetch the next page.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of cost types","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CostTypes"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get cost types","tags":["CostType"]},"post":{"description":"Create or update a single cost type. This is one endpoint for both operations: omit `id` to\ncreate a new cost type (responds 201), or pass the `id` of an existing cost type to update it\n(responds 200). The URL and request shape are identical either way.\n\nOn create, `name`, `cost_per_unit`, `unit_type_id` and `allow_inline_edits` are all required.\nOn update the body is sparse — only the fields you send are changed, and any field you omit\nkeeps its current value. `unit_type_id` is immutable: once a cost type has a unit type it cannot\nbe reassigned, so sending a different `unit_type_id` on update is rejected. `name` must be unique\nwithin your company, compared case-insensitively, among cost types that are both active and not\nsoft-deleted; inactive or deleted cost types do not reserve their name, so deactivating or\ndeleting a cost type frees its name for reuse. `cost_per_unit` must be non-zero (it may be\nnegative).\n\nA cost type is configuration, not a transaction: this endpoint writes only the cost-type\ntemplate itself. It does not create, consume, reserve, or release inventory, and it never syncs\nto Metrc or BioTrack — cost types have no compliance identity of their own. It also does not\ntouch costs already applied from this cost type: each applied cost (on a plant, a package or\nbatch, an assembly or breakdown output, a purchase item, or a product) snapshots its own amount\nand quantity at apply time, so changing this cost type's `cost_per_unit` or `allow_inline_edits`\naffects only future applications, never historical cost records.\n\nRequired permission: `costs_permissions_manage_cost_types`.\n","operationId":"DistruPublic.V1.CostTypeController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID of an existing cost type to update. Omit to create a new cost type. When given, the matching cost type in your company is updated in place; the update is sparse, so only the other fields you send are changed."},"name":{"type":"string","description":"Display name of the cost type (e.g. \"Freight\", \"Labor\"). Required on create. Leading and trailing whitespace is trimmed. Must be unique within your company, compared case-insensitively, among cost types that are both active and not soft-deleted; the name of a deleted or inactive cost type may be reused. On update, omit to leave unchanged."},"description":{"type":"string","description":"Optional free-text description of the cost type. Nullable — send null or omit on create to leave it empty. Leading and trailing whitespace is trimmed. On update, omit to leave the current description unchanged, or send null to clear it."},"cost_per_unit":{"type":"string","description":"Default cost amount per one unit of `unit_type_id`, as a decimal string (e.g. \"12.50\"), with up to 9 decimal places. Required on create. Must be non-zero; may be negative. This is the amount pre-filled when the cost type is applied to a record: when `allow_inline_edits` is true it can be overridden at apply time, otherwise the applied amount is locked to this value. Changing it does not rewrite costs already applied. On update, omit to leave unchanged."},"unit_type_id":{"type":"string","description":"ID of the unit of measure this cost is priced per — call GET /public/v1/unit-types to list valid IDs. Required on create and immutable afterwards: once set it cannot be changed, so sending a different value on update is rejected. On update, omit (or resend the same value) to leave unchanged."},"active":{"type":"boolean","description":"Whether the cost type is active and selectable when applying new costs. Defaults to true when omitted on create. Inactive cost types are still returned by the read endpoints. Name uniqueness only considers active cost types, so setting this to false releases the name for another active cost type to use. On update, omit to leave unchanged."},"allow_inline_edits":{"type":"boolean","description":"Required on create. When true, the per-unit amount can be overridden each time this cost type is applied to a record; when false, the applied amount is locked to this cost type's `cost_per_unit`. On update, omit to leave unchanged."}},"required":["name","cost_per_unit","unit_type_id","allow_inline_edits"]}}},"required":true},"responses":{"200":{"description":"The updated cost type","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CostTypeResponse"}}}},"201":{"description":"The created cost type","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CostTypeResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a cost type","tags":["CostType"]}},"/public/v1/payment-methods":{"get":{"description":"List the [payment methods](#model-payment_method) configured for your company. A payment method\nis how money changes hands on a payment — its `type` is one of CASH, CHECK, CREDIT_CARD,\nBANK_REMITTANCE, or BANK_TRANSFER, and each method has a company-chosen `name` on top of that\ntype. Use this to resolve the payment method a payment references, or to discover the set of\nmethods available when recording payments.\n\nPayment methods can be linked to a QuickBooks Online payment method; when they are, the response's\n`qb_payment_method_id` holds the QuickBooks Online identifier the method is mapped to (null when it\nisn't mapped).\n\nThis endpoint does not paginate: it returns every matching payment method in a single `data` array,\nand `next_page` is always null. Results are scoped to your company only.\n\nNote: this endpoint returns eventually consistent data — a create, edit, or delete can take up to\n1 second to be reflected here.\n\nRequired permission: `settings_permissions_payment_methods`.\n","operationId":"DistruPublic.V1.PaymentMethodController.index","parameters":[{"description":"Restrict the result to specific payment methods by ID (the same ID returned as each payment method's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Controls whether soft-deleted payment methods are included. `no` (the default) returns only active, non-deleted methods; `only` returns exclusively soft-deleted methods (those with a non-null `deleted_at`); `include` returns both together. Soft-deleted methods are hidden from new use but stay attached to the historical payments that already referenced them, so `only`/`include` are how you resolve those older references. Values are lowercase. Example: `?deleted=include`.","in":"query","name":"deleted","required":false,"schema":{"type":"string","enum":["no","include","only"],"default":"no"}},{"description":"Filter to payment methods by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to payment methods by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}}],"responses":{"200":{"description":"A list of payment methods","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PaymentMethods"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get payment methods","tags":["PaymentMethod"]}},"/public/v1/file-attachments":{"post":{"description":"Uploads a file and attaches it to exactly one existing record. On success the file is stored and\nserved back through the `url` on the response, so a single call both persists the document and\nmakes it downloadable.\n\nSend the request as `multipart/form-data` with three parts:\n• `file` — the binary to upload. Required, and must be non-empty (a zero-byte file is rejected).\n• `name` — optional display name; defaults to the uploaded file's original filename when omitted or blank.\n• exactly one reference id (`product_id`, `order_id`, `purchase_id`, ...) naming the record to attach to. Supply the id — the same id that record's own endpoint returns. Providing no reference id is rejected, and providing more than one is also rejected; exactly one is required.\n\nThis endpoint is create-only. There is no public endpoint to update or delete an attachment, and a\nfile cannot be replaced once uploaded — upload a new attachment instead. The response echoes the\nattachment with all reference-id fields present but only the one you set populated; the rest are null.\n\nThe attachment is owned by the company tied to the API key you authenticate with, and the response's\n`uploader` is the user that key belongs to. On success you get a 201 with the created attachment\nwrapped in a `data` envelope; its `url` is immediately downloadable.\n\nSystem effects: the upload counts against your company's storage quota. If it would exceed the\nremaining quota the call fails with 400 and nothing is stored — free space under Settings (or remove\nother attachments) and retry. This endpoint does not move inventory and does not push to Metrc or\nBioTrack; it only associates a document with the referenced record.\n\nRequired permission: `products_permissions_edit`.\n","operationId":"DistruPublic.V1.FileAttachmentController.insert","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"description":"The binary file to upload, sent as a multipart part. Required and must be non-empty — a zero-byte file is rejected. When `name` is omitted or blank, the uploaded file's original filename becomes the attachment name.","type":"string","format":"binary"},"name":{"description":"Display name for the attachment. Defaults to the uploaded file's original filename when omitted or blank. Max 255 characters.","type":"string"},"product_id":{"description":"ID of the product to attach this file to, as returned by the product endpoints. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"order_id":{"description":"ID of the order to attach this file to, as returned by the order endpoints. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"purchase_id":{"description":"ID of the purchase to attach this file to, as returned by the purchase endpoints. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"invoice_id":{"description":"ID of the invoice to attach this file to, as returned by the invoice endpoints. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"batch_id":{"description":"ID of the batch to attach this file to, as returned by the batch endpoints. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"contact_id":{"description":"ID of the contact to attach this file to, as returned by the contact endpoints. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"company_relationship_id":{"description":"ID of the company relationship to attach this file to. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"request_id":{"description":"ID of the request to attach this file to. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"task_id":{"description":"ID of the task to attach this file to. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"stock_transfer_id":{"description":"ID of the stock transfer to attach this file to. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"assembly_id":{"description":"ID of the assembly to attach this file to, as returned by the assembly endpoints. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"return_id":{"description":"ID of the return to attach this file to. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"order_shipment_id":{"description":"ID of the order shipment to attach this file to. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"license_id":{"description":"ID of the license to attach this file to. Exactly one reference id must be provided across all `*_id` fields on this request — providing none, or more than one, is rejected.","example":"550e8400-e29b-41d4-a716-446655440000","type":"string"}},"required":["file"]}}},"required":true},"responses":{"201":{"description":"File attachment inserted successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/FileAttachmentResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Insert a file attachment","tags":["FileAttachment"]}},"/public/v1/returns/{id}":{"get":{"description":"Fetch a single return by its Distru ID, including its line items, generated credits, associated sales order and invoice numbers, and the computed return quantities.\n\nReturns 404 if no return with that ID exists within your company, or if it has been deleted. This endpoint is eventually consistent: a change can take up to 1 second to appear here.\n\nRequired permission: `returns_permissions_view`.\n","operationId":"DistruPublic.V1.ReturnController.show","parameters":[{"description":"Distru ID of the return — the same id string returned as `id` in a return response.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single return","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReturnResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a return","tags":["Return"]}},"/public/v1/locations":{"get":{"description":"Returns the paginated list of locations belonging to your company, oldest first (ascending creation date). A location is a physical or logical place that holds inventory and appears throughout the rest of the API: it is where a [purchase](#model-purchase) receives inventory, where an [order](#model-order) ships from, and where stock and packages are held. Each location optionally links to a compliance [license](#model-license); that link (`license` / `license_id`) is null for locations with no compliance license. When the linked license is a Metrc license, `metrc_id` is the identifier of the corresponding Metrc room; it is null for locations not synced to Metrc, and it is a raw Metrc integer id, not a Distru id.\n\nBy default only non-deleted locations are returned; use `deleted` to include or isolate soft-deleted locations. Filter the result set by `name` (case-insensitive substring), `license_number` (exact match on the linked license), creation window (`inserted_datetime`), and last-modified window (`updated_datetime`); when several are supplied a location must satisfy all of them to be returned (AND). Results are scoped to your own company; you never see another tenant's locations.\n\nResults are paginated: read the top-level `next_page` URL to fetch the following page, and stop when it is null. Page size is fixed by the server and is not client-controllable.\n\nThis endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.\n\nRequired permission: `companies_permissions_view`.\n","operationId":"DistruPublic.V1.LocationController.index","parameters":[{"description":"Restrict the result to specific locations by ID (the same ID returned as each location's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by creation datetime. Accepts a comma-separated `from,to` range in ISO-8601 UTC; both bounds are inclusive and either side may be omitted. `2022-07-10T00:00:00Z,` returns locations created on or after that time; `,2022-07-10T00:00:00Z` returns those created on or before it; `2022-07-01T00:00:00Z,2022-07-31T00:00:00Z` returns those in the window. Omit the param to apply no creation-date filter. Combines with `updated_datetime` via AND — a location must fall in both windows to be returned.","in":"query","name":"inserted_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Controls whether soft-deleted locations are returned. `no` (the default when omitted) returns only non-deleted locations, `only` returns only soft-deleted ones, `include` returns both. A soft-deleted location has a non-null `deleted_at` in the response.","in":"query","name":"deleted","required":false,"schema":{"type":"string","enum":["no","include","only"],"default":"no"}},{"description":"Filter by the exact `license_number` of the location's linked compliance license (as returned under `license.license_number`). Case-sensitive exact match; locations with no linked license never match. Send a single value, not a list.","in":"query","name":"license_number","required":false,"example":"?license_number=C11-0000001-LIC","schema":{"type":"string"}},{"description":"Case-insensitive substring match on the location `name` (partial matches count; e.g. `main` matches `Main Warehouse`). Send a single value, not a list.","in":"query","name":"name","required":false,"example":"?name=warehouse","schema":{"type":"string"}},{"description":"Page number to fetch, 1-based, passed as `page[number]`. Defaults to page 1 when omitted; must be greater than 0. Each response carries a `next_page` URL when more pages remain (null on the last page). Page size is fixed by the server, not client-controllable.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter by last-modified datetime. Accepts a comma-separated `from,to` range in ISO-8601 UTC; both bounds are inclusive and either side may be omitted. `,2022-07-10T00:00:00Z` returns locations last modified on or before that time; `2022-07-10T00:00:00Z,` returns those modified on or after it. Omit the param to apply no last-modified filter.","in":"query","name":"updated_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}}],"responses":{"200":{"description":"A list of locations","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Locations"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get locations","tags":["Location"]}},"/public/v1/official-product-categories":{"get":{"description":"List Distru's official product categories — the global, system-defined reference\ntaxonomy (e.g. `FLOWER`, `PRE_ROLL`, `EDIBLE`) that is identical for every company and\nis not scoped to your API key. These records are singletons maintained by Distru; they\ncannot be created, edited, or deleted through the API.\n\nUse this to resolve the `official_product_category_id` on your own\n[product categories](#model-productcategory): each of your product categories maps to\nexactly one of these, and that mapping is what lets Distru report across accounts, drive\nconsistent menu/marketplace filtering, and let buyers configure POS integrations without\nknowing your custom category names. Fetch this list to discover the valid category `id`\nvalues before you set that mapping; you do not need it for anything else.\n\nThe full set is returned in a single call ordered by `id` ascending — there is no\nfiltering, no pagination, and no `next_page`. The list is small and stable, so it is\nsafe to cache; new entries are added only when Distru extends the taxonomy.\n","operationId":"DistruPublic.V1.OfficialProductCategoryController.index","responses":{"200":{"description":"A list of official product categories","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OfficialProductCategories"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get official product categories","tags":["OfficialProductCategory"]}},"/public/v1/invoices/{id}/test-results/pdf":{"get":{"description":"Download a single combined Certificate of Analysis (COA) PDF for an invoice, assembled by merging the primary lab-result file of each line item's batch or package into one document. Use it to hand a customer the lab paperwork backing everything they were billed for. This is a read-only operation with no side effects.\n\nReturns 404 when the invoice has no attached lab results — i.e. none of its line items resolve to a batch or package that has a lab-result file. A COA that exists on the batch/package but is not yet uploaded as a file will not appear here.\n\nThe same permissions that govern downloading this PDF in the Distru web app apply to this endpoint.\n\nBy default the response body is the binary PDF with content type `application/pdf`. Pass `?format=url` to instead receive a JSON envelope `{\"data\": {\"url\": ..., \"expires_datetime\": ...}}` containing a temporary signed download URL.\n\nAll PDF download endpoints share a single, combined rate limit for your account: 20 requests per minute and 1000 per day in total across every PDF endpoint (not per endpoint). Requests over the limit return 429 with a Retry-After header, and only successful downloads count toward it.\n","operationId":"DistruPublic.V1.InvoiceController.download_test_results_pdf","parameters":[{"description":"The invoice's ID — the `id` returned by the list and show invoice endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Controls the response shape. Omit it (the default) to receive the binary PDF directly (`Content-Type: application/pdf`). Pass the value `url` to instead receive a JSON body holding a short-lived signed download URL (see the [PdfDownloadUrl](#model-pdfdownloadurl) response); useful when you want to hand the download off to a browser or store the link rather than stream the bytes.","in":"query","name":"format","required":false,"schema":{"type":"string","enum":["url"]}}],"responses":{"200":{"description":"By default the binary PDF file (Content-Type: application/pdf). When ?format=url is passed, a JSON object with a temporary signed download URL (see PdfDownloadUrl schema).","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/PdfDownloadUrl"}}}},"400":{"description":"Invalid parameters","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No test results are attached to this invoice","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests — the account's shared PDF download rate limit (20/minute, 1000/day across all PDF endpoints) was exceeded. Retry after the period in the Retry-After header.","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Download an invoice's Certificate of Analysis (COA) PDF","tags":["Invoice"]}},"/public/v1/orders/{id}/pdf":{"get":{"description":"Download the rendered order slip PDF for a single sales order.\n\nThe same permissions that govern downloading this PDF in the Distru web app apply to this endpoint.\n\nBy default the response body is the binary PDF with content type `application/pdf`. Pass `?format=url` to instead receive a JSON envelope `{\"data\": {\"url\": ..., \"expires_datetime\": ...}}` containing a temporary signed download URL.\n\nAll PDF download endpoints share a single, combined rate limit for your account: 20 requests per minute and 1000 per day in total across every PDF endpoint (not per endpoint). Requests over the limit return 429 with a Retry-After header, and only successful downloads count toward it.\n","operationId":"DistruPublic.V1.OrderController.download_pdf","parameters":[{"description":"ID for an order","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Pass `url` to receive a signed download URL instead of the binary PDF","in":"query","name":"format","required":false,"schema":{"type":"string","enum":["url"]}}],"responses":{"200":{"description":"By default the binary PDF file (Content-Type: application/pdf). When ?format=url is passed, a JSON object with a temporary signed download URL (see PdfDownloadUrl schema).","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/PdfDownloadUrl"}}}},"400":{"description":"Invalid parameters","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests — the account's shared PDF download rate limit (20/minute, 1000/day across all PDF endpoints) was exceeded. Retry after the period in the Retry-After header.","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Download a sales order slip PDF","tags":["Order"]}},"/public/v1/company-groups/{id}":{"delete":{"description":"Permanently deletes a company group. This is a hard delete: the group is removed and cannot be\nrecovered — there is no soft-delete or undo. Returns 404 if no group with that ID belongs to the\nauthenticated company.\n\nDeleting a group also strips it from any price tier that targeted or excluded companies by this\ngroup: those price tiers keep working but lose this group from their include/exclude filters. The\ngroup is only a label, so deleting it does not delete any customer or vendor relationship that was\ntagged with it — those companies simply lose the tag.\n\nRequired permission: `settings_permissions_company_relationship_groups`.\n","operationId":"DistruPublic.V1.CompanyGroupController.delete","parameters":[{"description":"ID of the company group to delete, as returned by the list and upsert endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a company group","tags":["CompanyGroup"]},"get":{"description":"Fetches a single company group by its ID. Returns 404 if no group with that ID belongs to the\nauthenticated company — a group owned by a different company is indistinguishable from one that\ndoes not exist.\n\nRequired permission: `settings_permissions_company_relationship_groups`.\n","operationId":"DistruPublic.V1.CompanyGroupController.show","parameters":[{"description":"ID of the company group to fetch, as returned by the list and upsert endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single company group","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CompanyGroupFullResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a company group","tags":["CompanyGroup"]}},"/public/v1/purchases":{"get":{"description":"List purchase orders, most recent Order Date first, filtered by the query parameters below. Each entry is the full [purchase](#model-purchase) shape (line items, charges, active payments, custom data). Draft purchases are never returned.\n\nResults are paginated: the response `data` array holds one page, and `next_page` is the URL for the following page (null on the last page).\n\nRequired permission: `purchases_permissions_view`. Results are further limited to the purchases the authenticated user can access under their team restrictions, so two users on different teams may see different subsets.\n","operationId":"DistruPublic.V1.PurchaseController.index","parameters":[{"description":"Restrict the result to specific purchases by ID (the same ID returned as each purchase's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter purchases by lifecycle status. Repeat the key to pass more than one value; a purchase matches if its status is any of the values given (OR). SCREAMING_CASE; accepted values are COMPLETED, DELIVERING, PARTIALLY_RECEIVED, PENDING, PROCESSING. Draft purchases are excluded from this endpoint and cannot be filtered for. At most 200 statuses may be given. See the `status` field on the [purchase response](#model-purchase) for what each value means.","in":"query","name":"statuses","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["COMPLETED","DELIVERING","PENDING","PARTIALLY_RECEIVED","PROCESSING"],"type":"string"}}},{"description":"Filter purchases by payment status, derived from the payments recorded against the purchase total. Repeat the key to pass several; purchases in ANY of the given statuses are returned. SCREAMING_CASE, one of:\n• NOT_PAID — nothing has been paid.\n• PARTIALLY_PAID — some but not the full amount has been paid.\n• FULLY_PAID — paid in full.\n• OVER_PAID — paid more than the purchase total.\nAt most 200 values.","in":"query","name":"payment_statuses","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["NOT_PAID","PARTIALLY_PAID","FULLY_PAID","OVER_PAID"],"type":"string"}}},{"description":"Filter to purchases whose supplier is any of these companies. Pass company relationship IDs — the same id returned as each purchase's `company.id` and by GET /public/v1/companies. Repeat the bracketed key once per ID. Unknown IDs (including ones that don't belong to your company) simply match nothing; an empty list is treated as no filter. At most 200 IDs.","in":"query","name":"company_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases whose supplier belongs to any of these company relationship groups. Pass company relationship group IDs (the same id returned by GET /public/v1/company-relationship-groups). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"company_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases owned by any of these Distru users (each purchase's `owner.id`). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases whose receiving warehouse is any of these Distru locations (each purchase's `location.id`). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases whose receiving location carries this license number (the `license_number` on each purchase's `location`). Exact match. A purchase whose receiving location has no license never matches when this filter is present.","in":"query","name":"license_number","required":false,"example":"?license_number=C11-0000123-LIC","schema":{"type":"string"}},{"description":"Filter to purchases whose purchase number contains this text, case-insensitively (substring match). For an exact match on one or more full purchase numbers, use `purchase_numbers` instead.","in":"query","name":"purchase_number","required":false,"example":"?purchase_number=PO-10","schema":{"type":"string"}},{"description":"Filter to purchases whose purchase number exactly matches any of these values, case-insensitively. Repeat the bracketed key once per value; an empty list is treated as no filter. At most 200 values. Use `purchase_number` for a substring search instead.","in":"query","name":"purchase_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases associated with any of these Metrc transfers, matching Metrc's own integer transfer id (each purchase's `metrc_transfer_id`). Repeat the bracketed key per value; empty list is no filter. At most 200 values.","in":"query","name":"metrc_transfer_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"integer"}}},{"description":"Filter to purchases associated with any of these BioTrack manifests (each purchase's `biotrack_id`). Exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values.","in":"query","name":"biotrack_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by the purchase `total` (the same value returned as each purchase's `total`). Inclusive range written as `min,max`; either side may be omitted. `100,` keeps purchases totaling 100 or more, `,500` keeps those totaling 500 or less, and `100,500` keeps those in between.","in":"query","name":"total","required":false,"example":"100,500","schema":{"type":"string"}},{"description":"Filter purchases by their due datetime. Value is an ISO8601 range `lower,upper` (both UTC): `lower,upper` keeps purchases due within the range, `lower,` keeps those due at or after `lower`, and `,upper` keeps those due at or before `upper`. At least one bound is required.","in":"query","name":"due_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter purchases by when they were created in Distru. ISO8601 range `lower,upper` (both UTC); omit either side for an open-ended bound (`lower,` or `,upper`).","in":"query","name":"inserted_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Filter purchases by their order datetime (the date the purchase was placed, which is also the sort key). ISO8601 range `lower,upper` (both UTC); omit either side for an open-ended bound.","in":"query","name":"order_datetime","required":false,"example":"2022-07-10T00:00:00Z,2022-07-11T00:00:00Z","schema":{"type":"string"}},{"description":"Filter purchases by when they were last modified in Distru. ISO8601 range `lower,upper` (both UTC); omit either side for an open-ended bound.","in":"query","name":"updated_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to purchases that contain a line item drawn from any of these batches (matching a line item's batch). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item drawn from any of these packages (matching a line item's package). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"package_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose package carries any of these compliance labels (the state-traceability tag, e.g. a Metrc tag). Case-sensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"package_compliance_labels","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose package has any of these batch numbers. Case-sensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"package_batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose batch has any of these batch numbers (matching the line item's batch). Case-sensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"batch_batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item of any of these products (matching a line item's product). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose product belongs to any of these product categories. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_category_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose product belongs to any of these product subcategories. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_subcategory_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose product belongs to any of these product groups. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose product has any of these brands. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose product has any of these vendors (the product's supplier company relationship). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose product has any of these strains. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_strain_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose product carries any of these tags. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_tag_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to purchases that contain a line item whose product has any of these SKUs. Case-insensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_skus","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Page to fetch, 1-based. Defaults to 1 when omitted; the page size is fixed, so page through until `next_page` is null. Must be greater than 0.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter by custom field values, as `custom_data[{id}]=value` where `{id}` is a custom field's numeric id. Repeat with different ids to filter on several fields at once; a record must match every one (AND). Matching is case-sensitive exact against the value stored on the record. The id must be a filterable custom field defined on this entity — use `GET /public/v1/custom-fields?parent_object=purchase` to list the ids, their types, and which are filterable. A non-numeric id, an id not defined on this entity, or an id that isn't filterable returns a 400.","in":"query","name":"custom_data","required":false,"schema":{"type":"object"}}],"responses":{"200":{"description":"A list of purchases","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Purchases"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get purchases","tags":["Purchase"]},"post":{"description":"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. The order's line items must be either all package-tracked or all not package-tracked — a mix of the two is rejected. \n\nSee 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. \n\nFor 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. \n\nTo 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. \n\nRequired 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.","operationId":"DistruPublic.V1.PurchaseController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"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":{"type":"string","description":"A free-text description of the purchase order. Optional; send null to clear it."},"status":{"enum":["COMPLETED","DELIVERING","PENDING","PARTIALLY_RECEIVED","PROCESSING"],"type":"string","description":"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":{"type":"integer","description":"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":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"The ID of the Distru user that owns this purchase order. Optional; send null to leave it unassigned."},"charges":{"items":{"$ref":"#/components/schemas/PurchaseChargeRequest"},"type":"array","description":"The extra lines added on top of the purchase order's items — fees, discounts, or taxes. Each entry follows the [PurchaseChargeRequest](#model-purchasechargerequest) shape. Optional on update: 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."},"items":{"items":{"$ref":"#/components/schemas/PurchaseItemRequest"},"type":"array","description":"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."},"custom_data":{"type":"object","description":"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."}}}}}},"responses":{"200":{"description":"A single purchase orders","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PurchaseResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a purchase order","tags":["Purchase"]}},"/public/v1/users":{"get":{"description":"Get a paginated list of the team members (users) in your company, sorted oldest-first by their creation datetime.\n\nUse this to enumerate everyone on your team along with their email, role, and full name, or to keep an external system in sync with your Distru user list. Results are always scoped to your own company; users in other companies are never returned. By default soft-deleted users are excluded — use the `deleted` filter to include or isolate them.\n\nThis is a read-only endpoint and does not change anything in the system. Its data is eventually consistent: a user that was just created, modified, or deleted may take up to ~1 second to appear or reflect the change here.\n\nThe response is an envelope of `{ data: [...], next_page: ... }`. Up to 1000 users are returned per page; when more pages remain, `next_page` is a ready-to-follow URL for the next page, and it is null on the last page.\n\nRequired permission: `settings_permissions_manage_team`.\n","operationId":"DistruPublic.V1.UserController.index","parameters":[{"description":"Restrict the result to specific users by ID (the same ID returned as each user's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to users created within a datetime window. Supply a comma-separated `after,before` pair of ISO8601 UTC datetimes; both bounds are inclusive and either side may be omitted to leave that end open. `2022-07-10T00:00:00Z,` matches users created on or after that instant; `,2022-07-10T00:00:00Z` matches users created on or before it; `2022-07-01T00:00:00Z,2022-07-31T00:00:00Z` matches users created within that closed range. An empty pair (`,`) is rejected.","in":"query","name":"inserted_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Which users to include based on soft-delete state. `no` (the default when omitted) returns only active users; `only` returns only soft-deleted users; `include` returns both. Soft-deleted users carry a non-null `deleted_at` in the response.","in":"query","name":"deleted","required":false,"schema":{"type":"string","enum":["no","include","only"],"default":"no"}},{"description":"Page selector via `page[number]`, 1-based and must be greater than 0; defaults to page 1 when omitted. A value of 0 or below is rejected. Up to 1000 users are returned per page (page size is fixed and not adjustable). A page number past the last page returns an empty `data` array with `next_page` null rather than an error. When more pages remain, the response's `next_page` field is a ready-to-follow URL for the next page, and is null on the last page.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter to users last modified within a datetime window. Same format as `inserted_datetime`: a comma-separated `after,before` pair of ISO8601 UTC datetimes, both bounds inclusive, either side optional. `,2022-07-10T00:00:00Z` matches users last modified on or before that instant.","in":"query","name":"updated_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}}],"responses":{"200":{"description":"A list of users","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Users"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get users","tags":["User"]}},"/public/v1/returns":{"get":{"description":"List returns, newest first (by return date), with optional filters: by customer (`company_ids`), by originating sales order (`order_ids`), by `statuses`, by custom field values (`custom_data`), and by the `inserted_datetime` / `return_datetime` / `updated_datetime` windows. When more than one filter is supplied they are combined with AND — a return must satisfy every filter to appear.\n\nA return records product a customer sent back. It reverses the related inventory and financials, and — when the return is set to create a credit — generates a customer [credit](#model-compactcredit), which can sync to QuickBooks Online as a credit memo. A return is usually tied to the original sales [order](#model-compactorder); a return created without an order is a generic return and leaves the order-derived fields (`order`, `order_quantity`, `return_quantity`, `return_type`, `invoice_numbers`) empty.\n\nReturned goods are added back to sellable inventory only once a return reaches `COMPLETED`; while `PROCESSING`, `SHIPPED`, or `RECEIVED` they are held aside as returning stock. Line items flagged as waste are written off rather than restocked.\n\nResults are ordered by return date, newest first, and paginated. Follow the `next_page` URL in the response to fetch the following page rather than incrementing the page number yourself. This endpoint is eventually consistent: a change can take up to 1 second to appear here.\n\nRequired permission: `returns_permissions_view`.\n","operationId":"DistruPublic.V1.ReturnController.index","parameters":[{"description":"Restrict the result to specific returns by ID (the same ID returned as each return's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to returns issued to specific customers by company ID (the same ID returned as each return's `company.id`). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"company_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by creation datetime, given as an inclusive `after,before` range of ISO 8601 timestamps separated by a comma. Either bound may be left empty: `after,` keeps only returns created on or after `after`; `,before` only those created on or before `before`; `after,before` keeps those inside the closed range. A range with both bounds empty is rejected.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Restrict to returns tied to specific sales orders by order ID (the same ID returned as each return's `order.id`). Repeat the bracketed key once per ID; matches ANY. A malformed ID is rejected with a 400; an unknown-but-well-formed ID matches nothing; an empty list is no filter. Generic returns (created without an order) never match. At most 200 IDs.","in":"query","name":"order_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to returns owned by any of these Distru users (each return's `owner.id`). Repeat the bracketed key once per ID; matches ANY. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to returns in any of the given statuses (SCREAMING_CASE, matches ANY): PROCESSING, SHIPPED, RECEIVED, COMPLETED. Repeat the bracketed key once per status. An empty list is no filter; at most 200 statuses.","in":"query","name":"statuses","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PROCESSING","SHIPPED","RECEIVED","COMPLETED"],"type":"string"}}},{"description":"1-based page number. Defaults to page 1 when omitted; must be greater than 0. Page size is fixed, so follow the `next_page` URL in the response to fetch the following page rather than incrementing this yourself; `next_page` is null on the last page.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter by return date — the business date on the return, which can differ from when the record was created — given as an inclusive `after,before` range of ISO 8601 timestamps separated by a comma, with the same empty-bound rules as `inserted_datetime`.","in":"query","name":"return_datetime","required":false,"example":"?return_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by last-modified datetime, given as an inclusive `after,before` range of ISO 8601 timestamps separated by a comma, with the same empty-bound rules as `inserted_datetime`.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=,2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by custom field values, as `custom_data[{id}]=value` where `{id}` is a custom field's numeric id. Repeat with different ids to filter on several fields at once; a record must match every one (AND). Matching is case-sensitive exact against the value stored on the record. The id must be a filterable custom field defined on this entity — use `GET /public/v1/custom-fields?parent_object=return` to list the ids, their types, and which are filterable. A non-numeric id, an id not defined on this entity, or an id that isn't filterable returns a 400.","in":"query","name":"custom_data","required":false,"schema":{"type":"object"}}],"responses":{"200":{"description":"A list of returns","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Returns"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get returns","tags":["Return"]}},"/public/v1/menus/{id}":{"get":{"description":"Fetch a single menu by its ID, scoped to the authenticated company. Returns the same shape as an entry in the list endpoint, including `product_count` (the number of active products on the menu) and `url` (its primary public link, null when the menu has no primary URL). Returns 404 when no menu with that ID exists in the company.\n\nRequired permission: `products_permissions_view` (admins are always allowed).\n","operationId":"DistruPublic.V1.MenuController.show","parameters":[{"description":"The menu's ID, as returned in the `id` field of the list and show responses.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single menu","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MenuResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a menu","tags":["Menu"]}},"/public/v1/product-subcategories":{"get":{"description":"List product subcategories for the authenticated company, oldest first (ascending creation order).\n\nA subcategory is a second-level classification beneath a product category; products point at a subcategory to place them in the catalog taxonomy. Only subcategories whose parent category belongs to your company and is not deleted are returned.\n\nOptionally narrow the list to one parent category with `?category_id=` (exact match on the parent category id).\n\nResults are paginated. When more rows remain, the response's `next_page` field holds the URL of the following page; it is null on the last page.\n\nRequired permission: `settings_permissions_product_categories`.\n","operationId":"DistruPublic.V1.ProductSubcategoryController.index","parameters":[{"description":"Restrict the result to specific product subcategories by ID (the same ID returned as each subcategory's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Return only subcategories whose parent product category id matches this value exactly. Omit to return every subcategory across all of your categories.","in":"query","name":"category_id","required":false,"example":"?category_id=b1c2d3e4-5f60-4a7b-8c9d-0e1f2a3b4c5d","schema":{"type":"string"}},{"description":"Filter to product subcategories by their creation datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range: `2022-07-10T00:00:00Z,` matches on or after that instant, `,2022-07-10T00:00:00Z` matches on or before it.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to product subcategories by their last-updated datetime, given as a comma-separated `start,end` pair of ISO8601 datetimes (inclusive). Either bound may be omitted for an open-ended range.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page selector, e.g. `?page[number]=1`. `number` is 1-based and defaults to 1 when omitted; up to 500 subcategories are returned per page. When more rows remain, the response's `next_page` field holds the URL for the next page.","in":"query","name":"page","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"A list of product subcategories","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductSubcategories"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get product subcategories","tags":["ProductSubcategory"]},"post":{"description":"Create or update one product subcategory. Omit `id` to create; pass the `id` of an existing subcategory to update it. The URL and body shape are the same for both — a create returns 201, an update returns 200.\n\nOn create, both `name` and `product_category_id` are required, and the parent category must belong to your company. `name` must be unique within its parent category, compared case-insensitively (so \"Flower\" and \"flower\" collide), and may not contain restricted special characters.\n\nThe parent category is fixed at creation: `product_category_id` cannot be changed on update. Sending a different value on update is rejected; omit it (or send the same value) to keep it. To reclassify products under a different category, reassign the products themselves rather than re-parenting the subcategory.\n\nSubcategories are a Distru-internal catalog taxonomy. Creating or renaming one reshapes how products are classified (via each product's subcategory) and feeds menus, price tiers, and retailer mapping, but it does not move inventory and is not synced to Metrc or BioTrack.\n\nRequired permission: `settings_permissions_product_categories`.\n","operationId":"DistruPublic.V1.ProductSubcategoryController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID of the subcategory to update. Omit to create a new subcategory; when present, the matching subcategory owned by your company is updated and the response status is 200 instead of 201."},"name":{"type":"string","description":"Name of the subcategory. Required on create; on update, omit it to leave the current name unchanged. Must be unique within its parent category, compared case-insensitively (\"Flower\" and \"flower\" are treated as the same, and the colliding create/rename is rejected with a 400). May contain letters, digits, spaces, underscores, and the characters `~ # - $ / | % & ' ( ) .`; any other special character is rejected."},"product_category_id":{"type":"string","description":"ID of the parent product category this subcategory belongs to; the category must be owned by your company. Required on create and fixed thereafter — it cannot be changed on update. On update, omit it or send the same value to keep it; a different value is rejected."}},"required":["name","product_category_id"]}}},"required":true},"responses":{"200":{"description":"The updated product subcategory","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductSubcategoryResponse"}}}},"201":{"description":"The created product subcategory","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductSubcategoryResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert a product subcategory","tags":["ProductSubcategory"]}},"/public/v1/reports/purchases-by-product":{"get":{"description":"Returns one row per purchased product, aggregating every non-draft purchase whose date falls in the reported range. Each row carries the product's total quantity purchased and total purchased amount (each purchase item's quantity times its price, summed and rounded to 2 decimal places), alongside the product's current descriptive attributes (SKU, unit type, category, subcategory, group, vendor, owner, unit cost, sale price, and wholesale price). The attribute columns reflect the product as it stands now, not as it was at purchase time.\n\nThis is a read-only aggregate. It does not touch inventory, compliance (Metrc/BioTrack), or any purchase records — call it to analyze historical purchasing, not to reconcile a single purchase (use the purchases endpoints for that).\n\nDate filtering, and the range echoed back under `meta`, are interpreted in the account's configured time zone. When no date filter is provided, the report defaults to the last 30 days. Draft purchases are always excluded; only official purchases are counted.\n\nEvery value is returned as it appears in the report's CSV export, with numeric cells returned as strings (currency and comma formatting stripped), matching the rest of the API. Any Product custom fields configured for the company are appended as extra columns (their keys vary per company). Report-level information (the resolved human-readable date range and the ordered column definitions) is returned under `meta`.\n\nRequired permission: `reports_permissions_purchases_by_product`.\n","operationId":"DistruPublic.V1.Report.PurchasesByProductController.index","parameters":[{"description":"Restrict the report to purchases dated within this range. Comma-separated pair of         ISO8601 timestamps, `after,before`, interpreted in the account's time zone. Either         side may be left empty for an open-ended bound: `2026-01-01T00:00:00Z,` includes         everything on or after that instant, `,2026-02-01T00:00:00Z` everything up to it.         When omitted entirely, the report covers the last 30 days.","in":"query","name":"order_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Keep only products whose name or SKU matches this text. Omit to include all products.","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Restrict to purchases whose owner (the purchase's assigned sales rep) is one of these         users. Repeat the key per id; values are Distru user ids. Multiple ids widen the match         (a purchase owned by any listed user is included). Omit to include every owner. This         filters the purchases that are aggregated and is unrelated to the product-level `owner`         column in each response row.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to purchases received into these locations. Repeat the key per id; values         are Distru location ids. Multiple ids widen the match. Omit to include every location         on the account, archived locations included, so historical purchases against         since-deleted locations still appear.","in":"query","name":"location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Purchases By Product report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PurchasesByProductReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Purchases By Product report","tags":["PurchasesByProduct"]}},"/public/v1/users/{id}":{"get":{"description":"Get a single team member (user) by their Distru user ID, scoped to your company.\n\nThis is a read-only endpoint and does not change anything in the system. Returns 404 if no user with that ID exists within your company — including a user that belongs to a different company, whose ID is treated as not found rather than leaked across companies. Soft-deleted users are still returned here (unlike the list endpoint, which hides them by default); a soft-deleted user comes back with a non-null `deleted_at`.\n\nLike the list endpoint, this data is eventually consistent and may lag a recent create, update, or delete by up to ~1 second.\n\nRequired permission: `settings_permissions_manage_team`.\n","operationId":"DistruPublic.V1.UserController.show","parameters":[{"description":"The user's Distru ID, as returned in the `id` field of the users list.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single user","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a user","tags":["User"]}},"/public/v1/packages/finish":{"post":{"description":"Finish a list of packages. Sets each package's status to `FINISHED` and records the finish time. Finishing marks a fully depleted package as closed out for compliance; it removes the package from active inventory listings but does not itself deduct any quantity.\n\n`package_ids` is a non-empty list of at most 300 package IDs. Optionally pass `finished_datetime` (ISO 8601) to set the finish time; it defaults to the current time.\n\nEvery package must be finishable, or the whole request fails. A package is rejected when it:\n• still has active quantity — a package must already be at zero quantity before it can be finished\n• is already finished\n• is discontinued, on hold, inactive, or has been transferred out of its license\n• is still syncing with Metrc from an earlier operation\n• has an unresolved Metrc compliance discrepancy\n\nThis operation is atomic: if any package cannot be finished the entire request is rejected and no packages are changed. In that case the error message lists the offending packages by their compliance label; it is a single human-readable string, not a per-package structured error.\n\nMetrc only — not supported for BioTrack packages, and a working Metrc connection is required. Metrc is updated asynchronously: a successful response means the packages were finished in Distru and a pending Metrc activity was created for each one. Distru syncs those activities to Metrc sequentially in the background, so the 200 response is not confirmation that the packages are finished in Metrc. Poll `GET /public/v1/packages` to observe the synced result. Requires a Metrc key configured with permission to finish packages in Metrc.\n\nRequired permission: `products_permissions_adjust_inventory`.\n","operationId":"DistruPublic.V1.PackageController.finish","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinishPackagesRequest"}}},"description":"The packages to finish","required":true},"responses":{"200":{"description":"The finished packages","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Packages"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Finish packages","tags":["Package"]}},"/public/v1/product-subcategories/{id}":{"delete":{"description":"Permanently delete the product subcategory. Unlike product categories (which are soft deleted and recoverable), subcategories are hard deleted: the record is removed and cannot be restored.\n\nDeleting a subcategory does not delete its products. Any product currently assigned to it keeps existing but has its subcategory cleared (it becomes uncategorized at the subcategory level). Any price tiers scoped to this subcategory are cleaned up as part of the delete. Inventory and compliance (Metrc / BioTrack) are unaffected.\n\nReturns 204 on success and 404 if no subcategory with that id exists beneath a live product category owned by your company.\n\nRequired permission: `settings_permissions_product_categories`.\n","operationId":"DistruPublic.V1.ProductSubcategoryController.delete","parameters":[{"description":"The product subcategory id, as returned in the `id` field of any subcategory response.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a product subcategory","tags":["ProductSubcategory"]},"get":{"description":"Fetch a single product subcategory by its id. Returns 404 if no subcategory with that id exists beneath a live product category owned by your company.\n\nRequired permission: `settings_permissions_product_categories`.\n","operationId":"DistruPublic.V1.ProductSubcategoryController.show","parameters":[{"description":"The product subcategory id, as returned in the `id` field of any subcategory response.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single product subcategory","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductSubcategoryResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a product subcategory","tags":["ProductSubcategory"]}},"/public/v1/strains":{"get":{"description":"List the strains in your catalog. A strain is a reference record — a genetic classification you attach to products — so this endpoint is a lookup of that catalog, not of inventory or orders. Use it to resolve a strain's `id` before referencing it elsewhere, or to sync your local copy of the strain list.\n\nResults are scoped to your company and paginated (up to 5000 strains per page). Filter by `name` (case-insensitive substring), `types` (one or more genetic classifications), or by creation/last-modified window. When more than one filter is supplied they combine with AND (a strain must satisfy every filter to be returned). Both datetime filters are inclusive on the bounds you provide. Strains are not soft-deleted or archived, so every strain in your catalog is returned — there are no hidden states to account for.\n\nResponses are eventually consistent: a strain you just created or updated may take up to 1 second to appear here or reflect its latest values.\n\nRequired permission: `settings_permissions_strains`.\n","operationId":"DistruPublic.V1.StrainController.index","parameters":[{"description":"Restrict the result to specific strains by ID (the same ID returned as each strain's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by creation datetime. Accepts a comma-separated `from,to` range (ISO-8601 UTC); either bound is inclusive and either side may be omitted. `2022-07-10T00:00:00Z,` returns strains created on or after that instant; `,2022-07-10T00:00:00Z` returns those created on or before it; `2022-07-01T00:00:00Z,2022-07-31T23:59:59Z` returns a closed window. Omit entirely to apply no creation-time filter.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Case-insensitive substring match on the strain `name` (partial matches count; e.g. `kush` matches `OG Kush`). Send a single value, not a list.","in":"query","name":"name","required":false,"example":"?name=kush","schema":{"type":"string"}},{"description":"Page to return via `page[number]` (1-based). Defaults to page 1 when omitted. Must be greater than 0; a value of 0 or below is rejected with a 400. Each page returns up to 5000 strains. When more results remain, the response's `next_page` holds the ready-to-call URL for the following page (same filters preserved); it is null on the last page.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Restrict the result to strains whose genetic classification (`strain_type`) is any of the given types (SCREAMING_CASE, matches ANY). Repeat the bracketed key once per type. An empty list is treated as no filter; at most 200 types may be given.","in":"query","name":"types","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["INDICA","INDICA_DOMINANT","SATIVA","SATIVA_DOMINANT","HYBRID","HIGH_CBD"],"type":"string"}}},{"description":"Filter by last-modified datetime. Accepts a comma-separated `from,to` range (ISO-8601 UTC); either bound is inclusive and either side may be omitted. `,2022-07-10T00:00:00Z` returns strains last modified on or before that instant; `2022-07-10T00:00:00Z,` returns those modified on or after it. Omit entirely to apply no update-time filter.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=,2022-07-10T00:00:00Z","schema":{"type":"string"}}],"responses":{"200":{"description":"A list of strains","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Strains"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get strains","tags":["Strain"]},"post":{"description":"Create or update a strain through a single endpoint: omit `id` to create a new strain, or pass the `id` of an existing one to update it in place. A strain is a catalog/reference record used to classify products by genetics — it holds no inventory and is not pushed to Metrc or BioTrack, so calling this never moves stock or touches state-compliance traceability. What it does affect is your product catalog and menus: products are grouped under a strain, so renaming a strain or changing its type reflects everywhere that strain is shown, without altering the products themselves. To remove a strain from your catalog entirely, use the delete endpoint instead.\n\nUpdates are sparse. Only the fields you send are changed; any field you omit keeps its current value (sending `id` with just `name` renames the strain and leaves `strain_type` untouched). The whole write is a single all-or-nothing operation — if validation fails, nothing is persisted and a 400 is returned with the field errors.\n\n`name` must be unique within your company; reusing an existing strain's name is rejected with a 400. On success the response is the full created or updated strain, the same shape as [show](#model-strain).\n\nRequired permission: `settings_permissions_strains`.\n","operationId":"DistruPublic.V1.StrainController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The Distru id of the strain to update, as returned by list/show/upsert. Omit to create a new strain instead. When present, the strain must belong to your company or the request returns 404. Not a Metrc or other external identifier."},"name":{"type":"string","description":"Display name of the strain (e.g. `\"Blue Dream\"`). Required when creating (no `id`); optional when updating, where omitting it leaves the current name unchanged. Leading/trailing whitespace is trimmed before it is stored and checked. Must be unique within your company — uniqueness is compared exactly and case-sensitively on the trimmed value, so `\"Blue Dream\"` and `\"blue dream\"` are treated as different names. May not contain special characters. A duplicate or invalid name returns 400."},"strain_type":{"enum":["INDICA","INDICA_DOMINANT","SATIVA","SATIVA_DOMINANT","HYBRID","HIGH_CBD"],"type":"string","description":"The strain's genetic classification, SCREAMING_CASE: `INDICA` or `SATIVA` for a pure variety, `INDICA_DOMINANT` or `SATIVA_DOMINANT` for a leaning hybrid, `HYBRID` for a balanced hybrid, or `HIGH_CBD`. Optional — a strain may have no classification, and omitting this on update leaves the current value unchanged."}}}}}},"responses":{"200":{"description":"The created or updated strain","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StrainResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Create or update a strain","tags":["Strain"]}},"/public/v1/orders/{id}":{"delete":{"description":"Deletes a sales order. This is a hard delete: the order is permanently removed together with its line items, charges, and shipments — it disappears from `GET /public/v1/orders`, `GET /public/v1/orders/{id}` returns 404 for it, and it cannot be recovered through the API. Responds 204 with no body on success, or 404 if no order with that `id` exists in your company (including one that belongs to another company or was already deleted).\n\nSome orders cannot be deleted; each of these is refused with a 400 and nothing is changed:\n\n• An order matched to a compliance transfer (Metrc or BioTrack). Unmatch it first if the transfer allows it; otherwise it can never be deleted through the API.\n• An order with MERGED status. Delete the combined order it was merged into instead — that resets its source orders back to PENDING, after which they can be deleted individually.\n• An order with [returns](#model-return) recorded against it.\n• A historical shared-license order.\n• Any custom validation rule your company has configured for order deletion can also refuse the delete.\n\n[Inventory](#model-inventory) assigned to the order's line items is released back to available inventory, whatever the order's status — including quantities already deducted by a COMPLETED order. Assemblies that were created to produce items for this order and are still PENDING are deleted too, releasing the ingredient inventory they had claimed.\n\nThe order's invoices are hard-deleted in the same call: their payments are removed, credits generated by overpaying them are canceled, and credit balance applied to them is released back onto the credits. If your company is integrated with QuickBooks Online, the linked invoices are scheduled for deletion there too (that sync is eventual — observe it in QuickBooks Online, not in the 204).\n\nOther effects, all in one atomic call: if this is a combined order, the orders that were merged into it are reset to PENDING status; tasks tied to the order are deleted; files attached to the order are detached but kept. Nothing is synced to Metrc or BioTrack, and an order imported from LeafLink is not rejected in LeafLink.\n\nRequired permission: `orders_permissions_delete` (plus access to the order under team restrictions).\n","operationId":"DistruPublic.V1.OrderController.delete","parameters":[{"description":"ID of the order to delete, as returned by the list, fetch, and upsert endpoints. An ID that doesn't exist for your company returns 404.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete an order","tags":["Order"]},"get":{"description":"Get a single [order](#model-order) by ID, including its line items, charges, invoices, returns, delivery/fulfillment details, and custom field values — the same object returned in each entry of GET /public/v1/orders and in the upsert response. An ID that doesn't exist, or belongs to another company, returns a not-found error.\n\nNote: this endpoint returns eventually consistent data — a write can take up to 1 second to be reflected here, so an order you just upserted may briefly read back with its previous values.\n\nRequired permission: `orders_permissions_view`. The authenticated user must also have access to the requested order under their team restrictions, so an order that exists on the company can still return not-found if it falls outside those restrictions.\n","operationId":"DistruPublic.V1.OrderController.show","parameters":[{"description":"Order ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single order","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get an order","tags":["Order"]}},"/public/v1/locations/{id}":{"get":{"description":"Returns a single location by its ID, including its address fields, geo coordinates, compliance `metrc_id`, and its linked [license](#model-license). Use this to resolve a location referenced elsewhere (for example the `location_id` on a [purchase](#model-purchase) or [order](#model-order)) into its full detail.\n\nSeveral fields can be null: `license` / `license_id` when the location has no compliance license; `metrc_id` when it is not synced to a Metrc room (and when present it is a raw Metrc integer id, not a Distru id); and `latitude` / `longitude` when no coordinates have been set.\n\nLookups are scoped to your own company: an ID that does not exist, or that belongs to another company, returns `404` — the two cases are indistinguishable. A soft-deleted location is still returned here (with a non-null `deleted_at`); it is only hidden from the list endpoint unless you opt in via its `deleted` filter.\n\nRequired permission: `companies_permissions_view`.\n","operationId":"DistruPublic.V1.LocationController.show","parameters":[{"description":"The location's Distru ID, as returned in the `id` field of a location response. IDs from external systems such as Metrc are not accepted here.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single location","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LocationResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a location","tags":["Location"]}},"/public/v1/product-categories/{id}":{"delete":{"description":"Soft-delete the product category: it stops appearing in list and fetch responses, while the row\nis retained. Returns 404 if the category does not exist for your company or is already deleted.\n\nDeleting also removes this category — and each of its subcategories — from any\n[price tier](#model-pricetier) filters that referenced them, so those price tiers stop scoping by\nthe removed category. The deleted category's name stays reserved: it cannot be reused by a new\ncategory until the deleted one is renamed.\n\nRequired permission: `settings_permissions_product_categories`.\n","operationId":"DistruPublic.V1.ProductCategoryController.delete","parameters":[{"description":"ID of the product category to delete, as returned in a category's `id` field.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a product category","tags":["ProductCategory"]},"get":{"description":"Fetch a single product category by ID, including its subcategories. Scoped to your company:\nreturns 404 if no category with that ID exists for your company or if it has been soft-deleted.\n\nRequired permission: `settings_permissions_product_categories`.\n","operationId":"DistruPublic.V1.ProductCategoryController.show","parameters":[{"description":"ID of the product category, as returned in a category's `id` field.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single product category","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a product category","tags":["ProductCategory"]}},"/public/v1/price-tiers/{id}":{"delete":{"description":"Soft-deletes the tier. It stops applying to new orders immediately; order items already priced\nby it keep the frozen version they reference, so their prices don't change. Its removal from\nstatewide marketplace price discovery propagates asynchronously, so a 204 confirms the tier was\ndeleted, not that every buyer-facing surface has dropped it yet. Deleting an unknown or\nalready-deleted tier returns 404.\n\nRequired permission: `settings_permissions_price_tiers`.\n","operationId":"DistruPublic.V1.PriceTierController.delete","parameters":[{"description":"Price tier ID, as returned by the list and upsert endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a price tier","tags":["PriceTier"]},"get":{"description":"Get a single price tier by id, with its conditions resolved into the referenced products,\ncategories, customers, and menus. Returns 404 for an unknown or soft-deleted tier.\n\nRequired permission: `settings_permissions_price_tiers`.\n","operationId":"DistruPublic.V1.PriceTierController.show","parameters":[{"description":"Price tier ID, as returned by the list and upsert endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single price tier","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PriceTierResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a price tier","tags":["PriceTier"]}},"/public/v1/drivers/{id}":{"delete":{"description":"Deletes a driver. This is a soft delete: the driver stops appearing in the list and fetch\nendpoints and can no longer be updated, but the record is retained. Returns 404 if no matching\ndriver exists in your company or it was already deleted. Responds 204 with no body on success.\n\nDeleting does not remove the driver from transfers, transfer templates, or manifests that\nalready reference it. For a BIOTRACK company, a successful delete queues an asynchronous removal\nin BioTrack — a 204 means the driver was removed in Distru, not that BioTrack has processed the\nremoval. The delete is rejected up front (and nothing is removed) when your company or user\nBioTrack credentials are missing or lack permission for this operation; once queued, a later\nrejection by BioTrack does not restore the Distru record. METRC companies have no such sync.\n\nRequired permission: `settings_permissions_drivers`.\n","operationId":"DistruPublic.V1.DriverController.delete","parameters":[{"description":"ID of the driver to delete, as returned by the list, fetch, and upsert endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Delete a driver","tags":["Driver"]},"get":{"description":"Fetches a single driver by its ID, scoped to the authenticated company. Returns 404 if no\ndriver with that ID exists in your company or if it has been soft-deleted.\n\nWhich contact fields are populated depends on the company's compliance type: METRC drivers\ncarry `phone_number` and `occupational_license_number` (and leave `birth_date`, `email`,\n`us_state`, `hire_date` null), while BIOTRACK drivers carry `birth_date`, `email`, `us_state`\nand `hire_date` (and leave `phone_number` and `occupational_license_number` null).\n\nRequired permission: `settings_permissions_drivers`.\n","operationId":"DistruPublic.V1.DriverController.show","parameters":[{"description":"ID of the driver to fetch, as returned by the list and upsert endpoints.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A single driver","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DriverResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get a driver","tags":["Driver"]}},"/public/v1/inventory":{"get":{"description":"Get on-hand inventory levels rolled up by a caller-chosen set of attributes. Each returned row is one group, and the `groupings` you request decides both how quantities are aggregated and which id fields (`product_id`, `location_id`, `batch_number`) appear on each row. Only active, positive-quantity, sellable stock is counted — inventory that has been consumed, voided, transferred out, or fully sold is excluded, as is any stock whose product has been deactivated. Results are always scoped to the company that owns the API key.\n\nResults can be narrowed to specific products (`product_ids`), locations (`location_ids`), and batches (`batch_ids`), or by attributes of the underlying product — category, subcategory, group, strain, brand, vendor, tag, and SKU. All filters are AND-ed together; within a single multi-valued filter the values are OR-ed.\n\nGroups with 0 active and 0 available quantity are omitted from the response. Groups are sorted ascending by the ids of the attributes they are grouped by, in the order those attributes appear in `groupings`.\n\nGrouping by `BATCH_NUMBER` behaves specially: \n• Products that track inventory at the product level (not by batch) are excluded entirely — they only surface when you do not group by `BATCH_NUMBER`. \n• `reserved` cannot be determined at the batch/package granularity, so it is always returned as `\"0\"` and `available` equals `active` for every row.\n\nThis is a read-only endpoint. It returns eventually consistent data: a change to inventory (a sale, a receipt, an adjustment) can take up to roughly 1 second to be reflected here, so a value read immediately after a write may still be stale.\n\nRequired permission: `products_permissions_view`.\n","operationId":"DistruPublic.V1.InventoryController.index","parameters":[{"description":"Required. The attributes to roll inventory up by, in SCREAMING_CASE. Accepted values are `PRODUCT`, `LOCATION` and `BATCH_NUMBER`. `PRODUCT` must always be included; a request without it is rejected. The order you list attributes in is the order rows are sorted by (ascending on each attribute's id). Each requested attribute adds its id field to every returned row: `PRODUCT` → `product_id`, `LOCATION` → `location_id`, `BATCH_NUMBER` → `batch_number`; attributes you omit are not broken out and their id field is absent from the rows. Including `BATCH_NUMBER` also drops product-tracked products from the results and forces `reserved` to `\"0\"` (see the endpoint description).","in":"query","name":"groupings","required":true,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PRODUCT","LOCATION","BATCH_NUMBER"],"type":"string"}}},{"description":"The 1-based page number to fetch, passed as `page[number]`. Must be a positive integer; defaults to `1` when omitted. Page size is fixed by the server and is not caller-configurable — follow the `next_page` URL in the response envelope to page through all groups rather than incrementing this yourself.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Restrict the results to these products, each identified by its Distru product id. Omit to include every product in the company. Ids that do not belong to the company are silently ignored. When `BATCH_NUMBER` is in `groupings`, product-tracked products among these ids are still excluded, just as they are for an unfiltered request (see the `groupings` param).","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the results to inventory held at these locations, each identified by its Distru location id. Omit to include every location. Applies whether or not `LOCATION` is in `groupings`; ids that do not belong to the company are silently ignored.","in":"query","name":"location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the results to inventory from these batches, each identified by its Distru batch id. Omit to include every batch. Applies whether or not `BATCH_NUMBER` is in `groupings`; ids that do not belong to the company are silently ignored.","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the results to inventory of products in any of these product categories, each identified by its Distru product category id. Multiple ids are OR-ed. Omit to include every category; ids that do not belong to the company match nothing. At most 200 ids.","in":"query","name":"product_category_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the results to inventory of products in any of these product subcategories, each identified by its Distru product subcategory id. Multiple ids are OR-ed. Omit to include every subcategory; ids that do not belong to the company match nothing. At most 200 ids.","in":"query","name":"product_subcategory_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the results to inventory of products in any of these product groups, each identified by its Distru product group id. Multiple ids are OR-ed. Omit to include every group; ids that do not belong to the company match nothing. At most 200 ids.","in":"query","name":"product_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the results to inventory of products with any of these strains, each identified by its Distru strain id. Multiple ids are OR-ed. Omit to include every strain; ids that do not belong to the company match nothing. At most 200 ids.","in":"query","name":"product_strain_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the results to inventory of products with any of these brands, each identified by its Distru brand id (a company-relationship id, not a raw company id). Multiple ids are OR-ed. Omit to include every brand; ids that do not belong to the company match nothing. At most 200 ids.","in":"query","name":"product_brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the results to inventory of products supplied by any of these vendors, each identified by its Distru vendor id (a company-relationship id, not a raw company id). Multiple ids are OR-ed. Omit to include every vendor; ids that do not belong to the company match nothing. At most 200 ids.","in":"query","name":"product_vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the results to inventory of products carrying any of these tags, each identified by its Distru tag id. Multiple ids are OR-ed. Omit to include products regardless of tags; ids that do not belong to the company match nothing. At most 200 ids.","in":"query","name":"product_tag_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict the results to inventory of products whose SKU exactly matches (case-insensitive) any value in the list. Multiple values are OR-ed. Omit to include every SKU. At most 200 values.","in":"query","name":"product_skus","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"A list of active and available quantity for each group","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Inventories"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get inventory levels","tags":["Inventory"]}},"/public/v1/reports/sales-order-item-history":{"get":{"description":"Returns one row per sales order line item, joining each item to its order, product, brand, vendor, and customer. Each row carries the order's dates and status, the product identity (name, SKU, UPC, category, default costs/prices), and the line item's own quantity, returned quantity, and price. This is a read-only reporting endpoint; it never changes orders, inventory, or compliance state.\n\nScoping and date default:\n• Rows are always scoped to the API key's company and to the orders the key's user is permitted to see.\n• When `order_datetime` is omitted the report covers the last 30 days by order date, evaluated in the company's timezone. This 30-day default applies only to `order_datetime` — the other date filters (`delivery_datetime`, `due_datetime`, `created_datetime`, `updated_datetime`) add no default and simply go unfiltered when omitted.\n• The resolved date range (human-readable) and the exact column set are returned under `meta`, so read `meta.columns` to discover which columns a given company's response actually contains before indexing into rows by key.\n\nColumn set varies by company:\n• Companies on a compliance integration (Metrc or BioTrack) additionally get package columns (label, batch number, expiration/harvest date), potency columns (THC/CBD % and mg per g/mL), the compliance manifest number, and the shipped-from license. Companies with no compliance integration do not receive these columns at all.\n• The trade-sample-package column is Metrc-only.\n• Any Order custom fields configured for the company are appended as extra columns keyed by the field's label.\n\nValue formatting (rows mirror the CSV export):\n• Dates come as display-formatted strings, each in two variants — one in the company's timezone (e.g. `order_date`) and one in UTC (e.g. `order_date_utc`).\n• Money and quantity cells are returned as strings (the currency/percent formatting of the CSV is stripped, e.g. `\"1234.56\"`) so they match the rest of the API. Identifier-like cells whose number has a significant leading zero (e.g. an order number `\"0042\"`) keep their display string so the zero is not lost.\n• `status` and other enum-bearing cells are returned as SCREAMING_CASE tokens (matching the rest of the public API), not the internal human-readable labels.\n• An empty cell is returned as an empty string or null depending on the column; treat any non-core column as possibly blank.\n\nOrdering and result size:\n• Rows come back newest-first by order date (descending). The endpoint exposes no sort or pagination controls, so a matching filter returns every line item in one response — there is no `next_page` cursor. Narrow the date range or other filters to bound the result size of a large history.\n\nRequired permission: `reports_permissions_sales_order_item_history` — the same permission that gates the Sales Order Item History report in the app.\n","operationId":"DistruPublic.V1.Report.SalesOrderItemHistoryController.index","parameters":[{"description":"Keep only line items whose order is in one of the given statuses. Repeat the key to pass several; multiple values are OR'd (an order matches if its status is any of them). SCREAMING_CASE enum. Omit to include every status.","in":"query","name":"status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"}}},{"description":"Keep only line items whose order has one of the given invoice payment statuses. Repeat the key for several values (OR'd). SCREAMING_CASE enum. Omit to include every payment status.","in":"query","name":"payment_status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["NOT_PAID","PARTIALLY_PAID","FULLY_PAID","OVER_PAID"],"type":"string"}}},{"description":"Keep only line items whose order was created through one of the given sources. Repeat the key for several values (OR'd). SCREAMING_CASE enum: LEAFLINK, EXTERNAL_BUYER, INTERNAL_USER, API. Omit to include every source.","in":"query","name":"order_source","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["LEAFLINK","EXTERNAL_BUYER","INTERNAL_USER","API"],"type":"string"}}},{"description":"Filter line items by whether they are samples. ONLY returns only sample line items; EXCLUDE drops all sample line items. Omit to include both sample and non-sample items.","in":"query","name":"sample","required":false,"example":"ONLY","schema":{"type":"string","enum":["ONLY","EXCLUDE"]}},{"description":"Filter line items by whether their package is a Metrc trade sample. ONLY returns only trade-sample-package items; EXCLUDE drops them. Omit to include both. Meaningful only for companies on Metrc — the trade-sample-package column exists only there.","in":"query","name":"trade_sample_packages","required":false,"example":"EXCLUDE","schema":{"type":"string","enum":["ONLY","EXCLUDE"]}},{"description":"Filter by order date, as a comma-separated ISO8601 range `start,end` interpreted in the company's timezone. Either side may be left blank for an open-ended range (`2026-01-01T00:00:00Z,` = on or after that instant; `,2026-02-01T00:00:00Z` = up to it). When this param is omitted entirely the report falls back to the last 30 days by order date; this default applies to `order_datetime` only.","in":"query","name":"order_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the order's delivery date, as a comma-separated ISO8601 range `start,end` (company timezone). Either side may be blank for an open-ended range. Omit to leave delivery date unfiltered — unlike `order_datetime`, this filter has no 30-day default.","in":"query","name":"delivery_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the order's due date, as a comma-separated ISO8601 range `start,end` (company timezone). Either side may be blank for an open-ended range. Omit to leave due date unfiltered.","in":"query","name":"due_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by when the order was created, as a comma-separated ISO8601 range `start,end` (company timezone). Either side may be blank for an open-ended range. Omit to leave creation date unfiltered.","in":"query","name":"created_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by when the order was last modified, as a comma-separated ISO8601 range `start,end` (company timezone). Either side may be blank for an open-ended range. Omit to leave last-modified date unfiltered.","in":"query","name":"updated_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Keep only line items whose order total falls within a comma-separated `min,max` range. Either side may be left blank for an open-ended bound (`100,` = 100 or more; `,500` = 500 or less). Bounds are inclusive order totals in the company's currency.","in":"query","name":"total","required":false,"example":"100,500","schema":{"type":"string"}},{"description":"When `true`, keep only orders matched to a compliance transfer (Metrc or BioTrack); when `false`, keep only orders not matched to one. Omit to include both.","in":"query","name":"matched_with_compliance_transfer","required":false,"example":true,"schema":{"type":"boolean"}},{"description":"Free-text search across order number, customer name, and LeafLink short ID; matches line items whose order matches on any of the three (substring, case-insensitive). Omit to skip text search.","in":"query","name":"search","required":false,"example":"SO-1001","schema":{"type":"string"}},{"description":"Keep only line items whose customer is one of the given customers, by Distru customer (company relationship) ID. Repeat the key for several (OR'd). IDs are the id strings returned elsewhere in the API.","in":"query","name":"company_relationship_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only line items whose customer belongs to one of the given customer groups, by group ID. Repeat the key for several (OR'd).","in":"query","name":"company_relationship_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only line items whose order ships from one of the given licenses, by license ID. Repeat the key for several (OR'd).","in":"query","name":"shipped_from_license_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only line items whose order was placed against one of the given menus, by menu ID. Repeat the key for several (OR'd).","in":"query","name":"menu_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only line items whose product belongs to one of the given brands, by brand ID. Repeat the key for several (OR'd).","in":"query","name":"brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only line items whose order owner (the assigned user) is one of the given users, by user ID. Repeat the key for several (OR'd).","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only line items whose order was created by one of the given users, by user ID. Repeat the key for several (OR'd).","in":"query","name":"creator_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only line items sourced from one of the given batches, by batch ID. Repeat the key for several (OR'd).","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only line items for one of the given products, by product ID. Repeat the key for several (OR'd).","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Keep only line items whose product belongs to one of the given product groups, by product group ID. Repeat the key for several (OR'd).","in":"query","name":"product_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Sales Order Item History report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SalesOrderItemHistoryReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Sales Order Item History report","tags":["SalesOrderItemHistory"]}},"/public/v1/reports/invoice-history":{"get":{"description":"Read-only report: returns one row per invoice with its dates, sales order, customer, payment status, and monetary totals (paid, outstanding, line item subtotal, taxes, charges, discounts, and grand total) along with the per-tax, per-charge, and per-discount summary strings. This endpoint only reads — it never mutates invoices, inventory, payments, or compliance state.\n\nScope and date defaulting:\n\n• Results are scoped to the company that owns the API key, and further to the invoices the key's user is allowed to view.\n• Filtering on `invoice_datetime` is optional; when you omit it the report covers the last 30 days (from the start of the day 30 days ago through the end of today, resolved in the API key user's timezone). No other filter has a default. `due_datetime` has no implicit range.\n• The resolved invoice-date window (whether you passed it or it defaulted) is echoed back as a human-readable string in `meta.date_range`.\n\nRow shape and value formatting:\n\n• Rows always come back ordered by invoice number, descending. This endpoint exposes no sort parameter — the order is fixed regardless of which filters (including `search`) you pass.\n• Monetary cells (`paid`, `outstanding`, `line_item_subtotal`, `total_taxes`, `total_charges`, `total_discounts`, `total`) are returned as strings (currency and comma formatting stripped) so they match the rest of the API, already coalesced to `\"0\"` when the invoice has no matching payments/taxes/charges/discounts — they are never null.\n• The summary cells (`tax_summary`, `charge_summary`, `discount_summary`) are human-readable breakdown strings and are null when the invoice has no taxes, no positive charges, or no negative charges (discounts) respectively.\n• `status` is a SCREAMING_CASE payment-status token (see the values below).\n\nExtra columns:\n\n• Companies on a compliance integration get two additional columns — the transfer manifest number and the shipped-from license number. They are labelled by system (Metrc vs BioTrack) but carry the same data.\n• Any Invoice custom fields configured for the company are appended as additional columns, keyed by a slug of the field label.\n\nBecause the set of columns varies by company (compliance integration and custom fields), read `meta.columns` to discover the exact keys present in each `data` row rather than hard-coding them.\n\nRequired permission: view the invoice history report.\n","operationId":"DistruPublic.V1.Report.InvoiceHistoryController.index","parameters":[{"description":"Filter by invoice payment status. Repeat the key to pass several values; an invoice matches if its status is any of them (OR). Values are SCREAMING_CASE: NOT_PAID, PARTIALLY_PAID, FULLY_PAID, OVER_PAID (OVER_PAID only occurs on legacy invoices). Omit to include every status.","in":"query","name":"status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["NOT_PAID","PARTIALLY_PAID","FULLY_PAID","OVER_PAID"],"type":"string"}}},{"description":"Filter by the status of the sales order each invoice belongs to. Repeat the key to pass several values; matches any (OR). Values are SCREAMING_CASE: PENDING, PROCESSING, READY_TO_SHIP, DELIVERING, DELIVERED, COMPLETED, CANCELED. Omit to include orders in any status.","in":"query","name":"order_status","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"}}},{"description":"Filter by invoice date, as a comma-separated ISO8601 datetime range `from,to`. Either side may be left empty for an open-ended range (`2026-01-01T00:00:00Z,` = on or after that instant; `,2026-02-01T00:00:00Z` = on or before it); both sides empty is rejected. When this filter is omitted entirely the report defaults to the last 30 days, and the resolved window is reflected in `meta.date_range`.","in":"query","name":"invoice_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by invoice due date, as a comma-separated ISO8601 datetime range `from,to`. Either side may be empty for an open-ended range. Unlike `invoice_datetime` this has no default — omit it to apply no due-date filter.","in":"query","name":"due_datetime","required":false,"example":"2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by the invoice grand total, as a comma-separated `min,max` numeric range. Bounds are inclusive; omit a side to leave it unbounded.","in":"query","name":"total","required":false,"example":"100,500","schema":{"type":"string"}},{"description":"Filter by the amount paid on the invoice, as a comma-separated `min,max` numeric range. Bounds are inclusive; omit a side to leave it unbounded.","in":"query","name":"paid","required":false,"example":"0,100","schema":{"type":"string"}},{"description":"Case-insensitive substring match on the invoice number. Combines with the other filters (AND); does not change the result ordering, which is always invoice number descending. Omit to skip the invoice-number search.","in":"query","name":"search","required":false,"example":"INV-1024","schema":{"type":"string"}},{"description":"Filter by customer. Each value is the Distru id of the customer's company relationship (the same id returned as the customer reference elsewhere in the API). Repeat the key to pass several; matches any (OR).","in":"query","name":"company_relationship_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by the license the order shipped from. Each value is a Distru license id. Repeat the key to pass several; matches any (OR).","in":"query","name":"shipped_from_license_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices whose line items reference any of these batches. Each value is a Distru batch id. Repeat the key to pass several; matches any (OR).","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to invoices whose line items reference any of these products. Each value is a Distru product id. Repeat the key to pass several; matches any (OR).","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"The Invoice History report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/InvoiceHistoryReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Invoice History report","tags":["InvoiceHistory"]}},"/public/v1/batches/{id}/primary-test-result/pdf":{"get":{"description":"Download the Certificate of Analysis (COA) PDF for a single batch, built from the batch's designated primary test result (the lab result flagged as primary among the batch's test results). Returns 404 when the batch has no primary test result, or that result has no attached file. This reads a document already stored in Distru; it does not generate, re-run, or fetch anything from a lab or a state traceability system.\n\nThe same permissions that govern downloading this PDF in the Distru web app apply to this endpoint.\n\nBy default the response body is the binary PDF with content type `application/pdf`. Pass `?format=url` to instead receive a JSON envelope `{\"data\": {\"url\": ..., \"expires_datetime\": ...}}` containing a temporary signed download URL.\n\nAll PDF download endpoints share a single, combined rate limit for your account: 20 requests per minute and 1000 per day in total across every PDF endpoint (not per endpoint). Requests over the limit return 429 with a Retry-After header, and only successful downloads count toward it.\n","operationId":"DistruPublic.V1.BatchController.download_primary_test_result_pdf","parameters":[{"description":"The Distru batch ID whose COA to download.","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Omit to receive the PDF bytes directly (`Content-Type: application/pdf`). Pass `url` to instead receive a JSON body containing a short-lived signed download URL for the same file (see the [PdfDownloadUrl](#model-pdfdownloadurl) schema).","in":"query","name":"format","required":false,"schema":{"type":"string","enum":["url"]}}],"responses":{"200":{"description":"By default the binary PDF file (Content-Type: application/pdf). When ?format=url is passed, a JSON object with a temporary signed download URL (see PdfDownloadUrl schema).","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/PdfDownloadUrl"}}}},"400":{"description":"Invalid parameters","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No test results are attached to this batch","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests — the account's shared PDF download rate limit (20/minute, 1000/day across all PDF endpoints) was exceeded. Retry after the period in the Retry-After header.","content":{"application/pdf,application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Download a batch's Certificate of Analysis (COA) PDF","tags":["Batch"]}},"/public/v1/orders":{"get":{"description":"List sales orders, most recent order date first, filtered by the query parameters below. Each entry is the same full [order](#model-order) object returned by GET /public/v1/orders/{id}, including its line items, charges, invoices, and returns.\n\nResults are paginated: the response is a `data` array plus a `next_page` URL. Follow `next_page` to walk subsequent pages; a null `next_page` means the last page was reached. All datetime filters accept an inclusive range and combine with AND (an order must satisfy every filter given).\n\nRequired permission: `orders_permissions_view`. Results are further limited to the orders the authenticated user can see under their team restrictions, so this may return fewer orders than exist on the company.\n","operationId":"DistruPublic.V1.OrderController.index","parameters":[{"description":"Restrict the result to specific orders by ID (the same ID returned as each order's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by the delivery datetime (an order's `delivery_datetime`). Inclusive ISO8601 range written as `after,before`; either side may be omitted. `2022-07-10T00:00:00Z,` keeps orders delivered on or after that instant, `,2022-07-10T00:00:00Z` keeps those on or before it, and supplying both bounds keeps orders in between. Orders with no delivery datetime are excluded whenever this filter is present.","in":"query","name":"delivery_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Filter by the due datetime — when the customer is expected to pay (an order's `due_datetime`). Inclusive ISO8601 range `after,before`; either side may be omitted (`,2022-07-10T00:00:00Z` keeps orders due on or before that instant).","in":"query","name":"due_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by when the order was created in Distru (its `inserted_datetime`). Inclusive ISO8601 range `after,before`; either side may be omitted.","in":"query","name":"inserted_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Filter by the order datetime — when the order was placed (its `order_datetime`). Inclusive ISO8601 range `after,before`; either side may be omitted. Results are always sorted by this field, newest first.","in":"query","name":"order_datetime","required":false,"example":"2022-07-10T00:00:00Z,2022-07-11T00:00:00Z","schema":{"type":"string"}},{"description":"Page to fetch, 1-based, as `page[number]=N`. Defaults to 1 when omitted; must be greater than 0. Page size is fixed (500 orders per page) — walk pages by following the response's `next_page` URL rather than incrementing this yourself.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter by lifecycle status; repeat the key to pass several and orders in ANY of the given statuses are returned. SCREAMING_CASE, one of: PENDING, PROCESSING, READY_TO_SHIP, DELIVERING, DELIVERED, COMPLETED, CANCELED. See the `status` field on the [order](#model-order) for what each value means. At most 200 statuses may be given.","in":"query","name":"statuses","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"}}},{"description":"Filter by when the order was last modified in Distru (its `updated_datetime`). Inclusive ISO8601 range `after,before`; either side may be omitted. Useful for polling only the orders that changed since your last sync.","in":"query","name":"updated_datetime","required":false,"example":",2022-07-10T00:00:00Z","schema":{"type":"string"}},{"description":"Filter to orders whose buyer (customer) is any of these companies. Pass company relationship IDs — the same id returned as each order's `company.id` and by GET /public/v1/companies. Repeat the bracketed key once per ID. Unknown IDs (including ones that don't belong to your company) simply match nothing; an empty list is treated as no filter. At most 200 IDs.","in":"query","name":"company_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders placed by any of these buyer companies — the DistruCommerce buyer that placed the order (each order's `buyer_company.id`), set only for menu orders placed by the buyer themselves. Distinct from `company_ids`, which is the order's customer/company relationship. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"buyer_company_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders owned by any of these Distru users (each order's `owner.id`). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders whose top-level location is any of these Distru locations (each order's `location.id`). This is the order's own location, not the per-line-item fulfillment location. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders whose billing location is any of these Distru locations (each order's `billing_location.id`). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"billing_location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders whose shipping location is any of these Distru locations (each order's `shipping_location.id`). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"shipping_location_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders placed through any of these DistruCommerce menus (each order's `menu.id`). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"menu_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders whose order number contains this text, case-insensitively (substring match). For an exact match on one or more full order numbers, use `order_numbers` instead.","in":"query","name":"order_number","required":false,"example":"?order_number=SO-10","schema":{"type":"string"}},{"description":"Filter to orders whose order number exactly matches any of these values, case-insensitively. Repeat the bracketed key once per value; an empty list is treated as no filter. At most 200 values. Use `order_number` for a substring search instead.","in":"query","name":"order_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders synced from any of these LeafLink orders, matching LeafLink's own order identifier (each order's `leaflink_id`). Exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values.","in":"query","name":"leaflink_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders associated with any of these Metrc transfers, matching Metrc's own integer transfer id (each order's `metrc_transfer_id`). Repeat the bracketed key per value; empty list is no filter. At most 200 values.","in":"query","name":"metrc_transfer_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"integer"}}},{"description":"Filter to orders associated with any of these BioTrack manifests (each order's `biotrack_id`). Exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values.","in":"query","name":"biotrack_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by when the order was marked Delivered or Completed (an order's `delivered_datetime`). Inclusive ISO8601 range `after,before`; either side may be omitted. Orders that never reached Delivered or Completed have no delivered datetime and are excluded whenever this filter is present.","in":"query","name":"delivered_datetime","required":false,"example":"2022-07-10T00:00:00Z,","schema":{"type":"string"}},{"description":"Filter to orders whose customer belongs to any of these company relationship groups. Pass company relationship group IDs (the same id returned by GET /public/v1/company-relationship-groups). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs.","in":"query","name":"company_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by payment status, derived from the order's invoices and their payments against the order total (net of returns). Repeat the key to pass several; orders in ANY of the given statuses are returned. SCREAMING_CASE, one of:\n• NOT_PAID — nothing has been paid.\n• PARTIALLY_PAID — some but not the full amount has been paid.\n• FULLY_PAID — paid in full.\n• OVER_PAID — paid more than the order total.\nAt most 200 values.","in":"query","name":"payment_statuses","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"enum":["NOT_PAID","PARTIALLY_PAID","FULLY_PAID","OVER_PAID"],"type":"string"}}},{"description":"Filter by the order `total` (the raw order total including line items, charges, discounts, and taxes — the same value returned as each order's `total`). Inclusive range written as `min,max`; either side may be omitted. `100,` keeps orders totaling 100 or more, `,500` keeps those totaling 500 or less, and `100,500` keeps those in between.","in":"query","name":"total","required":false,"example":"100,500","schema":{"type":"string"}},{"description":"Filter to orders that contain a line item drawn from any of these batches (matching an order item's batch). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item drawn from any of these packages (matching an order item's package). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"package_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose package carries any of these compliance labels (the package's Metrc/BioTrack label). Case-sensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"package_compliance_labels","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose package has any of these batch numbers (the batch number stored on the package). Case-sensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"package_batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose batch has any of these batch numbers (matching the order item's batch). Case-sensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"batch_batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item of any of these products (matching an order item's product). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose product belongs to any of these product categories. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_category_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose product belongs to any of these product subcategories. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_subcategory_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose product belongs to any of these product groups. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose product has any of these brands. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose product has any of these vendors (the product's supplier company relationship). Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose product has any of these strains. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_strain_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose product carries any of these tags. Repeat the bracketed key per ID; unknown IDs match nothing; empty list is no filter. At most 200 IDs. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_tag_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter to orders that contain a line item whose product has any of these SKUs. Case-insensitive exact match. Repeat the bracketed key per value; empty list is no filter. At most 200 values. When combined with the other item and product filters, a single line item must satisfy all of them together (e.g. the same line is both in the given batch and of the given product).","in":"query","name":"product_skus","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by custom field values, as `custom_data[{id}]=value` where `{id}` is a custom field's numeric id. Repeat with different ids to filter on several fields at once; a record must match every one (AND). Matching is case-sensitive exact against the value stored on the record. The id must be a filterable custom field defined on this entity — use `GET /public/v1/custom-fields?parent_object=order` to list the ids, their types, and which are filterable. A non-numeric id, an id not defined on this entity, or an id that isn't filterable returns a 400.","in":"query","name":"custom_data","required":false,"schema":{"type":"object"}}],"responses":{"200":{"description":"A list of orders","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Orders"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get orders","tags":["Order"]},"post":{"description":"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.\n\nUpdates 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. 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.\n\nSetting `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.\n\nCompliance: 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.\n\nInvoicing: 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.\n\nRequired permission: `orders_permissions_create` to create, `orders_permissions_edit` (plus access to the order under team restrictions) to update.\n","operationId":"DistruPublic.V1.OrderController.upsert","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"biotrack_id":{"type":"string","description":"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":{"enum":["CASH","CREDIT","DEBIT","COD","ACH_TRANSFER","CHEQUE","OTHER"],"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"ISO8601 datetime the order was / will be delivered. Optional; null when the order has no delivery datetime set."},"due_datetime":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"boolean","description":"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":{"type":"boolean","description":"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":{"type":"string","description":"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":{"type":"string","description":"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":{"type":"string","description":"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":{"items":{"$ref":"#/components/schemas/OrderChargeRequest"},"type":"array","description":"The extra lines added on top of the order's items — fees, discounts, or taxes. Omit this field to leave the order's existing charges untouched. Send it and it fully replaces the charge set: an existing charge whose `id` you omit is deleted, an entry whose `id` matches an existing charge updates it (fields you omit on the entry keep their current value), and an entry with a new or omitted `id` is added. Each entry follows the [OrderChargeRequest](#model-orderchargerequest) shape."},"items":{"items":{"$ref":"#/components/schemas/OrderItemRequest"},"type":"array","description":"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."},"internal_notes":{"type":"string","description":"Free-form notes visible only inside Distru; never shown to the customer. Use `external_notes` for a customer-facing message."},"metrc_transfer_id":{"type":"integer","description":"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":{"type":"string","description":"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":{"type":"string","description":"The billing location's ID (a Distru location ID). Optional."},"shipping_location_id":{"type":"string","description":"The shipping location's ID (a Distru location ID). Optional."},"owner_id":{"type":"string","description":"The Distru user that owns this order, as a user ID (the same id in the response's `owner.id`). Optional."},"status":{"enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string","description":"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":{"type":"object","description":"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."}}}}}},"responses":{"200":{"description":"A single order","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert an order","tags":["Order"]}},"/public/v1/assemblies":{"get":{"description":"Get a page of assemblies, each with its full outputs, inputs, and costs, ordered oldest to newest\nby their last modified date. All filters below are ANDed together, and results are paginated.\n\nNested `input_*` and `output_*` filters match against an assembly's inputs and outputs. When you\ncombine several filters for the same side, a single line must satisfy all of them: e.g.\n`input_product_ids` + `input_batch_ids` keeps only assemblies that have one input matching both.\nInput and output filters combine across sides too — an assembly must have a matching input AND a\nmatching output to be returned.\n\nThis endpoint returns eventually consistent data, with changes taking up to 1 second to propagate\nin responses — an assembly you just created or updated (including via the upsert or split-package\nendpoints) may not appear here for up to a second.\n\nRequired permission: `assemblies_permissions_view`. Results are scoped to your company and further\nfiltered to only the assemblies the authenticated user's team restrictions allow them to see, so a\nrestricted key may see fewer rows than exist.\n","operationId":"DistruPublic.V1.AssemblyController.index","parameters":[{"description":"Restrict the result to specific assemblies by ID (the same ID returned as each assembly's `id`). Repeat the bracketed key once per ID. Unknown IDs simply match nothing; an empty list is treated as no filter. At most 200 IDs may be given.","in":"query","name":"ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Filter by the datetime an assembly was completed, as an inclusive `after,before` range of ISO 8601 timestamps. Either side may be left blank: `2022-07-10T00:00:00Z,` returns everything completed on or after that instant, `,2022-07-10T00:00:00Z` everything completed on or before it, and `2022-07-01T00:00:00Z,2022-07-31T00:00:00Z` the closed interval between the two. Only COMPLETED assemblies have a completion datetime, so any value here implicitly excludes PENDING assemblies.","in":"query","name":"completion_datetime","required":false,"example":"?completion_datetime=2022-07-01T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Page number, 1-based. Omit to get the first page. Use the `next_page` URL in the response envelope to walk subsequent pages; a page past the end returns an empty `data` array and a null `next_page`.","in":"query","name":"page","required":false,"schema":{"type":"number"}},{"description":"Filter to assemblies that originated a given way, matching exactly one value. • `MANUALLY_CREATED` — built by hand in Distru or via the upsert endpoint (the only source this API can modify). • `SALES_ORDER` — generated to fulfill a sales order. • `SPLIT_PACKAGE` — produced by splitting a Metrc package (including via the split-package endpoint). • `LAB_TESTING` — created to pull a test sample. Note: this attribute is unreliable for assemblies created before September 2020, which may report `MANUALLY_CREATED` regardless of their true origin.","in":"query","name":"creation_source","required":false,"schema":{"type":"string","enum":["MANUALLY_CREATED","SALES_ORDER","SPLIT_PACKAGE","LAB_TESTING"]}},{"description":"Filter to assemblies tied to the license with this exact license number within your company (exact match, not a substring). Assemblies on non-compliance (NONE) licenses have no license number and are excluded when this is set.","in":"query","name":"license_number","required":false,"example":"?license_number=C11-0000123-LIC","schema":{"type":"string"}},{"description":"Filter by assembly status, matching exactly one value. • `PENDING` — still in progress; ingredient inventory is claimed but not yet consumed and the assembly can still be edited or deleted. • `COMPLETED` — finished; inputs are consumed and outputs produced into inventory, and the assembly is frozen.","in":"query","name":"status","required":false,"schema":{"type":"string","enum":["PENDING","COMPLETED"]}},{"description":"Filter by custom field values, as `custom_data[{id}]=value` where `{id}` is a custom field's numeric id. Repeat with different ids to filter on several fields at once; a record must match every one (AND). Matching is case-sensitive exact against the value stored on the record. The id must be a filterable custom field defined on this entity — use `GET /public/v1/custom-fields?parent_object=assembly` to list the ids, their types, and which are filterable. A non-numeric id, an id not defined on this entity, or an id that isn't filterable returns a 400.","in":"query","name":"custom_data","required":false,"schema":{"type":"object"}},{"description":"Filter by when the assembly was created, as an inclusive `after,before` range of ISO 8601 timestamps. Either side may be left blank to make the range open-ended.","in":"query","name":"inserted_datetime","required":false,"example":"?inserted_datetime=2022-07-01T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Filter by when the assembly was last modified, as an inclusive `after,before` range of ISO 8601 timestamps. Either side may be left blank to make the range open-ended.","in":"query","name":"updated_datetime","required":false,"example":"?updated_datetime=2022-07-01T00:00:00Z,2022-07-31T00:00:00Z","schema":{"type":"string"}},{"description":"Restrict to assemblies owned by any of these user IDs (matched against each assembly's `owner_id`). Repeat the bracketed key once per ID. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"owner_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies tied to any of these license IDs. This is the exact-ID counterpart to `license_number`; both may be combined (ANDed). Assemblies on non-compliance (NONE) licenses have no license and are excluded when this is set. Repeat the bracketed key once per ID. Unknown IDs match nothing; an empty list is no filter. At most 200 IDs.","in":"query","name":"license_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input of any of these product IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"input_product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input drawn from any of these package IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"input_package_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input drawn from any of these batch IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"input_batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input package carrying any of these Metrc compliance labels (case-sensitive exact match per value). Repeat the bracketed key per label. At most 200.","in":"query","name":"input_package_compliance_labels","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input package whose Distru batch number is any of these (case-sensitive exact match per value). Repeat the bracketed key per value. At most 200.","in":"query","name":"input_package_batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input batch whose batch number is any of these (case-sensitive exact match per value). Repeat the bracketed key per value. At most 200.","in":"query","name":"input_batch_batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input whose product belongs to any of these category IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"input_product_category_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input whose product belongs to any of these subcategory IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"input_product_subcategory_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input whose product SKU is any of these (case-insensitive exact match per value). Repeat the bracketed key per SKU. At most 200.","in":"query","name":"input_product_skus","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input whose product belongs to any of these product group IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"input_product_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input whose product belongs to any of these brand IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"input_product_brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input whose product is tied to any of these vendor (company relationship) IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"input_product_vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input whose product carries any of these tag IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"input_product_tag_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an input whose product belongs to any of these strain IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"input_product_strain_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output of any of these product IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"output_product_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output produced into any of these package IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"output_package_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output produced into any of these batch IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"output_batch_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output package carrying any of these Metrc compliance labels (case-sensitive exact match per value). Repeat the bracketed key per label. At most 200.","in":"query","name":"output_package_compliance_labels","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output package whose Distru batch number is any of these (case-sensitive exact match per value). Repeat the bracketed key per value. At most 200.","in":"query","name":"output_package_batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output batch whose batch number is any of these (case-sensitive exact match per value). Repeat the bracketed key per value. At most 200.","in":"query","name":"output_batch_batch_numbers","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output whose product belongs to any of these category IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"output_product_category_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output whose product belongs to any of these subcategory IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"output_product_subcategory_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output whose product SKU is any of these (case-insensitive exact match per value). Repeat the bracketed key per SKU. At most 200.","in":"query","name":"output_product_skus","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output whose product belongs to any of these product group IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"output_product_group_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output whose product belongs to any of these brand IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"output_product_brand_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output whose product is tied to any of these vendor (company relationship) IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"output_product_vendor_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output whose product carries any of these tag IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"output_product_tag_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}},{"description":"Restrict to assemblies with an output whose product belongs to any of these strain IDs. Repeat the bracketed key per ID. At most 200. Unknown IDs match nothing; an empty list is no filter.","in":"query","name":"output_product_strain_ids","required":false,"style":"form","explode":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"A list of assemblies","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Assemblies"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get assemblies","tags":["Assembly"]},"post":{"description":"Create, update, or delete an assembly and its outputs, inputs, and costs in a single request.\n\nEvery row — the assembly and each nested output, input, and cost — carries a required `action` of\n`CREATE`, `UPDATE`, or `DELETE`. `UPDATE` and `DELETE` must include the row's `id`; `CREATE` omits\nit. `DELETE` on the assembly removes it along with all of its outputs, inputs, and costs. Updates\nare sparse: only the fields you send are changed. A nested row (output, input, or cost) you leave\nout entirely is left untouched — omission never deletes it; removing one always requires sending it\nwith `action` `DELETE`. The whole request is applied atomically: if any row is rejected, none of the\nchanges are saved.\n\nHow this moves inventory: while the assembly is `PENDING`, each fulfilled input (`status` `PENDING`\nor `COMPLETED`) claims specific on-hand inventory from its batch or package, and each `DRAFT` input\nreserves product-level quantity without committing a specific lot. Completing an output consumes its\ninputs and produces the output into inventory at its `location_id` — as a batch for batch-/product-\ntracked outputs, or as a new package for package-tracked (Metrc) outputs. Completing the whole\nassembly requires all of its outputs to be completed.\n\nHow this moves compliance: on a Metrc license, completing a package-tracked output (or a Metrc\nprocessing job) is pushed to Metrc as a side effect after the request commits. A 2xx therefore means\nthe change was saved in Distru, not that Metrc has finished syncing — the created packages' Metrc\nidentifiers may still be absent and the affected packages briefly flagged as syncing. Re-fetch the\nassembly with `GET /public/v1/assemblies/{id}` to observe the synced result. This endpoint supports\nMetrc and non-compliance (NONE) licenses only; BioTrack is not supported.\n\nOnly assemblies with `creation_source=MANUALLY_CREATED` (i.e. created via the Assembly form in\nDistru or via the API) can be modified or deleted by this endpoint; system-generated assemblies\n(`SALES_ORDER`, `SPLIT_PACKAGE`, `LAB_TESTING`) are rejected.\n\nMetrc processing jobs: set `metrc_processing_job.name` and `metrc_processing_job.type_id` together\nto make the assembly a Metrc processing job. Both are required together, the license must be a\nMetrc license with processing-job capability, `type_id` must be the Metrc ID of an existing Metrc\nprocessing job type, and `name` must be non-empty and not already used by a processing job in Metrc.\nOnce set, `name` and `type_id` are permanent — they\ncannot be changed on a later update; only `notes` and `waste` stay editable. `metrc_processing_job.id`\nin the response is the job's Metrc-assigned ID (set by Metrc once Distru creates the job there); it\nis read-only and null until then. Completing a processing job (setting `status` to `COMPLETED`) requires\n`notes`. `waste` is what Distru reports to Metrc when the job is finished (each quantity sent with\nits unit name); record it while the assembly is still `PENDING` — at the latest in the same request\nthat completes it. Once the assembly is `COMPLETED` the waste fields are read-only.\n\nCompleted assemblies: once an assembly's `status` is `COMPLETED`, the only assembly-level fields\nyou can still change are `description`, `custom_data`, `estimated_work_hours`,\n`estimated_work_minutes`, and `owner_id`; every other assembly field is read-only, and the assembly\ncan be neither un-completed nor deleted. Its outputs and inputs are frozen — they cannot be edited\nor deleted. Costs behave differently: an existing cost on a completed output cannot be edited or\ndeleted, but you can still add new costs to that output. Completing an assembly requires all of its\noutputs to be completed. For a Metrc processing job, packages (inputs) cannot be added or removed\nonce any output is completed; to delete such an assembly, finish the job in Metrc and Distru removes\nit automatically about 30 minutes later.\n\nRequired permission: `assemblies_permissions_create` to create, `assemblies_permissions_edit`\nto update, `assemblies_permissions_delete` to delete.\n","operationId":"DistruPublic.V1.AssemblyController.upsert","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertAssemblyRequest"}}},"description":"The assembly to create, update or delete","required":true},"responses":{"200":{"description":"The updated assembly","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AssemblyResponse"}}}},"201":{"description":"The created assembly","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AssemblyResponse"}}}},"204":{"description":"The assembly was deleted"},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Upsert an assembly","tags":["Assembly"]}},"/public/v1/reports/cogs":{"get":{"description":"Read-only report. Returns one row per sold sales order line item over the reported date range, each carrying the product's descriptive attributes (name, SKU, brand, category), its package, batch, and Metrc production batch, plus the item's quantity, unit type, unit and total price, and both actual and default cost figures (unit cost, total cost, total profit, profit per unit, and margin). A row is included only when its order has reached COMPLETED status and the line item has not been fully returned; the reported `quantity` is the ordered quantity net of any returns. Rows are grouped by order and line item. Sample line items are omitted when your company is configured to exclude samples from cost reporting, so a completed order's sample lines may not appear here even though the order does.\n\nEvery row returned by this endpoint is a sold-item row — `final_input` is always `Final` and `cost_origin` is always null. The per-component cost breakdown that can otherwise appear (input rows) is not exposed here.\n\nFiltering:\n• `order_datetime` and `delivery_datetime` each narrow the set by a date range and are combined with AND when both are supplied.\n• When neither filter is provided, the report defaults to orders whose order date falls in the last 30 days (up to now) to avoid scanning your entire order history. There is no all-time default — send an explicit range to widen it.\n\nCost figures (`unit_cost_*`, `total_cost_*`, `total_profits_*`, `profit_unit_*`, `margin_*`) can be null on a row when Distru cannot trace a cost back to the inputs and components that produced the sold inventory; the `*_actual` and `*_default` variants value those inputs at their real cost versus each product's configured unit cost. Margin is also null when the row's total price is 0.\n\nEvery cell is returned as it appears in the report's CSV export, with numeric cells returned as strings (currency, thousands, and percent formatting stripped, e.g. `\"1234.56\"`) so they match the rest of the API; identifiers with a significant leading zero (e.g. an order number like \"0042\") keep their display string. Companies on the BioTrack compliance integration do not receive the `metrc_production_batch_number` key at all — the column is dropped for them. Report-level information (the generated-at date and the column definitions) is returned under `meta`.\n\nRequired permission: `reports_permissions_cogs`.\n","operationId":"DistruPublic.V1.Report.CogsController.index","parameters":[{"description":"Restrict the report to orders whose order date falls in this range. Format is two         comma-separated ISO8601 UTC datetimes, `<after>,<before>`, both bounds inclusive.         Either side may be left empty for an open-ended range: `2026-01-01T00:00:00Z,` keeps         only orders on or after Jan 1, and `,2026-02-01T00:00:00Z` keeps only orders on or         before Feb 1. Both sides empty is rejected. Combined with `delivery_datetime` (AND)         when both are given. When neither this nor `delivery_datetime` is supplied, the         report defaults to the last 30 days by order date.","in":"query","name":"order_datetime","required":false,"example":"?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}},{"description":"Restrict the report to orders whose delivery date falls in this range. Same format         as `order_datetime`: two comma-separated ISO8601 UTC datetimes, `<after>,<before>`,         both bounds inclusive, with either side omittable for an open-ended range and both         empty rejected. Combined with `order_datetime` (AND) when both are given. Supplying         this filter (with or without `order_datetime`) suppresses the default 30-day order-date         window.","in":"query","name":"delivery_datetime","required":false,"example":"?delivery_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z","schema":{"type":"string"}}],"responses":{"200":{"description":"The Cost of Goods Sold report","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CogsReport"}}}},"400":{"description":"Invalid parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API token lacks the required permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"summary":"Get the Cost of Goods Sold report","tags":["Cogs"]}}},"security":[{"Bearer":[]}],"servers":[{"url":"https://app.distru.com"}],"components":{"securitySchemes":{"Bearer":{"type":"http","scheme":"bearer","description":"API token generated in Distru (Settings → Distru API). Send it on every request as `Authorization: Bearer YOUR_API_TOKEN`."}},"schemas":{"HarvestOutputsReportColumn":{"properties":{"key":{"description":"The key used for this column in each data row","type":"string"},"label":{"description":"The human-readable label of the column","type":"string"}},"required":["label","key"],"title":"HarvestOutputsReportColumn","type":"object"},"AssemblyMetrcProcessingJob":{"description":"The Metrc processing job details for an assembly","properties":{"id":{"description":"The processing job's Metrc ID, assigned by Metrc when the job is created there. This is a Metrc identifier, not a Distru ID. Null until the job has been created in Metrc.","type":"integer"},"name":{"description":"The processing job name reported to Metrc, or null when not set","type":"string"},"notes":{"description":"Free-text notes reported to Metrc for this processing job, or null when none","type":"string"},"type_id":{"description":"The Metrc processing job type ID selected for this job. A Metrc identifier, not a Distru ID. Null when not set.","type":"integer"},"waste":{"$ref":"#/components/schemas/AssemblyMetrcProcessingJobWaste"}},"title":"Assembly Metrc Processing Job","type":"object"},"InvoiceItemRequest":{"description":"Invoice item params","properties":{"description":{"description":"An optional free-text description for this billed line.","type":"string"},"id":{"description":"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.","type":"string"},"order_item_id":{"description":"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.","type":"string"},"quantity":{"description":"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.","type":"number"}},"required":["quantity","order_item_id"],"title":"InvoiceItem (Request)","type":"object"},"Error":{"description":"A single error describing one problem with the request.","properties":{"message":{"description":"Human-readable, customer-facing description of what went wrong.","type":"string"},"pointer":{"description":"Path to the offending field in the request: field names and, for items in a list, their integer position — e.g. [\"items\", 0, \"quantity\"]. [\"base\"] means the error applies to the request as a whole rather than one field.","items":{},"type":"array"}},"required":["message","pointer"],"title":"Error","type":"object"},"DriverResponse":{"description":"A single driver","properties":{"data":{"$ref":"#/components/schemas/Driver"}},"title":"Driver Response","type":"object"},"InventoryAssetsReportRow":{"description":"A single row of the Inventory Assets report. The `final_input`, `cost_origin`, and `cost_quantity` keys are present only when `style=granular`. Cost keys (`unit_cost_actual`, `unit_cost_default`, `total_cost_actual`, `total_cost_default`) are omitted for users without permission to view costs.\n","properties":{"active_quantity":{"description":"On-hand quantity in the `active` state as of the snapshot — sellable stock that is neither committed to a sales order nor claimed by an assembly. Null on `Input` rows.","type":"string"},"assembling_quantity":{"description":"Quantity claimed by in-progress assemblies as of the snapshot. Null on `Input` rows.","type":"string"},"batch_number":{"description":"Batch identifier — the batch's name and number combined (`Name - (Number)`), or the package's batch number when the asset is package-tracked. Null when neither applies.","type":"string"},"category":{"description":"Product's category name.","type":"string"},"cost_origin":{"description":"Present only in `granular` style. Where this cost input came from (e.g. the purchase, assembly, or adjustment that contributed cost). Null on `Final` rows; absent in `collapsed` style.","type":"string"},"cost_quantity":{"description":"Present only in `granular` style. Quantity of this cost input attributed to the asset. Null on `Final` rows; absent in `collapsed` style.","type":"string"},"expiration_date":{"description":"Asset's expiration date, taken from its batch or package and formatted as a date (`YYYY-MM-DD`) in the company's timezone. Null when neither carries one, and null on `Input` rows.","type":"string"},"final_input":{"description":"Present only in `granular` style. `Final` on an asset's own row, `Input` on each cost-component row that follows it. Absent in `collapsed` style.","type":"string"},"harvest_date":{"description":"Harvest date recorded on the asset's package. Null for assets that are not package-tracked, and null on `Input` rows.","type":"string"},"license":{"description":"License number of the asset's location. Null when the location has no license, and null on `Input` rows.","type":"string"},"location":{"description":"Location name. Null on `Input` rows.","type":"string"},"owner":{"description":"Full name of the product's owner. Null when the product has no owner.","type":"string"},"package_number":{"description":"Compliance label of the asset's package. Null for assets that are not package-tracked.","type":"string"},"product":{"description":"Product name. On a `Final` row an inactive product is suffixed with ` (INACTIVE)`. On a cost-input row this is the cost component's product name, and is null when the component is not tied to a product.","type":"string"},"selling_quantity":{"description":"Quantity committed to open sales orders as of the snapshot. Null on `Input` rows.","type":"string"},"sku":{"description":"Product SKU, or an empty string when the product has no SKU.","type":"string"},"subcategory":{"description":"Product's subcategory name. Null when the product has none.","type":"string"},"total_cost_actual":{"description":"Actual total cost — `unit_cost_actual` multiplied by the row's quantity. Present only for callers permitted to view costs; null when no cost could be traced.","type":"string"},"total_cost_default":{"description":"Default (standard) total cost — `unit_cost_default` multiplied by the row's quantity. Present only for callers permitted to view costs; null when no cost could be traced.","type":"string"},"tracking_method":{"description":"How the product's inventory is tracked: PACKAGE, BATCH, or PRODUCT. Null on `Input` rows whose cost component is not tied to a product.","enum":["PACKAGE","BATCH","PRODUCT"],"type":"string"},"unit_cost_actual":{"description":"Actual cost per unit — the real cost traced to the inputs and components that produced this inventory (purchase prices, assembly costs, stock-adjustment costs, and so on), per unit. Present only for callers permitted to view costs; null when no cost could be traced for the asset.","type":"string"},"unit_cost_default":{"description":"Default (standard) cost per unit — traced the same way as `unit_cost_actual`, but each input/component is valued at its product's configured unit cost instead of its real cost. Present only for callers permitted to view costs; null when no cost could be traced.","type":"string"},"unit_price":{"description":"Product's configured unit price. Null on `Input` rows.","type":"string"},"unit_type":{"description":"Unit-of-measure name for the row's quantities.","type":"string"},"vendor":{"description":"Product's vendor name.","type":"string"}},"title":"InventoryAssetsReportRow","type":"object"},"MetrcItems":{"description":"A collection of Metrc items.\nNote: This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.\n","properties":{"data":{"description":"Metrc Items","items":{"$ref":"#/components/schemas/MetrcItem"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Metrc Items","type":"object"},"ProductSubcategoryCompact":{"description":"A finer classification within a product category (e.g. \"Pre-Rolls\" under Flower).","properties":{"id":{"description":"ID for this subcategory","type":"string"},"name":{"description":"Human readable name for this subcategory","type":"string"}},"required":["name","id"],"title":"ProductSubcategoryCompact","type":"object"},"PurchaseResponse":{"description":"A single purchase order envelope","properties":{"data":{"$ref":"#/components/schemas/Purchase"}},"title":"Purchase Response","type":"object"},"AddProductCostsRequest":{"properties":{"costs":{"description":"Required. Non-empty list of costs; each entry is applied to every listed product","items":{"$ref":"#/components/schemas/CostEntryInput"},"type":"array"},"distribute_by_quantity":{"description":"When true, split each cost across the listed products in proportion to each product's active quantity (all products must share the same unit type category). Defaults to false when omitted, applying the full cost to every product","type":"boolean"},"location_ids":{"description":"Optional list of location IDs scoping which locations' stock the cost applies to. Omit to apply across all locations; an explicit empty list is rejected","items":{"type":"string"},"type":"array"},"product_ids":{"description":"Required. Non-empty list of product IDs; every one must exist, be accessible to the authenticated company, and be product-tracked. Each cost is applied to all listed products","items":{"type":"string"},"type":"array"}},"required":["costs","product_ids"],"title":"Add Product Costs Request","type":"object"},"SalesByCompanyReportColumn":{"properties":{"key":{"description":"The key used for this column in each data row","type":"string"},"label":{"description":"The human-readable label of the column","type":"string"}},"required":["label","key"],"title":"SalesByCompanyReportColumn","type":"object"},"SalesByCompanyReportRow":{"description":"A single row of the Sales By Company report. Companies with CompanyRelationship custom fields will see additional keys.\n","properties":{"category":{"description":"The customer's category, or null when unset.","type":"string"},"last_order_date":{"description":"The date of the customer's most recent order, formatted `MM/DD/YYYY` in the API user's timezone. This is the latest order of any status and may fall outside the reported date range.","type":"string"},"name":{"description":"The customer's company name","type":"string"},"order_count":{"description":"The number of the customer's orders counted under the active filters (status and date range). Always at least 1, since only customers with a qualifying order appear.","type":"string"},"owner":{"description":"The full name of the sales rep assigned to this customer, or null when no owner is assigned.","type":"string"},"relationship_type":{"description":"The name of the customer's relationship type, or null when none is assigned.","type":"string"},"total_received":{"description":"The sum of payments applied to the customer's counted orders, as a currency amount.","type":"string"},"total_sales":{"description":"The sum of the counted orders' totals minus any returns against them. Can be negative when returns exceed order totals.","type":"string"}},"required":["total_sales","total_received","order_count","last_order_date","name"],"title":"SalesByCompanyReportRow","type":"object"},"UnitTypes":{"description":"A collection of unit types","properties":{"data":{"description":"Unit Types","items":{"$ref":"#/components/schemas/UnitTypeFull"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Unit Types","type":"object"},"BatchesWithActiveQuantityByLocation":{"description":"A location and the product's batches that hold active quantity there.","properties":{"batches":{"description":"The product's batches with active quantity at this location, each carrying its active quantity there, ordered by id.","items":{"$ref":"#/components/schemas/BatchCompactWithQuantityActive"},"type":"array"},"location":{"$ref":"#/components/schemas/LocationCompact"}},"required":["batches","location"],"title":"BatchesWithActiveQuantityByLocation","type":"object"},"ProductTagRef":{"description":"A tag associated with a product","properties":{"id":{"description":"ID for this tag","type":"string"},"name":{"description":"The name of this tag","type":"string"}},"required":["name","id"],"title":"ProductTagRef","type":"object"},"Returns":{"description":"A collection of Returns","properties":{"data":{"description":"Returns","items":{"$ref":"#/components/schemas/Return"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Returns","type":"object"},"CompactInvoice":{"description":"A compact view of an invoice as nested inside another entity (an order or a payment) in Distru. Use its id to fetch the full invoice from the invoices endpoint.","properties":{"id":{"description":"ID for this invoice","type":"string"},"invoice_number":{"description":"The invoice number as shown in the Distru UI (e.g. \"INV-0001\"). Unique per company.","type":"string"},"status":{"description":"The payment status of this invoice (SCREAMING_CASE), reflecting how much of its total has been paid — distinct from the parent order's fulfillment `status`.• NOT_PAID: no payments applied.• PARTIALLY_PAID: paid in part but less than the total.• FULLY_PAID: paid in full.• OVER_PAID: payments exceed the total.","type":"string"},"total":{"description":"The invoice total (line items plus charges/taxes minus discounts), as a decimal string (e.g. \"150.50\").","type":"string"}},"required":["total","status","invoice_number","id"],"title":"CompactInvoice","type":"object"},"CreditUse":{"description":"An application of a credit to an invoice payment","properties":{"amount":{"description":"The amount of the credit applied to this invoice payment, as a decimal string (e.g. `\"25.00\"`). Always greater than 0.","type":"string"},"credit":{"$ref":"#/components/schemas/CompactCredit"},"id":{"description":"ID for this credit use","type":"string"},"inserted_datetime":{"description":"The ISO8601 UTC datetime at which this credit use was created in Distru.","type":"string"},"payment":{"$ref":"#/components/schemas/Payment"}},"required":["inserted_datetime","amount","id"],"title":"CreditUse","type":"object"},"UpsertCredit":{"description":"Parameters for creating or updating a credit","example":{"amount":100,"company_id":"00000000-0000-0000-0000-00000000000a","external_note":"External note","internal_note":"Internal note"},"properties":{"amount":{"description":"The credit's spendable face value. Must be greater than 0. Required when creating. On update, omitting it leaves the amount unchanged; when provided it cannot be set below the amount already applied to invoices by this credit (its used amount). Sets `original_amount` only at create time; `original_amount` never changes afterward.","type":"number"},"company_id":{"description":"ID of the customer (company relationship) this credit applies to. Required when creating and the customer must exist and not be deleted. Immutable on update — sending a different value is rejected; omit it when updating.","type":"string"},"external_note":{"description":"A note on this credit, visible to the customer. Omit to leave unchanged on update; send null to clear.","type":"string"},"id":{"description":"ID of the credit to update. Omit to create a new credit; include it to update an existing one. Only manually-created (USER-source) credits can be updated.","type":"string"},"internal_note":{"description":"An internal note on this credit, not shown to the customer. Omit to leave unchanged on update; send null to clear.","type":"string"},"owner_id":{"description":"ID of the user who owns this credit. Defaults to the API key's user when creating if omitted. On update, omitting it leaves the owner unchanged; the owner can be reassigned on any credit but cannot be removed once set.","type":"string"},"quickbooks_sales_item_id":{"description":"Optional ID of the QuickBooks Online sales item this credit maps to, used only when QuickBooks Online credit sync is enabled. Omit or send null to use the default \"Distru Sales\" item. When set it must reference an active QuickBooks Online sales item. Never required.","type":"string"}},"title":"Upsert Credit","type":"object"},"CustomFieldDefinitions":{"description":"A collection of custom field definitions","properties":{"data":{"description":"CustomFieldDefinitions","items":{"$ref":"#/components/schemas/CustomFieldDefinition"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"CustomFieldDefinitions","type":"object"},"PackagesWithActiveQuantityByLocation":{"description":"A location and the product's packages that hold active quantity there.","properties":{"location":{"$ref":"#/components/schemas/LocationCompact"},"packages":{"description":"The product's packages held at this location that have active quantity, ordered by id.","items":{"$ref":"#/components/schemas/Package"},"type":"array"}},"required":["packages","location"],"title":"PackagesWithActiveQuantityByLocation","type":"object"},"CultivationTransactionHistoryReportColumn":{"properties":{"key":{"description":"The machine key for this column; identical to the corresponding property name in each `data` row (e.g. `batch_name`, `total_cost`).","type":"string"},"label":{"description":"The human-readable column header (e.g. `Batch Name`, `Total Cost`).","type":"string"}},"required":["label","key"],"title":"CultivationTransactionHistoryReportColumn","type":"object"},"Companies":{"description":"A collection of companies","properties":{"data":{"description":"Companies","items":{"$ref":"#/components/schemas/Company"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Companies","type":"object"},"SplitPackageRequest":{"description":"A Metrc source package and the output packages to split it into","properties":{"outputs":{"description":"The output packages to create, between 1 and 300","items":{"$ref":"#/components/schemas/SplitPackageOutput"},"type":"array"},"source_package_id":{"description":"The package to split. Must be package-tracked and in a Metrc license.","type":"string"}},"required":["outputs","source_package_id"],"title":"Split Package Request","type":"object"},"Batches":{"description":"A collection of Batches","properties":{"data":{"description":"Batches","items":{"$ref":"#/components/schemas/BatchFull"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Batches","type":"object"},"PurchaseOrderHistoryReportColumn":{"properties":{"key":{"description":"The key this column is stored under in every data row (a slugified version of the label, e.g. `purchase_number`).","type":"string"},"label":{"description":"The human-readable column header, e.g. `Purchase Number`.","type":"string"}},"required":["label","key"],"title":"PurchaseOrderHistoryReportColumn","type":"object"},"BatchCompactWithQuantityActive":{"description":"A compact [batch](#model-batch) reference plus its active on-hand quantity at the location it is nested under.","properties":{"batch_number":{"description":"The batch number for this batch, or null when none is set","type":"string"},"id":{"description":"ID for this batch","type":"string"},"name":{"description":"Human readable name for this batch","type":"string"},"quantity_active":{"description":"The batch's active on-hand quantity at this location, as a decimal string (e.g. \"100\"). Always positive.","type":"string"}},"required":["quantity_active","name","id"],"title":"BatchCompactWithQuantityActive","type":"object"},"ProductCategories":{"description":"A collection of product categories","properties":{"data":{"description":"Product Categories","items":{"$ref":"#/components/schemas/ProductCategory"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Product Categories","type":"object"},"CostTypeResponse":{"description":"A single cost type","properties":{"data":{"$ref":"#/components/schemas/CostType"}},"title":"Cost Type Response","type":"object"},"OfficialProductCategories":{"description":"A collection of official product categories","properties":{"data":{"description":"OfficialProductCategories","items":{"$ref":"#/components/schemas/OfficialProductCategory"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"OfficialProductCategories","type":"object"},"CogsReport":{"description":"The Cost of Goods Sold report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/CogsReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/CogsReportMeta"}},"required":["meta","data"],"title":"CogsReport","type":"object"},"SalesOrderTaxReportColumn":{"properties":{"key":{"description":"The key this column appears under in every `data` row (e.g. `total_tax`).","type":"string"},"label":{"description":"The human-readable column header (e.g. \"Total Tax\").","type":"string"}},"required":["label","key"],"title":"SalesOrderTaxReportColumn","type":"object"},"TaxResponse":{"description":"A single tax envelope","properties":{"data":{"$ref":"#/components/schemas/Tax"}},"title":"Tax Response","type":"object"},"CostTypes":{"description":"A collection of cost types","properties":{"data":{"description":"Cost Types","items":{"$ref":"#/components/schemas/CostType"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Cost Types","type":"object"},"PlantLifecycleReportColumn":{"properties":{"key":{"description":"The key under which this column's value appears in each data row (e.g. `plant_batch_name`).","type":"string"},"label":{"description":"The human-readable column heading (e.g. `Plant Batch Name`).","type":"string"}},"required":["label","key"],"title":"PlantLifecycleReportColumn","type":"object"},"LocationWithLicense":{"description":"A location with its license number inlined, as nested on orders/invoices/purchases","properties":{"address":{"description":"The full address as a single formatted line, built from the street, apt, city, state, zip, and country fields","example":"1234 Warehouse Street, Oakland, CA","type":"string"},"company_id":{"description":"ID of the [company](#model-company) that owns this location","example":"02c88a3f-d759-4973-88f9-60049d682524","type":"string"},"id":{"description":"ID for this location","example":"02c88a3f-d759-4973-88f9-60049d682524","type":"string"},"license_id":{"description":"ID of the [license](#model-license) this location is associated with, or null when the location has no license","example":"02c88a3f-d759-4973-88f9-60049d682524","type":"string"},"license_number":{"description":"The license number of the location's [license](#model-license), inlined so you need not fetch it; null when the location has no license","example":"C11-1234567890","type":"string"},"name":{"description":"Human readable name for this location","example":"Warehouse 1","type":"string"}},"required":["name","address","id"],"title":"LocationWithLicense","type":"object"},"InvoiceResponse":{"description":"A single invoice wrapped in a data envelope","properties":{"data":{"$ref":"#/components/schemas/Invoice"}},"title":"Invoice Response","type":"object"},"CustomField":{"description":"A user-defined field attached to a record, with the value set for this particular record. Which custom fields exist is configured in Distru; use `GET /public/v1/custom-fields` to list the definitions and their IDs.","properties":{"id":{"description":"The ID of this custom field's definition (matches an `id` from `GET /public/v1/custom-fields`). An integer.","type":"integer"},"name":{"description":"The name of this custom field","type":"string"},"value":{"description":"This record's value for the custom field, as a string, or null when no value has been set for this record","type":"string"}},"required":["name","id"],"title":"CustomField","type":"object"},"CogsReportColumn":{"properties":{"key":{"description":"The key used for this column in each data row","type":"string"},"label":{"description":"The human-readable label of the column","type":"string"}},"required":["label","key"],"title":"CogsReportColumn","type":"object"},"PriceTierConditionsInput":{"description":"The conditions to set on a price tier. Send id arrays for each criterion the tier should apply to (one_of_*) or be excluded from (not_one_of_*). On update this is sparse: only the conditions you include change, and sending an empty array or null clears that one. A create must resolve to at least one condition.","properties":{"min_quantity":{"description":"order_item.quantity must be at least this much. When sent, include both quantity and unit_type_id (unit_type_id may be null for unit-agnostic). Send the object as null to clear the condition.","properties":{"quantity":{"description":"Minimum quantity (at least 1)","type":"integer"},"unit_type_id":{"description":"Unit type the minimum is measured in; omit for unit-agnostic","type":"string"}},"type":"object"},"not_one_of_company_ids":{"description":"order_item.order.company must not be one of these","items":{"type":"string"},"type":"array"},"not_one_of_company_relationship_group_ids":{"description":"order_item.order.company.group must not be one of these","items":{"type":"string"},"type":"array"},"not_one_of_product_brand_ids":{"description":"order_item.product.brand must not be one of these","items":{"type":"string"},"type":"array"},"not_one_of_product_category_ids":{"description":"order_item.product.category must not be one of these","items":{"type":"string"},"type":"array"},"not_one_of_product_group_ids":{"description":"order_item.product.group must not be one of these","items":{"type":"string"},"type":"array"},"not_one_of_product_ids":{"description":"order_item.product must not be one of these","items":{"type":"string"},"type":"array"},"not_one_of_product_subcategory_ids":{"description":"order_item.product.subcategory must not be one of these","items":{"type":"string"},"type":"array"},"one_of_company_ids":{"description":"order_item.order.company must be one of these","items":{"type":"string"},"type":"array"},"one_of_company_relationship_group_ids":{"description":"order_item.order.company.group must be one of these","items":{"type":"string"},"type":"array"},"one_of_product_brand_ids":{"description":"order_item.product.brand must be one of these","items":{"type":"string"},"type":"array"},"one_of_product_category_ids":{"description":"order_item.product.category must be one of these","items":{"type":"string"},"type":"array"},"one_of_product_group_ids":{"description":"order_item.product.group must be one of these","items":{"type":"string"},"type":"array"},"one_of_product_ids":{"description":"order_item.product must be one of these","items":{"type":"string"},"type":"array"},"one_of_product_subcategory_ids":{"description":"order_item.product.subcategory must be one of these","items":{"type":"string"},"type":"array"},"total_thc_percentage_range":{"description":"order_item.product's total THC % must fall in this window. When sent, include both min and max (either may be null for an open-ended range, but not both). Send the object as null to clear the condition.","properties":{"max":{"description":"Maximum total THC %","type":"number"},"min":{"description":"Minimum total THC %","type":"number"}},"type":"object"}},"title":"PriceTierConditionsInput","type":"object"},"Order":{"description":"A sale of products to a customer. Holds the line items sold, their quantities and prices, any extra charges/discounts/taxes, delivery and fulfillment details, and links to the resulting invoices and returns.","properties":{"buyer_company":{"$ref":"#/components/schemas/CompanyCompact"},"updated_datetime":{"description":"The datetime at which the order was last updated in Distru","type":"string"},"inserted_datetime":{"description":"The datetime at which the order was created in Distru","type":"string"},"order_number":{"description":"The order number as shown in the Distru UI. Distru-assigned and unique per company.","type":"string"},"creator":{"$ref":"#/components/schemas/User"},"charges":{"description":"A collection of Charges","items":{"$ref":"#/components/schemas/Charge"},"title":"Charges","type":"array"},"location":{"$ref":"#/components/schemas/LocationWithLicense"},"combined_order":{"$ref":"#/components/schemas/CompactOrder"},"invoices":{"description":"A collection of the invoices on this order","items":{"$ref":"#/components/schemas/CompactInvoice"},"title":"OrderInvoices","type":"array"},"items":{"description":"A collection of SalesOrderItems","items":{"$ref":"#/components/schemas/SalesOrderItem"},"title":"SalesOrderItems","type":"array"},"delivery_datetime":{"description":"The datetime the order was / will be delivered, or null if none is set","type":"string"},"blaze_payment_type":{"description":"The payment type reported to Blaze for an order whose buyer company is mapped to a Blaze retailer through the Distru integration. Null for orders not tied to a Blaze-associated company.","enum":["CASH","CREDIT","DEBIT","COD","ACH_TRANSFER","CHEQUE","OTHER"],"type":"string"},"metrc_transfer_id":{"description":"Metrc's own integer id for the outgoing transfer this order is associated with, or null when the order is not linked to a Metrc transfer. An order can be linked to only one compliance transfer, so this is null whenever `biotrack_id` is set.","type":"integer"},"metrc_transfer_template_id":{"description":"The ID of the Metrc transfer template Distru created in Metrc for this order, or null if none has been created","type":"integer"},"biotrack_id":{"description":"The BioTrack manifest this order is associated with, or null when the order is not linked to a BioTrack transfer. An order can be linked to only one compliance transfer, so this is null whenever `metrc_transfer_id` is set.","type":"string"},"order_datetime":{"description":"The datetime on which the order was placed","type":"string"},"billing_location":{"$ref":"#/components/schemas/LocationWithLicense"},"status":{"description":"Where this order is in its lifecycle, which also governs how it affects inventory.• PENDING: does not affect inventory — assigning packages/batches to line items does not change their `active` quantity, unfulfilled items do not add to the product's `reserved` quantity, and the order cannot be associated with a compliance transfer.• PROCESSING: affects inventory — assigning a package/batch to a line item moves that quantity out of `active` and into a committed selling state, unfulfilled items add to the product's `reserved` quantity, and the order may be associated with a compliance transfer.• READY_TO_SHIP: same inventory behavior as PROCESSING, but every line item must be fulfilled.• DELIVERING, DELIVERED, and COMPLETED: every line item must be fulfilled, and the order must be associated with a compliance transfer if it contains any package-tracked items.• CANCELED: the order has been canceled — like PENDING, it does not affect inventory and cannot be associated with a compliance transfer.","enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"},"leaflink_id":{"description":"LeafLink's own identifier for this order, or null for orders not synced from LeafLink. Set together with `leaflink_order_number`.","type":"string"},"returns":{"description":"A collection of the returns on this order","items":{"$ref":"#/components/schemas/CompactReturn"},"title":"OrderReturns","type":"array"},"id":{"description":"ID for this order","type":"string"},"delivered_datetime":{"description":"The datetime the order was marked Delivered or Completed, or null if it never reached those statuses","type":"string"},"metrc_transfer_template_status":{"description":"The sync status of this order's Metrc transfer template, or null if no template sync has been requested.• PENDING: the template is queued to be created or updated in Metrc.• COMPLETED: the template exists in Metrc and matches this order.• FAILED: the last sync attempt failed — see `metrc_transfer_template_error` for the reason.","enum":["PENDING","COMPLETED","FAILED"],"type":"string"},"shipping_location":{"$ref":"#/components/schemas/LocationWithLicense"},"custom_data":{"description":"A collection of CustomData","items":{"$ref":"#/components/schemas/CustomField"},"title":"CustomData","type":"array"},"owner":{"$ref":"#/components/schemas/User"},"internal_notes":{"description":"Free-form notes visible only inside Distru, never shown to the customer. Null when unset.","type":"string"},"payment_term_name":{"description":"The name of the payment term applied to this order (e.g. \"Net 30\", \"COD\"), or null if no payment term is set. Used to derive `due_datetime` on create.","type":"string"},"menu":{"$ref":"#/components/schemas/CompactMenu"},"leaflink_order_number":{"description":"The LeafLink order number for this order, or null for orders not synced from LeafLink. Set together with `leaflink_id` — either both are present or both are null.","type":"string"},"company":{"$ref":"#/components/schemas/CompanyCompact"},"metrc_transfer_template_error":{"description":"The error explaining why this order's Metrc transfer template failed to sync. Only set while `metrc_transfer_template_status` is FAILED, null otherwise.","type":"string"},"total":{"description":"The order total including all line items, charges, discounts, and taxes, as a decimal string. Recomputed by Distru on every write; defaults to \"0\" for an order with no items or charges.","type":"string"},"due_datetime":{"description":"The datetime by which the customer is expected to pay for this order. Always present — every order has a due date (enforced on write).","type":"string"},"external_notes":{"description":"The \"Message to Customer\" shown on this order's slips, or null when unset.","type":"string"},"buyer_note":{"description":"A note left by the buyer when the order was placed through a Distru menu, or null for orders not placed through a menu (or placed without a note)","type":"string"}},"required":["custom_data","updated_datetime","inserted_datetime","order_datetime","due_datetime","total","order_number","status","id"],"title":"Order","type":"object"},"Purchase":{"description":"An order to buy inventory from a vendor. Holds the line items being bought, their quantities and prices, any extra charges, and the payments made against it. Receiving against it brings the inventory in.","properties":{"billing_location":{"$ref":"#/components/schemas/LocationWithLicense"},"biotrack_id":{"description":"The incoming BioTrack transfer ID this purchase was matched with. Null unless the purchase was matched with a BioTrack transfer (mutually exclusive with `metrc_transfer_id`).","type":"string"},"charges":{"description":"A collection of Charges","items":{"$ref":"#/components/schemas/Charge"},"title":"Charges","type":"array"},"company":{"$ref":"#/components/schemas/CompanyCompact"},"creator":{"$ref":"#/components/schemas/User"},"custom_data":{"description":"The purchase's custom field values, one entry per field defined for purchases. Always present; an empty array when no custom fields are configured. See GET /public/v1/custom-fields?parent_object=purchase for the field definitions.","items":{"$ref":"#/components/schemas/CustomField"},"type":"array"},"description":{"description":"A free-text description of the purchase order. Null if none was set.","type":"string"},"due_datetime":{"description":"The datetime by which the purchase order should be paid (ISO8601 UTC). Null if no due date is set.","type":"string"},"id":{"description":"ID for this purchase order.","type":"string"},"inserted_datetime":{"description":"The datetime at which the order was created in Distru (ISO8601 UTC).","type":"string"},"items":{"description":"A collection of PurchaseOrderItems","items":{"$ref":"#/components/schemas/PurchaseOrderItem"},"title":"PurchaseOrderItems","type":"array"},"location":{"$ref":"#/components/schemas/LocationWithLicense"},"metrc_transfer_id":{"description":"The incoming Metrc transfer ID this purchase was matched with. Null unless the purchase was matched with a Metrc transfer (mutually exclusive with `biotrack_id`).","type":"integer"},"order_datetime":{"description":"The datetime on which the order was placed (ISO8601 UTC). Null if unset. Also the field this endpoint sorts by.","type":"string"},"owner":{"$ref":"#/components/schemas/User"},"paid":{"description":"The total amount paid towards this purchase order, summed across its active (non-voided) payments, as a decimal string with 2 decimal places. `\"0.00\"` when nothing has been paid; never null.","type":"string"},"payment_status":{"description":"How much of the order has been paid, derived from its payments. • NOT_PAID: no active payments recorded. • PARTIALLY_PAID: payments cover part of the total. • FULLY_PAID: payments equal the total. • OVER_PAID: payments exceed the total. May be null for orders that have never had their payment status computed.","enum":["NOT_PAID","PARTIALLY_PAID","FULLY_PAID","OVER_PAID"],"type":"string"},"payments":{"description":"A collection of the purchase's payments","items":{"$ref":"#/components/schemas/Payment"},"title":"Payments","type":"array"},"purchase_number":{"description":"The human-readable purchase order number as shown in the Distru UI. Assigned by Distru; unique within your company.","type":"string"},"qb_bill_id":{"description":"The ID of the associated bill in QuickBooks Online. Null unless your company is integrated with QuickBooks Online and this order has synced to a bill.","type":"string"},"status":{"description":"Where this purchase order is in its lifecycle, which also governs when inventory is received. Only COMPLETED may be associated with a compliance transfer — no other status can. Once received (PARTIALLY_RECEIVED or COMPLETED) an order can no longer be moved back to PENDING, PROCESSING, or DELIVERING, and once it is associated with a compliance transfer it is effectively locked at COMPLETED.• PENDING, PROCESSING, and DELIVERING behave identically: the order has not been received and does not affect inventory.• PARTIALLY_RECEIVED: works together with each line item's `received_quantity` — when at least one item has a positive `received_quantity` but not every item has `received_quantity` equal to its `quantity`, the order must be in this status, and the received amounts are brought into inventory. Not supported for orders that contain package-tracked items.• COMPLETED: the whole order has been received, bringing its inventory into your facility; if the order has package-tracked items it must be associated with a compliance transfer.","enum":["COMPLETED","DELIVERING","PENDING","PARTIALLY_RECEIVED","PROCESSING"],"type":"string"},"supplier_location":{"$ref":"#/components/schemas/LocationCompact"},"total":{"description":"The grand total for this order, including all line items, charges, discounts, and taxes, as a decimal string with 2 decimal places (e.g. `\"150.00\"`). Never null; `\"0.00\"` when there is nothing to total.","type":"string"},"updated_datetime":{"description":"The datetime at which the order was last modified in Distru (ISO8601 UTC).","type":"string"}},"required":["custom_data","payments","charges","items","paid","updated_datetime","inserted_datetime","total","purchase_number","status","id"],"title":"Purchase","type":"object"},"PurchasesByProductReport":{"description":"The Purchases By Product report","properties":{"data":{"description":"One row per purchased product matching the filters. Empty array when no               non-draft purchases fall in the range.","items":{"$ref":"#/components/schemas/PurchasesByProductReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/PurchasesByProductReportMeta"}},"required":["meta","data"],"title":"PurchasesByProductReport","type":"object"},"PlantLifecycleReport":{"description":"The Plant Lifecycle report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/PlantLifecycleReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/PlantLifecycleReportMeta"}},"required":["meta","data"],"title":"PlantLifecycleReport","type":"object"},"PriceTierConditions":{"description":"What decides whether a tier can apply to a sales order item. Every populated condition must be met by the item, or the tier is not applicable. An item satisfies a one_of_* condition when it matches at least one entity in the list, and a not_one_of_* condition when it matches none. Unused lists are empty.","properties":{"min_quantity":{"description":"order_item.quantity must be at least this much, or null","properties":{"quantity":{"description":"Minimum quantity, e.g. \"10\"","type":"string"},"unit_type":{"$ref":"#/components/schemas/UnitType"}},"type":"object"},"not_one_of_companies":{"description":"order_item.order.company must not be one of these","items":{"$ref":"#/components/schemas/CompanyCompact"},"type":"array"},"not_one_of_company_relationship_groups":{"description":"order_item.order.company.group must not be one of these","items":{"$ref":"#/components/schemas/CompanyGroup"},"type":"array"},"not_one_of_product_brands":{"description":"order_item.product.brand must not be one of these","items":{"$ref":"#/components/schemas/CompanyCompact"},"type":"array"},"not_one_of_product_categories":{"description":"order_item.product.category must not be one of these","items":{"$ref":"#/components/schemas/ProductCategoryCompact"},"type":"array"},"not_one_of_product_groups":{"description":"order_item.product.group must not be one of these","items":{"$ref":"#/components/schemas/ProductGroupCompact"},"type":"array"},"not_one_of_product_subcategories":{"description":"order_item.product.subcategory must not be one of these","items":{"$ref":"#/components/schemas/ProductSubcategoryCompact"},"type":"array"},"not_one_of_products":{"description":"order_item.product must not be one of these","items":{"$ref":"#/components/schemas/Product"},"type":"array"},"one_of_companies":{"description":"order_item.order.company must be one of these","items":{"$ref":"#/components/schemas/CompanyCompact"},"type":"array"},"one_of_company_relationship_groups":{"description":"order_item.order.company.group must be one of these","items":{"$ref":"#/components/schemas/CompanyGroup"},"type":"array"},"one_of_product_brands":{"description":"order_item.product.brand must be one of these","items":{"$ref":"#/components/schemas/CompanyCompact"},"type":"array"},"one_of_product_categories":{"description":"order_item.product.category must be one of these","items":{"$ref":"#/components/schemas/ProductCategoryCompact"},"type":"array"},"one_of_product_groups":{"description":"order_item.product.group must be one of these","items":{"$ref":"#/components/schemas/ProductGroupCompact"},"type":"array"},"one_of_product_subcategories":{"description":"order_item.product.subcategory must be one of these","items":{"$ref":"#/components/schemas/ProductSubcategoryCompact"},"type":"array"},"one_of_products":{"description":"order_item.product must be one of these","items":{"$ref":"#/components/schemas/Product"},"type":"array"},"total_thc_percentage_range":{"description":"order_item.product's total THC % must fall in this window, or null","properties":{"max":{"description":"Maximum total THC %, or null","type":"string"},"min":{"description":"Minimum total THC %, or null","type":"string"}},"type":"object"}},"required":["not_one_of_company_relationship_groups","not_one_of_companies","not_one_of_product_groups","not_one_of_product_subcategories","not_one_of_product_categories","not_one_of_product_brands","not_one_of_products","one_of_company_relationship_groups","one_of_companies","one_of_product_groups","one_of_product_subcategories","one_of_product_categories","one_of_product_brands","one_of_products"],"title":"PriceTierConditions","type":"object"},"ProductResponse":{"description":"A single Product","properties":{"data":{"$ref":"#/components/schemas/Product"}},"title":"Product Response","type":"object"},"Menu":{"properties":{"active":{"description":"Whether the menu is active. Inactive menus stay configured but are not served to customers. Always present.","type":"boolean"},"available_delivery_days":{"description":"Weekdays a customer may pick for delivery at checkout, as SCREAMING_CASE names drawn from MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY. Defaults to all seven days. Always present.","items":{"type":"string"},"type":"array"},"default_order_status":{"description":"Status assigned to every order a customer places through this menu. One of PENDING or PROCESSING. Always present.","enum":["PENDING","PROCESSING"],"type":"string"},"discoverable":{"description":"Whether the menu is listed on the DistruCommerce marketplace. Can only be true when `visibility` is PUBLIC. Always present.","type":"boolean"},"external_name":{"description":"The customer-facing menu name shown to people viewing the menu. Always present (may be an empty string if never set).","type":"string"},"id":{"description":"The menu's ID. Use it as the `{id}` path segment of the show endpoint.","type":"string"},"inserted_datetime":{"description":"When the menu was created, as a UTC ISO-8601 timestamp. Always present.","type":"string"},"internal_name":{"description":"The menu's name used internally in Distru; never shown to customers. Unique within the company, so it can be used as a stable business key. Always present.","type":"string"},"minimum_order_lead_time_days":{"description":"Number of days after order placement that are blocked for delivery (0–999); 0 means same-day delivery is allowed. Defaults to 0. Always present.","type":"integer"},"minimum_order_subtotal":{"description":"Minimum order subtotal a customer must reach to check out through this menu, as a positive decimal string (e.g. `\"250.00\"`); null when no minimum is set.","type":"string"},"product_count":{"description":"Number of active products currently on the menu; inactive and deleted products are excluded. Always present (0 when the menu has no active products).","type":"integer"},"updated_datetime":{"description":"When the menu was last updated, as a UTC ISO-8601 timestamp. Always present.","type":"string"},"url":{"description":"The menu's primary public URL; null when the menu has no primary URL configured.","type":"string"},"visibility":{"description":"Who can view the menu. One of PUBLIC (anyone, no login required), PRIVATE (only logged-in users from the menu's own company), or PASSCODE_PROTECTED (that company's users plus anyone holding the passcode).","enum":["PUBLIC","PRIVATE","PASSCODE_PROTECTED"],"type":"string"}},"required":["updated_datetime","inserted_datetime","minimum_order_lead_time_days","available_delivery_days","default_order_status","discoverable","active","visibility","product_count","external_name","internal_name","id"],"title":"Menu","type":"object"},"BinCompact":{"description":"Minimal details about a bin, as nested on other records","properties":{"id":{"description":"ID for this bin","type":"string"},"name":{"description":"The name of the bin","type":"string"}},"required":["name","id"],"title":"BinCompact","type":"object"},"CultivationTransactionHistoryReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The ordered column definitions present in this response. Each entry's `key` matches a property name in every `data` row; the set reflects the caller's permissions — the `total_cost` column appears only when the user can view costs.","items":{"$ref":"#/components/schemas/CultivationTransactionHistoryReportColumn"},"type":"array"},"date_range":{"description":"The human-readable date range the report covers, derived from the `datetime` filter (or the default last-30-days window when it was omitted). Mirrors the range applied to the rows. Always present — a range is always resolved, even when `datetime` is omitted.","type":"string"},"report":{"description":"The report identifier, always `cultivation_transaction_history`.","type":"string"}},"required":["columns","date_range","report"],"title":"CultivationTransactionHistoryReportMeta","type":"object"},"BillOfMaterialsProductInput":{"description":"A specific product consumed by a bill of materials","properties":{"id":{"description":"ID for this input","type":"string"},"product":{"$ref":"#/components/schemas/Product"},"quantity":{"description":"How much of the product this recipe consumes, in the product's unit, as a decimal string (e.g. \"2.5\").","type":"string"}},"required":["quantity","id"],"title":"BillOfMaterialsProductInput","type":"object"},"BillOfMaterialsCost":{"description":"An additional cost applied by a bill of materials","properties":{"cost_type":{"$ref":"#/components/schemas/BillOfMaterialsCostType"},"description":{"description":"Free-text description of this cost, or null when none was entered","type":"string"},"id":{"description":"ID for this cost","type":"string"},"quantity":{"description":"How many units of the cost type this line applies, as a decimal string (e.g. \"3\"). Multiply by the cost type's `cost_per_unit` to get the line total.","type":"string"}},"required":["quantity","id"],"title":"BillOfMaterialsCost","type":"object"},"ProductPosMappingResponse":{"properties":{"data":{"$ref":"#/components/schemas/ProductPosMapping"}},"required":["data"],"title":"Product POS Mapping Response","type":"object"},"CompanyGroup":{"description":"A label used to group companies together (for example by territory or account tier) for organizing and reporting.","properties":{"id":{"description":"ID for this company group","type":"string"},"name":{"description":"Name of the company group","type":"string"}},"required":["name","id"],"title":"CompanyGroup","type":"object"},"PurchasesByProductReportColumn":{"description":"Defines one column of the report. The `columns` array lists every column in order, including any per-company custom-field columns, so an integrator can render the report without hard-coding the key set.\n","properties":{"key":{"description":"The key this column uses in each data row (e.g. `quantity_purchased`). Matches               the property names on a report row.","type":"string"},"label":{"description":"The human-readable column header as shown in the CSV export (e.g. `Quantity               Purchased`).","type":"string"}},"required":["label","key"],"title":"PurchasesByProductReportColumn","type":"object"},"StockAdjustment":{"description":"A manual change to on-hand inventory that isn't a sale, purchase, or transfer — for example recording waste, theft, damage, a physical recount, or a reconciliation with the state compliance system. A positive quantity adds inventory; a negative quantity removes it.","properties":{"batch_id":{"description":"The ID of this adjustment's batch, or null when the adjusted product is not batch-tracked","type":"string"},"completion_datetime":{"description":"ISO 8601 datetime this adjustment took effect on inventory","type":"string"},"compliance_quantity":{"description":"The adjustment quantity expressed in the package's unit type, as reported to the state compliance system, as a decimal string. Null when the adjusted product is not package-tracked (no `package_id`).","type":"string"},"compliance_unit_type":{"$ref":"#/components/schemas/UnitType"},"creator":{"$ref":"#/components/schemas/User"},"description":{"description":"A free-text note explaining this adjustment, or null when none was entered","type":"string"},"id":{"description":"ID for this stock adjustment","type":"string"},"inserted_datetime":{"description":"ISO 8601 datetime this adjustment was created at","type":"string"},"license_id":{"description":"ID of the [license](#model-license) this adjustment is associated with — taken from the adjustment's location. Null when there is no location or the location has no license.","type":"string"},"location_id":{"description":"ID of the [location](#model-location) this adjustment is associated with, or null when none is set","type":"string"},"owner_id":{"description":"The ID of the Distru [user](#model-user) who owns this adjustment, or null when unassigned","type":"string"},"package_id":{"description":"The ID of this adjustment's package, or null when the adjusted product is not package-tracked","type":"string"},"product_id":{"description":"The ID of this adjustment's product. Populated regardless of the product's inventory tracking method.","type":"string"},"quantity":{"description":"The size of the adjustment in the product's unit type, as a decimal string. A positive value adds inventory; a negative value removes it (e.g. \"-5\").","type":"string"},"reason":{"description":"Why the inventory was adjusted. Returned lowercase (not SCREAMING_CASE), one of: \"waste\", \"stolen\", \"damaged\", \"fire\", \"write-off\", \"expired\", \"lab-testing\", \"revaluation\", or \"other\".","enum":["damaged","expired","fire","lab-testing","other","revaluation","stolen","waste","write-off"],"type":"string"},"total_cost":{"description":"The total cost of this adjustment, as a decimal string, or null when no cost was recorded","type":"string"},"unit_cost":{"description":"The cost per unit — `total_cost` divided by the absolute adjustment quantity, as a decimal string. Null when `total_cost` is null or the quantity is zero.","type":"string"},"unit_type":{"$ref":"#/components/schemas/UnitType"},"updated_datetime":{"description":"ISO 8601 datetime this adjustment was last modified at","type":"string"}},"required":["updated_datetime","inserted_datetime","completion_datetime","reason","quantity","product_id","id"],"title":"StockAdjustment","type":"object"},"CompanyGroupFull":{"description":"A company group","properties":{"id":{"description":"ID of this company group. Stable across renames; use it as the `id` for the upsert, fetch, and delete endpoints, and as the filter value in price tiers that target or exclude companies by group.","type":"string"},"inserted_datetime":{"description":"When the company group was created, as a UTC ISO-8601 timestamp (e.g. `\"2026-08-20T14:30:00Z\"`). Always present.","type":"string"},"name":{"description":"Display name of the company group. Always present, unique within your company.","type":"string"},"updated_datetime":{"description":"When the company group was last modified, as a UTC ISO-8601 timestamp (e.g. `\"2026-08-20T14:30:00Z\"`). Equal to `inserted_datetime` until the group is first renamed. Always present.","type":"string"}},"required":["updated_datetime","inserted_datetime","name","id"],"title":"CompanyGroup","type":"object"},"SalesOrderItem":{"description":"A single product line on a sales order — what is being sold, how much, at what price, and which inventory (batch/package) fulfills it.","properties":{"batch":{"$ref":"#/components/schemas/Batch"},"compliance_quantity":{"description":"The quantity of this order item expressed in its package's unit type, as reported to the state compliance system, as a decimal string. Null when the item is not package-tracked.","type":"string"},"cost_per_unit":{"description":"Actual cost per unit — `total_cost_actual` divided by this order item's quantity.","type":"string"},"cost_per_unit_default":{"description":"Default (standard) cost per unit — `total_cost_default` divided by this order item's quantity.","type":"string"},"id":{"description":"ID for this order item","type":"string"},"inserted_datetime":{"description":"The datetime this order item was created at","type":"string"},"is_sample":{"description":"True if this order item is a sample given away rather than sold.","type":"boolean"},"leaflink_id":{"description":"The LeafLink line-item id this order item maps to (a LeafLink identifier, not a Distru id). Null when the order did not originate from LeafLink.","type":"integer"},"location":{"$ref":"#/components/schemas/LocationCompact"},"note":{"description":"Free-text note on this order item, or null when none was entered.","type":"string"},"package":{"$ref":"#/components/schemas/Package"},"price":{"description":"Price per unit actually charged on this order item — the per-unit price after any line-level price tier discount has been applied, as a decimal string (e.g. \"25.00\"). Equals `price_base` when no discount applied.","type":"string"},"price_base":{"description":"The per-unit list price of this order item before any price tier discount, as a decimal string (e.g. \"30.00\").","type":"string"},"price_tier_mode":{"description":"How [price tiers](#model-pricetier) determine this order item's `price` (SCREAMING_CASE):• AUTO: Distru searches for the best applicable tier on every save of the order. The moment one matches, the line locks to that tier's current version — the stored mode becomes OVERRIDE and `price_tier_version` is set — so an item created as AUTO reads back as OVERRIDE once a tier has applied. It stays AUTO (with a null `price_tier_version`) only while no tier matches.• OVERRIDE: the line is locked to the exact tier snapshot in `price_tier_version`. Its discount is re-applied to `price_base` on every save, and later edits to the live tier do not change this line's pricing.• NONE: price tiers are disabled for this line; `price` equals `price_base` and `price_tier_version` is always null.","enum":["AUTO","OVERRIDE","NONE"],"type":"string"},"price_tier_version":{"$ref":"#/components/schemas/PriceTierVersion"},"product":{"$ref":"#/components/schemas/Product"},"quantity":{"description":"Quantity sold on this order item, expressed in the product's unit type, as a decimal string (e.g. \"10\")","type":"string"},"returned_quantity":{"description":"Quantity returned against this order item so far, expressed in the product's unit type, as a decimal string (e.g. \"2\"). Null when nothing has been returned. The cost fields above value only the non-returned quantity (`quantity` minus this).","type":"string"},"thc_percentage_total":{"description":"The total THC % this order line is reserved at, used when the company sells by potency. Only meaningful for batch- or package-tracked products (always null for product-tracked items). Set from the order/menu selection when the line is created — it is not derived from or changed by the assigned package (a package can only fulfill the line if its primary test result's total THC matches this value). Null when not selling by potency.","type":"string"},"total_cost_actual":{"description":"Total actual cost of the non-returned quantity in this order item (i.e. `quantity` minus `returned_quantity`). Distru traces the inputs and components that produced the shipped inventory and sums the real costs incurred along that chain — for example the price paid when a component was purchased, assembly costs, and costs added by stock adjustments, among others.","type":"string"},"total_cost_default":{"description":"Total default (standard) cost of the non-returned quantity in this order item. Traced the same way as `total_cost_actual`, but each input/component is valued at its product's configured unit cost (the product's `unit_cost`) instead of its real cost.","type":"string"}},"required":["inserted_datetime","price_tier_mode","price_base","price","quantity","is_sample","id"],"title":"SalesOrderItem","type":"object"},"InventoryValuationReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The ordered column definitions for the rows in `data`, including one entry per Product custom field. The value column key reflects `calculation_method` (`active_value_price` or `active_value_cost`).","items":{"$ref":"#/components/schemas/InventoryValuationReportColumn"},"type":"array"},"date_range":{"description":"Human-readable date the report was generated, in the company's timezone. Despite               the name this is the generation timestamp, not a filtered date range — inventory               valuation is a point-in-time snapshot with no date-range filter.","type":"string"},"report":{"description":"The report identifier (always `inventory_valuation`)","type":"string"}},"required":["columns","date_range","report"],"title":"InventoryValuationReportMeta","type":"object"},"UpsertAssemblyCost":{"description":"A cost added directly to an assembly output","properties":{"action":{"description":"CREATE, UPDATE, or DELETE. Required.","enum":["CREATE","UPDATE","DELETE"],"type":"string"},"cost_per_unit":{"description":"The per-unit rate applied to this cost. Optional.","type":"number"},"cost_type_id":{"description":"The cost type this cost is an instance of. Required when creating. Use GET /public/v1/cost-types to list the available cost types and their IDs.","type":"string"},"description":{"description":"A free-text description for this cost. Optional.","type":"string"},"id":{"description":"The cost to update or delete. Required for UPDATE and DELETE; omit for CREATE.","type":"string"},"quantity":{"description":"The quantity of this cost; must be greater than 0. Required when creating.","type":"number"}},"required":["action"],"title":"Upsert Assembly Cost","type":"object"},"InventoryAssetsReportColumn":{"properties":{"key":{"description":"Key under which this column's value appears on every row in `data`.","type":"string"},"label":{"description":"Human-readable column label as shown in the CSV export.","type":"string"}},"required":["label","key"],"title":"InventoryAssetsReportColumn","type":"object"},"RelationshipType":{"description":"How a company relates to your business — whether they are a customer you sell to, a vendor you buy from, or both.","properties":{"id":{"description":"ID for this relationship type","type":"string"},"name":{"description":"Name of the relationship type (e.g. Customer, Vendor)","type":"string"}},"required":["name","id"],"title":"RelationshipType","type":"object"},"Contacts":{"description":"A collection of Contacts","properties":{"data":{"description":"Contacts","items":{"$ref":"#/components/schemas/Contact"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Contacts","type":"object"},"SalesOrderItemHistoryReport":{"description":"The Sales Order Item History report","properties":{"data":{"description":"The report rows, one per matching sales order line item; empty when nothing matches the filters.","items":{"$ref":"#/components/schemas/SalesOrderItemHistoryReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/SalesOrderItemHistoryReportMeta"}},"required":["meta","data"],"title":"SalesOrderItemHistoryReport","type":"object"},"QuantityActiveByLocation":{"description":"Active on-hand quantity of an entity held at a single location.","properties":{"location":{"$ref":"#/components/schemas/LocationCompact"},"quantity":{"description":"Active on-hand quantity at this location, as a decimal string (e.g. \"100\"). Always positive.","type":"string"}},"required":["quantity","location"],"title":"QuantityActiveByLocation","type":"object"},"InvoiceItem":{"description":"A single billed line on an invoice. Read product, inventory, and cost details from the embedded `order_item`. Responses may also include deprecated legacy copies of some order-item fields at the top level; these are omitted here — use `order_item`.","properties":{"description":{"description":"A free-text description for this billed line, or null when none was set.","type":"string"},"id":{"description":"ID for this invoice item.","type":"string"},"inserted_datetime":{"description":"The datetime this invoice item was created.","type":"string"},"order_item":{"$ref":"#/components/schemas/SalesOrderItem"},"order_item_id":{"description":"The `id` of the sales order item this line bills. Always present. This is the raw order item id; it is also the `id` of the embedded `order_item`.","type":"string"},"quantity":{"description":"Quantity billed on this line, as a decimal string, expressed in the product's unit type. May be less than the order item's quantity when only part of the line was billed; null when no quantity was recorded.","type":"string"}},"required":["inserted_datetime","order_item_id","id"],"title":"InvoiceItem","type":"object"},"CogsReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The report's column definitions, in row order. Reflects the columns actually               returned, so the `metrc_production_batch_number` column is absent for companies               on the BioTrack compliance integration.","items":{"$ref":"#/components/schemas/CogsReportColumn"},"type":"array"},"date_range":{"description":"Human-readable date the report was generated, formatted in the requesting               user's timezone. Despite the name this is the generation timestamp, not the               filtered date range.","type":"string"},"report":{"description":"The report identifier (always `cogs`).","type":"string"}},"required":["columns","date_range","report"],"title":"CogsReportMeta","type":"object"},"CompactReturn":{"description":"A compact representation of a return","properties":{"company":{"$ref":"#/components/schemas/CompanyCompact"},"id":{"description":"ID for this return","type":"string"},"return_datetime":{"description":"The date of this return, as `YYYY-MM-DD` (e.g. `2022-07-10`)","type":"string"},"return_number":{"description":"The return number as shown in the Distru UI (e.g. `RET-1001`)","type":"string"},"status":{"description":"The return's status (SCREAMING_CASE): PROCESSING, SHIPPED, RECEIVED, or COMPLETED.","enum":["PROCESSING","SHIPPED","RECEIVED","COMPLETED"],"type":"string"},"total":{"description":"The total value of this return","type":"number"}},"required":["total","id"],"title":"CompactReturn","type":"object"},"Taxes":{"description":"A collection of taxes","properties":{"data":{"description":"Taxes","items":{"$ref":"#/components/schemas/Tax"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Taxes","type":"object"},"ProductPosMappingsResponse":{"properties":{"data":{"description":"The matching POS mappings. Empty when your company has no mappings (or none match the filter). Not paginated — the full matching set is returned in this one response, so there is never a second page to fetch.","items":{"$ref":"#/components/schemas/ProductPosMapping"},"type":"array"},"next_page":{"description":"Always present for envelope consistency, but this endpoint is not paginated: the whole matching set is already in `data`. Do not follow this URL — a next page would only return the same set again. Kept only so the response shape matches other list endpoints.","type":"string"}},"required":["data"],"title":"Product POS Mappings Response","type":"object"},"ProductCategory":{"description":"A product category","properties":{"id":{"description":"The product category's ID.","type":"string"},"inserted_datetime":{"description":"When the product category was created (UTC ISO-8601).","type":"string"},"name":{"description":"The category's display name. Unique within the company.","type":"string"},"official_product_category_id":{"description":"ID of the official product category this maps to (Distru's standard, system-defined category list; see `GET /public/v1/official-product-categories`), or null if the category has not been mapped to one. A non-null mapping is locked and cannot be changed once set. This is an external-facing string ID, not a numeric one.","type":"string"},"subcategories":{"description":"The subcategories that belong to this category, each as `id` and `name`. Always present; an empty array when the category has no subcategories.","items":{"$ref":"#/components/schemas/ProductSubcategoryCompact"},"type":"array"},"updated_datetime":{"description":"When the product category was last updated (UTC ISO-8601). Equals `inserted_datetime` until the category is first edited.","type":"string"}},"required":["updated_datetime","inserted_datetime","subcategories","name","id"],"title":"ProductCategory","type":"object"},"ProductGroupResponse":{"description":"A single product group","properties":{"data":{"$ref":"#/components/schemas/ProductGroup"}},"title":"Product Group Response","type":"object"},"Driver":{"description":"A driver","properties":{"birth_date":{"description":"The driver's date of birth as an ISO-8601 calendar date, `YYYY-MM-DD` (e.g. `1990-05-15`). Populated for BIOTRACK drivers; null for METRC drivers.","type":"string"},"driver_license":{"description":"The driver's license number. Populated for both METRC and BIOTRACK drivers, but may be null on older records.","type":"string"},"email":{"description":"The driver's email. Populated for BIOTRACK drivers; null for METRC drivers.","type":"string"},"first_name":{"description":"The driver's first name. Always present.","type":"string"},"hire_date":{"description":"The date the driver was hired as an ISO-8601 calendar date, `YYYY-MM-DD` (e.g. `2023-01-09`). Populated for BIOTRACK drivers; null for METRC drivers.","type":"string"},"id":{"description":"ID of this driver. Use it to fetch, update, or delete the driver.","type":"string"},"inserted_datetime":{"description":"When the driver was created, as a UTC ISO-8601 timestamp. Always present.","type":"string"},"last_name":{"description":"The driver's last name. Always present.","type":"string"},"occupational_license_number":{"description":"The driver's occupational license number. Populated for METRC drivers; null for BIOTRACK drivers.","type":"string"},"phone_number":{"description":"The driver's phone number. Populated for METRC drivers; null for BIOTRACK drivers.","type":"string"},"updated_datetime":{"description":"When the driver was last updated, as a UTC ISO-8601 timestamp. Always present.","type":"string"},"us_state":{"description":"The US state that issued the driver's license (e.g. `CA`). Populated for BIOTRACK drivers; null for METRC drivers.","type":"string"}},"required":["updated_datetime","inserted_datetime","last_name","first_name","id"],"title":"Driver","type":"object"},"UpsertProductPosMapping":{"description":"Body for creating or updating a product POS mapping. Send `product_id` plus exactly one complete POS pair; the POS type is derived from which pair you supply — do not send it. Mixing fields from more than one POS is rejected. Which existing mapping (if any) gets updated is keyed on `product_id` and the retailer of the supplied POS.","example":{"blaze_product_id":"blaze_123","blaze_retailer_id":"456e7890-e89b-12d3-a456-426614174000","product_id":"123e4567-e89b-12d3-a456-426614174000"},"properties":{"blaze_asset_id":{"description":"Optional Blaze asset (image) id, accepted only on a Blaze mapping. Omit on update to leave the current value untouched; send `null` to clear it. May be null even on a Blaze mapping.","type":"string"},"blaze_product_id":{"description":"Blaze's own id for the product to link to. Provide together with `blaze_retailer_id` to make this a Blaze mapping. Must already exist in Distru's synced copy of that Blaze retailer's catalog.","type":"string"},"blaze_retailer_id":{"description":"Distru ID of the connected Blaze retailer to scope the mapping to. Provide together with `blaze_product_id`.","type":"string"},"dutchie_product_id":{"description":"Dutchie's own numeric id for the product to link to. Provide together with `dutchie_retailer_id` to make this a Dutchie mapping. Must already exist in Distru's synced copy of that Dutchie retailer's catalog.","type":"integer"},"dutchie_retailer_id":{"description":"Distru ID of the connected Dutchie retailer to scope the mapping to. Provide together with `dutchie_product_id`.","type":"string"},"product_id":{"description":"ID of the Distru product to map. Must be a product in your company. Required.","type":"string"},"treez_photo_url":{"description":"Optional product photo URL, accepted only on a Treez mapping. Omit on update to leave the current value untouched; send `null` to clear it. May be null even on a Treez mapping.","type":"string"},"treez_product_id":{"description":"Treez's own id for the product to link to. Provide together with `treez_retailer_id` to make this a Treez mapping. Must already exist in Distru's synced copy of that Treez retailer's catalog.","type":"string"},"treez_retailer_id":{"description":"Distru id of the connected Treez retailer to scope the mapping to, as an integer. Provide together with `treez_product_id`.","type":"integer"}},"required":["product_id"],"title":"Upsert Product POS Mapping","type":"object"},"AssemblyOutput":{"description":"A finished product produced by an assembly, along with the quantity made and its cost.","properties":{"batch":{"$ref":"#/components/schemas/Batch"},"batch_number":{"description":"The batch number for this output, or null when none is set","type":"string"},"bins":{"description":"The bins this output's package is stored in. Empty when the output is not stored in any bin (including when it is not package-tracked).","items":{"$ref":"#/components/schemas/BinCompact"},"type":"array"},"compliance_label":{"description":"The unique tag assigned by the state compliance system (the Metrc package tag or BioTrack barcode). Null when this output has no compliance tag.","type":"string"},"compliance_quantity":{"description":"The quantity of this output expressed in the package's unit type, as reported to the state compliance system, as a decimal string. Null when this output is not package-tracked.","type":"string"},"copy_custom_data_from_input":{"description":"True when this output copies its custom field values from its input rather than carrying its own.","type":"boolean"},"cost_per_unit":{"description":"Actual cost per unit — `total_cost_actual` divided by this output's quantity (in its product's unit).","type":"string"},"cost_per_unit_default":{"description":"Default (standard) cost per unit — `total_cost_default` divided by this output's quantity (in its product's unit).","type":"string"},"costs":{"description":"The costs added directly to this assembly output (e.g. labor or packaging), on top of the material cost carried over from its inputs","items":{"$ref":"#/components/schemas/AssemblyCost"},"type":"array"},"expiration_date":{"description":"The expiration date for this output (e.g. \"2026-08-20\"), or null when none is set","type":"string"},"id":{"description":"ID for this assembly output","type":"string"},"inputs":{"description":"The inputs (source inventory) consumed to produce this output. Their cost propagates to this output and is reflected in its actual cost fields (`total_cost_actual` and `cost_per_unit`).","items":{"$ref":"#/components/schemas/AssemblyInput"},"type":"array"},"is_donation":{"description":"True when this output is marked as a donation.","type":"boolean"},"is_finished_good":{"description":"True if this output is a finished, sellable product (rather than an intermediate/work-in-progress item). Only applies to Metrc-tracked outputs.","type":"boolean"},"is_production_batch":{"description":"True if this output is a new production lot created by the assembly (rather than adding to existing inventory). Only applies to Metrc-tracked outputs.","type":"boolean"},"is_test_sample":{"description":"True when this output is a test sample.","type":"boolean"},"is_trade_sample":{"description":"True when this output is a trade sample.","type":"boolean"},"location":{"$ref":"#/components/schemas/LocationCompact"},"metrc_item_id":{"description":"The Metrc item id for this output. A Metrc identifier, not a Distru ID. Null when not applicable.","type":"integer"},"metrc_location_id":{"description":"The Metrc location id for this output. A Metrc identifier, not a Distru ID. Null when not applicable.","type":"integer"},"metrc_notes":{"description":"Notes recorded on this output that are sent to Metrc as the package's note when the package is created. Null when none.","type":"string"},"metrc_production_batch_number":{"description":"The Metrc production batch number, set only when this output is a production batch (see `is_production_batch`); otherwise null.","type":"string"},"package":{"$ref":"#/components/schemas/Package"},"package_date":{"description":"The date this output's package was created (e.g. \"2026-08-20\"), or null when not set","type":"string"},"package_unit_type":{"$ref":"#/components/schemas/UnitType"},"product":{"$ref":"#/components/schemas/Product"},"quantity":{"description":"The quantity of this output in its product's unit, as a decimal string (e.g. \"100\").","type":"string"},"status":{"description":"The status of this output. One of PENDING (not yet produced into inventory) or COMPLETED (produced, once the assembly is completed).","type":"string"},"total_cost_actual":{"description":"Total actual cost of this output. Distru traces the inputs and components consumed to produce it and sums the real costs incurred along that chain — for example the price paid when a component was purchased, assembly costs, and costs added by stock adjustments, among others — plus the costs added directly on this output (see `costs`).","type":"string"},"total_cost_default":{"description":"Total default (standard) cost of this output. Traced the same way as `total_cost_actual`, but each input/component is valued at its product's configured unit cost (the product's `unit_cost`) instead of its real cost.","type":"string"},"use_same_item":{"description":"True when this output reuses the source package's Metrc item rather than mapping to a new one.","type":"boolean"}},"required":["inputs","costs","bins","is_finished_good","is_production_batch","is_trade_sample","is_test_sample","is_donation","use_same_item","status","quantity","id"],"title":"AssemblyOutput","type":"object"},"BatchFullResponse":{"description":"A single batch","properties":{"data":{"$ref":"#/components/schemas/BatchFull"}},"title":"Batch Response","type":"object"},"BillOfMaterialsDynamicInput":{"description":"A dynamic input of a bill of materials. It selects products by attribute rather than naming a specific product. Each attribute list holds the entities matched by that criterion, or is empty when the criterion is not used.","properties":{"id":{"description":"ID for this input","type":"string"},"product_categories":{"description":"The product categories this input matches on, or empty when it does not match on category. A product must match every non-empty criterion.","items":{"$ref":"#/components/schemas/ProductCategoryCompact"},"type":"array"},"product_groups":{"description":"The product groups this input matches on, or empty when it does not match on group.","items":{"$ref":"#/components/schemas/ProductGroupCompact"},"type":"array"},"product_subcategories":{"description":"The product subcategories this input matches on, or empty when it does not match on subcategory.","items":{"$ref":"#/components/schemas/ProductSubcategoryCompact"},"type":"array"},"quantity":{"description":"How much this recipe consumes of whatever product matches, as a decimal string (e.g. \"2.5\").","type":"string"},"strains":{"description":"The strains this input matches on, or empty when it does not match on strain.","items":{"$ref":"#/components/schemas/Strain"},"type":"array"},"tags":{"description":"The tags this input matches on, or empty when it does not match on tag.","items":{"$ref":"#/components/schemas/ProductTagRef"},"type":"array"},"unit_type":{"$ref":"#/components/schemas/UnitType"}},"required":["tags","strains","product_groups","product_subcategories","product_categories","quantity","id"],"title":"BillOfMaterialsDynamicInput","type":"object"},"PaymentTerms":{"description":"A collection of Payment Terms","properties":{"data":{"description":"Payment Terms","items":{"$ref":"#/components/schemas/PaymentTerm"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Payment Terms","type":"object"},"CancelCredit":{"description":"Options for canceling a credit","example":{"should_delete_credit_uses":false},"properties":{"should_delete_credit_uses":{"description":"When true, also removes this credit's existing applications to invoices (its credit uses), returning the used amounts to the affected invoices and payments. When false or omitted, those applications are left in place and only the remaining balance is voided. Defaults to false.","type":"boolean"}},"title":"Cancel Credit","type":"object"},"Vehicles":{"description":"A collection of vehicles","properties":{"data":{"description":"Vehicles","items":{"$ref":"#/components/schemas/Vehicle"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Vehicles","type":"object"},"ProductGroup":{"description":"A product group","properties":{"id":{"description":"The product group's Distru ID. Stable for the life of the group; use it to fetch, update, or delete the group.","type":"string"},"inserted_datetime":{"description":"When the product group was created, as a UTC ISO-8601 timestamp (e.g. `2026-08-20T14:03:00Z`).","type":"string"},"name":{"description":"The group's display name. Unique within the company (case-insensitive).","type":"string"},"updated_datetime":{"description":"When the product group was last updated, as a UTC ISO-8601 timestamp. Equals `inserted_datetime` until the group is first edited.","type":"string"}},"required":["updated_datetime","inserted_datetime","name","id"],"title":"ProductGroup","type":"object"},"Image":{"description":"An image as shown in Distru","properties":{"id":{"description":"ID for this image","type":"string"},"name":{"description":"Original file name of this image, or null when unknown","type":"string"},"rank":{"description":"Sort position of this image among the product's images (lower ranks shown first), or null when unranked","type":"integer"},"url":{"description":"URL to the full-size (original) image file, or null when no file is available","type":"string"}},"required":["id"],"title":"Image","type":"object"},"PurchasesByProductReportRow":{"description":"A single row of the Purchases By Product report — one purchased product with its purchase totals over the reported range and its current descriptive attributes. Companies with Product custom fields configured will see additional keys, one per custom field, appended after `wholesale_price`.\n","properties":{"category":{"description":"The product's current category name, or null when the product is uncategorized.","type":"string"},"group":{"description":"The product's current group name, or null when the product has no group.","type":"string"},"name":{"description":"The product name. Always present. When the product is inactive,             its name is returned with an inactive marker prepended so it reads as archived.","type":"string"},"owner":{"description":"Full name of the user who owns the product, or null when the product has no               owner. This is the product's owner and is distinct from the `owner_ids` request               filter, which narrows by each purchase's assigned owner (sales rep).","type":"string"},"quantity_purchased":{"description":"Total quantity purchased across all non-draft purchases of this product in the               reported range, expressed in the product's `unit_type`. Always a number and never               null (the row only exists because at least one purchase item matched).","type":"string"},"sale_price":{"description":"The product's current sale price as a string. Always present and never null.               Reflects the product now, not any historical purchase.","type":"string"},"sku":{"description":"The product SKU. Always present, but an empty string (not null) when the product               has no SKU set.","type":"string"},"subcategory":{"description":"The product's current subcategory name, or null when none is set.","type":"string"},"total_purchased":{"description":"Total purchased amount over the range: each purchase item's quantity times its               price, rounded per item to 2 decimals, then summed across the matching purchases.               A number in the account's currency, always present and never null.","type":"string"},"unit_cost":{"description":"The product's current unit cost as a string, or null when no unit cost is set.               Reflects the product as it stands now, not the cost recorded on any individual               purchase in the range.","type":"string"},"unit_type":{"description":"The product's unit type, returned verbatim as its configured display name (for               example `Grams`, `Each`, or a company-defined unit). This is NOT a normalized               SCREAMING_CASE enum — it is the label as it appears in the CSV export, so match on               it case-sensitively and expect company-specific values. Always present.","type":"string"},"vendor":{"description":"The product's current vendor name, or null when no vendor is assigned. This is               the product's own vendor attribute, unrelated to which purchases were aggregated.","type":"string"},"wholesale_price":{"description":"The product's current wholesale price as a string, or null when no wholesale               price is set. Reflects the product now, not any historical purchase.","type":"string"}},"required":["sale_price","unit_type","total_purchased","quantity_purchased","sku","name"],"title":"PurchasesByProductReportRow","type":"object"},"InventoryValuationReportRow":{"description":"A single row of the Inventory Valuation report (one product). Companies with Product custom fields will see additional keys. When `calculation_method=cost`, the `active_value_price` key is returned as `active_value_cost` instead.\n","properties":{"active_quantity":{"description":"The active on-hand quantity, counting only active inventory in the requested locations/users. Defaults to 0 when the product has no active stock.","type":"string"},"active_value_price":{"description":"`active_quantity` × unit price. Present only under the default `calculation_method=price`; when `calculation_method=cost`, this key is replaced by `active_value_cost` (`active_quantity` × unit cost, using 0 when unit cost is null).","type":"string"},"assembling_quantity":{"description":"The quantity currently reserved inside in-progress assemblies. 0 when none.","type":"string"},"available_quantity":{"description":"`active_quantity` minus `reserved_quantity`. Can be negative when reservations exceed on-hand stock.","type":"string"},"brand":{"description":"The name of the product's brand, or null when it has none","type":"string"},"category":{"description":"The product's category, or null when it has none","type":"string"},"group":{"description":"The product's group, or null when it has none","type":"string"},"image_url":{"description":"URL of the product's thumbnail image, or null when it has no image","type":"string"},"incoming_quantity":{"description":"The quantity on open (not-yet-received) purchases. 0 when none.","type":"string"},"inventory_threshold_max":{"description":"The product's inventory alert maximum, or null when no threshold is set","type":"string"},"inventory_threshold_min":{"description":"The product's inventory alert minimum, or null when no threshold is set","type":"string"},"name":{"description":"The product name. Inactive products are prefixed to mark them.","type":"string"},"owner":{"description":"The full name of the product's owner, or null when unassigned","type":"string"},"pending_output_quantity":{"description":"The output quantity still owed by open assemblies. 0 when none.","type":"string"},"reserved_quantity":{"description":"The quantity reserved against open sales orders. 0 when none.","type":"string"},"sku":{"description":"The product SKU","type":"string"},"subcategory":{"description":"The product's subcategory, or null when it has none","type":"string"},"unit_cost":{"description":"The product's unit cost, or null when no cost is set. Used for the value column when `calculation_method=cost`.","type":"string"},"unit_price":{"description":"The product's unit price. Used for the value column by default (`calculation_method=price`).","type":"string"},"unit_type":{"description":"The product's unit of measure (e.g. `Each`, `Gram`)","type":"string"},"vendor":{"description":"The name of the product's vendor","type":"string"}},"required":["unit_price","pending_output_quantity","incoming_quantity","available_quantity","reserved_quantity","assembling_quantity","active_quantity","unit_type","vendor","sku","name"],"title":"InventoryValuationReportRow","type":"object"},"Users":{"description":"A collection of Users","properties":{"data":{"description":"Users","items":{"$ref":"#/components/schemas/User"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Users","type":"object"},"PaymentResponse":{"description":"A single Payment","properties":{"data":{"$ref":"#/components/schemas/Payment"}},"title":"Payment Response","type":"object"},"CompanyGroups":{"description":"A collection of company groups","properties":{"data":{"description":"Company Groups","items":{"$ref":"#/components/schemas/CompanyGroupFull"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Company Groups","type":"object"},"UserResponse":{"description":"A single user envelope","properties":{"data":{"$ref":"#/components/schemas/User"}},"title":"User Response","type":"object"},"CompactOrder":{"description":"A compact view of an order as nested inside another entity (an invoice or a payment) in Distru. Use its id to fetch the full order from the orders endpoint.","properties":{"id":{"description":"ID for this order","type":"string"},"order_number":{"description":"The order number as shown in the Distru UI (e.g. \"ORD-0001\"). Unique per company.","type":"string"},"status":{"description":"The fulfillment status of this order (SCREAMING_CASE) — its lifecycle stage, distinct from an invoice's payment status.","type":"string"},"total":{"description":"The order total (line items plus charges/taxes minus discounts), as a 2-decimal string (e.g. \"150.50\").","type":"string"}},"required":["total","status","order_number","id"],"title":"CompactOrder","type":"object"},"Packages":{"description":"A collection of Packages","properties":{"data":{"description":"Packages","items":{"$ref":"#/components/schemas/PackageFull"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Packages","type":"object"},"PlantLifecycleReportRow":{"description":"A single row of the Plant Lifecycle report (one plant batch). The four cost keys (`total_cost_batch_stage`, `total_cost_veg_to_last_harvest`, `destroyed_plant_cost`, `total_lifecycle_cost`) are present only for a caller with permission to view costs; for anyone else they are omitted from the row entirely (not returned as null).\n","properties":{"batch_creation_date":{"description":"The date the batch was created (planted), formatted `MM/DD/YYYY` (e.g. `01/15/2026`). Always present — this is the date the `datetime` filter matches on.","type":"string"},"days_as_batch":{"description":"Whole days from the batch creation date to its veg-promotion date, or to today when the batch has not been promoted yet (so it keeps increasing for a still-un-promoted batch). Always present.","type":"string"},"days_veg_to_last_harvest":{"description":"Whole days from the veg-promotion date to the last harvest date. Null until the batch has both been promoted to veg and harvested.","type":"string"},"destroyed_plant_cost":{"description":"Cost of the batch's destroyed plants, as a plain decimal amount. Present only with cost-view permission; null for a batch with no recorded cost.","type":"string"},"first_harvest_date":{"description":"The date of the batch's earliest harvest, formatted `MM/DD/YYYY`. Null when the batch has no harvest yet.","type":"string"},"harvest_name_s":{"description":"Comma-separated, alphabetically-ordered list of the distinct harvest names this batch produced (e.g. `Harvest A, Harvest B`). Null when the batch has no harvests.","type":"string"},"last_harvest_date":{"description":"The date of the batch's most recent harvest, formatted `MM/DD/YYYY`. Null when the batch has no harvest yet.","type":"string"},"plant_batch_name":{"description":"The plant batch name.","type":"string"},"plants_destroyed":{"description":"Count of the batch's plants that have been destroyed. `0` when none.","type":"string"},"plants_harvested":{"description":"Count of the batch's plants that have been harvested or packaged. `0` when none.","type":"string"},"plants_promoted_to_veg":{"description":"Count of the batch's plants that have entered the vegetative stage. `0` when none.","type":"string"},"plants_started":{"description":"Count of plants ever started in this batch (its non-removed plants). `0` when none.","type":"string"},"promoted_to_veg_date":{"description":"The date the batch's first plant entered the vegetative stage, formatted `MM/DD/YYYY`. Null when the batch has not yet been promoted to veg.","type":"string"},"strain":{"description":"The batch's Metrc strain name. Always present — this report covers only Metrc-licensed plant batches, which always carry a strain.","type":"string"},"total_cost_batch_stage":{"description":"Cost accrued while the batch was in the batch stage, as a plain decimal amount. Present only with cost-view permission; null for a batch with no recorded cost.","type":"string"},"total_cost_veg_to_last_harvest":{"description":"Cost accrued from veg promotion to the last harvest, as a plain decimal amount. Present only with cost-view permission; null for a batch with no recorded cost.","type":"string"},"total_lifecycle_cost":{"description":"Sum of `total_cost_batch_stage`, `total_cost_veg_to_last_harvest`, and `destroyed_plant_cost`, as a plain decimal amount. Present only with cost-view permission; null when all three components are null.","type":"string"},"total_lifecycle_days":{"description":"Total whole days across the lifecycle — `days_as_batch` plus `days_veg_to_last_harvest`, or just `days_as_batch` when the veg-to-harvest span is still null. Always present.","type":"string"}},"required":["total_lifecycle_days","days_as_batch","plants_harvested","plants_destroyed","plants_promoted_to_veg","plants_started","strain","batch_creation_date","plant_batch_name"],"title":"PlantLifecycleReportRow","type":"object"},"InventoryTransactionHistoryReport":{"description":"The Inventory Transaction History report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/InventoryTransactionHistoryReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/InventoryTransactionHistoryReportMeta"}},"required":["meta","data"],"title":"InventoryTransactionHistoryReport","type":"object"},"Package":{"description":"A specific, compliance-tracked quantity of a product identified by a unique tag (e.g. a Metrc package). This is the physical unit of inventory for package-tracked products. This is the compact reference; see PackageFull for all fields.","properties":{"batch_number":{"description":"A free-text batch number set on the package, separate from any compliance tag. Null when none is set.","type":"string"},"compliance_label":{"description":"The unique tag assigned by the state compliance system (e.g. the Metrc package tag). Always present for Metrc-tracked packages; may be null for BioTrack packages, and null when the package is not compliance-tracked.","type":"string"},"distru_status":{"description":"The package's inventory lifecycle status. One of:• ACTIVE: on hand and available.• ASSEMBLING: allocated to a pending assembly.• SELLING: reserved on an open sales order.• SOLD: consumed by a completed sale.• RETURNING: on an in-progress return.• TRANSFERRED: sent out on a compliance transfer.• ONHOLD: placed on hold in the compliance system.• FINISHED: finished in the compliance system.• DISCONTINUED: discontinued.• DESTROYED: destroyed.","enum":["ACTIVE","ASSEMBLING","DESTROYED","DISCONTINUED","FINISHED","ONHOLD","RETURNING","SELLING","SOLD","TRANSFERRED"],"type":"string"},"id":{"description":"ID for this package in Distru","type":"string"},"license_id":{"description":"The ID of the [license](#model-license) this package is held under","type":"string"},"location_id":{"description":"The ID of the [location](#model-location) where this package is physically stored","type":"string"},"metrc_id":{"description":"The Metrc package ID for this package; null for non-Metrc packages","type":"integer"},"quantity":{"description":"The total on-hand quantity of this package, in the package's `unit_type`, as a decimal string (e.g. \"100\"). Always equals `quantity_active` + `quantity_assembling`. Never negative.","type":"string"},"quantity_active":{"description":"The freely usable portion of `quantity` — what can be used as an assembly input, moved to another location, added to a sales order, or adjusted down. Equals `quantity` minus `quantity_assembling`, in the package's `unit_type`, as a decimal string. Never negative.","type":"string"}},"required":["distru_status","quantity_active","quantity","location_id","license_id","id"],"title":"Package","type":"object"},"ProductSubcategories":{"description":"A collection of product subcategories","properties":{"data":{"description":"Product Subcategories","items":{"$ref":"#/components/schemas/ProductSubcategory"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Product Subcategories","type":"object"},"UpsertAssemblyInput":{"description":"An ingredient consumed by the assembly to produce an output.\n\nA fulfilled input (`status` `PENDING` or `COMPLETED`) draws specific on-hand inventory, and its fields depend on how the product is tracked (send only the ones listed; omit the rest):\n\n- Package-tracked: `package_id` + `compliance_quantity`. The product and location are derived from the package.\n- Batch-tracked: `batch_id` + `quantity` + `location_id`. The product is derived from the batch.\n- Product-tracked: `product_id` + `quantity` + `location_id`.\n\nA `DRAFT` input is a planned ingredient with no specific lot chosen yet — it only reserves product-level quantity. Send `product_id` + `quantity` + `location_id` and omit `batch_id`, `package_id`, and `compliance_quantity`. DRAFT is only for batch- and package-tracked products (a product-tracked product has a single implicit lot, so there is nothing to defer). When the input later moves to `PENDING`/`COMPLETED` the reservation is released and a specific batch or package is consumed. While the assembly is `PENDING` an input may be `DRAFT`, `PENDING`, or `COMPLETED`; once the assembly is `COMPLETED` every input must be `COMPLETED`.\n","properties":{"action":{"description":"CREATE, UPDATE, or DELETE. Required.","enum":["CREATE","UPDATE","DELETE"],"type":"string"},"batch_id":{"description":"The source batch. Required for a fulfilled batch-tracked input — its product is derived from it — and must be omitted for package/product-tracked and DRAFT inputs.","type":"string"},"compliance_quantity":{"description":"The consumed quantity in the package's compliance unit (Metrc). Required for a fulfilled package-tracked input; must be omitted otherwise (including DRAFT).","type":"number"},"id":{"description":"The input to update or delete. Required for UPDATE and DELETE; omit for CREATE.","type":"string"},"location_id":{"description":"The location the input is drawn from. Required for batch-tracked, product-tracked, and DRAFT inputs; omit for a package-tracked input (derived from the package).","type":"string"},"package_id":{"description":"The source package. Required for a fulfilled package-tracked input — its product and location are derived from it — and must be omitted for batch/product-tracked and DRAFT inputs.","type":"string"},"product_id":{"description":"The input product. Required for product-tracked and DRAFT inputs; for package/batch-tracked inputs it is derived from the package or batch, so omit it.","type":"string"},"quantity":{"description":"The consumed quantity in the product's unit. Required for batch-tracked, product-tracked, and DRAFT inputs; omit for package-tracked inputs (use `compliance_quantity` there).","type":"number"},"status":{"description":"PENDING, COMPLETED, or DRAFT. Required when creating. DRAFT reserves product-level inventory without picking a specific lot; PENDING/COMPLETED consume specific inventory.","enum":["COMPLETED","DRAFT","PENDING"],"type":"string"}},"required":["action"],"title":"Upsert Assembly Input","type":"object"},"MetrcTag":{"description":"A Metrc tag: a unique compliance identifier provisioned to a license for tracking a package or plant in the state cannabis system.","properties":{"assigned_datetime":{"description":"ISO 8601 datetime this tag was assigned to a package or plant, or null while it is still unassigned","type":"string"},"commissioned_date":{"description":"The ISO 8601 date this tag was commissioned in Metrc (e.g. \"2026-08-20\"), or null when not yet commissioned","type":"string"},"id":{"description":"Distru's ID for this Metrc tag (not a Metrc identifier)","type":"string"},"inserted_datetime":{"description":"The datetime this tag was created in Distru (not a Metrc timestamp)","type":"string"},"is_assigned":{"description":"True once this tag has been assigned to a package or plant, false while still available. Mirrors whether `assigned_datetime` is set.","type":"boolean"},"kind":{"description":"Whether the tag is for a package or a plant","enum":["PACKAGE","PLANT"],"type":"string"},"license_id":{"description":"ID of the [license](#model-license) this tag is provisioned to","type":"string"},"tag":{"description":"The Metrc tag label","type":"string"},"updated_datetime":{"description":"The datetime this tag was last updated in Distru (not a Metrc timestamp)","type":"string"}},"required":["updated_datetime","inserted_datetime","is_assigned","license_id","kind","tag","id"],"title":"Metrc Tag","type":"object"},"Product":{"description":"A sellable or trackable item in your catalog — its name, pricing, category, unit of measure, and how its inventory is tracked (see `inventory_tracking_method`).","properties":{"gross_weight":{"description":"The gross weight of one unit including packaging, as a decimal string, measured in `gross_weight_unit_type`. Null when not set; it is always set together with `gross_weight_unit_type` and null together with it.","type":"string"},"total_cannabinoid_unit":{"description":"The unit `total_thc` and `total_cbd` are measured in — one of MG (milligrams per unit) or PERCENT (percent by weight). Null when neither potency value is set.","type":"string"},"updated_datetime":{"description":"The datetime this product was last updated at","type":"string"},"inserted_datetime":{"description":"The datetime this product was created at","type":"string"},"bill_of_materials":{"$ref":"#/components/schemas/BillOfMaterials"},"category":{"$ref":"#/components/schemas/ProductCategoryCompact"},"inventory_tracking_method":{"description":"How this product's inventory is tracked. One of: `BATCH` (grouped into batches sharing traits such as expiration dates and test results), `PACKAGE` (inventory is defined by packages), `PRODUCT` (ungrouped; inventory exists directly on the product).","enum":["PACKAGE","BATCH","PRODUCT"],"type":"string"},"creator":{"$ref":"#/components/schemas/User"},"unit_serving_size":{"description":"The serving size per unit, as a decimal string, measured in `unit_net_weight_serving_size_unit_type` (the same unit as `unit_net_weight`). Null when not set. When both are set, this is at most `unit_net_weight`.","type":"string"},"msrp":{"description":"Manufacturer's suggested retail price, as a decimal string (e.g. \"19.99\"), or null when not set","type":"string"},"menu_visibility":{"description":"Which menus this product is shown in. One of `DO_NOT_INCLUDE`, `INCLUDE_IN_ALL`, `INCLUDE_IN_SELECT` (same values accepted by the upsert endpoint); defaults to `DO_NOT_INCLUDE`. The `menus` field below lists the specific menus the product belongs to — this is how you see which menus when the value is `INCLUDE_IN_SELECT`. When the value is `INCLUDE_IN_ALL`, `menus` lists every menu (newly created menus are automatically added).","enum":["DO_NOT_INCLUDE","INCLUDE_IN_ALL","INCLUDE_IN_SELECT"],"type":"string"},"leaflink_product_id":{"description":"The LeafLink product ID this product is synced to, or null if not synced","type":"integer"},"total_cbd":{"description":"Total CBD potency, as a decimal string, measured in `total_cannabinoid_unit`. Null when not set. Non-negative; when the unit is PERCENT it is between 0 and 100. Whenever this is set, `total_cannabinoid_unit` is also set. This is a static label on the product, not a lab-test value.","type":"string"},"product_group":{"$ref":"#/components/schemas/ProductGroupCompact"},"description_markdown":{"description":"The same description in Markdown. Null when `description` is null; populated whenever `description` is populated.","type":"string"},"gross_weight_unit_type":{"$ref":"#/components/schemas/UnitType"},"sku":{"description":"The stock keeping unit (SKU) configured for this product","type":"string"},"deleted_at":{"description":"ISO 8601 datetime the product was soft-deleted at, or null when the product has not been deleted","type":"string"},"menus":{"description":"Menus this product is associated with, ordered by menu creation time then id (includes inactive menus). Reflects `menu_visibility`: empty for `DO_NOT_INCLUDE`, the selected subset for `INCLUDE_IN_SELECT`, and every menu for `INCLUDE_IN_ALL`.","items":{"$ref":"#/components/schemas/CompactMenu"},"type":"array"},"description":{"description":"Plain-text description of this product, or null when none was entered. Always null together with `description_markdown` and non-null together with it — one is never set without the other.","type":"string"},"name":{"description":"Human readable name for this product","type":"string"},"id":{"description":"ID for this product","type":"string"},"total_thc":{"description":"Total THC potency, as a decimal string, measured in `total_cannabinoid_unit`. Null when not set. Non-negative; when the unit is PERCENT it is between 0 and 100. Whenever this is set, `total_cannabinoid_unit` is also set. This is a static label on the product, not a lab-test value.","type":"string"},"unit_cost":{"description":"The cost (purchase price) of one unit of this product, as a decimal string, or null when not set","type":"string"},"unit_type":{"$ref":"#/components/schemas/UnitType"},"tags":{"description":"The tags associated with this product. Empty array when the product has no tags.","items":{"$ref":"#/components/schemas/ProductTagRef"},"type":"array"},"quantity_available_threshold_min":{"description":"Low-stock alert threshold: Distru flags the product when its available quantity drops below this value. Decimal string, or null when no low-stock alert is configured. When both thresholds are set, this is strictly less than `quantity_available_threshold_max`.","type":"string"},"images":{"description":"The images associated with this product, ordered by their id. Empty array when the product has no images.","items":{"$ref":"#/components/schemas/Image"},"type":"array"},"is_featured":{"description":"True when the product is flagged as featured","type":"boolean"},"custom_data":{"description":"The custom data for this product","items":{"$ref":"#/components/schemas/CustomField"},"type":"array"},"owner":{"$ref":"#/components/schemas/User"},"brand":{"$ref":"#/components/schemas/CompanyCompact"},"quantity_active":{"description":"Total active on-hand quantity of this product across all locations, as a decimal string (e.g. \"100\"). Sums the product's active stock — positive quantity held at a location. Equals the sum of the per-location amounts in `quantity_active_by_location`, so the two always reconcile. \"0\" when the product has no active stock. Reserved stock is still physically on-hand, so it is included here; subtract `quantity_reserved` to get `quantity_available`. Sold stock and in-transit stock (held by a user rather than a location) is excluded.","type":"string"},"quantity_active_by_location":{"description":"The product's active on-hand quantity broken down by location — one entry per location holding active stock, ordered by location id. Empty array when the product has no active stock. The entries sum to `quantity_active`.","items":{"$ref":"#/components/schemas/QuantityActiveByLocation"},"type":"array"},"quantity_reserved":{"description":"Total quantity of this product currently reserved across all locations, as a decimal string (e.g. \"100\"). Reserved quantity is a soft hold tracked separately from on-hand stock (`quantity_active`): it rises as unfulfilled sales order items and draft assembly inputs are created, and falls as they are deleted or fulfilled. \"0\" when nothing is reserved.","type":"string"},"wholesale_unit_price":{"description":"The wholesale unit price of this product, as a JSON number (e.g. 12.5) rather than a decimal string — unlike `unit_price`. Null when not set.","type":"number"},"external_name":{"description":"Customer-facing name shown on DistruCommerce menus and the Order Tracker, or null when none is set (it does not fall back to `name`)","type":"string"},"vendor":{"$ref":"#/components/schemas/CompanyCompact"},"quantity_available_threshold_max":{"description":"Over-stock alert threshold: Distru flags the product when its available quantity rises above this value. Decimal string, or null when no over-stock alert is configured. When both thresholds are set, this is strictly greater than `quantity_available_threshold_min`.","type":"string"},"batches_with_active_quantity_by_location":{"description":"The product's batches that hold active quantity, grouped by location. Present only when the request passes `include_batches_with_active_quantity_by_location=true`; the field is absent otherwise. Empty array when the product is not batch-tracked or holds no active batches.","items":{"$ref":"#/components/schemas/BatchesWithActiveQuantityByLocation"},"type":"array"},"treez_wholesale_price":{"description":"The Treez wholesale price of this product, as a decimal string, or null when not set","type":"string"},"upc":{"description":"The UPC barcode of this product, or null when not set","type":"string"},"unit_net_weight":{"description":"The net contents of one unit of this product (weight, volume, or count of the contents), as a decimal string, measured in `unit_net_weight_serving_size_unit_type`. Null when not set.","type":"string"},"quantity_available":{"description":"Active on-hand quantity minus reserved quantity, as a decimal string (e.g. \"100\"). Equals `quantity_active` − `quantity_reserved` — what is on-hand and not already spoken for by unfulfilled sales orders or draft assemblies. Can be negative when more is reserved than is on-hand at a location. \"0\" when the product has no active stock and nothing reserved.","type":"string"},"strain":{"$ref":"#/components/schemas/Strain"},"is_active":{"description":"True when the product is active. False when it has been marked inactive (archived), which hides it from most product pickers.","type":"boolean"},"subcategory":{"$ref":"#/components/schemas/ProductSubcategoryCompact"},"unit_price":{"description":"The list (sale) price of one unit of this product, as a decimal string (e.g. \"25.00\")","type":"string"},"units_per_case":{"description":"The number of units of this product packed in one case, as a decimal string (e.g. \"12\"), or null when not set","type":"string"},"unit_net_weight_serving_size_unit_type":{"$ref":"#/components/schemas/UnitType"},"packages_with_active_quantity_by_location":{"description":"The product's packages that hold active quantity, grouped by location. Present only when the request passes `include_packages_with_active_quantity_by_location=true`; the field is absent otherwise. Empty array when the product is not package-tracked or holds no active packages.","items":{"$ref":"#/components/schemas/PackagesWithActiveQuantityByLocation"},"type":"array"}},"required":["quantity_reserved","quantity_available","quantity_active_by_location","quantity_active","custom_data","inventory_tracking_method","unit_price","is_featured","is_active","updated_datetime","inserted_datetime","sku","name","id"],"title":"Product","type":"object"},"FileAttachment":{"description":"A file attachment","properties":{"assembly_id":{"description":"ID of the assembly this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"batch_id":{"description":"ID of the batch this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"company_relationship_id":{"description":"ID of the company relationship this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"contact_id":{"description":"ID of the contact this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"id":{"description":"ID of this file attachment. Always present.","type":"string"},"invoice_id":{"description":"ID of the invoice this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"license_id":{"description":"ID of the license this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"mime_type":{"description":"MIME type detected at upload from the multipart part's content type (e.g. `application/pdf`). Populated whenever the file record exists; null only when the underlying file record is missing.","type":"string"},"name":{"description":"Display name of the attachment — the name you sent, or the uploaded file's original filename when none was given. Always present.","type":"string"},"order_id":{"description":"ID of the order this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"order_shipment_id":{"description":"ID of the order shipment this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"product_id":{"description":"ID of the product this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"purchase_id":{"description":"ID of the purchase this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"request_id":{"description":"ID of the request this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"return_id":{"description":"ID of the return this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"size_in_bytes":{"description":"Byte size of the stored file. For attachments created through this API it is always set (it is computed from the uploaded bytes). Null only when the underlying file record is missing, or when a file's size was never recorded.","type":"integer"},"stock_transfer_id":{"description":"ID of the stock transfer this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"task_id":{"description":"ID of the task this file is attached to, or null. Exactly one of the reference-id fields is non-null on any attachment; the others are all null.","type":"string"},"upload_datetime":{"description":"When the attachment was created, as a UTC ISO-8601 timestamp (e.g. `2026-08-20T14:30:00Z`). Always present.","type":"string"},"uploader":{"description":"The user who uploaded the file, as `{ id, name }`. Null when that user is not visible to you. `name` may itself be null when the user has no profile name.","properties":{"id":{"type":"string"},"name":{"type":"string"}},"type":"object"},"url":{"description":"Download URL for the file, ready to fetch directly. Populated whenever the file record exists; null only when the underlying file record is missing.","type":"string"}},"required":["upload_datetime","name","id"],"title":"FileAttachment","type":"object"},"SalesOrderHistoryReportColumn":{"properties":{"key":{"description":"The slugified key this column uses in every data row map (e.g. \"order_date\", \"discounts_taxes_not_included\"). Use it to read the corresponding value from each row.","type":"string"},"label":{"description":"The human-readable column label as shown in the CSV export (e.g. \"Order Date\", \"Discounts (taxes not included)\")","type":"string"}},"required":["label","key"],"title":"SalesOrderHistoryReportColumn","type":"object"},"PurchaseChargeRequest":{"description":"Purchase charge params","properties":{"id":{"description":"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.","type":"string"},"name":{"description":"The name of this charge","type":"string"},"percent":{"description":"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.","type":"number"},"price":{"description":"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.","type":"number"},"type":{"description":"Type of this line item. Note: Tax charges should be sent as CHARGE with a tax_id","enum":["CHARGE","DISCOUNT"],"type":"string"},"unit_type":{"description":"Determines if this line is tracked as a percentage or a flat charge","enum":["PERCENT","PRICE"],"type":"string"}},"required":["unit_type","type","name"],"title":"PurchaseCharge (Request)","type":"object"},"SalesByProductReportColumn":{"properties":{"key":{"description":"The key this column appears under in every data row (the label slugified to lower_snake_case, e.g. `quantity_sold`). Custom-field columns key off the field's slug.","type":"string"},"label":{"description":"The human-readable column heading, e.g. `Quantity Sold`.","type":"string"}},"required":["label","key"],"title":"SalesByProductReportColumn","type":"object"},"Vehicle":{"description":"A vehicle","properties":{"color":{"description":"Color of the vehicle, or null if never set. Always populated for BioTrack-synced vehicles.","type":"string"},"description":{"description":"Free-text name or description for the vehicle, or null if never set. Always populated for BioTrack-synced vehicles.","type":"string"},"id":{"description":"ID of the vehicle. Stable across updates.","type":"string"},"inserted_datetime":{"description":"When the vehicle was created, as a UTC ISO-8601 timestamp. Always present.","type":"string"},"license_plate_number":{"description":"License plate number. Always present.","type":"string"},"license_plate_state":{"description":"State the license plate is registered in, as a two-letter US state code (e.g. \"CA\"), or null if never set. Always populated for BioTrack-synced vehicles.","type":"string"},"make":{"description":"Manufacturer of the vehicle. Always present.","type":"string"},"model":{"description":"Model of the vehicle. Always present.","type":"string"},"updated_datetime":{"description":"When the vehicle was last updated, as a UTC ISO-8601 timestamp. Always present.","type":"string"},"vin":{"description":"Vehicle identification number (VIN), or null if never set. Always populated for BioTrack-synced vehicles.","type":"string"},"year":{"description":"Model year as a free-form string (e.g. \"2021\"), or null if never set. Always populated for BioTrack-synced vehicles.","type":"string"}},"required":["updated_datetime","inserted_datetime","license_plate_number","model","make","id"],"title":"Vehicle","type":"object"},"Assemblies":{"description":"A collection of Assemblies","properties":{"data":{"description":"Assemblies","items":{"$ref":"#/components/schemas/Assembly"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Assemblies","type":"object"},"Bins":{"description":"A collection of bins","properties":{"data":{"description":"Bins","items":{"$ref":"#/components/schemas/Bin"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Bins","type":"object"},"TagResponse":{"description":"A single tag envelope","properties":{"data":{"$ref":"#/components/schemas/Tag"}},"title":"Tag Response","type":"object"},"License":{"description":"A cannabis license held by a company or tied to a location, identifying it to the state and its compliance system.","properties":{"active":{"description":"Whether this license is currently active","type":"boolean"},"expiry_datetime":{"description":"ISO 8601 datetime this license expires (e.g. \"2026-08-20T00:00:00Z\")","type":"string"},"id":{"description":"ID for this license","example":"02c88a3f-d759-4973-88f9-60049d682524","type":"string"},"inserted_datetime":{"description":"ISO 8601 datetime this license was created at","type":"string"},"issue_datetime":{"description":"ISO 8601 datetime this license was issued, or null when not set","type":"string"},"license_number":{"description":"License number","example":"C11-1234567890","type":"string"},"license_type":{"description":"The license type as configured in Distru. A state-specific free-form value, e.g. \"Distributor\" or \"Type 11 Distributor-Transport\"","type":"string"}},"required":["active","inserted_datetime","expiry_datetime","license_type","license_number","id"],"title":"License","type":"object"},"Inventories":{"description":"A list of active and available quantity for each group","properties":{"data":{"description":"Inventories","items":{"$ref":"#/components/schemas/Inventory"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Inventories","type":"object"},"CustomFieldDefinitionResponse":{"description":"A single custom field definition wrapped in a data envelope","properties":{"data":{"$ref":"#/components/schemas/CustomFieldDefinition"}},"title":"CustomFieldDefinition Response","type":"object"},"StockAdjustmentResponse":{"description":"A single stock adjustment envelope","properties":{"data":{"$ref":"#/components/schemas/StockAdjustment"}},"title":"Stock Adjustment Response","type":"object"},"AssemblyInput":{"description":"An ingredient consumed by an assembly — the inventory used up to produce an output, and its cost.","properties":{"batch":{"$ref":"#/components/schemas/Batch"},"compliance_quantity":{"description":"The quantity of this input expressed in the package's unit type, as reported to the state compliance system, as a decimal string. Null when this input is not package-tracked.","type":"string"},"cost_per_unit":{"description":"Actual cost per unit — `total_cost_actual` divided by this input's quantity (in its product's unit).","type":"string"},"cost_per_unit_default":{"description":"Default (standard) cost per unit — `total_cost_default` divided by this input's quantity (in its product's unit).","type":"string"},"id":{"description":"ID for this assembly input","type":"string"},"location":{"$ref":"#/components/schemas/LocationCompact"},"package":{"$ref":"#/components/schemas/Package"},"product":{"$ref":"#/components/schemas/Product"},"quantity":{"description":"The quantity of this input in its product's unit, as a decimal string (e.g. \"25\").","type":"string"},"status":{"description":"The status of this input. One of DRAFT (not yet fulfilled with a batch or package), PENDING (fulfilled and waiting to be consumed), or COMPLETED (consumed once the assembly was completed).","type":"string"},"total_cost_actual":{"description":"Total actual cost of the inventory consumed by this input. Distru traces the components that produced the consumed inventory and sums the real costs incurred along that chain — for example the price paid when a component was purchased, assembly costs, and costs added by stock adjustments, among others. This cost propagates to the output the input feeds.","type":"string"},"total_cost_default":{"description":"Total default (standard) cost of this input. Traced the same way as `total_cost_actual`, but each component is valued at its product's configured unit cost (the product's `unit_cost`) instead of its real cost.","type":"string"}},"required":["quantity","status","id"],"title":"AssemblyInput","type":"object"},"Purchases":{"description":"A collection of Purchases","properties":{"data":{"description":"Purchases","items":{"$ref":"#/components/schemas/Purchase"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Purchases","type":"object"},"ProductGroupCompact":{"description":"A named grouping of products, defined per company (e.g. a brand line or product family).","properties":{"id":{"description":"ID for this product group","type":"string"},"name":{"description":"The name of this product group","type":"string"}},"required":["name","id"],"title":"ProductGroupCompact","type":"object"},"AdditionalTestResult":{"description":"The full breakdown of individual analytes measured on a lab test, grouped by category (cannabinoids, terpenes, pesticides, heavy metals, microbials, mycotoxins, residual solvents, and more). Each value is a string. The unit is encoded in the field-name suffix: `_percentage` is percent by weight, `_mg_per_unit` is milligrams per unit, `_ug_per_g` is micrograms per gram, `_ug_per_kg` is micrograms per kilogram, and `_cfu_per_g` is colony-forming units per gram. A null or empty value means the analyte was not measured.","properties":{"imazalil_ug_per_g":{"description":"Pesticide","type":"string"},"chlorfenapyr_ug_per_g":{"description":"Pesticide","type":"string"},"ethyl_ether_ug_per_g":{"description":"Solvent","type":"string"},"acequinocyl_ug_per_g":{"description":"Pesticide","type":"string"},"borneol_mg_per_unit":{"description":"Terpene","type":"string"},"piperonylbutoxide_ug_per_g":{"description":"Pesticide","type":"string"},"chlormequat_chloride_percentage":{"description":"Other","type":"string"},"dimethylacetamide_ug_per_g":{"description":"Solvent","type":"string"},"butane_ug_per_g":{"description":"Solvent","type":"string"},"limonene_percentage":{"description":"Terpene","type":"string"},"alpha_cyfluthrin_ug_per_g":{"description":"Pesticide","type":"string"},"propiconazole_ug_per_g":{"description":"Pesticide","type":"string"},"ethyl_acetate_ug_per_g":{"description":"Solvent","type":"string"},"permethrin_trans_ug_per_g":{"description":"Pesticide","type":"string"},"alpha_phellandrene_mg_per_unit":{"description":"Terpene","type":"string"},"cbn_percentage":{"description":"Cannabinoid","type":"string"},"farnesene_mg_per_unit":{"description":"Terpene","type":"string"},"chlormequat_chloride_ug_per_g":{"description":"Other","type":"string"},"beta_pinene_mg_per_unit":{"description":"Terpene","type":"string"},"kresoxim_methyl_ug_per_g":{"description":"Pesticide","type":"string"},"trichloroethylene_ug_per_g":{"description":"Solvent","type":"string"},"guaiol_percentage":{"description":"Terpene","type":"string"},"cbdv_percentage":{"description":"Cannabinoid","type":"string"},"cumene_ug_per_g":{"description":"Solvent","type":"string"},"thcva_mg_per_unit":{"description":"Cannabinoid","type":"string"},"aspergillus_niger_cfu_per_g":{"description":"Microbial","type":"string"},"dichloromethane_ug_per_g":{"description":"Solvent","type":"string"},"coumaphos_ug_per_g":{"description":"Pesticide","type":"string"},"linalool_mg_per_unit":{"description":"Terpene","type":"string"},"toluene_ug_per_g":{"description":"Solvent","type":"string"},"ethephon_ug_per_g":{"description":"Pesticide","type":"string"},"moisture_percentage":{"description":"Moisture","type":"string"},"salmonella_cfu_per_g":{"description":"Microbial","type":"string"},"other_terpenes_mg_per_unit":{"description":"Terpene","type":"string"},"isobutyl_acetate_ug_per_g":{"description":"Solvent","type":"string"},"terpineol_mg_per_unit":{"description":"Terpene","type":"string"},"cyfluthrin_ug_per_g":{"description":"Pesticide","type":"string"},"pyrethrins_jasmolin_i_ug_per_g":{"description":"Pesticide","type":"string"},"chromium_ug_per_g":{"description":"Heavy Metal","type":"string"},"chlordane_trans_ug_per_g":{"description":"Pesticide","type":"string"},"other_microbials_cfu_per_g":{"description":"Microbial","type":"string"},"methyl_propanol_ug_per_g":{"description":"Solvent","type":"string"},"paclobutrazol_ug_per_g":{"description":"Pesticide","type":"string"},"guaiol_mg_per_unit":{"description":"Terpene","type":"string"},"ethylene_oxide_ug_per_g":{"description":"Solvent","type":"string"},"alpha_pinene_mg_per_unit":{"description":"Terpene","type":"string"},"cbl_percentage":{"description":"Cannabinoid","type":"string"},"ochratoxin_a_ug_per_kg":{"description":"Mycotoxin","type":"string"},"chlordane_ug_per_g":{"description":"Pesticide","type":"string"},"yeast_cfu_per_g":{"description":"Microbial","type":"string"},"spinosad_a_ug_per_g":{"description":"Pesticide","type":"string"},"terpineol_percentage":{"description":"Terpene","type":"string"},"spinetoram_l_ug_per_g":{"description":"Pesticide","type":"string"},"ocimene_percentage":{"description":"Terpene","type":"string"},"formic_acid_percentage":{"description":"Other","type":"string"},"antimony_ug_per_g":{"description":"Heavy Metal","type":"string"},"linalool_percentage":{"description":"Terpene","type":"string"},"prallethrin_cis_ug_per_g":{"description":"Pesticide","type":"string"},"mold_cfu_per_g":{"description":"Microbial","type":"string"},"cbt_mg_per_unit":{"description":"Cannabinoid","type":"string"},"arsenic_ug_per_g":{"description":"Heavy Metal","type":"string"},"methoxybenzene_ug_per_g":{"description":"Solvent","type":"string"},"caryophyllene_oxide_percentage":{"description":"Terpene","type":"string"},"methylisobutyl_ketone_ug_per_g":{"description":"Solvent","type":"string"},"pyrethrins_ug_per_g":{"description":"Pesticide","type":"string"},"pyrethrins_cinerin_i_ug_per_g":{"description":"Pesticide","type":"string"},"naled_ug_per_g":{"description":"Pesticide","type":"string"},"nerolidol_mg_per_unit":{"description":"Terpene","type":"string"},"thcv_percentage":{"description":"Cannabinoid","type":"string"},"cbc_mg_per_unit":{"description":"Cannabinoid","type":"string"},"methyl_acetate_ug_per_g":{"description":"Solvent","type":"string"},"gamma_terpinene_percentage":{"description":"Terpene","type":"string"},"fenoxycarb_ug_per_g":{"description":"Pesticide","type":"string"},"hexane_ug_per_g":{"description":"Solvent","type":"string"},"beta_cyfluthrin_ug_per_g":{"description":"Pesticide","type":"string"},"spinetoram_j_ug_per_g":{"description":"Pesticide","type":"string"},"cbdv_mg_per_unit":{"description":"Cannabinoid","type":"string"},"ancymidol_ug_per_g":{"description":"Pesticide","type":"string"},"alpha_myrcene_percentage":{"description":"Terpene","type":"string"},"beta_caryophyllene_mg_per_unit":{"description":"Terpene","type":"string"},"propiconazole_cis_ug_per_g":{"description":"Pesticide","type":"string"},"ethanol_ug_per_g":{"description":"Solvent","type":"string"},"ethyl_formate_ug_per_g":{"description":"Other","type":"string"},"delta_3_carene_mg_per_unit":{"description":"Terpene","type":"string"},"methyl_butanol_ug_per_g":{"description":"Solvent","type":"string"},"sabinene_percentage":{"description":"Terpene","type":"string"},"dinotefuran_ug_per_g":{"description":"Pesticide","type":"string"},"cyclohexane_ug_per_g":{"description":"Solvent","type":"string"},"methylcyclohexane_ug_per_g":{"description":"Solvent","type":"string"},"terpinolene_mg_per_unit":{"description":"Terpene","type":"string"},"diazinon_ug_per_g":{"description":"Pesticide","type":"string"},"other_mycotoxins_ug_per_kg":{"description":"Mycotoxin","type":"string"},"cbga_mg_per_unit":{"description":"Cannabinoid","type":"string"},"isopropanol_ug_per_g":{"description":"Solvent","type":"string"},"ocimene_mg_per_unit":{"description":"Terpene","type":"string"},"methyl_parathion_ug_per_g":{"description":"Pesticide","type":"string"},"nitromethane_ug_per_g":{"description":"Solvent","type":"string"},"pentane_ug_per_g":{"description":"Solvent","type":"string"},"terpinolene_percentage":{"description":"Terpene","type":"string"},"other_pesticides_ug_per_g":{"description":"Pesticide","type":"string"},"beta_myrcene_percentage":{"description":"Terpene","type":"string"},"nickel_ug_per_g":{"description":"Heavy Metal","type":"string"},"aspergillus_fumigatus_cfu_per_g":{"description":"Microbial","type":"string"},"cadmium_ug_per_g":{"description":"Heavy Metal","type":"string"},"permethrin_ug_per_g":{"description":"Pesticide","type":"string"},"clothianidin_ug_per_g":{"description":"Pesticide","type":"string"},"cbt_percentage":{"description":"Cannabinoid","type":"string"},"malathion_ug_per_g":{"description":"Pesticide","type":"string"},"dimethomorph_ug_per_g":{"description":"Pesticide","type":"string"},"pulegone_percentage":{"description":"Terpene","type":"string"},"delta_3_carene_percentage":{"description":"Terpene","type":"string"},"carbofuran_ug_per_g":{"description":"Pesticide","type":"string"},"dioxane_ug_per_g":{"description":"Solvent","type":"string"},"spinosad_ug_per_g":{"description":"Pesticide","type":"string"},"eucalyptol_percentage":{"description":"Terpene","type":"string"},"mevinphos_ii_ug_per_g":{"description":"Pesticide","type":"string"},"sand_and_soil_and_cinders_and_dirt_percentage":{"description":"Other","type":"string"},"eucalyptol_mg_per_unit":{"description":"Terpene","type":"string"},"tetrahydrofuran_ug_per_g":{"description":"Solvent","type":"string"},"lambda_cyhalothrin_ug_per_g":{"description":"Pesticide","type":"string"},"terpenes_mg_per_unit_total":{"description":"Terpene","type":"string"},"metalaxyl_ug_per_g":{"description":"Pesticide","type":"string"},"cbga_percentage":{"description":"Cannabinoid","type":"string"},"flurprimidol_ug_per_g":{"description":"Pesticide","type":"string"},"mevinphos_ug_per_g":{"description":"Pesticide","type":"string"},"cbg_percentage":{"description":"Cannabinoid","type":"string"},"ethyl_formate_percentage":{"description":"Other","type":"string"},"dimethoate_ug_per_g":{"description":"Pesticide","type":"string"},"water_activity_aw":{"description":"Water Activity","type":"string"},"valencene_percentage":{"description":"Terpene","type":"string"},"pentachloronitrobenzene_ug_per_g":{"description":"Pesticide","type":"string"},"fenchol_percentage":{"description":"Terpene","type":"string"},"isopulegol_mg_per_unit":{"description":"Terpene","type":"string"},"cbda_mg_per_unit":{"description":"Cannabinoid","type":"string"},"beta_caryophyllene_percentage":{"description":"Terpene","type":"string"},"other_solvents_ug_per_g":{"description":"Solvent","type":"string"},"propyl_acetate_ug_per_g":{"description":"Solvent","type":"string"},"borneol_percentage":{"description":"Terpene","type":"string"},"spirotetramat_ug_per_g":{"description":"Pesticide","type":"string"},"camphene_percentage":{"description":"Terpene","type":"string"},"azoxystrobin_ug_per_g":{"description":"Pesticide","type":"string"},"bifenazate_ug_per_g":{"description":"Pesticide","type":"string"},"dichloroethane_ug_per_g":{"description":"Solvent","type":"string"},"boscalid_ug_per_g":{"description":"Pesticide","type":"string"},"methiocarb_ug_per_g":{"description":"Pesticide","type":"string"},"isopropyl_acetate_ug_per_g":{"description":"Solvent","type":"string"},"pyrethrins_cinerin_ii_ug_per_g":{"description":"Pesticide","type":"string"},"acetonitrile_ug_per_g":{"description":"Solvent","type":"string"},"dimethyl_sulfoxide_ug_per_g":{"description":"Solvent","type":"string"},"pentanol_ug_per_g":{"description":"Solvent","type":"string"},"fludioxonil_ug_per_g":{"description":"Pesticide","type":"string"},"acephate_ug_per_g":{"description":"Pesticide","type":"string"},"beta_humulene_percentage":{"description":"Terpene","type":"string"},"filth_and_foreign_material_percentage":{"description":"Other","type":"string"},"dichlorvos_ug_per_g":{"description":"Pesticide","type":"string"},"lead_ug_per_g":{"description":"Heavy Metal","type":"string"},"etoxazole_ug_per_g":{"description":"Pesticide","type":"string"},"methomyl_ug_per_g":{"description":"Pesticide","type":"string"},"benzene_ug_per_g":{"description":"Solvent","type":"string"},"propoxur_ug_per_g":{"description":"Pesticide","type":"string"},"cannabinoids_mg_per_unit_total":{"description":"Cannabinoid","type":"string"},"alpha_humulene_percentage":{"description":"Terpene","type":"string"},"daminozide_ug_per_g":{"description":"Pesticide","type":"string"},"e_coli_cfu_per_g":{"description":"Microbial","type":"string"},"chlorantraniliprole_ug_per_g":{"description":"Pesticide","type":"string"},"beta_pinene_percentage":{"description":"Terpene","type":"string"},"alpha_pinene_percentage":{"description":"Terpene","type":"string"},"beta_humulene_mg_per_unit":{"description":"Terpene","type":"string"},"acetic_acid_percentage":{"description":"Other","type":"string"},"chlorobenzene_ug_per_g":{"description":"Solvent","type":"string"},"oxamyl_ug_per_g":{"description":"Pesticide","type":"string"},"cbn_mg_per_unit":{"description":"Cannabinoid","type":"string"},"tert_butyl_methyl_ether_ug_per_g":{"description":"Solvent","type":"string"},"methoxyethanol_ug_per_g":{"description":"Solvent","type":"string"},"sabinene_mg_per_unit":{"description":"Terpene","type":"string"},"cbca_percentage":{"description":"Cannabinoid","type":"string"},"aspergillus_terreus_cfu_per_g":{"description":"Microbial","type":"string"},"acetic_acid_ug_per_g":{"description":"Other","type":"string"},"camphor_mg_per_unit":{"description":"Terpene","type":"string"},"aflatoxin_g2_ug_per_kg":{"description":"Mycotoxin","type":"string"},"cypermethrin_ug_per_g":{"description":"Pesticide","type":"string"},"thcv_mg_per_unit":{"description":"Cannabinoid","type":"string"},"acetamiprid_ug_per_g":{"description":"Pesticide","type":"string"},"alpha_terpinene_percentage":{"description":"Terpene","type":"string"},"phosmet_ug_per_g":{"description":"Pesticide","type":"string"},"prallethrin_trans_ug_per_g":{"description":"Pesticide","type":"string"},"vitamin_e_acetate_percentage":{"description":"Other","type":"string"},"isopulegol_percentage":{"description":"Terpene","type":"string"},"mgk_264_ug_per_g":{"description":"Pesticide","type":"string"},"prallethrin_ug_per_g":{"description":"Pesticide","type":"string"},"chlorpyrifos_ug_per_g":{"description":"Pesticide","type":"string"},"imidacloprid_ug_per_g":{"description":"Pesticide","type":"string"},"ethoxyethanol_ug_per_g":{"description":"Solvent","type":"string"},"hexythiazox_ug_per_g":{"description":"Pesticide","type":"string"},"clofentezine_ug_per_g":{"description":"Pesticide","type":"string"},"camphor_percentage":{"description":"Terpene","type":"string"},"spiroxamine_ug_per_g":{"description":"Pesticide","type":"string"},"thca_mg_per_unit":{"description":"Cannabinoid","type":"string"},"vitamin_e_acetate_ug_per_g":{"description":"Other","type":"string"},"methyl_butyl_ketone_ug_per_g":{"description":"Solvent","type":"string"},"thiacloprid_ug_per_g":{"description":"Pesticide","type":"string"},"cbca_mg_per_unit":{"description":"Cannabinoid","type":"string"},"methanol_ug_per_g":{"description":"Solvent","type":"string"},"cbl_mg_per_unit":{"description":"Cannabinoid","type":"string"},"cymene_percentage":{"description":"Terpene","type":"string"},"permethrin_cis_ug_per_g":{"description":"Pesticide","type":"string"},"diuron_ug_per_g":{"description":"Pesticide","type":"string"},"valencene_mg_per_unit":{"description":"Terpene","type":"string"},"ethylene_glycol_ug_per_g":{"description":"Other","type":"string"},"spinetoram_ug_per_g":{"description":"Pesticide","type":"string"},"acetone_ug_per_g":{"description":"Solvent","type":"string"},"thca_percentage":{"description":"Cannabinoid","type":"string"},"pyridaben_ug_per_g":{"description":"Pesticide","type":"string"},"aflatoxin_b2_ug_per_kg":{"description":"Mycotoxin","type":"string"},"other_heavy_metals_ug_per_g":{"description":"Heavy Metal","type":"string"},"terpenes_percentage_total":{"description":"Terpene","type":"string"},"bifenthrin_ug_per_g":{"description":"Pesticide","type":"string"},"captan_ug_per_g":{"description":"Pesticide","type":"string"},"m_and_p_xylene_ug_per_g":{"description":"Solvent","type":"string"},"spiromesifen_ug_per_g":{"description":"Pesticide","type":"string"},"aldicarb_ug_per_g":{"description":"Pesticide","type":"string"},"spiroxamine_b_ug_per_g":{"description":"Pesticide","type":"string"},"sulfolane_ug_per_g":{"description":"Solvent","type":"string"},"fenhexamid_ug_per_g":{"description":"Pesticide","type":"string"},"aflatoxin_b1_ug_per_kg":{"description":"Mycotoxin","type":"string"},"beta_myrcene_mg_per_unit":{"description":"Terpene","type":"string"},"geraniol_mg_per_unit":{"description":"Terpene","type":"string"},"propanol_ug_per_g":{"description":"Solvent","type":"string"},"camphene_mg_per_unit":{"description":"Terpene","type":"string"},"chlordane_cis_ug_per_g":{"description":"Pesticide","type":"string"},"butyl_acetate_ug_per_g":{"description":"Solvent","type":"string"},"heptane_ug_per_g":{"description":"Solvent","type":"string"},"propane_ug_per_g":{"description":"Solvent","type":"string"},"l_monocytogenes_cfu_per_g":{"description":"Microbial","type":"string"},"farnesene_percentage":{"description":"Terpene","type":"string"},"alpha_humulene_mg_per_unit":{"description":"Terpene","type":"string"},"mercury_ug_per_g":{"description":"Heavy Metal","type":"string"},"gamma_terpinene_mg_per_unit":{"description":"Terpene","type":"string"},"pulegone_mg_per_unit":{"description":"Terpene","type":"string"},"mevinphos_i_ug_per_g":{"description":"Pesticide","type":"string"},"aflatoxins_ug_per_kg":{"description":"Mycotoxin","type":"string"},"nerolidol_percentage":{"description":"Terpene","type":"string"},"tebuconazole_ug_per_g":{"description":"Pesticide","type":"string"},"candida_albicans_cfu_per_g":{"description":"Microbial","type":"string"},"formamide_ug_per_g":{"description":"Pesticide","type":"string"},"dimethomorph_e_ug_per_g":{"description":"Pesticide","type":"string"},"dimethomorph_z_ug_per_g":{"description":"Pesticide","type":"string"},"alpha_terpinene_mg_per_unit":{"description":"Terpene","type":"string"},"alpha_phellandrene_percentage":{"description":"Terpene","type":"string"},"spinosad_d_ug_per_g":{"description":"Pesticide","type":"string"},"carbaryl_ug_per_g":{"description":"Pesticide","type":"string"},"aspergillus_flavus_cfu_per_g":{"description":"Microbial","type":"string"},"other_terpenes_percentage":{"description":"Terpene","type":"string"},"ethylene_glycol_percentage":{"description":"Other","type":"string"},"geraniol_percentage":{"description":"Terpene","type":"string"},"pyrethrins_jasmolin_ii_ug_per_g":{"description":"Pesticide","type":"string"},"pyridine_ug_per_g":{"description":"Solvent","type":"string"},"cannabinoids_percentage_total":{"description":"Cannabinoid","type":"string"},"formic_acid_ug_per_g":{"description":"Other","type":"string"},"aflatoxin_g1_ug_per_kg":{"description":"Mycotoxin","type":"string"},"thiabendazole_ug_per_g":{"description":"Pesticide","type":"string"},"alpha_myrcene_mg_per_unit":{"description":"Terpene","type":"string"},"flonicamid_ug_per_g":{"description":"Pesticide","type":"string"},"dimethylformamide_ug_per_g":{"description":"Solvent","type":"string"},"trifloxystrobin_ug_per_g":{"description":"Pesticide","type":"string"},"cymene_mg_per_unit":{"description":"Terpene","type":"string"},"fipronil_ug_per_g":{"description":"Pesticide","type":"string"},"phytol_percentage":{"description":"Terpene","type":"string"},"butanol_ug_per_g":{"description":"Solvent","type":"string"},"zinc_ug_per_g":{"description":"Heavy Metal","type":"string"},"fenchol_mg_per_unit":{"description":"Terpene","type":"string"},"cbg_mg_per_unit":{"description":"Cannabinoid","type":"string"},"alpha_cypermethrin_ug_per_g":{"description":"Pesticide","type":"string"},"fenpyroximate_ug_per_g":{"description":"Pesticide","type":"string"},"phytol_mg_per_unit":{"description":"Terpene","type":"string"},"beta_cypermethrin_ug_per_g":{"description":"Pesticide","type":"string"},"tetralin_ug_per_g":{"description":"Solvent","type":"string"},"methyl_ethyl_ketone_ug_per_g":{"description":"Solvent","type":"string"},"alpha_bisabolol_percentage":{"description":"Terpene","type":"string"},"spiroxamine_a_ug_per_g":{"description":"Pesticide","type":"string"},"alpha_bisabolol_mg_per_unit":{"description":"Terpene","type":"string"},"cbc_percentage":{"description":"Cannabinoid","type":"string"},"pyriproxyfen_ug_per_g":{"description":"Pesticide","type":"string"},"thcva_percentage":{"description":"Cannabinoid","type":"string"},"pyrethrins_pyrethrin_i_ug_per_g":{"description":"Pesticide","type":"string"},"propiconazole_trans_ug_per_g":{"description":"Pesticide","type":"string"},"n_methylpyrrolidone_ug_per_g":{"description":"Solvent","type":"string"},"cbda_percentage":{"description":"Cannabinoid","type":"string"},"delta_8_thc_mg_per_unit":{"description":"Cannabinoid","type":"string"},"thiamethoxam_ug_per_g":{"description":"Pesticide","type":"string"},"caryophyllene_oxide_mg_per_unit":{"description":"Terpene","type":"string"},"myclobutanil_ug_per_g":{"description":"Pesticide","type":"string"},"copper_ug_per_g":{"description":"Heavy Metal","type":"string"},"chloroform_ug_per_g":{"description":"Solvent","type":"string"},"enterobacteriacaea_cfu_per_g":{"description":"Microbial","type":"string"},"ethoprophos_ug_per_g":{"description":"Pesticide","type":"string"},"xylene_ug_per_g_total":{"description":"Solvent","type":"string"},"pyrethrins_pyrethrin_ii_ug_per_g":{"description":"Pesticide","type":"string"},"etofenprox_ug_per_g":{"description":"Pesticide","type":"string"},"limonene_mg_per_unit":{"description":"Terpene","type":"string"},"aspergillus_cfu_per_g":{"description":"Microbial","type":"string"},"delta_8_thc_percentage":{"description":"Cannabinoid","type":"string"},"dimethoxyethane_ug_per_g":{"description":"Solvent","type":"string"}},"title":"AdditionalTestResult","type":"object"},"StockAdjustments":{"description":"A collection of Stock Adjustments","properties":{"data":{"description":"Stock Adjustments","items":{"$ref":"#/components/schemas/StockAdjustment"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Stock Adjustments","type":"object"},"CompactMenu":{"description":"A lightweight reference to a DistruCommerce menu — the online catalog a buyer browses to place an order — carrying just its id and name. Where it appears on a sales order, it is the menu that order was placed through. Use the id to fetch the full menu from the menus endpoint.","properties":{"id":{"description":"ID of the menu","type":"string"},"name":{"description":"Display name of the menu","type":"string"}},"required":["name","id"],"title":"CompactMenu","type":"object"},"UnitType":{"description":"A unit of measure used for quantities and pricing (e.g. Gram, Each, Pound). This is the compact reference carrying just id and name; the full unit type (its measurement category and conversion factor) is available from the unit types endpoint.","properties":{"id":{"description":"ID for this unit type","type":"string"},"name":{"description":"Human readable name for this unit type","type":"string"}},"required":["name","id"],"title":"Unit Type","type":"object"},"SalesOrderTaxReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The report's ordered column definitions, pairing each row key with its display label.","items":{"$ref":"#/components/schemas/SalesOrderTaxReportColumn"},"type":"array"},"date_range":{"description":"Human-readable label for the resolved order-date range the report covers (e.g. \"Jul 21, 2026 to Aug 20, 2026\"), rendered in the authenticated user's timezone. Reflects the default last-30-days window when `order_datetime` is omitted.","type":"string"},"report":{"description":"The report identifier; always `sales_order_tax` for this endpoint.","type":"string"}},"required":["columns","date_range","report"],"title":"SalesOrderTaxReportMeta","type":"object"},"AddPackageCostsRequest":{"properties":{"costs":{"description":"Required. Non-empty list of costs; each entry is applied to every listed package","items":{"$ref":"#/components/schemas/CostEntryInput"},"type":"array"},"distribute_by_quantity":{"description":"When true, split each cost across the listed packages in proportion to each package's current quantity (all packages must share the same unit type category). Defaults to false when omitted, applying the full cost to every package. Packages carry their own location, so this endpoint accepts no location scoping","type":"boolean"},"package_ids":{"description":"Required. Non-empty list of package IDs; every one must exist and be accessible to the authenticated company. Each cost is applied to all listed packages, against each package's full current quantity regardless of status","items":{"type":"string"},"type":"array"}},"required":["costs","package_ids"],"title":"Add Package Costs Request","type":"object"},"CogsReportRow":{"description":"A single row of the Cost of Goods Sold report (one sales order line item). Companies on the BioTrack compliance integration do not get the `metrc_production_batch_number` key.\n","properties":{"batch":{"description":"The Distru batch. For a package-tracked item this is the package's batch               number; for a batch-tracked item it is the batch name, with the batch number               appended in parentheses when set (e.g. \"OG Kush (B-0001)\"). `null` when               the item has neither a package nor a batch.","type":"string"},"cost_origin":{"description":"Where a component's cost came from. Always null on this endpoint, since only               the sold-item rows are returned.","type":"string"},"final_input":{"description":"Always `Final` on this endpoint, marking the row as the sold line item. The               per-component `Input` breakdown that can otherwise carry this value is not               exposed here.","type":"string"},"margin_actual":{"description":"Actual profit as a fraction of total price (`total_profits_actual` / `total_price`). Null when the actual cost cannot be traced or the total price is 0.","type":"string"},"margin_default":{"description":"Default profit as a fraction of total price (`total_profits_default` / `total_price`). Null when the default cost cannot be traced or the total price is 0.","type":"string"},"metrc_production_batch_number":{"description":"The package's Metrc production batch number, or `null` when there is               no package or none has been synced. Not present at all for companies on the               BioTrack compliance integration — the key is omitted for them.","type":"string"},"order_number":{"description":"The sales order number. Always returned as a string; one that carries a               significant leading zero (e.g. \"0042\") keeps its full display string so the               zero isn't lost.","type":"string"},"package":{"description":"The package's compliance label, or `null` when the line item is not               package-tracked or the package has no label.","type":"string"},"product_brand":{"description":"The product's brand name, or `null` when the product has no brand assigned.","type":"string"},"product_category":{"description":"The product's category name. Always present — every product has a category.","type":"string"},"product_name":{"description":"The product name.","type":"string"},"profit_unit_actual":{"description":"`unit_price` minus `unit_cost_actual`. Null when the actual cost cannot be traced.","type":"string"},"profit_unit_default":{"description":"`unit_price` minus `unit_cost_default`. Null when the default cost cannot be traced.","type":"string"},"quantity":{"description":"The quantity sold, net of any returned quantity on the line item.","type":"string"},"sku":{"description":"The product SKU.","type":"string"},"total_cost_actual":{"description":"Actual total cost — `unit_cost_actual` multiplied by the row's quantity. Null when `unit_cost_actual` is null.","type":"string"},"total_cost_default":{"description":"Default (standard) total cost — `unit_cost_default` multiplied by the row's quantity. Null when `unit_cost_default` is null.","type":"string"},"total_price":{"description":"The total price (unit price times quantity).","type":"string"},"total_profits_actual":{"description":"Total price minus `total_cost_actual`. Null when the actual cost cannot be traced.","type":"string"},"total_profits_default":{"description":"Total price minus `total_cost_default`. Null when the default cost cannot be traced.","type":"string"},"unit_cost_actual":{"description":"Actual cost per unit — the real cost Distru traces to the inputs and components that produced this inventory (purchase prices, assembly costs, stock-adjustment costs, and so on), per unit. Null when Distru cannot trace a cost for the row.","type":"string"},"unit_cost_default":{"description":"Default (standard) cost per unit — traced the same way as `unit_cost_actual`, but each input/component is valued at its product's configured unit cost instead of its real cost. Null when no cost can be traced.","type":"string"},"unit_price":{"description":"The price per unit.","type":"string"},"unit_type":{"description":"The item's unit type name.","type":"string"}},"required":["total_price","unit_price","unit_type","quantity","product_category","sku","product_name","final_input","order_number"],"title":"CogsReportRow","type":"object"},"CompanyResponse":{"description":"A single company relationship","properties":{"data":{"$ref":"#/components/schemas/Company"}},"title":"Company Response","type":"object"},"Assembly":{"description":"A production job that turns input inventory (ingredients/components) into one or more finished output products — for example packaging bulk flower into units or producing pre-rolls.","properties":{"assembly_number":{"description":"Human-readable reference number for this assembly, shown in Distru (e.g. \"AS-0000001\")","type":"string"},"completion_datetime":{"description":"ISO 8601 datetime the assembly was completed at, or null until its `status` becomes COMPLETED.","type":"string"},"compliance_type":{"description":"Which state compliance system, if any, this assembly reports to. One of METRC, BIOTRACK, or NONE.","type":"string"},"creation_source":{"description":"How this assembly was created.• MANUALLY_CREATED: created by a user in Distru or via the API.• SALES_ORDER: created automatically to repackage inventory while fulfilling a sales order.• SPLIT_PACKAGE: created by splitting an existing package into smaller packages.• LAB_TESTING: created to pull a test sample for lab testing.","enum":["MANUALLY_CREATED","SALES_ORDER","SPLIT_PACKAGE","LAB_TESTING"],"type":"string"},"creator":{"$ref":"#/components/schemas/User"},"custom_data":{"description":"The custom data for this assembly","items":{"$ref":"#/components/schemas/CustomField"},"type":"array"},"description":{"description":"Free-text description of this assembly, or null when none was entered","type":"string"},"estimated_start_datetime":{"description":"ISO 8601 datetime the assembly is expected to start, or null when not set.","type":"string"},"estimated_work_hours":{"description":"The whole-hours part of the estimated work time. Pairs with `estimated_work_minutes` (e.g. 90 minutes of work reads as `estimated_work_hours` 1, `estimated_work_minutes` 30). Null when no estimate is set.","type":"integer"},"estimated_work_minutes":{"description":"The leftover-minutes part (0-59) of the estimated work time, on top of `estimated_work_hours` — not the total minutes. Null when no estimate is set.","type":"integer"},"fulfilled":{"description":"True when every input across all outputs has been fulfilled with a batch or package, false otherwise. A precondition for completing the assembly.","type":"boolean"},"id":{"description":"ID for this assembly","type":"string"},"inserted_datetime":{"description":"The datetime this assembly was created at","type":"string"},"license":{"$ref":"#/components/schemas/License"},"metrc_processing_job":{"$ref":"#/components/schemas/AssemblyMetrcProcessingJob"},"outputs":{"description":"The outputs for this assembly","items":{"$ref":"#/components/schemas/AssemblyOutput"},"type":"array"},"owner_id":{"description":"The ID of the Distru user who owns this assembly, or null when unassigned","type":"string"},"status":{"description":"Where this assembly is in its lifecycle.• PENDING: still in progress — its ingredient inventory is already claimed (each ingredient's `active` quantity is decreased to hold it for this assembly), but its output products have not been produced into inventory yet.• COMPLETED: the assembly has been finished, consuming its claimed ingredient inventory and creating its output products; a completed assembly can no longer be deleted and only a limited set of its fields can be edited.","enum":["PENDING","COMPLETED"],"type":"string"},"updated_datetime":{"description":"The datetime this assembly was last updated at","type":"string"}},"required":["custom_data","outputs","updated_datetime","inserted_datetime","fulfilled","status","creation_source","compliance_type","assembly_number","id"],"title":"Assembly","type":"object"},"PaymentMethodResponse":{"description":"A single Payment Method","properties":{"data":{"$ref":"#/components/schemas/PaymentMethod"}},"title":"Payment Method Response","type":"object"},"LocationResponse":{"description":"A single location wrapped in a data envelope","properties":{"data":{"$ref":"#/components/schemas/Location"}},"title":"Location Response","type":"object"},"SalesByProductReportRow":{"description":"A single row of the Sales By Product report. Companies with Product custom fields will see additional keys.\n","properties":{"category":{"description":"The product's category name. Always present (every product is categorized).","type":"string"},"group":{"description":"The product's group name, or null when the product has none.","type":"string"},"name":{"description":"The product name. Inactive products are prefixed to mark them as such.","type":"string"},"product_owner":{"description":"The product owner's full name, or null when no owner is set.","type":"string"},"quantity_sold":{"description":"Units sold over the date range, net of returns. Can be negative when returns exceed sales.","type":"string"},"sale_price":{"description":"The product's sale price. Always present.","type":"string"},"shipped_from_license":{"description":"The license number the sold items shipped from, or null when none applies.","type":"string"},"sku":{"description":"The product SKU. Always present; an empty string when the product has no SKU. A purely numeric SKU is still returned as a string; one with a significant leading zero keeps its full display string so the zero isn't lost.","type":"string"},"subcategory":{"description":"The product's subcategory name, or null when none is set.","type":"string"},"total_sales":{"description":"Total sales value over the date range, net of returns. Can be negative when returns exceed sales.","type":"string"},"unit_cost":{"description":"The product's unit cost, or null when no cost is recorded.","type":"string"},"unit_type":{"description":"The product's unit type name. Always present.","type":"string"},"upc":{"description":"The product's UPC, or null when the product has no UPC. A purely numeric UPC is still returned as a string; one with a significant leading zero keeps its full display string so the zero isn't lost.","type":"string"},"vendor":{"description":"The product's vendor (supplier) company name. Always present.","type":"string"},"wholesale_price":{"description":"The product's wholesale price, or null when no wholesale price is set.","type":"string"}},"required":["sale_price","vendor","category","unit_type","total_sales","quantity_sold","sku","name"],"title":"SalesByProductReportRow","type":"object"},"Charge":{"description":"An order-level adjustment applied on top of the line items — a charge, a discount, or a tax. It shifts the order/invoice total but is not itself a product line. Whether it adds or subtracts is set by `type`; whether the amount is a flat sum or a percentage is set by `unit_type`.","properties":{"id":{"description":"ID for this charge","type":"string"},"inserted_datetime":{"description":"The datetime this charge was created at","type":"string"},"name":{"description":"Human-readable label for this line (e.g. \"Delivery Fee\").","type":"string"},"percent":{"description":"The rate of this line when `unit_type` is PERCENT, as a decimal string in the range -100 to 100 (discounts are negative, e.g. \"-10\"). Null when `unit_type` is PRICE.","type":"string"},"price":{"description":"The flat money amount of this line, as a 2-decimal string (e.g. \"25.00\"). Always present. Negative for a DISCOUNT.","type":"string"},"tax":{"description":"A tax configured in Distru Settings","properties":{"id":{"description":"ID for this Tax","type":"string"},"name":{"description":"The name of this tax","type":"string"}},"title":"Tax","type":"object"},"type":{"description":"Whether this line adds to or subtracts from the total (SCREAMING_CASE).• CHARGE: adds to the total (e.g. a delivery fee). Tax lines are also returned as CHARGE, with the `tax` object populated.• DISCOUNT: subtracts from the total.","enum":["DISCOUNT","CHARGE"],"type":"string"},"unit_type":{"description":"How this line's amount is expressed (SCREAMING_CASE).• PRICE: a flat money amount; `percent` is null.• PERCENT: a percentage; the rate is carried in `percent`.","enum":["PERCENT","PRICE"],"type":"string"}},"required":["inserted_datetime","price","name","unit_type","type","id"],"title":"Charge","type":"object"},"SalesByUserReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"Definitions for every column in each data row, in order: the `key` matches a field name on the row objects and `label` is its display name.","items":{"$ref":"#/components/schemas/SalesByUserReportColumn"},"type":"array"},"date_range":{"description":"The resolved date range the report covers, as a human-readable label (reflects the `order_datetime` filter, or the default last-30-days window when it was omitted).","type":"string"},"report":{"description":"The report identifier, always `sales_by_user`.","type":"string"}},"required":["columns","date_range","report"],"title":"SalesByUserReportMeta","type":"object"},"InvoiceHistoryReport":{"description":"The Invoice History report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/InvoiceHistoryReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/InvoiceHistoryReportMeta"}},"required":["meta","data"],"title":"InvoiceHistoryReport","type":"object"},"Tag":{"description":"A tag","properties":{"id":{"description":"ID for this tag.","type":"string"},"name":{"description":"The tag's display name. May be null when the tag was created without one.","type":"string"}},"required":["id"],"title":"Tag","type":"object"},"OrderFulfillmentReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The report's column definitions in display order: the fixed product columns, one column per matching order, and the trailing Total Units / Unit Price / Total Value columns.","items":{"$ref":"#/components/schemas/OrderFulfillmentReportColumn"},"type":"array"},"date_range":{"description":"The human-readable date range the report covers (the resolved default window, or the requested `order_datetime` range).","type":"string"},"report":{"description":"The report identifier; always `order_fulfillment` for this endpoint.","type":"string"}},"required":["columns","date_range","report"],"title":"OrderFulfillmentReportMeta","type":"object"},"OrderFulfillmentReport":{"description":"The Order Fulfillment report","properties":{"data":{"description":"The report rows, one per product.","items":{"$ref":"#/components/schemas/OrderFulfillmentReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/OrderFulfillmentReportMeta"}},"required":["meta","data"],"title":"OrderFulfillmentReport","type":"object"},"CompactOrderItem":{"description":"A compact view of an order line item as nested inside another entity in Distru — what was sold, how much, at what price, and which inventory (batch/package) fulfills it.","properties":{"batch":{"$ref":"#/components/schemas/Batch"},"compliance_quantity":{"description":"The quantity of this order item expressed in its package's unit type, as reported to the state compliance system, as a decimal string. Null when the item is not package-tracked.","type":"string"},"id":{"description":"ID for this order item","type":"string"},"is_sample":{"description":"True if this order item is a sample given away rather than sold.","type":"boolean"},"location":{"$ref":"#/components/schemas/LocationCompact"},"package":{"$ref":"#/components/schemas/Package"},"price":{"description":"Price per unit actually charged on this order item — the per-unit price after any line-level price tier discount has been applied, as a decimal string (e.g. \"25.00\"). Equals `price_base` when no discount applied.","type":"string"},"price_base":{"description":"The per-unit list price of this order item before any price tier discount, as a decimal string (e.g. \"30.00\").","type":"string"},"product":{"$ref":"#/components/schemas/Product"},"quantity":{"description":"Quantity sold on this order item, expressed in the product's unit type, as a decimal string (e.g. \"10\")","type":"string"}},"required":["price_base","price","quantity","is_sample","id"],"title":"CompactOrderItem","type":"object"},"SalesOrderItemHistoryReportColumn":{"description":"One column definition for the report. Iterate `meta.columns` to discover the exact set of keys present in each data row for this company (the set varies with the company's compliance integration and configured Order custom fields).\n","properties":{"key":{"description":"The key this column appears under in every data row (e.g. `order_number`).","type":"string"},"label":{"description":"The human-readable column heading (e.g. `Order Number`).","type":"string"}},"required":["label","key"],"title":"SalesOrderItemHistoryReportColumn","type":"object"},"CompanyCompact":{"description":"A lightweight reference to a company — just its identity — embedded on other entities (orders, invoices, products, etc.) to point at the full company without inlining it. The company is a trading partner (a customer or vendor) in your Distru network. Use the id to fetch its full details from the companies endpoint.","properties":{"id":{"description":"ID for this company","example":"3f128a34-cc59-4b49-8883-23bf10e59c6c","type":"string"},"name":{"description":"Human readable name for this company","example":"Retailer 1","type":"string"},"updated_datetime":{"description":"ISO 8601 datetime this company was last updated at","type":"string"}},"required":["updated_datetime","name","id"],"title":"CompanyCompact","type":"object"},"MetrcTags":{"description":"A collection of Metrc tags.\nNote: This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.\n","properties":{"data":{"description":"Metrc Tags","items":{"$ref":"#/components/schemas/MetrcTag"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Metrc Tags","type":"object"},"Role":{"description":"A permission role that determines what a [user](#model-user) can do in Distru.","properties":{"id":{"description":"ID for this role","type":"string"},"name":{"description":"Name of the role (e.g. Admin, Sales)","type":"string"}},"required":["name","id"],"title":"Role","type":"object"},"PdfDownloadUrl":{"description":"The JSON envelope a PDF download endpoint returns when `?format=url` is passed","properties":{"data":{"description":"The signed download URL payload","properties":{"expires_datetime":{"description":"ISO 8601 datetime when the signed `url` expires (e.g. \"2026-08-20T00:00:00Z\")","type":"string"},"url":{"description":"Temporary signed URL to download the PDF. Stops working once `expires_datetime` passes; request the endpoint again for a fresh URL.","type":"string"}},"type":"object"}},"title":"PdfDownloadUrl","type":"object"},"SalesByUserReportColumn":{"description":"Definition of one column in the report, pairing its data-row key with a display label.","properties":{"key":{"description":"The key used for this column in each data row; matches a field name on the row objects (for example `leaderboard_rank`, `sales_pre_tax`).","type":"string"},"label":{"description":"The human-readable label of the column, suitable for a table header (for example `Sales (Pre-Tax)`).","type":"string"}},"required":["label","key"],"title":"SalesByUserReportColumn","type":"object"},"InventoryTransactionHistoryReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The report's column definitions, in order, each mapping a row key to its               human-readable label. For BioTrack companies this list omits the               `metrc_unit_name` and `metrc_production_batch_number` columns, matching the row               shape.","items":{"$ref":"#/components/schemas/InventoryTransactionHistoryReportColumn"},"type":"array"},"date_range":{"description":"The human-readable date range the report covers, reflecting the resolved               `datetime` filter (or the default last-30-days window when it was omitted).","type":"string"},"report":{"description":"The report identifier, always `inventory_transaction_history`.","type":"string"}},"required":["columns","date_range","report"],"title":"InventoryTransactionHistoryReportMeta","type":"object"},"OrderItemRequest":{"description":"Order item params","properties":{"batch_id":{"description":"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.","type":"string"},"compliance_quantity":{"description":"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.","type":"number"},"id":{"description":"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.","type":"string"},"is_sample":{"description":"Marks this line as a sample rather than a normal sale. Defaults to false when omitted.","type":"boolean"},"location_id":{"description":"The location this line item is fulfilled from, as a Distru location ID. Optional.","type":"string"},"note":{"description":"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.","type":"string"},"package_id":{"description":"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.","type":"string"},"price_base":{"description":"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`.","type":"number"},"price_tier_mode":{"description":"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)).","enum":["AUTO","OVERRIDE","NONE"],"type":"string"},"price_tier_version_id":{"description":"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.","type":"string"},"product_id":{"description":"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`.","type":"string"},"quantity":{"description":"Quantity used on this order item, expressed in the product's unit type","type":"number"}},"required":["price_base","quantity"],"title":"OrderItem (Request)","type":"object"},"PurchasesByCompanyReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"Ordered column definitions for the rows in `data`, including any appended CompanyRelationship custom-field columns. Always present.\n","items":{"$ref":"#/components/schemas/PurchasesByCompanyReportColumn"},"type":"array"},"date_range":{"description":"Human-readable label of the date range the report covers, derived from the `order_datetime` filter, or the default last-30-days window when that filter is omitted. Always present.\n","type":"string"},"report":{"description":"The report identifier, always `purchases_by_company`.","type":"string"}},"required":["columns","date_range","report"],"title":"PurchasesByCompanyReportMeta","type":"object"},"SalesByProductReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The report's column definitions in order, including any appended Product custom-field columns. Use these keys to read each data row.","items":{"$ref":"#/components/schemas/SalesByProductReportColumn"},"type":"array"},"date_range":{"description":"The resolved date range the report covers, as a display string (reflects the last-30-days default when `order_datetime` was omitted).","type":"string"},"report":{"description":"The report identifier, always `sales_by_product`.","type":"string"}},"required":["columns","date_range","report"],"title":"SalesByProductReportMeta","type":"object"},"SalesOrderHistoryReport":{"description":"The Sales Order History report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/SalesOrderHistoryReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/SalesOrderHistoryReportMeta"}},"required":["meta","data"],"title":"SalesOrderHistoryReport","type":"object"},"SalesByProductReport":{"description":"The Sales By Product report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/SalesByProductReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/SalesByProductReportMeta"}},"required":["meta","data"],"title":"SalesByProductReport","type":"object"},"CostEntryInput":{"description":"A single cost to apply to a record","properties":{"cost_per_unit":{"description":"Per-unit amount as a decimal (e.g. \"10.00\"). When omitted, the cost type's own cost per unit is used. Must be omitted for cost types with a locked cost per unit (those that don't allow inline editing) — sending it for such a type is rejected. It is only required when an inline-editable cost type has no cost per unit of its own","type":"number"},"cost_type_id":{"description":"Required. The cost type to apply, given as its ID from GET /public/v1/cost-types. Must exist and be accessible to the authenticated company","type":"string"},"description":{"description":"Optional free-form text stored on the cost","type":"string"},"quantity":{"description":"Required. How many units of the cost type to apply, as a decimal (e.g. \"2.5\"). Must be greater than 0. The amount added to each record's cost basis is cost_per_unit × quantity","type":"number"}},"required":["quantity","cost_type_id"],"title":"Cost Entry","type":"object"},"OrderChargeRequest":{"description":"Order charge params","properties":{"id":{"description":"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.","type":"string"},"name":{"description":"The label for this line (e.g. \"Delivery Fee\"), shown on the order and its invoice. Required.","type":"string"},"percent":{"description":"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.","type":"number"},"price":{"description":"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`.","type":"number"},"tax_id":{"description":"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. 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":"string"},"type":{"description":"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.","enum":["CHARGE","DISCOUNT"],"type":"string"},"unit_type":{"description":"How the line is measured: PERCENT (a percentage of the subtotal, set via `percent`) or PRICE (a flat amount, set via `price`). SCREAMING_CASE.","enum":["PERCENT","PRICE"],"type":"string"}},"required":["unit_type","name"],"title":"OrderCharge (Request)","type":"object"},"AddBatchCostsRequest":{"properties":{"batch_ids":{"description":"Required. Non-empty list of batch IDs; every one must exist, be accessible to the authenticated company, and belong to a batch-tracked product. Each cost is applied to all listed batches","items":{"type":"string"},"type":"array"},"costs":{"description":"Required. Non-empty list of costs; each entry is applied to every listed batch","items":{"$ref":"#/components/schemas/CostEntryInput"},"type":"array"},"distribute_by_quantity":{"description":"When true, split each cost across the listed batches in proportion to each batch's active quantity (all batches must share the same unit type category). Defaults to false when omitted, applying the full cost to every batch","type":"boolean"},"location_ids":{"description":"Optional list of location IDs scoping which locations' stock the cost applies to. Omit to apply across all locations; an explicit empty list is rejected","items":{"type":"string"},"type":"array"}},"required":["costs","batch_ids"],"title":"Add Batch Costs Request","type":"object"},"ErrorResponse":{"description":"The envelope returned by every failed request (400, 401, 403, 404, 429). `errors` always holds at least one entry.","properties":{"errors":{"description":"One or more errors describing why the request failed.","items":{"$ref":"#/components/schemas/Error"},"type":"array"}},"required":["errors"],"title":"ErrorResponse","type":"object"},"OrderFulfillmentReportRow":{"description":"A single row of the Order Fulfillment report (one product). In addition to the keys below, each row carries one dynamic key per matching order, named after the slugified order number (e.g. `so_1042`), whose value is the net quantity of this product on that order (order-item quantity minus returns), or an empty string (`\"\"`) when this product was not on that order.\n","properties":{"category":{"description":"The product's category name, or null when the product has no category.","type":"string"},"group":{"description":"The product's group name, or null when the product has no group.","type":"string"},"product":{"description":"The product name; inactive products carry a prefix marking them inactive. Always present — this is the pivot key each row is built around.","type":"string"},"subcategory":{"description":"The product's subcategory name, or null when the product has no subcategory.","type":"string"},"total_units":{"description":"Total units of this product sold across the matching orders, net of returns (summed order-item quantities minus return quantities).","type":"string"},"total_value":{"description":"Total value of this product across the matching orders, computed as the product's base `unit_price` × `total_units`. Because it uses the base unit price rather than the actual negotiated line prices, it may differ from the orders' real revenue when the orders applied custom pricing or discounts.","type":"string"},"unit_price":{"description":"The product's configured base unit price — taken from the product itself, not from any individual order line — so it is the same regardless of per-order pricing or discounts.","type":"string"}},"required":["total_value","unit_price","total_units","product"],"title":"OrderFulfillmentReportRow","type":"object"},"Invoices":{"description":"A collection of Invoices","properties":{"data":{"description":"Invoices","items":{"$ref":"#/components/schemas/Invoice"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Invoices","type":"object"},"PackageFullResponse":{"description":"A single package wrapped in a data envelope","properties":{"data":{"$ref":"#/components/schemas/PackageFull"}},"title":"PackageFull Response","type":"object"},"TestResultResponse":{"description":"A single test result envelope","properties":{"data":{"$ref":"#/components/schemas/TestResult"}},"title":"Test Result Response","type":"object"},"InvoiceHistoryReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"Ordered definitions of every column in each `data` row, including the compliance and custom-field columns that vary by company. Use this to discover the exact keys present.","items":{"$ref":"#/components/schemas/InvoiceHistoryReportColumn"},"type":"array"},"date_range":{"description":"Human-readable invoice-date window the report covers, reflecting either the `invoice_datetime` filter or the default last-30-days window when it was omitted.","type":"string"},"report":{"description":"The report identifier, always `invoice_history`","type":"string"}},"required":["columns","date_range","report"],"title":"InvoiceHistoryReportMeta","type":"object"},"SalesByCompanyReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The report's column definitions in order, including any custom-field columns appended for this company. The `key` of each entry matches the property name used in every `data` row.","items":{"$ref":"#/components/schemas/SalesByCompanyReportColumn"},"type":"array"},"date_range":{"description":"The human-readable resolved date range the report covers (in the API user's timezone), reflecting the default 30-day window when no `order_datetime` filter was supplied.","type":"string"},"report":{"description":"The report identifier, always `sales_by_company`","type":"string"}},"required":["columns","date_range","report"],"title":"SalesByCompanyReportMeta","type":"object"},"SalesOrderItemHistoryReportRow":{"description":"A single row of the Sales Order Item History report (one sales order line item). The properties below are the columns every company receives. Companies on a compliance integration receive extra keys that are not declared above — their exact set, and the manifest column's key name, vary by company, so read `meta.columns` for the authoritative list. Those compliance keys are: `package_label`, `package_batch_number`, `package_expiration_date`, `package_harvest_date`, the potency keys `thc`, `thc_mg_g`, `thc_mg_ml`, `total_thc`, `total_thc_mg_g`, `total_thc_mg_ml`, `cbd`, `cbd_mg_g`, `cbd_mg_ml`, `total_cbd`, `total_cbd_mg_g`, `total_cbd_mg_ml` (the percentage keys and the `mg_*` keys are all decimal strings), `shipped_from_license`, and a manifest-number key named for the active compliance system (`metrc_manifest_number` or `biotrack_manifest_number`). Any Order custom fields configured for the company are also appended, keyed by the slugified field label. Every compliance and custom-field key is null when it has no value for a row, so null-check any key outside the declared set.\n","properties":{"vendor_id":{"description":"The vendor's ID.","type":"string"},"default_unit_cost":{"description":"The product's default unit cost as a string, or `null` if unset. This is the product default, not the line item's negotiated cost.","type":"string"},"category":{"description":"The product's category.","type":"string"},"order_number":{"description":"The order's human-readable order number. Always returned as a string; one with a significant leading zero (e.g. `\"0042\"`) is preserved as-is so the zero isn't lost.","type":"string"},"product_sku":{"description":"The product's SKU, or an empty string if none is set.","type":"string"},"due_date_utc":{"description":"The same due date in UTC, or `null` if the order has no due date.","type":"string"},"customer":{"description":"The order's customer name, or `null` if the order has no customer.","type":"string"},"order_date":{"description":"The order date, formatted in the company's timezone.","type":"string"},"customer_id":{"description":"The customer's ID, or `null` if the order has no customer.","type":"string"},"order_id":{"description":"The ID of the order this line item belongs to.","type":"string"},"delivery_date":{"description":"The order's delivery date in the company's timezone, or `null` if the order has no delivery date.","type":"string"},"batch_number":{"description":"The batch number the line item was sourced from, or `null` if not batch-sourced.","type":"string"},"delivery_date_utc":{"description":"The same delivery date in UTC, or `null` if the order has no delivery date.","type":"string"},"order_date_utc":{"description":"The same order date, formatted in UTC.","type":"string"},"returned_quantity":{"description":"The quantity returned on this line item as a string; 0 when nothing was returned.","type":"string"},"default_unit_price":{"description":"The product's default unit price as a string.","type":"string"},"source_package":{"description":"The compliance label of the source package this line item's package was repackaged from, or `null` when it was not repackaged from another package.","type":"string"},"status":{"description":"The order's status as a SCREAMING_CASE token.","enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"},"sales_rep":{"description":"The order owner's name, treated as the sales rep; `null` if unassigned.","type":"string"},"order_item_price":{"description":"The line item's unit price as a string.","type":"string"},"due_date":{"description":"The order's due date in the company's timezone, or `null` if the order has no due date.","type":"string"},"brand_id":{"description":"The brand's ID, or `null` if the product has no brand.","type":"string"},"invoice_numbers":{"description":"Comma-separated invoice numbers linked to this line item's order, sorted for a stable order; `null` if the order has no invoices.","type":"string"},"quantity":{"description":"The line item's ordered quantity as a string.","type":"string"},"brand":{"description":"The product's brand name, or `null` if the product has no brand.","type":"string"},"default_wholesale_price":{"description":"The product's default wholesale price as a string, or `null` if unset.","type":"string"},"vendor":{"description":"The product's vendor name.","type":"string"},"group":{"description":"The product's group, or `null` if the product is in no group.","type":"string"},"upc":{"description":"The product's UPC, or `null` if none is set.","type":"string"},"product_id":{"description":"The product's ID.","type":"string"},"subcategory":{"description":"The product's subcategory, or `null` if none.","type":"string"},"product":{"description":"The product's name. Inactive products carry a prefix on the name.","type":"string"},"line_item_id":{"description":"The line item's ID.","type":"string"}},"required":["category","quantity","order_item_price","vendor_id","vendor","default_unit_price","product_id","product","status","order_number","order_date_utc","order_date","order_id","line_item_id"],"title":"SalesOrderItemHistoryReportRow","type":"object"},"InventoryAssetsReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"Ordered column definitions for exactly the columns present in this response. The granular-only and cost columns are included here only when they appear in `data`, so this is the authoritative list of keys to expect on each row.","items":{"$ref":"#/components/schemas/InventoryAssetsReportColumn"},"type":"array"},"date_range":{"description":"Resolved snapshot instant the report was computed for, formatted for display in the company's timezone. Reflects the `datetime` parameter, or now when it was omitted.","type":"string"},"report":{"description":"Report identifier; always `inventory_assets`.","type":"string"}},"required":["columns","date_range","report"],"title":"InventoryAssetsReportMeta","type":"object"},"CreditResponse":{"description":"A single credit wrapped in a data envelope","properties":{"data":{"$ref":"#/components/schemas/Credit"}},"title":"Credit Response","type":"object"},"Bin":{"description":"A bin used to track where inventory is physically stored","properties":{"id":{"description":"ID for this bin","type":"string"},"inserted_datetime":{"description":"When the bin was created (UTC ISO-8601)","type":"string"},"name":{"description":"The name of the bin","type":"string"},"updated_datetime":{"description":"When the bin was last updated (UTC ISO-8601)","type":"string"}},"required":["updated_datetime","inserted_datetime","name","id"],"title":"Bin","type":"object"},"AssemblyMetrcProcessingJobWaste":{"description":"Waste recorded when a Metrc processing job is finished","properties":{"count_quantity":{"description":"Count-based waste, as a decimal string (e.g. \"5\"). Reported to Metrc as TotalCountWaste. Null when no count waste was recorded.","type":"string"},"count_unit_name":{"description":"The Metrc unit name for `count_quantity` (e.g. \"Each\"), or null when `count_quantity` is null","type":"string"},"volume_quantity":{"description":"Volume-based waste, as a decimal string (e.g. \"10.5\"). Reported to Metrc as TotalVolumeWaste. Null when no volume waste was recorded.","type":"string"},"volume_unit_name":{"description":"The Metrc unit name for `volume_quantity` (e.g. \"Milliliters\"), or null when `volume_quantity` is null","type":"string"},"weight_quantity":{"description":"Weight-based waste, as a decimal string (e.g. \"2.75\"). Reported to Metrc as TotalWeightWaste. Null when no weight waste was recorded.","type":"string"},"weight_unit_name":{"description":"The Metrc unit name for `weight_quantity` (e.g. \"Grams\"), or null when `weight_quantity` is null","type":"string"}},"title":"Assembly Metrc Processing Job Waste","type":"object"},"ProductSubcategoryResponse":{"description":"A single product subcategory","properties":{"data":{"$ref":"#/components/schemas/ProductSubcategory"}},"title":"Product Subcategory Response","type":"object"},"CustomFieldDefinition":{"description":"A custom field definition","properties":{"description":{"description":"Free-text note describing the field, or null when none was set.","type":"string"},"disabled_field_options":{"description":"The subset of `field_options` that have been turned off. Applies only to `dropdown` and `checkbox` fields; always empty for `text` and `date` fields. A disabled option can no longer be selected on new or edited records, but it stays in `field_options` and is listed here so that historical records already holding the value continue to display it. Every value here also appears in `field_options`. Example: a dropdown with `field_options` `[\"Small\", \"Medium\", \"Large\"]` and `disabled_field_options` `[\"Medium\"]` keeps showing \"Medium\" on records saved with it, but \"Medium\" is no longer offered when picking a value. Always present (an empty array when nothing is disabled). Set it on create or update via the `disabled_field_options` request field.","items":{"type":"string"},"type":"array"},"field_options":{"description":"The selectable values for `dropdown` and `checkbox` fields, in display order; an empty array for `text` and `date` fields. For a `checkbox` field a record may hold several of these; for a `dropdown` at most one.","items":{"type":"string"},"type":"array"},"field_type":{"description":"The kind of value this field stores: `text` (free text), `date` (a calendar date), `dropdown` (a single choice from `field_options`), or `checkbox` (one or more choices from `field_options`). Fixed at creation. Always present.","type":"string"},"filterable":{"description":"Whether records of this entity type can be filtered by this field's value. Always false for `text` and `date` fields. Always present.","type":"boolean"},"id":{"description":"The field's numeric id. This is the key used to read and write the field's value inside a record's `custom_data` map. Always present.","type":"integer"},"name":{"description":"Display name of the field. Unique (case-insensitively) among the fields on the same `parent_object` for the company. Always present.","type":"string"},"parent_object":{"description":"The entity type this field is attached to. One of: `assembly`, `batch`, `company`, `contact`, `invoice`, `order`, `package`, `product`, `purchase`, `request`, `return`, `shipment`, `stock_transfer`, `task`.","type":"string"},"required":{"description":"Whether a value must be supplied when a record of this entity type is saved in the Distru app. Always present.","type":"boolean"}},"required":["disabled_field_options","required","filterable","field_type","name","id"],"title":"CustomFieldDefinition","type":"object"},"PriceTierResponse":{"description":"A single price tier","properties":{"data":{"$ref":"#/components/schemas/PriceTier"}},"title":"Price Tier Response","type":"object"},"Orders":{"description":"A collection of Orders","properties":{"data":{"description":"Orders","items":{"$ref":"#/components/schemas/Order"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Orders","type":"object"},"CultivationTransactionHistoryReportRow":{"description":"A single row of the Cultivation Transaction History report (one cultivation transaction). Values mirror the report's CSV export. `type` is a human-readable display string (e.g. `Move Plant(s)`), not a SCREAMING_CASE enum token; `related_entity_status` is the underlying teardown workflow status (`PREPARING`, `PENDING`, or `COMPLETED`) passed through as stored rather than remapped through the public API's enum layer. The `total_cost` key is present only for API keys whose user can view costs, and is omitted entirely otherwise.\n","properties":{"amount":{"description":"The signed transaction amount. Positive for gains, negative for downward adjustments (e.g. plants removed via a Plant Batch Adjustment or Split Plant Batch). Additive applications distribute the applied quantity across their sibling rows so the amounts sum to the total applied. Expressed in `unit`.","type":"string"},"batch_name":{"description":"The plant batch (plant group) name, or null for rows not tied to a specific batch.","type":"string"},"date":{"description":"The transaction date, formatted `MM/DD/YYYY` in the company's timezone (not an ISO8601 timestamp). Always present.","type":"string"},"description":{"description":"The transaction's free-text description, or null when none was recorded.","type":"string"},"package_label_s":{"description":"Comma-separated Metrc package compliance label(s) tied to the transaction, or null when no package is involved.","type":"string"},"plant_tag_s":{"description":"Comma-separated Metrc plant tag(s) involved, ordered by tag. Null for Plant Batch Creation rows and for any row with no individual plant tags.","type":"string"},"product_name":{"description":"The product name associated with the transaction, or null when none applies.","type":"string"},"related_entity":{"description":"The name of the teardown or harvest this transaction belongs to, or null for transaction types with no related entity. For harvest-created-from-teardown rows this is the harvest name, falling back to the teardown name.","type":"string"},"related_entity_status":{"description":"The workflow status of the related teardown — one of `PREPARING`, `PENDING`, or `COMPLETED` — passed through as stored rather than remapped through the public API's enum layer. Populated only for teardown-backed rows (a teardown itself, or a Harvest Created from Teardown). Null when there is no related entity, and also null for harvest-only rows such as Create Package from Harvest, whose `related_entity` names the harvest but which carry no teardown status.","type":"string"},"strain":{"description":"The strain name of the plant batch, or null for rows not tied to a strain (e.g. some harvest- or teardown-level rows).","type":"string"},"total_cost":{"description":"The transaction's total cost, in the company's currency. Present only for API keys whose user can view costs (the key is omitted for everyone else — check `meta.columns`). May be null when no cost is associated with the transaction.","type":"string"},"type":{"description":"The transaction's display type, e.g. `Move Plant(s)`, `Destroy Plant`, `Package Plant Batch`, or `Record Waste (Harvest)`. Growth phase changes append the transition, e.g. `Growth Phase Change (Immature → Vegetative)`. Human-readable names (not SCREAMING_CASE tokens); the same values the `transaction_type` filter accepts. Always present.","type":"string"},"unit":{"description":"The unit the `amount` is expressed in (e.g. `Unit` for whole-plant counts, or a product/harvest unit-type name such as `Grams`). May be null when no unit type applies.","type":"string"}},"required":["type","date"],"title":"CultivationTransactionHistoryReportRow","type":"object"},"BatchFull":{"description":"A lot of a product with all its details — a group of inventory sharing a harvest/production run, expiration date, potency, lab results, and cost. Used for batch-tracked products.","properties":{"batch_number":{"description":"The batch number for this batch, or null when none is set","type":"string"},"bins":{"description":"The bins this batch is stored in. Only present when bin inventory tracking is enabled for the company.","items":{"$ref":"#/components/schemas/BinCompact"},"type":"array"},"cbd":{"description":"A free-form CBD value set directly on the batch record. This is a static label, independent of any lab result — the batch's `primary_test_result` may report different potency values (e.g. `cbd_percentage`). Null if unset.","type":"string"},"cost_per_unit_actual":{"description":"Actual cost per unit — `total_cost_actual` divided by the batch quantity. Returned only when the request passes `include_costs=true` and the batch has on-hand quantity; the field is absent otherwise.","type":"string"},"cost_per_unit_default":{"description":"Default (standard) cost per unit — `total_cost_default` divided by the batch quantity. Returned only when the request passes `include_costs=true` and the batch has on-hand quantity; the field is absent otherwise.","type":"string"},"creator":{"$ref":"#/components/schemas/User"},"custom_data":{"description":"The custom data for this batch","items":{"$ref":"#/components/schemas/CustomField"},"type":"array"},"deleted_at":{"description":"ISO 8601 datetime this batch was soft-deleted at, or null when the batch has not been deleted","type":"string"},"description":{"description":"Free-text description of this batch, or null when none was entered","type":"string"},"expiration_datetime":{"description":"ISO 8601 datetime this batch expires, or null when none is set.","type":"string"},"harvest_datetime":{"description":"ISO 8601 datetime this batch was harvested, or null when none is set","type":"string"},"id":{"description":"ID for this batch","type":"string"},"inserted_datetime":{"description":"The datetime this batch was created (ISO 8601)","type":"string"},"manufactured_datetime":{"description":"ISO 8601 datetime this batch was manufactured. Defaults to the batch's creation time when none is supplied, so it is always present.","type":"string"},"name":{"description":"Human readable name for this batch","type":"string"},"owner_id":{"description":"The ID of the Distru user who owns this batch, or null when unassigned","type":"string"},"primary_test_result":{"$ref":"#/components/schemas/PrimaryTestResult"},"product":{"$ref":"#/components/schemas/Product"},"product_id":{"description":"The ID of the [product](#model-product) this batch is a lot of","type":"string"},"quantity_active":{"description":"Total active on-hand quantity of this batch across all locations, as a decimal string (e.g. \"100\"). Sums the batch's active stock — positive quantity held at a location. Equals the sum of the per-location amounts in `quantity_active_by_location`, so the two always reconcile. \"0\" when the batch has no active stock. Reserved stock is still physically on-hand, so it is included here. Sold stock and in-transit stock (held by a user rather than a location) is excluded.","type":"string"},"quantity_active_by_location":{"description":"The batch's active on-hand quantity broken down by location — one entry per location holding active stock, ordered by location id. Empty array when the batch has no active stock. The entries sum to `quantity_active`.","items":{"$ref":"#/components/schemas/QuantityActiveByLocation"},"type":"array"},"thc":{"description":"A free-form THC value set directly on the batch record. This is a static label, independent of any lab result — the batch's `primary_test_result` may report different potency values (e.g. `thc_percentage`). Null if unset.","type":"string"},"total_cost_actual":{"description":"Total actual cost of this batch. Distru traces the inputs and components that produced the batch and sums the real costs incurred along that chain — for example the price paid when a component was purchased, assembly costs, and costs added by stock adjustments, among others. Returned only when the request passes `include_costs=true` and the batch has on-hand quantity; the field is absent otherwise.","type":"string"},"total_cost_default":{"description":"Total default (standard) cost of this batch. Traced the same way as `total_cost_actual`, but each input/component is valued at its product's configured unit cost (the product's `unit_cost`) instead of its real cost. Returned only when the request passes `include_costs=true` and the batch has on-hand quantity; the field is absent otherwise.","type":"string"},"updated_datetime":{"description":"The datetime this batch was last modified (ISO 8601)","type":"string"}},"required":["custom_data","quantity_active_by_location","quantity_active","product","product_id","updated_datetime","inserted_datetime","manufactured_datetime","name","id"],"title":"BatchFull","type":"object"},"PaymentTerm":{"description":"The agreed timeframe a customer has to pay — for example \"Net 30\" means payment is due 30 days after the invoice.","properties":{"days":{"description":"Number of days after the invoice date until payment is due. 0 means due the same day.","type":"integer"},"id":{"description":"ID for this payment term","type":"string"},"inserted_datetime":{"description":"The datetime this payment term was created at","type":"string"},"locked":{"description":"True when this is a built-in Distru default payment term. A locked term cannot be deleted and only its `time_of_day` can be edited; an unlocked one is fully editable.","type":"boolean"},"name":{"description":"Name of the payment term. Unique per company (case-insensitive).","example":"Net 30","type":"string"},"time_of_day":{"description":"Time of day on the due date that payment is due, as \"HH:MM:SS\" (e.g. \"17:00:00\"). Defaults to \"17:00:00\".","example":"17:00:00","type":"string"},"updated_datetime":{"description":"The datetime this payment term was last updated at","type":"string"}},"required":["updated_datetime","inserted_datetime","locked","time_of_day","days","name","id"],"title":"PaymentTerm","type":"object"},"Drivers":{"description":"A collection of drivers","properties":{"data":{"description":"Drivers","items":{"$ref":"#/components/schemas/Driver"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Drivers","type":"object"},"HarvestOutputsReportRow":{"description":"A single row of the Harvest Outputs report (one input, output, or cost line item of a harvest assembly). The cost keys (`unit_cost_actual`, `unit_cost_default`, `total_cost_actual`, `total_cost_default`, `cost_type`, `cost_type_description`) are omitted for users without permission to view costs.\n","properties":{"cost_input_output":{"description":"The line item type: `Input` (harvested material consumed), `Output` (product produced), or `Cost` (a cost line item on an output). Determines which of the type-specific fields below are populated. Always present.","type":"string"},"cost_type":{"description":"The cost type name. Populated only on `Cost` rows; null on `Input` and `Output` rows. Omitted entirely (key absent) for users without permission to view costs.","type":"string"},"cost_type_description":{"description":"The cost line item's description. Populated only on `Cost` rows; null on `Input` and `Output` rows. Omitted entirely (key absent) for users without permission to view costs.","type":"string"},"distru_product":{"description":"The Distru product name — the harvest name on `Input` rows, the output's product on `Output` rows. Null on `Cost` rows.","type":"string"},"harvest_assembly_date":{"description":"The assembly's creation date, formatted `MM/DD/YYYY` in the requesting user's timezone (e.g. `01/15/2026`). Always present.","type":"string"},"harvest_assembly_number":{"description":"The assembly's human-readable number. Always present.","type":"string"},"harvest_name":{"description":"The harvest name. For an input row this is that input's harvest; for output and cost rows it is the assembly's first input's harvest. Null when the harvest has no name.","type":"string"},"line_item_id":{"description":"Identifies the specific line item this row represents (the input, output, or cost line item, matching `cost_input_output`). Always present.","type":"string"},"location":{"description":"The location name — the input's location on `Input` rows, the output's location on `Output` and `Cost` rows. Null when no location is set.","type":"string"},"output_batch_number":{"description":"The output's batch number. Populated only on `Output` rows; null on `Input` and `Cost` rows.","type":"string"},"output_package_number":{"description":"The output package's compliance label (its package tag in the state traceability system — Metrc or BioTrack), falling back to the output's Metrc label. Populated only on `Output` rows; null on `Input` and `Cost` rows, and null on `Output` rows that carry neither a package compliance tag nor a Metrc label.","type":"string"},"output_reference_id":{"description":"The ID of the output this cost line item belongs to. Populated only on `Cost` rows; null on `Input` and `Output` rows.","type":"string"},"product_category":{"description":"The output product's category. Populated only on `Output` rows; null on `Input` and `Cost` rows.","type":"string"},"quantity":{"description":"The line item's quantity in `unit_type`: the input quantity consumed, the output quantity produced, or the cost line item's quantity. For package-tracked outputs this is the compliance (Metrc) quantity when available, otherwise the output's own quantity. Always present.","type":"string"},"status":{"description":"The assembly status: `PENDING` (not yet completed) or `COMPLETED`. Same value on every row of the assembly. SCREAMING_CASE. Always present.","enum":["PENDING","COMPLETED"],"type":"string"},"strain":{"description":"The strain name (the harvest's Metrc strain). Sourced like `harvest_name`. Null when the harvest has no strain recorded.","type":"string"},"total_cost_actual":{"description":"Actual total cost — `unit_cost_actual` multiplied by the row's quantity. Null when no cost is traced for the row. Omitted entirely (key absent) for users without permission to view costs.","type":"string"},"total_cost_default":{"description":"Default (standard) total cost — `unit_cost_default` multiplied by the row's quantity. Null when no cost is traced for the row. Omitted entirely (key absent) for users without permission to view costs.","type":"string"},"unit_cost_actual":{"description":"Actual cost per unit — the real cost Distru traces to the inputs and components that produced this inventory (purchase prices, assembly costs, stock-adjustment costs, and so on), per unit. Null when no cost is traced for the row. Omitted entirely (key absent) for users without permission to view costs.","type":"string"},"unit_cost_default":{"description":"Default (standard) cost per unit — traced the same way as `unit_cost_actual`, but each input/component is valued at its product's configured unit cost instead of its real cost. Null when no cost is traced for the row. Omitted entirely (key absent) for users without permission to view costs.","type":"string"},"unit_type":{"description":"The unit of measure for `quantity` (e.g. the harvest's unit on inputs, the product's unit on outputs, the cost type's unit on costs). Null when no unit applies.","type":"string"}},"required":["line_item_id","quantity","cost_input_output","status","harvest_assembly_number","harvest_assembly_date"],"title":"HarvestOutputsReportRow","type":"object"},"PriceTiers":{"description":"A collection of price tiers","properties":{"data":{"description":"Price Tiers","items":{"$ref":"#/components/schemas/PriceTier"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Price Tiers","type":"object"},"AssemblyCost":{"description":"A cost added directly to an assembly output (e.g. labor or packaging), as shown in Distru","properties":{"cost_per_unit":{"description":"The per-unit rate applied, as a decimal string (e.g. \"10.50\"). Null when no rate is set.","type":"string"},"description":{"description":"Free-text description of this cost, or null when none was entered","type":"string"},"id":{"description":"ID for this assembly cost","type":"string"},"name":{"description":"The name of the assembly cost","type":"string"},"quantity":{"description":"How many units of the cost type this line applies. Multiply by `cost_per_unit` to get the line's total. Decimal string, e.g. \"2\".","type":"string"},"unit_type":{"$ref":"#/components/schemas/UnitType"}},"required":["quantity","name","id"],"title":"AssemblyCost","type":"object"},"PurchaseOrderHistoryReport":{"description":"The Purchase Order History report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/PurchaseOrderHistoryReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/PurchaseOrderHistoryReportMeta"}},"required":["meta","data"],"title":"PurchaseOrderHistoryReport","type":"object"},"UnitTypeFull":{"description":"A unit type","properties":{"active":{"description":"Whether this unit type is active and selectable when choosing a unit of measure. Always present; inactive unit types are hidden from most pickers but are still returned by the read endpoints.","type":"boolean"},"category":{"description":"Physical dimension this unit measures. Populated only for Distru's built-in standard units (`locked` is true); null for custom units you define, which have no physical dimension. Always populated together with `qty_per_si_unit` — either both are set or both are null.","enum":["COUNT","VOLUME","WEIGHT"],"type":"string"},"id":{"description":"ID of this unit type. Stable across renames; use it as the `id` for the fetch endpoint.","type":"string"},"inserted_datetime":{"description":"When the unit type was created, as a UTC ISO-8601 timestamp (e.g. `\"2026-08-20T14:30:00Z\"`). Always present.","type":"string"},"locked":{"description":"Whether this unit type is one of Distru's built-in standard units, which cannot be renamed or deleted. Always present. When true, `category` and `qty_per_si_unit` are populated; when false (a custom unit you defined) both are null.","type":"boolean"},"name":{"description":"Display name of the unit type (e.g. `\"Gram\"`, `\"Each\"`). Always present and unique within your company (case-insensitive).","type":"string"},"qty_per_si_unit":{"description":"Decimal string giving how many of this unit make up one SI base unit of its `category`. For WEIGHT the SI base unit is the kilogram (a Gram is `\"1000\"`, a Pound is ~`\"2.20462\"`). For VOLUME the SI base unit is the liter (a Milliliter is `\"1000\"`, a Gallon is ~`\"0.264172\"`). For COUNT it is `\"1\"`, since a count unit has no physical measure. Populated only for Distru's built-in standard units (`locked` is true); null for custom units, and always null exactly when `category` is null.","type":"string"},"updated_datetime":{"description":"When the unit type was last modified, as a UTC ISO-8601 timestamp. Equal to `inserted_datetime` until the unit type is first updated. Always present.","type":"string"}},"required":["updated_datetime","inserted_datetime","locked","active","name","id"],"title":"UnitType","type":"object"},"PaymentCredit":{"description":"A compact representation of a credit related to a payment","properties":{"amount":{"description":"The credit's current remaining amount as a decimal string (e.g. `\"50.00\"`); reflects credit already spent, not the original issued amount. Always present.","type":"string"},"credit_number":{"description":"The credit number as shown in the Distru UI. Always present.","type":"string"},"id":{"description":"ID for this credit. Always present.","type":"string"},"source":{"description":"How the credit originated. INVOICE_PAYMENT (from an invoice overpayment), RETURN (from a return), USER (manually created), or QB_CREDIT_MEMO / QB_PAYMENT (synced from QuickBooks Online). Always present.","enum":["INVOICE_PAYMENT","QB_CREDIT_MEMO","QB_PAYMENT","RETURN","USER"],"type":"string"}},"required":["source","amount","credit_number","id"],"title":"PaymentCredit","type":"object"},"PaymentMethod":{"description":"A way payments are made or received (e.g. Cash, Check, Credit Card, Bank Transfer).","properties":{"active":{"description":"True when this payment method is active and can be selected on new payments. Defaults to false.","type":"boolean"},"deleted_at":{"description":"ISO 8601 datetime this payment method was soft-deleted, or null when it has not been deleted","type":"string"},"id":{"description":"ID for this payment method","type":"string"},"inserted_datetime":{"description":"The datetime this payment method was created at","type":"string"},"name":{"description":"Name of the payment method (e.g. \"Cash\")","type":"string"},"qb_payment_method_id":{"description":"The ID of the matching payment method in QuickBooks Online, or null when this payment method is not mapped to one","type":"string"},"type":{"description":"The payment method type (SCREAMING_CASE). One of CASH, CHECK, CREDIT_CARD, BANK_REMITTANCE, or BANK_TRANSFER.","enum":["CASH","CHECK","CREDIT_CARD","BANK_REMITTANCE","BANK_TRANSFER"],"type":"string"},"updated_datetime":{"description":"The datetime this payment method was last updated at","type":"string"}},"required":["updated_datetime","inserted_datetime","active","type","name","id"],"title":"PaymentMethod","type":"object"},"FileAttachmentResponse":{"description":"A single file attachment wrapped in a data envelope","properties":{"data":{"$ref":"#/components/schemas/FileAttachment"}},"title":"FileAttachment Response","type":"object"},"TestResult":{"description":"Lab results for a batch or package — the Certificate of Analysis (COA). Headline potency figures (THC/CBD) sit on this object; the full analyte breakdown (terpenes, pesticides, heavy metals, and more) is nested under `additional_test_results`.","properties":{"additional_test_results":{"$ref":"#/components/schemas/AdditionalTestResult"},"batch_id":{"description":"The ID of the batch this test result belongs to, or null when it is attached to a package instead (see `package_id`)","type":"string"},"biotrack_id":{"description":"The test result's ID in BioTrack. A BioTrack identifier, not a Distru ID. Null when the result did not come from BioTrack.","type":"string"},"cbd_mg_per_unit":{"description":"CBD content in milligrams per unit, as a decimal string, or null when not measured","type":"string"},"cbd_percentage":{"description":"CBD content as a percentage by weight, as a decimal string, or null when not measured","type":"string"},"coa_url":{"description":"Public URL to view/download this test result's Certificate of Analysis (COA), or null when no file is attached","type":"string"},"id":{"description":"ID for this test result","type":"string"},"inserted_datetime":{"description":"ISO 8601 datetime this test result was created at","type":"string"},"is_primary":{"description":"True when this is the primary test result for its batch or package (the one whose potency figures represent the product)","type":"boolean"},"lab_license_number":{"description":"The license number of the lab that performed this test, or null when not set","type":"string"},"lab_name":{"description":"The name of the lab that performed this test, or null when not set","type":"string"},"metrc_id":{"description":"The test result's ID in Metrc. A Metrc identifier, not a Distru ID. Null when the result did not come from Metrc.","type":"integer"},"mg_per_unit_type":{"description":"The unit that the `*_mg_per_unit` figures are measured against (e.g. the per-unit size), or null when not set","type":"string"},"name":{"description":"The name of the test result","type":"string"},"package_id":{"description":"The ID of the package this test result belongs to, or null when it is attached to a batch instead (see `batch_id`)","type":"string"},"release_date":{"description":"The date this test result was released (e.g. \"2026-08-20\"), or null when not set","type":"string"},"thc_mg_per_unit":{"description":"THC content in milligrams per unit, as a decimal string, or null when not measured","type":"string"},"thc_percentage":{"description":"THC content as a percentage by weight, as a decimal string, or null when not measured","type":"string"},"total_cbd_mg_per_unit":{"description":"Total CBD (including its acid precursor) in milligrams per unit, as a decimal string, or null when not measured","type":"string"},"total_cbd_percentage":{"description":"Total CBD (including its acid precursor) as a percentage by weight, as a decimal string, or null when not measured","type":"string"},"total_thc_mg_per_unit":{"description":"Total THC (including its acid precursor) in milligrams per unit, as a decimal string, or null when not measured","type":"string"},"total_thc_percentage":{"description":"Total THC (including its acid precursor) as a percentage by weight, as a decimal string, or null when not measured","type":"string"},"updated_datetime":{"description":"ISO 8601 datetime this test result was last updated at","type":"string"}},"required":["inserted_datetime","updated_datetime","is_primary","name","additional_test_results","id"],"title":"TestResult","type":"object"},"PurchasesByCompanyReportColumn":{"properties":{"key":{"description":"The key this column appears under on every data row (the slugified label).","type":"string"},"label":{"description":"The human-readable label of the column.","type":"string"}},"required":["label","key"],"title":"PurchasesByCompanyReportColumn","type":"object"},"SalesByUserReport":{"description":"The Sales By User report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/SalesByUserReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/SalesByUserReportMeta"}},"required":["meta","data"],"title":"SalesByUserReport","type":"object"},"MetrcItem":{"description":"A Metrc item: an item definition synced from Metrc, scoped to one of your licenses.","properties":{"inserted_datetime":{"description":"The datetime this item was first cached in Distru (not a Metrc timestamp)","type":"string"},"is_deleted":{"description":"True when the item has been deleted in Metrc.","type":"boolean"},"license":{"$ref":"#/components/schemas/License"},"metrc_id":{"description":"The item's identifier in Metrc. A Metrc identifier, not a Distru ID.","type":"integer"},"metrc_inserted_datetime":{"description":"ISO 8601 datetime this item was created in Metrc, or null when Metrc reports none","type":"string"},"metrc_strain_id":{"description":"The item's strain identifier in Metrc. A Metrc identifier, not a Distru ID. Null when the item has no strain.","type":"integer"},"metrc_unit_name":{"description":"The Metrc unit-of-measure name for this item (e.g. \"Grams\"), or null when none is reported","type":"string"},"name":{"description":"The item name as reported by Metrc, or null when Metrc reports none","type":"string"},"product_category_name":{"description":"The Metrc product category name, or null when none is reported","type":"string"},"product_category_type":{"description":"The Metrc product category type, or null when none is reported","type":"string"},"quantity_type":{"description":"How the item's quantity is measured. Null when Metrc reports none.","enum":["COUNT_BASED","VOLUME_BASED","WEIGHT_BASED"],"type":"string"},"strain_name":{"description":"The strain name as reported by Metrc, or null when the item has no strain","type":"string"},"unit_type":{"$ref":"#/components/schemas/UnitType"},"updated_datetime":{"description":"The datetime this item's cache was last updated in Distru (not a Metrc timestamp)","type":"string"}},"required":["updated_datetime","inserted_datetime","is_deleted","metrc_id"],"title":"Metrc Item","type":"object"},"PurchaseOrderItem":{"description":"A single product line on a [purchase](#model-purchase) — what is being bought, how much, at what per-unit price, and how much has been received into inventory so far.","properties":{"batch":{"$ref":"#/components/schemas/Batch"},"compliance_quantity":{"description":"The received quantity expressed in the package's unit type, as reported to the state compliance system (Metrc or BioTrack), as a decimal string. Null when this line is not package-tracked.","type":"string"},"id":{"description":"ID for this order item","type":"string"},"is_sample":{"description":"True when this line is a sample rather than a bought-for-resale item. Defaults to false.","type":"boolean"},"location":{"$ref":"#/components/schemas/LocationCompact"},"package":{"$ref":"#/components/schemas/Package"},"price":{"description":"Per-unit price paid for this line, as a decimal string (e.g. \"12.50\"). On a purchase order item this always equals `price_base` — per-unit discounts apply to sales orders, not purchases.","type":"string"},"price_base":{"description":"Per-unit list price for this line, before any discount, as a decimal string. Equal to `price` on a purchase order item.","type":"string"},"product":{"$ref":"#/components/schemas/Product"},"quantity":{"description":"Quantity ordered on this line, in the product's own unit, as a decimal string (e.g. \"100\").","type":"string"},"received_quantity":{"description":"Quantity received into inventory against this line so far, in the product's own unit, as a decimal string. Always present on a purchase order item and ranges from \"0\" (nothing received) up to `quantity` (fully received); it never exceeds `quantity`. Rises as the purchase is received and drives whether the parent purchase reads as partially or fully received.","type":"string"}},"required":["price_base","price","quantity","is_sample","id"],"title":"PurchaseOrderItem","type":"object"},"UpsertAssemblyMetrcProcessingJobWaste":{"description":"Waste reported to Metrc when finishing a Metrc processing job. Each quantity must be sent with its unit name. Submit it while the assembly is still PENDING (at the latest in the request that sets status to COMPLETED); it is read-only once the assembly is COMPLETED.","properties":{"count_quantity":{"description":"Count-based waste. Required together with `count_unit_name`.","type":"number"},"count_unit_name":{"description":"The Metrc unit name for `count_quantity`, e.g. \"Each\".","type":"string"},"volume_quantity":{"description":"Volume-based waste. Required together with `volume_unit_name`.","type":"number"},"volume_unit_name":{"description":"The Metrc unit name for `volume_quantity`, e.g. \"Milliliters\".","type":"string"},"weight_quantity":{"description":"Weight-based waste. Required together with `weight_unit_name`.","type":"number"},"weight_unit_name":{"description":"The Metrc unit name for `weight_quantity`, e.g. \"Grams\".","type":"string"}},"title":"Upsert Assembly Metrc Processing Job Waste","type":"object"},"ProductGroups":{"description":"A collection of product groups","properties":{"data":{"description":"Product Groups","items":{"$ref":"#/components/schemas/ProductGroup"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Product Groups","type":"object"},"SalesByUserReportRow":{"description":"A single row of the Sales By User report.","properties":{"leaderboard_rank":{"description":"The user's rank by total sales within this report, starting at 1 for the top seller. Rows are always returned in this order.","type":"string"},"order_count":{"description":"How many of the user's orders fall in the report's date range and status set.","type":"string"},"sales_pre_tax":{"description":"The user's pre-tax sales total for the counted orders, net of returns. Excludes tax; a returned order lowers this figure.","type":"string"},"total_sales":{"description":"The user's total sales for the counted orders, net of returns. Same orders as `sales_pre_tax` but including tax.","type":"string"},"user":{"description":"The user's (sales rep's) name","type":"string"}},"required":["total_sales","sales_pre_tax","order_count","user","leaderboard_rank"],"title":"SalesByUserReportRow","type":"object"},"SalesOrderTaxReport":{"description":"The Sales Order Tax report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/SalesOrderTaxReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/SalesOrderTaxReportMeta"}},"required":["meta","data"],"title":"SalesOrderTaxReport","type":"object"},"PaymentPurchase":{"description":"A compact representation of the purchase a payment belongs to","properties":{"id":{"description":"The purchase's ID. Always present.","type":"string"},"purchase_number":{"description":"The purchase number as shown in the Distru UI. Always present.","type":"string"},"status":{"description":"The purchase's status, or null if the purchase has no status set.","type":"string","nullable":true},"total":{"description":"The purchase's total as a decimal string (e.g. `\"1200.00\"`). Defaults to `\"0\"`. Always present.","type":"string"}},"required":["total","purchase_number","id"],"title":"PaymentPurchase","type":"object"},"BinResponse":{"description":"A single bin","properties":{"data":{"$ref":"#/components/schemas/Bin"}},"title":"Bin Response","type":"object"},"HarvestOutputsReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The report's column definitions, in order, each mapping a `data` row key to its human-readable label. Reflects the columns actually present in `data` — the cost columns are absent for users without permission to view costs. Always present.","items":{"$ref":"#/components/schemas/HarvestOutputsReportColumn"},"type":"array"},"date_range":{"description":"The resolved date range the report covers, as a human-readable label (reflects the `datetime` filter, or the default last-7-days range when it was omitted). Always present.","type":"string"},"report":{"description":"The report identifier — always `harvest_outputs`.","type":"string"}},"required":["columns","date_range","report"],"title":"HarvestOutputsReportMeta","type":"object"},"Tags":{"description":"A collection of tags","properties":{"data":{"description":"Tags","items":{"$ref":"#/components/schemas/Tag"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Tags","type":"object"},"PurchaseOrderHistoryReportRow":{"description":"A single row of the Purchase Order History report. The keys below are always present; companies on a compliance integration see an additional manifest-number key (`metrc_manifest_number` or `biotrack_manifest_number`), and companies with Purchase custom fields see one extra key per field, slugified from its label. Read `meta.columns` for the exact key set of a given response.\n","properties":{"amount":{"description":"The purchase total; 0 when the purchase has no line items.","type":"string"},"due_date":{"description":"The due date as an ISO8601 timestamp in the company's timezone; null when the purchase has no due date.","type":"string"},"owner":{"description":"The purchase owner's full name; null when no owner is assigned.","type":"string"},"paid":{"description":"Total amount paid on the purchase across its non-voided payments; 0 when nothing has been paid.","type":"string"},"purchase_date":{"description":"The purchase (order) date as an ISO8601 timestamp in the company's timezone; null when the purchase has no order date.","type":"string"},"purchase_number":{"description":"The purchase number.","type":"string"},"status":{"description":"The purchase status. One of PENDING, PROCESSING, DELIVERING, PARTIALLY_RECEIVED, COMPLETED — DRAFT never appears here.","enum":["COMPLETED","DELIVERING","PENDING","PARTIALLY_RECEIVED","PROCESSING"],"type":"string"},"vendor":{"description":"The vendor (supplier) company name.","type":"string"}},"required":["amount","paid","status","vendor","purchase_number"],"title":"PurchaseOrderHistoryReportRow","type":"object"},"Tax":{"description":"A tax","properties":{"description":{"description":"Free-text note describing the tax. Null when no description was set.","type":"string"},"id":{"description":"ID for this tax.","type":"string"},"inserted_datetime":{"description":"When the tax was created, as a UTC ISO-8601 timestamp (e.g. 2024-01-15T09:30:00Z).","type":"string"},"name":{"description":"The tax's display name, unique within the company.","type":"string"},"qb_account_id":{"description":"The linked QuickBooks Online account ID. Null when the company is not connected to QuickBooks Online or the tax has not been mapped to an account.","type":"string"},"qb_product_id":{"description":"The linked QuickBooks Online product ID. Null when the company is not connected to QuickBooks Online or the tax has not been mapped to a product.","type":"string"},"tags":{"description":"The [tag](#model-tag)s associated with this tax, each rendered with only its `id` and `name`. Always present; an empty array when the tax has no tags.","items":{"$ref":"#/components/schemas/Tag"},"type":"array"},"tax_applied_after_charges":{"description":"Controls the base amount this tax is applied to on every [order](#model-order) and [invoice](#model-invoice) line it covers. When true, the tax is calculated on the line amount after other charges (fees/discounts) are added; when false, on the pre-charge amount. Always present. Fixed at creation and never changes for an existing tax, so an integrator can cache it safely.","type":"boolean"},"tax_applied_after_price_tiers":{"description":"Controls whether this tax is applied before or after price tier (tiered/volume) pricing adjustments on the [order](#model-order) and [invoice](#model-invoice) lines it covers. When true, the tax is calculated after those adjustments; when false, before them. Independent of `tax_applied_after_charges`. Always present. Fixed at creation and never changes for an existing tax.","type":"boolean"},"tax_code":{"description":"The tax code, unique within the company.","type":"string"},"tax_rate_percent":{"description":"The tax rate as a percentage applied to the [order](#model-order) and [invoice](#model-invoice) line totals this tax covers, e.g. 8.25 means 8.25%. Always present; never null.","type":"number"},"updated_datetime":{"description":"When the tax was last updated, as a UTC ISO-8601 timestamp (e.g. 2024-01-15T09:30:00Z).","type":"string"}},"required":["updated_datetime","inserted_datetime","tags","tax_applied_after_price_tiers","tax_applied_after_charges","tax_rate_percent","tax_code","name","id"],"title":"Tax","type":"object"},"Inventory":{"properties":{"active":{"description":"Total on-hand quantity for this group, as a decimal string (e.g. \"10.000000000\"). Always present.","type":"string"},"available":{"description":"Quantity free to sell or use, i.e. `active` minus `reserved`, as a decimal string. Can be negative when more is reserved than is on hand. Equals `active` when `BATCH_NUMBER` is in `groupings`. Always present.","type":"string"},"batch_number":{"description":"Batch number of the underlying batch or package. Present only when `BATCH_NUMBER` is in `groupings`; absent otherwise. May be null: inventory whose batch/package has no batch number is aggregated together under a single null-batch group.","type":"string"},"cost_per_unit_actual":{"description":"Actual cost per unit — `total_cost_actual` divided by the active quantity, as a decimal string. Null when `total_cost_actual` is null or the active quantity is not greater than 0.","type":"string"},"cost_per_unit_default":{"description":"Default (standard) cost per unit — `total_cost_default` divided by the active quantity, as a decimal string. Null when `total_cost_default` is null or the active quantity is not greater than 0.","type":"string"},"location_id":{"description":"Distru location id this group is held at. Present only when `LOCATION` is in `groupings`; absent otherwise, and may be null for stock that has no location (e.g. inventory held by a user rather than at a location).","type":"string"},"product_id":{"description":"Distru product id this group belongs to. Always present.","type":"string"},"reserved":{"description":"Quantity spoken for but not yet fulfilled, and therefore not sellable, as a decimal string. This is the quantity on unfulfilled line items of PROCESSING sales orders plus the quantity on unfulfilled inputs of pending assemblies — \"unfulfilled\" meaning no specific package or batch has been assigned yet. Always `\"0\"` when `BATCH_NUMBER` is in `groupings`, since reservations cannot be attributed at batch/package granularity. Always present.","type":"string"},"total_cost_actual":{"description":"Total actual cost of the active quantity, as a decimal string, or null when no cost could be traced. Distru traces the inputs and components that produced the currently active inventory and sums their real costs incurred along the chain that led to this inventory — for example the price paid when a component was purchased, assembly costs, and costs added by stock adjustments, among others.","type":"string"},"total_cost_default":{"description":"Total default (standard) cost of the active quantity, as a decimal string, or null when no cost could be traced. Traced the same way as `total_cost_actual`, but each input/component is valued at its product's configured unit cost instead of its real cost.","type":"string"},"updated_datetime":{"description":"ISO8601 UTC timestamp of the most recent change to any stock counted in this group. Always present.","format":"datetime","type":"string"}},"required":["updated_datetime","available","reserved","active","product_id"],"title":"Inventory","type":"object"},"VehicleResponse":{"description":"A single vehicle envelope","properties":{"data":{"$ref":"#/components/schemas/Vehicle"}},"title":"Vehicle Response","type":"object"},"PurchasesByProductReportMeta":{"description":"Report-level metadata returned alongside the report rows.","properties":{"columns":{"description":"Ordered definitions of every column in the report, including any per-company               custom-field columns.","items":{"$ref":"#/components/schemas/PurchasesByProductReportColumn"},"type":"array"},"date_range":{"description":"Human-readable description of the date range the report resolved to, in the               account's time zone (e.g. `Last 30 Days`, or an explicit range when               `order_datetime` was supplied). Reflects the effective range, including the               30-day default when no filter was given.","type":"string"},"report":{"description":"Stable identifier of this report, always `purchases_by_product`.","type":"string"}},"required":["columns","date_range","report"],"title":"PurchasesByProductReportMeta","type":"object"},"MenuResponse":{"description":"A single menu wrapped in a data envelope","properties":{"data":{"$ref":"#/components/schemas/Menu"}},"title":"Menu Response","type":"object"},"PurchasesByCompanyReportRow":{"description":"A single row of the Purchases By Company report. If your company has CompanyRelationship custom fields configured, each is appended as an additional key on every row (keyed by the slugified field label, listed in `meta.columns`).\n","properties":{"category":{"description":"The related company's category, or null when the company has no category set.","type":"string"},"last_purchase_date":{"description":"Date of the vendor's most recent non-draft purchase, formatted `M/D/YYYY` in your company's timezone (month and day are not zero-padded, e.g. `8/5/2026`). This is the latest purchase across all time, not limited to the reported date range. An empty string `\"\"` when that purchase has no order date recorded.\n","type":"string"},"name":{"description":"The vendor's related company name. Always present.","type":"string"},"product_owner":{"description":"Full name of the sales rep assigned to this vendor, or null when no owner is assigned.","type":"string"},"purchase_order_count":{"description":"Number of non-draft purchases counted for this vendor within the reported date range (and matching the `owner_ids` filter). Always at least 1, since vendors with no qualifying purchases are omitted.\n","type":"string"},"relationship_type":{"description":"The vendor's relationship type name, or null when none is assigned.","type":"string"},"total_purchases":{"description":"Total amount spent across the counted purchases, in your company's currency, summed over the reported date range (and `owner_ids` filter). Never null; a vendor whose purchases all total zero reports 0.\n","type":"string"}},"required":["total_purchases","purchase_order_count","name"],"title":"PurchasesByCompanyReportRow","type":"object"},"Invoice":{"description":"A bill to a customer for what they owe, tracking the total, how much has been paid, and what remains. Always generated from a sales order.","properties":{"billing_location":{"$ref":"#/components/schemas/LocationWithLicense"},"charges":{"description":"A collection of Charges","items":{"$ref":"#/components/schemas/Charge"},"title":"Charges","type":"array"},"company":{"$ref":"#/components/schemas/CompanyCompact"},"creator":{"$ref":"#/components/schemas/User"},"custom_data":{"description":"A collection of CustomData","items":{"$ref":"#/components/schemas/CustomField"},"title":"CustomData","type":"array"},"due_datetime":{"description":"The datetime by which the customer should pay the invoice.","type":"string"},"external_notes":{"description":"Notes on this invoice that are visible to the customer, or null.","type":"string"},"id":{"description":"ID for this invoice.","type":"string"},"inserted_datetime":{"description":"The datetime at which the invoice was created in Distru.","type":"string"},"internal_notes":{"description":"Notes on this invoice that are only visible internally, or null.","type":"string"},"invoice_datetime":{"description":"The datetime the invoice is dated for.","type":"string"},"invoice_number":{"description":"The human-readable, per-company sequential invoice number shown in the Distru UI (e.g. `1042`). Assigned by Distru on creation; you cannot set it via the upsert endpoint.","type":"string"},"items":{"description":"A collection of InvoiceItems","items":{"$ref":"#/components/schemas/InvoiceItem"},"title":"InvoiceItems","type":"array"},"order":{"$ref":"#/components/schemas/CompactOrder"},"owner":{"$ref":"#/components/schemas/User"},"paid_amount":{"description":"The total amount recorded against this invoice across its active (non-voided, non-deleted) payments, as a decimal string; `0` when nothing has been paid. Excludes any portion of a payment that went toward an overpayment credit.","type":"string"},"payment_term_name":{"description":"The name of the payment term applied to this invoice (e.g. \"Net 30\"), or null when no payment term is set.","type":"string"},"payments":{"description":"A collection of the invoice's payments","items":{"$ref":"#/components/schemas/Payment"},"title":"Payments","type":"array"},"remaining_amount":{"description":"The outstanding balance, as a decimal string: the invoice `total` minus `paid_amount`. Negative when the invoice is over-paid.","type":"string"},"status":{"description":"The payment status of this invoice, always present and SCREAMING_CASE: NOT_PAID (nothing paid yet), PARTIALLY_PAID (some but not all paid), FULLY_PAID (paid in full), or OVER_PAID (payments exceed the total). OVER_PAID is rare and effectively legacy — a new overpayment is turned into a customer credit rather than moving the invoice into this status.","enum":["NOT_PAID","PARTIALLY_PAID","FULLY_PAID","OVER_PAID"],"type":"string"},"total":{"description":"The invoice total as a decimal string, including all line items, taxes, and discounts. Recomputed by Distru on every save.","type":"string"},"updated_datetime":{"description":"The datetime at which the invoice was last updated in Distru.","type":"string"},"voided_datetime":{"description":"The datetime the invoice was voided. An invoice is automatically voided when its sales order is canceled, and un-voided (cleared back to null) if that order later leaves the canceled status. Null for invoices that have never been voided.","type":"string"}},"required":["custom_data","payments","charges","items","invoice_datetime","due_datetime","updated_datetime","inserted_datetime","total","remaining_amount","paid_amount","invoice_number","status","id"],"title":"Invoice","type":"object"},"Location":{"description":"A place where inventory is held. This is flexible: it can be a whole site such as a warehouse or store, or a more specific spot like a room or area within one. Has an address and optionally a license.","properties":{"address":{"description":"The full address as a single formatted line, built from the street, apt, city, state, zip, and country fields","example":"1234 Warehouse Street, Oakland, CA","type":"string"},"apt":{"description":"The apartment/suite/unit of this location, or null when none was entered","type":"string"},"city":{"description":"The city of this location","type":"string"},"company_id":{"description":"ID of the [company](#model-company) that owns this location","example":"02c88a3f-d759-4973-88f9-60049d682524","type":"string"},"country":{"description":"The country of this location","type":"string"},"deleted_at":{"description":"ISO 8601 datetime this location was deleted at, or null when it is not deleted","type":"string"},"id":{"description":"ID for this location","example":"02c88a3f-d759-4973-88f9-60049d682524","type":"string"},"inserted_datetime":{"description":"ISO 8601 datetime this location was created at","type":"string"},"latitude":{"description":"The latitude of this location, or null when it has not been geocoded","type":"number"},"license":{"$ref":"#/components/schemas/License"},"license_id":{"description":"ID of the [license](#model-license) this location is associated with, or null when the location has no license","example":"02c88a3f-d759-4973-88f9-60049d682524","type":"string"},"longitude":{"description":"The longitude of this location, or null when it has not been geocoded","type":"number"},"metrc_id":{"description":"The location's ID in Metrc. A Metrc identifier, not a Distru ID. Null when the location is not linked to Metrc.","type":"integer"},"name":{"description":"Human readable name for this location","example":"Warehouse 1","type":"string"},"state":{"description":"The state of this location","type":"string"},"street_address":{"description":"The street address of this location","type":"string"},"updated_datetime":{"description":"ISO 8601 datetime this location was last updated at","type":"string"},"zip":{"description":"The postal code of this location","type":"string"}},"required":["country","zip","state","city","street_address","updated_datetime","inserted_datetime","name","address","id"],"title":"Location","type":"object"},"MetrcTagResponse":{"description":"A single Metrc tag","properties":{"data":{"$ref":"#/components/schemas/MetrcTag"}},"title":"Metrc Tag Response","type":"object"},"PlantLifecycleReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The ordered column definitions for the rows in `data`, matching the keys present on each row. The four cost columns are dropped from this list for a caller without permission to view costs.","items":{"$ref":"#/components/schemas/PlantLifecycleReportColumn"},"type":"array"},"date_range":{"description":"The resolved reporting window as a human-readable string (e.g. `Last 30 Days` or an explicit date range). Null when no date filter was applied and none could be resolved.","type":"string"},"report":{"description":"The report identifier — always `plant_lifecycle`.","type":"string"}},"required":["columns","report"],"title":"PlantLifecycleReportMeta","type":"object"},"SalesOrderHistoryReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The ordered column definitions for this response, including any compliance and custom-field columns present for the company. Each entry pairs the row `key` with its display `label`.","items":{"$ref":"#/components/schemas/SalesOrderHistoryReportColumn"},"type":"array"},"date_range":{"description":"The human-readable date range the report covers, reflecting the resolved `order_datetime` filter (or the last-30-days default when none was given)","type":"string"},"report":{"description":"The report identifier; always \"sales_order_history\"","type":"string"}},"required":["columns","date_range","report"],"title":"SalesOrderHistoryReportMeta","type":"object"},"ProductCategoryCompact":{"description":"The top-level classification of a product (e.g. Flower, Edibles, Concentrates). Categories are defined per company and can be organized into subcategories.","properties":{"id":{"description":"ID for this category","type":"string"},"name":{"description":"Human readable name for this category","type":"string"},"official_product_category_id":{"description":"The ID of Distru's standardized (official) category this maps to, used to normalize categories across companies. Null when this category is not mapped to a standardized category.","type":"string"}},"required":["name","id"],"title":"ProductCategoryCompact","type":"object"},"PaymentMethods":{"description":"A collection of Payment Methods","properties":{"data":{"description":"Payment Methods","items":{"$ref":"#/components/schemas/PaymentMethod"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Payment Methods","type":"object"},"HarvestOutputsReport":{"description":"The Harvest Outputs report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/HarvestOutputsReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/HarvestOutputsReportMeta"}},"required":["meta","data"],"title":"HarvestOutputsReport","type":"object"},"ReturnItem":{"description":"A single line on a return — how much of an order line item was sent back.","properties":{"id":{"description":"Distru ID for this return item.","type":"string"},"order_item":{"$ref":"#/components/schemas/CompactOrderItem"},"quantity":{"description":"Quantity returned on this line, always greater than 0. Always expressed in the product's own unit type, even when the order line item was fulfilled from a package measured in a different unit.","type":"number"},"waste":{"description":"Whether this returned quantity is marked as waste. Waste items are written off rather than restocked into sellable inventory when the return reaches `COMPLETED`. Defaults to false.","type":"boolean"}},"required":["waste","quantity","id"],"title":"ReturnItem","type":"object"},"StrainResponse":{"description":"A single Strain","properties":{"data":{"$ref":"#/components/schemas/Strain"}},"title":"Strain Response","type":"object"},"CompanyGroupFullResponse":{"description":"A single company group","properties":{"data":{"$ref":"#/components/schemas/CompanyGroupFull"}},"title":"Company Group Response","type":"object"},"Products":{"description":"A collection of Products","properties":{"data":{"description":"Products","items":{"$ref":"#/components/schemas/Product"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Products","type":"object"},"Strains":{"description":"A collection of Strains.\nNote: This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.\n","properties":{"data":{"description":"Strains","items":{"$ref":"#/components/schemas/Strain"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Strains","type":"object"},"Payment":{"description":"A record of money exchanged — received from a customer against an invoice, or paid to a vendor against a purchase order.","properties":{"amount":{"description":"The payment amount as a decimal string with two fractional digits (e.g. `\"150.00\"`), in the currency of the related invoice or purchase. Normally at least `0.01`; the one exception is a payment synced from QuickBooks Online to represent a void, which appears as `\"0.00\"`. Always present.","type":"string"},"company":{"$ref":"#/components/schemas/CompanyCompact"},"credit_uses":{"description":"Credits applied toward this invoice payment (an empty array when none were applied). Null for PURCHASE payments.","items":{"$ref":"#/components/schemas/PaymentCreditUse"},"type":"array","nullable":true},"description":{"description":"Free-text note attached to the payment, or null if none was entered.","type":"string","nullable":true},"fully_paid_with_credits":{"description":"True when the payment was covered entirely by credits; in that case `payment_method` is null. Defaults to false. Always present.","type":"boolean"},"id":{"description":"The payment's ID. Always present.","type":"string"},"inserted_datetime":{"description":"ISO8601 datetime the payment record was created in Distru. Distinct from `payment_datetime`. Always present.","type":"string"},"invoice":{"$ref":"#/components/schemas/CompactInvoice"},"overpayment_credits":{"description":"Credits generated from overpaying this invoice payment (an empty array when there was no overpayment). Null for PURCHASE payments.","items":{"$ref":"#/components/schemas/PaymentCredit"},"type":"array","nullable":true},"payment_datetime":{"description":"The full ISO8601 datetime the payment was recorded as made (e.g. `\"2022-07-10T00:00:00Z\"`). This is the user-entered payment datetime, distinct from `inserted_datetime` (when the record was created in Distru). Always present.","type":"string"},"payment_method":{"$ref":"#/components/schemas/PaymentMethod"},"payment_number":{"description":"The payment number as shown in the Distru UI. Unique per company. Always present.","type":"string"},"payment_type":{"description":"What this payment is tied to: INVOICE (money received from a customer against an invoice) or PURCHASE (money paid to a vendor against a purchase). Determines which of `invoice`/`purchase` is populated and whether `credit_uses`/`overpayment_credits` are present. Always present.","type":"string"},"purchase":{"$ref":"#/components/schemas/PaymentPurchase"},"quickbooks_deposit_account_id":{"description":"ID of the QuickBooks Online deposit account this payment posts to, or null when the payment is not linked to a deposit account.","type":"string","nullable":true},"quickbooks_deposit_account_name":{"description":"Human-readable name of the QuickBooks Online deposit account in `quickbooks_deposit_account_id`. Only returned on the single-payment (show) response, and null there when no deposit account is linked; absent from list responses.","type":"string","nullable":true},"quickbooks_sync_enqueued":{"description":"Whether a QuickBooks Online sync was queued for this payment. Only present on the payment creation response; absent from list and show responses.","type":"boolean"},"status":{"description":"The payment's status. POSTED for a live payment; VOIDED for one that was voided (retained for history rather than deleted). Always present.","type":"string"},"updated_datetime":{"description":"ISO8601 datetime the payment record was last modified in Distru. Equals `inserted_datetime` until the payment is edited or voided. Always present.","type":"string"}},"required":["updated_datetime","inserted_datetime","fully_paid_with_credits","status","payment_type","payment_datetime","payment_number","amount","id"],"title":"Payment","type":"object"},"Return":{"description":"Product a customer sent back, reversing the related inventory and financials. Usually tied to the original order, and may generate a credit for the customer.","properties":{"company":{"$ref":"#/components/schemas/CompanyCompact"},"creator":{"$ref":"#/components/schemas/User"},"credits":{"description":"Customer credits generated from this return, in compact form. Empty array when the return did not create a credit.","items":{"$ref":"#/components/schemas/CompactCredit"},"type":"array"},"custom_data":{"description":"Custom field values for this return, keyed by custom field id. Empty object when none are set.","type":"object"},"description":{"description":"Free-text note on the return, or null.","type":"string"},"id":{"description":"Distru ID for this return.","type":"string"},"inserted_datetime":{"description":"The datetime the return was created in Distru.","type":"string"},"invoice_numbers":{"description":"Invoice numbers of the associated order, sorted ascending. Empty array when the return has no order, or the order has no invoices.","items":{"type":"string"},"type":"array"},"items":{"description":"The line items on this return.","items":{"$ref":"#/components/schemas/ReturnItem"},"type":"array"},"location":{"$ref":"#/components/schemas/LocationCompact"},"order":{"$ref":"#/components/schemas/CompactOrder"},"order_quantity":{"description":"Total quantity across all line items on the associated order, as a decimal string (e.g. `\"12\"`). Null for a generic return with no order.","type":"string"},"owner":{"$ref":"#/components/schemas/User"},"qb_credit_memo_id":{"description":"Id of the QuickBooks Online credit memo this return's credit maps to, once synced to QuickBooks Online. Null until synced, or if the return does not create a credit.","type":"string"},"return_datetime":{"description":"The business date of the return. May differ from `inserted_datetime` (when the record was created). Null if not set.","type":"string"},"return_number":{"description":"Human-readable return number shown in the Distru UI. Unique within your company.","type":"string"},"return_quantity":{"description":"Total quantity returned across all items on this return, as a decimal string (e.g. `\"4\"`). Null for a generic return with no order.","type":"string"},"return_type":{"description":"`Full Return` when every line item on the associated order has been fully returned, otherwise `Partial Return`. Null for a generic return with no order.","enum":["Full Return","Partial Return"],"type":"string"},"status":{"description":"Where this return is in its lifecycle. While PROCESSING, SHIPPED, or RECEIVED, the returned goods are set aside as returning stock and have not yet been added back to sellable inventory. Once COMPLETED, the returned goods are restocked into inventory (except items flagged as waste, which are written off instead), and the return can no longer be deleted.","enum":["PROCESSING","SHIPPED","RECEIVED","COMPLETED"],"type":"string"},"total":{"description":"Total monetary value of the returned line items, summed from the return's items. Defaults to 0 when the return has no items.","type":"number"},"updated_datetime":{"description":"The datetime the return was last modified in Distru.","type":"string"}},"required":["updated_datetime","inserted_datetime","items","credits","custom_data","creator","invoice_numbers","total","status","return_number","id"],"title":"Return","type":"object"},"BillOfMaterialsCostType":{"description":"The cost type applied by a bill-of-materials cost","properties":{"cost_per_unit":{"description":"The cost per unit, as a decimal string (e.g. \"5.00\"). Present only when the caller has the `costs_permissions_view_cost_types_cost_per_unit` permission; the field is omitted otherwise.","type":"string"},"id":{"description":"ID for this cost type","type":"string"},"name":{"description":"Human readable name for this cost type","type":"string"},"unit_type":{"$ref":"#/components/schemas/UnitType"}},"required":["name","id"],"title":"BillOfMaterialsCostType","type":"object"},"MovePackagesRequest":{"properties":{"location_id":{"description":"ID of the destination Distru location (the `location.id` string on a package). Must belong to the same license as every package in `package_ids`; the packages' on-hand inventory is relocated there.","type":"string"},"metrc_location_id":{"description":"Metrc's own numeric location id (a foreign Metrc identifier, not a Distru ID). Omit to move the packages only within Distru. When provided, the packages are also moved to this Metrc location; this path is Metrc-only (unavailable for BioTrack licenses) and requires that your state has Metrc locations enabled and a Metrc key with permission to move packages.","type":"string"},"package_ids":{"description":"Non-empty list of 1 to 300 package IDs to move, each the `id` string returned by `GET /public/v1/packages`. All must resolve to packages in your company, all must belong to the same license, and each must be active with a positive active quantity. The move is all-or-nothing: one invalid id rejects the whole request.","items":{"type":"string"},"type":"array"}},"required":["location_id","package_ids"],"title":"Move Packages Request","type":"object"},"Strain":{"description":"A cannabis strain (its genetics), such as \"Blue Dream\". Products can be linked to a strain to carry its name and type.","properties":{"id":{"description":"ID for this strain","type":"string"},"inserted_datetime":{"description":"The datetime this strain was created at","type":"string"},"name":{"description":"Name of the strain","type":"string"},"strain_type":{"description":"The genetic classification of the strain. Null when unset.","enum":["INDICA","INDICA_DOMINANT","SATIVA","SATIVA_DOMINANT","HYBRID","HIGH_CBD"],"type":"string"},"updated_datetime":{"description":"The datetime this strain was last updated at","type":"string"}},"required":["updated_datetime","inserted_datetime","name","id"],"title":"Strain","type":"object"},"SplitPackageOutput":{"description":"A single output package produced by the split","properties":{"batch_number":{"description":"Distru batch number stored on the output package.","type":"string"},"bin_ids":{"description":"Bins to store the output package in (requires bin inventory tracking enabled for the company). Optional; omit or send an empty array to assign no bins.","items":{"type":"string"},"type":"array"},"compliance_label":{"description":"The Metrc tag for the new package. Must be an available tag in the source package's license.","type":"string"},"copy_custom_data_from_input":{"description":"When true, copies the source package's custom field values onto the output package.","type":"boolean"},"costs":{"description":"Costs to apply to the output package (see [CostEntryInput](#model-costentryinput)). Optional.","items":{"$ref":"#/components/schemas/CostEntryInput"},"type":"array"},"expiration_date":{"description":"Expiration date reported to Metrc, e.g. \"2027-08-19\".","type":"string"},"input_compliance_quantity":{"description":"Amount drawn from the source package, in the source package's compliance unit. Must be > 0.","type":"number"},"location_id":{"description":"The output location ID. Must be in the same Metrc license as the source package.","type":"string"},"metrc_item_id":{"description":"The Metrc item id for the output. Required unless use_same_item is true, and must be omitted when it is. Must exist in the source package's Metrc license.","type":"integer"},"metrc_notes":{"description":"Notes sent to Metrc as the output package's note when it is created (max 255 characters).","type":"string"},"metrc_production_batch_number":{"description":"When set, flags the output as a Metrc production batch with this batch number.","type":"string"},"output_compliance_quantity":{"description":"Size of the new package, in the output package's compliance unit (the source unit when use_same_item is true, otherwise the unit of metrc_item_id). Must be > 0.","type":"number"},"package_date":{"description":"The output package's packaged date. Defaults to today when omitted.","type":"string"},"product_id":{"description":"The output product ID. Must be package-tracked.","type":"string"},"use_same_item":{"description":"When true, the output package reuses the source package's Metrc item and metrc_item_id must be omitted.","type":"boolean"}},"required":["compliance_label","location_id","product_id","output_compliance_quantity","input_compliance_quantity"],"title":"Split Package Output","type":"object"},"User":{"description":"A member of your Distru team — the account behind actions like owning or creating records.","properties":{"banned":{"description":"True when this user has been banned and can no longer sign in","type":"boolean"},"deleted_at":{"description":"ISO 8601 datetime this user was deleted at, or null when the user is not deleted","type":"string"},"email":{"description":"The email address of this user","type":"string"},"full_name":{"description":"The full name of this user, or null when not set","type":"string"},"id":{"description":"ID for this user","type":"string"},"inserted_datetime":{"description":"ISO 8601 datetime this user was created at","type":"string"},"role":{"$ref":"#/components/schemas/Role"}},"required":["inserted_datetime","email","id"],"title":"User","type":"object"},"PriceTierVersion":{"description":"An immutable snapshot of a [price tier](#model-pricetier), frozen at one of its edits. Every create or update of a tier produces a new version, and a sales order item priced by the tier locks to the version that priced it — so the discount recorded here never changes, even after the live tier is edited.","properties":{"id":{"description":"ID for this price tier version — the value to submit back as an order item's `price_tier_version_id`","type":"string"},"is_live":{"description":"True when this snapshot is the tier's latest version (the tier's `current_version_id`). False means the tier was edited after this snapshot was taken, so the pricing frozen here may differ from the live tier's current state.","type":"boolean"},"price_tier":{"description":"The tier as of this snapshot — its frozen fields rendered in the [price tier](#model-pricetier)'s shape, under the live tier's `id`. This is NOT the tier's current state (which may have changed since); it carries no conditions, menus, or ownership.","properties":{"id":{"description":"ID of the live [price tier](#model-pricetier) this snapshot belongs to — fetch it via GET /public/v1/price-tiers for its conditions and current state","type":"string"},"is_flat":{"description":"When true the `price` replaces the order item's `price_base` outright instead of discounting off it. Only meaningful when `price_or_percent` is PRICE — never true with PERCENT.","type":"boolean"},"name":{"description":"Internal name of the tier as of this snapshot","type":"string"},"percent":{"description":"The discount percentage (0-100) off the order item's `price_base` used when `price_or_percent` is PERCENT; null when PRICE","type":"integer"},"price":{"description":"The money amount used when `price_or_percent` is PRICE, as a decimal string (e.g. \"5.00\"); null when PERCENT. A per-unit discount off the order item's `price_base`, or — when `is_flat` is true — the replacement per-unit price itself.","type":"string"},"price_or_percent":{"description":"Whether the frozen discount is a fixed amount (PRICE) or a percentage (PERCENT)","enum":["PRICE","PERCENT"],"type":"string"}},"type":"object"}},"required":["price_tier","is_live","id"],"title":"PriceTierVersion","type":"object"},"Payments":{"description":"A collection of Payments","properties":{"data":{"description":"Payments","items":{"$ref":"#/components/schemas/Payment"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Payments","type":"object"},"OrderResponse":{"description":"A single order wrapped in a data envelope","properties":{"data":{"$ref":"#/components/schemas/Order"}},"title":"Order Response","type":"object"},"SalesOrderHistoryReportRow":{"description":"A single sales order in the report. Monetary columns are returned as JSON numbers; date and text columns are strings. Companies on a compliance integration see two extra keys — `manifest_number` (the Metrc or BioTrack manifest number, whichever system is active) and `shipped_from_license` — and companies with Order custom fields see one extra key per field, named by the slugified custom field label. Use the `key` values under `meta.columns` to map these dynamic keys without hardcoding them.\n","properties":{"charges_taxes_not_included":{"description":"Total non-tax charges on the order, excluding taxes; 0 when none","type":"string"},"customer":{"description":"The customer (company relationship) name, or `null` when the order has no linked customer.","type":"string"},"delivery_date":{"description":"The delivery date as a display-formatted string in the company's timezone, or `null` when the order has no delivery date.","type":"string"},"delivery_date_utc":{"description":"The same delivery date in UTC, or `null` when the order has no delivery date.","type":"string"},"discounts_taxes_not_included":{"description":"Total discounts applied to the order, excluding taxes; 0 when none","type":"string"},"due_date":{"description":"The due date as a display-formatted string in the company's timezone. Always present — every order has a due date (enforced on write).","type":"string"},"due_date_utc":{"description":"The same due date in UTC. Always present.","type":"string"},"order_date":{"description":"The order date as a display-formatted string in the company's timezone, e.g. \"01/15/2026\" (not ISO8601)","type":"string"},"order_date_utc":{"description":"The same order date as a display-formatted string in UTC","type":"string"},"order_number":{"description":"The order number, always returned as a string; even a purely numeric order number stays a string, and one with non-digit characters or a significant leading zero (e.g. \"0042\") keeps its full display string so the zero isn't lost.","type":"string"},"outstanding":{"description":"Unpaid balance: total minus returns minus payments; 0 when fully settled","type":"string"},"owner":{"description":"The order owner's name, or `null` when the order has no owner.","type":"string"},"paid":{"description":"Total payments applied to the order; 0 when none","type":"string"},"returns":{"description":"Total value of returns against the order; 0 when none","type":"string"},"status":{"description":"The order status in SCREAMING_CASE. A rare internal status the public API does not expose (e.g. a merged order) is returned as its raw internal string. Always present.","enum":["PENDING","PROCESSING","READY_TO_SHIP","DELIVERING","DELIVERED","COMPLETED","CANCELED"],"type":"string"},"subtotal":{"description":"Sum of the order's line-item quantity times price, before taxes, charges, and discounts; 0 when the order has no items","type":"string"},"taxes":{"description":"Total tax charges on the order; 0 when none","type":"string"},"total":{"description":"The order grand total (subtotal plus charges and taxes, minus discounts); 0 when the order is empty","type":"string"}},"required":["total","returns","charges_taxes_not_included","discounts_taxes_not_included","taxes","subtotal","outstanding","paid","status","order_number","due_date_utc","due_date","order_date_utc","order_date"],"title":"SalesOrderHistoryReportRow","type":"object"},"InvoiceHistoryReportColumn":{"properties":{"key":{"description":"The key this column uses in each `data` row","type":"string"},"label":{"description":"The human-readable label of the column","type":"string"}},"required":["label","key"],"title":"InvoiceHistoryReportColumn","type":"object"},"Contact":{"description":"A person in Distru's CRM, optionally linked to a company. Name, title, and phone/email fields all come from the underlying profile and are individually optional.","properties":{"company":{"description":"The [company](#model-company) this contact belongs to, carrying just its `id`; null when the contact is not linked to a company","properties":{"id":{"description":"The ID of the [company](#model-company) this contact belongs to","type":"string"}},"type":"object"},"custom_data":{"description":"The custom data for this contact","items":{"$ref":"#/components/schemas/CustomField"},"type":"array"},"deleted_at":{"description":"ISO 8601 datetime this contact was deleted at, or null when it is not deleted","type":"string"},"description":{"description":"Free-text description of this contact, or null when none was entered","type":"string"},"driver_license_issuing_state":{"description":"The state that issued the driver license used on shipping manifests, or null when not set","type":"string"},"driver_license_number":{"description":"Driver license number used on shipping manifests, or null when not set","type":"string"},"email":{"description":"The email address of this contact, or null when not set","type":"string"},"first_name":{"description":"The first name of this contact, or null when not set","type":"string"},"full_name":{"description":"The full name of this contact, or null when not set","type":"string"},"id":{"description":"ID for this contact","type":"string"},"inserted_datetime":{"description":"ISO 8601 datetime this contact was created at","type":"string"},"last_name":{"description":"The last name of this contact, or null when not set","type":"string"},"owner":{"$ref":"#/components/schemas/User"},"phone_number":{"description":"The phone number of this contact, or null when not set","type":"string"},"title":{"description":"The job title of this contact, or null when not set","type":"string"},"updated_datetime":{"description":"ISO 8601 datetime this contact was last updated at","type":"string"},"work_phone_number":{"description":"The work phone number of this contact, or null when not set","type":"string"}},"required":["custom_data","updated_datetime","inserted_datetime","id"],"title":"Contact","type":"object"},"Credits":{"description":"A collection of Credits","properties":{"data":{"description":"Credits","items":{"$ref":"#/components/schemas/Credit"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Credits","type":"object"},"PriceTier":{"description":"A price tier lowers the price of a single sales order item when the item meets the tier's conditions. It is applied per order item, not at the order level.","properties":{"conditions":{"$ref":"#/components/schemas/PriceTierConditions"},"creator":{"$ref":"#/components/schemas/User"},"current_version_id":{"description":"ID of the tier's current version — the immutable snapshot taken at the tier's latest edit. Every edit of a tier produces a new version; a [sales order item](#model-salesorderitem) records the version that priced it in its `price_tier_version`, so its pricing stays frozen at that snapshot even after the tier changes. An order item reflects this tier's latest state exactly when its `price_tier_version.id` equals this value (its `price_tier_version.is_live` is true).","type":"string"},"external_name":{"description":"Buyer-facing name shown on menus, or null (falls back to name)","type":"string"},"id":{"description":"ID for this price tier","type":"string"},"inserted_datetime":{"description":"When the tier was created (UTC ISO-8601)","type":"string"},"is_flat":{"description":"When true the `price` replaces the list price outright instead of discounting off it. Only meaningful when `price_or_percent` is PRICE — never true with PERCENT.","type":"boolean"},"menu_mode":{"description":"Which menus the tier appears on","enum":["ALL","NONE","SPECIFIC"],"type":"string"},"menu_promo_card_background_hex":{"description":"Background color of a TEXT promo card, as a hex string (e.g. \"#FF0000\"); null for an IMAGE card","type":"string"},"menu_promo_card_emoji":{"description":"Emoji shown on a TEXT promo card, or null when none is set","type":"string"},"menu_promo_card_text_hex":{"description":"Text color of a TEXT promo card, as a hex string (e.g. \"#FFFFFF\"); null for an IMAGE card","type":"string"},"menu_promo_card_type":{"description":"The promo card style (SCREAMING_CASE): TEXT renders a colored card using the hex/emoji fields, IMAGE renders an uploaded image instead.","enum":["TEXT","IMAGE"],"type":"string"},"menu_promo_enabled":{"description":"Whether the tier renders a promo card on menus. The `menu_promo_card_*` fields below are only shown to buyers when this is true.","type":"boolean"},"menus":{"description":"Menus the tier applies to. Populated only when menu_mode is SPECIFIC","items":{"$ref":"#/components/schemas/CompactMenu"},"type":"array"},"name":{"description":"Internal name of the tier","type":"string"},"owner":{"$ref":"#/components/schemas/User"},"percent":{"description":"The discount percentage (0-100) used when `price_or_percent` is PERCENT; null when `price_or_percent` is PRICE","type":"integer"},"price":{"description":"The money amount used when `price_or_percent` is PRICE, as a decimal string (e.g. \"5.00\"); null when `price_or_percent` is PERCENT. Interpreted as a per-unit discount off the list price, or — when `is_flat` is true — as the replacement per-unit price itself.","type":"string"},"price_or_percent":{"description":"Whether the discount is a fixed amount or a percentage","enum":["PRICE","PERCENT"],"type":"string"},"updated_datetime":{"description":"When the tier was last updated (UTC ISO-8601)","type":"string"},"valid_from_datetime":{"description":"ISO 8601 datetime the tier starts being applicable, or null for no start bound (applicable from any time up to `valid_until_datetime`)","type":"string"},"valid_until_datetime":{"description":"ISO 8601 datetime the tier stops being applicable, or null for no end bound (applicable indefinitely from `valid_from_datetime`)","type":"string"}},"required":["updated_datetime","inserted_datetime","conditions","menu_promo_card_type","menus","menu_mode","is_flat","price_or_percent","current_version_id","name","id"],"title":"PriceTier","type":"object"},"UnitTypeFullResponse":{"description":"A single unit type envelope","properties":{"data":{"$ref":"#/components/schemas/UnitTypeFull"}},"title":"Unit Type Response","type":"object"},"ProductSubcategory":{"description":"A product subcategory","properties":{"category":{"$ref":"#/components/schemas/ProductCategoryCompact"},"id":{"description":"ID for this product subcategory. Use it as the `id` for the fetch, upsert, and delete endpoints.","type":"string"},"inserted_datetime":{"description":"When the product subcategory was created, as a UTC ISO-8601 timestamp (e.g. `\"2026-08-20T14:30:00Z\"`). Always present.","type":"string"},"name":{"description":"The name of the subcategory. Always present; unique within its parent category (case-insensitive).","type":"string"},"updated_datetime":{"description":"When the product subcategory was last updated, as a UTC ISO-8601 timestamp. Equal to `inserted_datetime` until the subcategory is first edited. Always present.","type":"string"}},"required":["updated_datetime","inserted_datetime","category","name","id"],"title":"ProductSubcategory","type":"object"},"TestResults":{"description":"A collection of Test Results","properties":{"data":{"description":"Test Results","items":{"$ref":"#/components/schemas/TestResult"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Test Results","type":"object"},"SalesOrderTaxReportRow":{"description":"A single aggregated row of the Sales Order Tax report: all matching orders' charges for one unique tax name and rate, summed together.","properties":{"tax_rate":{"description":"The tax rate as a percentage, e.g. `27` for 27%, rounded to at least 2 decimal places; ranges from -100 to 100. Null when the tax is a flat/fixed amount rather than a percentage.","type":"string"},"tax_type":{"description":"The name of the tax charge (e.g. \"Cannabis Excise Tax\"). May be null when the underlying tax charge has no name.","type":"string"},"total_tax":{"description":"Sum of this tax collected across every matching order, as a string in the company's currency. Always present and non-null; a row exists only when at least one matching tax charge was found, so this is the aggregated amount for that tax name and rate. When no orders match the filters at all, `data` is an empty array rather than a zero row.","type":"string"}},"required":["total_tax"],"title":"SalesOrderTaxReportRow","type":"object"},"SalesOrderItemHistoryReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"The ordered column definitions for this response, including any compliance-only and custom-field columns. Use these keys to read the data rows.","items":{"$ref":"#/components/schemas/SalesOrderItemHistoryReportColumn"},"type":"array"},"date_range":{"description":"The human-readable order-date range the report actually covers, reflecting the `order_datetime` filter or the last-30-days default when it was omitted.","type":"string"},"report":{"description":"The report identifier, always `sales_order_item_history`.","type":"string"}},"required":["columns","date_range","report"],"title":"SalesOrderItemHistoryReportMeta","type":"object"},"ProductCategoryResponse":{"description":"A single product category","properties":{"data":{"$ref":"#/components/schemas/ProductCategory"}},"title":"Product Category Response","type":"object"},"InvoiceChargeRequest":{"description":"Invoice charge params","properties":{"id":{"description":"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.","type":"string"},"name":{"description":"The label for this charge line, shown on the invoice.","type":"string"},"percent":{"description":"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.","type":"number"},"price":{"description":"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":"number"},"type":{"description":"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).","enum":["CHARGE","DISCOUNT"],"type":"string"},"unit_type":{"description":"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`).","enum":["PERCENT","PRICE"],"type":"string"}},"required":["unit_type","type"],"title":"InvoiceCharge (Request)","type":"object"},"InventoryTransactionHistoryReportColumn":{"properties":{"key":{"description":"The key used for this column in each data row","type":"string"},"label":{"description":"The human-readable label of the column","type":"string"}},"required":["label","key"],"title":"InventoryTransactionHistoryReportColumn","type":"object"},"Locations":{"description":"A collection of Locations","properties":{"data":{"description":"Locations","items":{"$ref":"#/components/schemas/Location"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when there is no next page","type":"string"}},"title":"Locations","type":"object"},"PurchaseOrderHistoryReportMeta":{"description":"Report-level metadata","properties":{"columns":{"description":"Ordered definitions of every column present in the data rows, including the compliance manifest column and any custom-field columns. Use this to map row keys to labels.","items":{"$ref":"#/components/schemas/PurchaseOrderHistoryReportColumn"},"type":"array"},"date_range":{"description":"The human-readable date range the report covers, after the default 30-day range is applied when `order_datetime` is omitted.","type":"string"},"report":{"description":"The report identifier; always `purchase_order_history`.","type":"string"}},"required":["columns","date_range","report"],"title":"PurchaseOrderHistoryReportMeta","type":"object"},"PurchaseItemRequest":{"description":"Purchase item params. Must provide either batch_id or product_id. If batch_id is provided, product_id will be auto-filled. The `metrc_package_id`, `biotrack_id`, and `compliance_quantity` fields are only used when matching the purchase with an incoming compliance transfer (see the endpoint description). `received_quantity` is only used when the purchase status is PARTIALLY_RECEIVED.","properties":{"batch_id":{"description":"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.","type":"string"},"biotrack_id":{"description":"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.","type":"string"},"compliance_quantity":{"description":"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.","type":"number"},"id":{"description":"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.","type":"string"},"location_id":{"description":"The ID of the location this line's inventory is received into. Defaults to the purchase's `location_id` when omitted.","type":"string"},"metrc_package_id":{"description":"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.","type":"integer"},"price":{"description":"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.","type":"number"},"product_id":{"description":"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.","type":"string"},"quantity":{"description":"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.","type":"number"},"received_quantity":{"description":"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.","type":"number"}},"required":["price","quantity"],"title":"PurchaseItem (Request)","type":"object"},"InventoryValuationReport":{"description":"The Inventory Valuation report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/InventoryValuationReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/InventoryValuationReportMeta"}},"required":["meta","data"],"title":"InventoryValuationReport","type":"object"},"CultivationTransactionHistoryReport":{"description":"The Cultivation Transaction History report","properties":{"data":{"description":"The report rows, one per cultivation transaction, sorted most-recent-first by transaction date. Empty when no transactions match the filters.","items":{"$ref":"#/components/schemas/CultivationTransactionHistoryReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/CultivationTransactionHistoryReportMeta"}},"required":["meta","data"],"title":"CultivationTransactionHistoryReport","type":"object"},"OfficialProductCategory":{"description":"A Distru standard, system-defined product category that your own product categories can map to","properties":{"id":{"description":"Stable code identifying this official product category (e.g. `FLOWER`, `PRE_ROLL`). This is a human-readable natural key that is the same in every Distru account — set it as the `official_product_category_id` on your own product categories. Always present.","type":"string"},"name":{"description":"Human-readable display label for the category (e.g. `Flower`, `Pre-Roll`). Suitable for showing in a UI; use `id`, not `name`, when storing the mapping. Always present.","type":"string"}},"required":["name","id"],"title":"OfficialProductCategory","type":"object"},"FinishPackagesRequest":{"properties":{"finished_datetime":{"description":"Finish timestamp recorded on every package in the request, ISO 8601 (e.g. `2026-08-20T15:04:05Z`). Defaults to the current time when omitted.","type":"string"},"package_ids":{"description":"Non-empty list of 1 to 300 package IDs to finish, each the `id` string returned by `GET /public/v1/packages`. All must belong to your company. Every package must be finishable: already-finished packages, packages still syncing with Metrc, and packages with a Metrc compliance discrepancy are rejected, and because the operation is all-or-nothing a single bad id fails the whole request.","items":{"type":"string"},"type":"array"}},"required":["package_ids"],"title":"Finish Packages Request","type":"object"},"CostType":{"description":"A cost type","properties":{"active":{"description":"Whether the cost type is active and selectable when applying new costs. Always present; inactive cost types are still returned by the read endpoints.","type":"boolean"},"allow_inline_edits":{"description":"Controls whether the per-unit cost amount can be overridden when a cost of this type is applied to a record (a plant, a package or batch, an assembly or breakdown output, a purchase item, or a product). When true, the applied amount can be entered or overridden at apply time; when false, the applied amount is locked to this cost type's configured `cost_per_unit` and cannot be changed. Always present.","type":"boolean"},"cost_per_unit":{"description":"Default cost amount per one unit of `unit_type`, as a decimal string (e.g. \"12.50\"). Always present and non-zero. This is the amount seeded when a cost of this type is applied; whether it can then be overridden depends on `allow_inline_edits`.","type":"string"},"deleted_at":{"description":"When the cost type was soft-deleted, as a UTC ISO-8601 timestamp, or null if it has not been deleted. In practice always null here, since the read endpoints exclude soft-deleted cost types.","type":"string"},"description":{"description":"Free-text description of the cost type, or null if none was set.","type":"string"},"id":{"description":"ID of this cost type. Stable across renames; use it as the `id` for the fetch, upsert, and delete endpoints.","type":"string"},"inserted_datetime":{"description":"When the cost type was created, as a UTC ISO-8601 timestamp (e.g. `\"2026-08-20T14:30:00Z\"`). Always present.","type":"string"},"name":{"description":"Display name of the cost type. Always present and unique within your company (case-insensitive) among active, non-deleted cost types.","type":"string"},"unit_type":{"$ref":"#/components/schemas/UnitType"},"updated_datetime":{"description":"When the cost type was last modified, as a UTC ISO-8601 timestamp. Equal to `inserted_datetime` until the cost type is first updated. Always present.","type":"string"}},"required":["updated_datetime","inserted_datetime","unit_type","allow_inline_edits","active","cost_per_unit","name","id"],"title":"CostType","type":"object"},"ProductPosMapping":{"description":"A link between a Distru product and the matching product in an external point-of-sale (POS) system such as Blaze, Dutchie, or Treez. A mapping always targets exactly one POS, given by `pos_type`; only that POS's fields are present in the response, and the fields for the other two POS systems are omitted entirely.","properties":{"blaze_asset_id":{"description":"Blaze asset (image) id for the product, or null if none was set. Present only when `pos_type` is `BLAZE`.","type":"string","nullable":true},"blaze_product_id":{"description":"Blaze's own id for the mapped product. Present and non-null only when `pos_type` is `BLAZE`; omitted entirely for other POS types.","type":"string"},"blaze_retailer_id":{"description":"Distru ID of the connected Blaze retailer this mapping is scoped to. Present and non-null only when `pos_type` is `BLAZE`; omitted entirely for other POS types.","type":"string"},"dutchie_product_id":{"description":"Dutchie's own numeric id for the mapped product (a Dutchie identifier, not a Distru id). Present and non-null only when `pos_type` is `DUTCHIE`; omitted entirely for other POS types.","type":"integer"},"dutchie_retailer_id":{"description":"Distru ID of the connected Dutchie retailer this mapping is scoped to. Present and non-null only when `pos_type` is `DUTCHIE`; omitted entirely for other POS types.","type":"string"},"id":{"description":"ID of this mapping. Use it to fetch or delete the mapping. Always present.","type":"string"},"inserted_datetime":{"description":"When the mapping was created, in UTC. Always present.","format":"datetime","type":"string"},"pos_type":{"description":"Which external POS this mapping targets. One of `BLAZE`, `DUTCHIE`, or `TREEZ` (SCREAMING_CASE). Determines which POS-specific fields below are present. Always present.","enum":["BLAZE","DUTCHIE","TREEZ"],"type":"string"},"product_id":{"description":"ID of the linked Distru product. Always present.","type":"string"},"treez_photo_url":{"description":"URL of the product photo in Treez, or null if none was set. Present only when `pos_type` is `TREEZ`.","type":"string","nullable":true},"treez_product_id":{"description":"Treez's own id for the mapped product (a Treez identifier, not a Distru id). Present and non-null only when `pos_type` is `TREEZ`; omitted entirely for other POS types.","type":"string"},"treez_retailer_id":{"description":"ID of the connected Treez retailer this mapping is scoped to, as an integer. Present and non-null only when `pos_type` is `TREEZ`; omitted entirely for other POS types.","type":"integer"},"updated_datetime":{"description":"When the mapping was last updated, in UTC. Equals `inserted_datetime` until the first update. Always present.","format":"datetime","type":"string"}},"required":["updated_datetime","inserted_datetime","pos_type","product_id","id"],"title":"Product POS Mapping","type":"object"},"LocationCompact":{"description":"A compact reference to a [location](#model-location) as nested inside another entity in Distru. Use its `id` to fetch the full location from the locations endpoint.","properties":{"address":{"description":"The full address as a single formatted line, built from the street, apt, city, state, zip, and country fields","example":"1234 Warehouse Street, Oakland, CA","type":"string"},"company_id":{"description":"ID of the [company](#model-company) that owns this location","example":"02c88a3f-d759-4973-88f9-60049d682524","type":"string"},"id":{"description":"ID for this location","example":"02c88a3f-d759-4973-88f9-60049d682524","type":"string"},"license_id":{"description":"ID of the [license](#model-license) this location is associated with, or null when the location has no license","example":"02c88a3f-d759-4973-88f9-60049d682524","type":"string"},"name":{"description":"Human readable name for this location","example":"Warehouse 1","type":"string"}},"required":["name","address","id"],"title":"LocationCompact","type":"object"},"ReturnResponse":{"description":"A single return envelope","properties":{"data":{"$ref":"#/components/schemas/Return"}},"title":"Return Response","type":"object"},"UpsertAssemblyOutput":{"description":"An output produced by the assembly. What you send depends on how the output product's inventory is tracked:\n\n- Product-tracked (non-compliance): `product_id` + `quantity` + `location_id`. The batch is the product's existing batch (derived — omit `batch_id`).\n- Batch-tracked (non-compliance): `product_id` + `quantity` + `location_id`. Optionally set `batch_id` to use an existing batch, or omit it and a new batch is created on completion (named by `batch_number`).\n- Package-tracked (Metrc): `product_id` + `location_id` (a Metrc-licensed facility) + `compliance_quantity` + `compliance_label` (the Metrc tag) + a Metrc item (`metrc_item_id` or `use_same_item=true`). Omit `batch_id` (derived from the product's batch) and `quantity` (derived from `compliance_quantity`). Distru creates the package when the output is completed.\n\nThese fields apply to package-tracked (Metrc) outputs only and must be omitted otherwise: `compliance_quantity`, `compliance_label`, `metrc_item_id`, `use_same_item`, `metrc_location_id`, `metrc_notes`, `metrc_production_batch_number`, `is_trade_sample`, `is_finished_good`, `expiration_date`, `package_date`.\n","properties":{"action":{"description":"CREATE, UPDATE, or DELETE. Required.","enum":["CREATE","UPDATE","DELETE"],"type":"string"},"batch_id":{"description":"The batch this output belongs to. Product-tracked and package-tracked: always omit it. Batch-tracked: optional — set it to an existing batch's ID and the output quantity lands in that batch, or leave it blank and the output quantity lands in a new batch (named by `batch_number`).","type":"string"},"batch_number":{"description":"The Distru batch number recorded on the output (1-255 characters). Batch-tracked: names the new batch created when `batch_id` is omitted. Package-tracked: sets the created package's Distru batch number.","type":"string"},"bin_ids":{"description":"The bins to store the output in, applied when the output is completed (requires bin inventory tracking enabled for the company). Package-tracked: sets the bins on the package Distru creates. Batch-tracked: replaces the batch's current bin set — an empty array clears it. Not applicable to product-tracked outputs.","items":{"type":"string"},"type":"array"},"compliance_label":{"description":"Package-tracked (Metrc) outputs only. The Metrc tag for the created package; must be an available tag in the output's license. Required to complete the output.","type":"string"},"compliance_quantity":{"description":"Package-tracked (Metrc) outputs only. The output quantity in the package's Metrc unit — the unit type of the package's Metrc item, which comes from `metrc_item_id` (or from the input package's item when `use_same_item=true`). `quantity` is derived from it by unit conversion. Send this when creating a package-tracked output.","type":"number"},"copy_custom_data_from_input":{"description":"Copy custom field values from the input onto this output.","type":"boolean"},"costs":{"description":"The costs added directly to this output.","items":{"$ref":"#/components/schemas/UpsertAssemblyCost"},"type":"array"},"expiration_date":{"description":"Package-tracked (Metrc) outputs only. The expiration date reported to Metrc for the created package, e.g. \"2027-08-19\".","type":"string"},"id":{"description":"The output to update or delete. Required for UPDATE and DELETE; omit for CREATE.","type":"string"},"inputs":{"description":"The inputs consumed to produce this output.","items":{"$ref":"#/components/schemas/UpsertAssemblyInput"},"type":"array"},"is_finished_good":{"description":"Package-tracked (Metrc) outputs only. When true, Distru flags the created package in Metrc as a Finished Good.","type":"boolean"},"is_trade_sample":{"description":"Package-tracked (Metrc) outputs only. Marks the created package as a Metrc trade sample.","type":"boolean"},"location_id":{"description":"The location the output is produced into. Required when creating.","type":"string"},"metrc_item_id":{"description":"Package-tracked (Metrc) outputs only. The Metrc item for the created package. Provide this or `use_same_item=true` (mutually exclusive).","type":"integer"},"metrc_location_id":{"description":"Package-tracked (Metrc) outputs only. The Metrc ID of the Metrc location the package will be created in. Only applicable if the output's Metrc license uses Metrc locations.","type":"integer"},"metrc_notes":{"description":"Package-tracked (Metrc) outputs only. Notes sent to Metrc when the package is created (max 255 characters).","type":"string"},"metrc_production_batch_number":{"description":"Package-tracked (Metrc) outputs only. Flags the created package in Metrc as a production batch and gives it this production batch number.","type":"string"},"package_date":{"description":"Package-tracked (Metrc) outputs only. The packaged date reported to Metrc for the created package. Defaults to today when omitted.","type":"string"},"product_id":{"description":"The product this output produces. Required when creating.","type":"string"},"quantity":{"description":"The output quantity in the product's unit. Required when creating product- and batch-tracked outputs. Omit it for package-tracked outputs — send `compliance_quantity` instead and `quantity` is derived from it by unit conversion.","type":"number"},"status":{"description":"PENDING or COMPLETED. Required when creating. On a Metrc processing-job assembly a package-tracked output may be COMPLETED (synced to Metrc) while the assembly stays PENDING; otherwise an output can only be COMPLETED together with the whole assembly.","enum":["PENDING","COMPLETED"],"type":"string"},"use_same_item":{"description":"Package-tracked (Metrc) outputs only. Reuse the source input package's Metrc item instead of `metrc_item_id` (mutually exclusive with it). Valid only when the output's inputs all share the same Metrc item.","type":"boolean"}},"required":["action"],"title":"Upsert Assembly Output","type":"object"},"Batch":{"description":"A lot of a product — a group of inventory that shares traits such as a harvest/production run, expiration date, and lab results. Used for batch-tracked products. This is the compact reference; see BatchFull for all fields.","properties":{"id":{"description":"ID for this batch","type":"string"},"name":{"description":"Human readable name for this batch","type":"string"}},"required":["name","id"],"title":"Batch","type":"object"},"UpsertAssemblyMetrcProcessingJob":{"description":"The Metrc processing job details for an assembly","properties":{"name":{"description":"The Metrc processing job name. Required together with `type_id`; must be non-empty and not already used by a processing job in Metrc. Permanent once set — cannot be changed on a later update.","type":"string"},"notes":{"description":"The Metrc processing job notes. Required to complete (status COMPLETED) a job. Editable after the job is created.","type":"string"},"type_id":{"description":"The Metrc ID of an existing Metrc processing job type. Required together with `name`. Permanent once set — cannot be changed on a later update.","type":"integer"},"waste":{"$ref":"#/components/schemas/UpsertAssemblyMetrcProcessingJobWaste"}},"title":"Upsert Assembly Metrc Processing Job","type":"object"},"Menus":{"description":"A collection of menus","properties":{"data":{"description":"The menus on this page, ordered oldest-first by creation time. An empty array when the company has no matching menus.","items":{"$ref":"#/components/schemas/Menu"},"type":"array"},"next_page":{"description":"URL for the next page of results; null when this is the last page.","type":"string"}},"required":["data"],"title":"Get menus","type":"object"},"InventoryTransactionHistoryReportRow":{"description":"A single row of the Inventory Transaction History report (one inventory transaction). Companies on the BioTrack compliance integration do not get the `metrc_unit_name` and `metrc_production_batch_number` keys. Potency keys are only populated for package-based transactions.\n","properties":{"amount":{"description":"The signed transaction quantity, measured in `unit_type`: negative when stock               left inventory, positive when it entered.","type":"string"},"batch_id":{"description":"The Distru batch ID of the transaction's batch. Null unless               the product is batch-tracked. Accepts the same value the `batch_ids` filter               takes.","type":"string"},"batch_number":{"description":"The Distru batch number of the transaction's batch. Null unless the product is               batch-tracked.","type":"string"},"cbd":{"description":"The package's CBD potency, as a percentage. Same population rule as `thc`.","type":"string"},"cbd_mg_g":{"description":"The package's CBD in mg per gram. Populated only when the primary test result               reports potency in mg/g; null otherwise — mutually exclusive with `cbd_mg_ml`.","type":"string"},"cbd_mg_ml":{"description":"The package's CBD in mg per millilitre. Populated only when the primary test               result reports potency in mg/mL; null otherwise. Mutually exclusive with               `cbd_mg_g`.","type":"string"},"company_relationship_id":{"description":"The Distru ID of the related customer or vendor (the company relationship).               Null when the transaction has no related entity, or that entity               has no associated company (e.g. assemblies, stock transfers, teardowns,               breakdowns).","type":"string"},"date":{"description":"The transaction's date and time, formatted `YYYY-MM-DD HH:MM` in the company's               timezone (e.g. `2026-01-15 14:30`).","type":"string"},"description":{"description":"The transaction's description.","type":"string"},"metrc_production_batch_number":{"description":"The package's Metrc production batch number. Null when there is no package or               the package has none. This key is absent entirely for companies on the BioTrack               integration.","type":"string"},"metrc_unit_name":{"description":"The Metrc unit-of-measure name for the package. Null for non-package               transactions. This key is absent entirely for companies on the BioTrack               integration.","type":"string"},"package_batch_number_or_batch_name":{"description":"For package transactions, the package's batch number; for batch-tracked               products, the batch's name. Null when the product is tracked at the product               level (no package and no batch).","type":"string"},"package_label":{"description":"The package's compliance label. Null when the transaction is not tied to a               package.","type":"string"},"product":{"description":"The product's name. When the product has been deactivated the name is prefixed               to mark it inactive.","type":"string"},"product_id":{"description":"The Distru product ID of the transaction's product. Accepts               the same value the `product_ids` filter takes.","type":"string"},"related_entity":{"description":"A human-readable label for the entity that caused the movement: an order or               purchase number, return number, assembly number, stock transfer number, a               teardown name, or a breakdown number. For system movements with no such entity               it is a synthesized label (e.g. `Stock Adjustment`, `Package Repair`, `Package               Changed Product`, `Update from Metrc`), or an empty string when neither applies.","type":"string"},"related_entity_customer_vendor":{"description":"The customer or vendor company name on the related entity. Null when the               transaction has no related entity, or that entity has no associated company               (e.g. assemblies, stock transfers, teardowns).","type":"string"},"related_entity_status":{"description":"The related entity's status as shown in Distru — populated for order,               purchase, return, assembly, and stock transfer movements. Null when the               transaction has no related entity, and for teardown and breakdown movements,               which carry no status. Returned as Distru's human-readable status text (e.g.               `Completed`, `Partially Received`), not the SCREAMING_CASE enum used elsewhere               in the API.","type":"string"},"thc":{"description":"The package's THC potency, as a percentage. Populated only for package               transactions whose package has a primary test result recorded; null otherwise               (including all non-package transactions).","type":"string"},"thc_mg_g":{"description":"The package's THC in mg per gram. Populated only when the package's primary               test result reports potency in mg/g; null otherwise — including when it reports               in mg/mL, in which case see `thc_mg_ml`.","type":"string"},"thc_mg_ml":{"description":"The package's THC in mg per millilitre. Populated only when the package's               primary test result reports potency in mg/mL; null otherwise. Mutually               exclusive with `thc_mg_g`.","type":"string"},"total_cbd":{"description":"The package's total CBD (post-decarboxylation), as a percentage. Same               population rule as `thc`.","type":"string"},"total_cbd_mg_g":{"description":"The package's total CBD in mg per gram. Populated only when the primary test               result reports potency in mg/g; null otherwise.","type":"string"},"total_cbd_mg_ml":{"description":"The package's total CBD in mg per millilitre. Populated only when the primary               test result reports potency in mg/mL; null otherwise.","type":"string"},"total_cost":{"description":"The transaction's total inventory cost over its quantity, in the company's               currency. Null when no unit cost is recorded for the stock consumed.","type":"string"},"total_thc":{"description":"The package's total THC (post-decarboxylation), as a percentage. Same               population rule as `thc`.","type":"string"},"total_thc_mg_g":{"description":"The package's total THC in mg per gram. Populated only when the primary test               result reports potency in mg/g; null otherwise.","type":"string"},"total_thc_mg_ml":{"description":"The package's total THC in mg per millilitre. Populated only when the primary               test result reports potency in mg/mL; null otherwise.","type":"string"},"type":{"description":"The kind of movement that produced this row, as a lowercase/mixed-case token               (e.g. `adjustment`, `package_repair`). Null when the transaction has no recorded               type. Unlike enums elsewhere in the API, this value is not normalized to               SCREAMING_CASE, so match it case-sensitively as returned.","type":"string"},"unit_type":{"description":"The name of the unit `amount` is measured in — the package's unit for package               transactions, otherwise the product's unit.","type":"string"}},"required":["description","unit_type","amount","product","date"],"title":"InventoryTransactionHistoryReportRow","type":"object"},"PurchasesByCompanyReport":{"description":"The Purchases By Company report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/PurchasesByCompanyReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/PurchasesByCompanyReportMeta"}},"required":["meta","data"],"title":"PurchasesByCompanyReport","type":"object"},"BillOfMaterials":{"description":"A product's bill of materials (recipe of inputs and additional costs)","properties":{"costs":{"description":"The additional (non-material) costs this recipe applies, ordered oldest first. Empty when none.","items":{"$ref":"#/components/schemas/BillOfMaterialsCost"},"type":"array"},"description":{"description":"Free-text description of this bill of materials, or null when none was entered","type":"string"},"dynamic_inputs":{"description":"The inputs that select products by attribute (category, strain, tag, and so on) rather than naming a specific product. Empty when this recipe has none.","items":{"$ref":"#/components/schemas/BillOfMaterialsDynamicInput"},"type":"array"},"id":{"description":"ID for this bill of materials","type":"string"},"name":{"description":"Human readable name for this bill of materials","type":"string"},"product_inputs":{"description":"The inputs that name a specific product to consume. Empty when this recipe has none. See `dynamic_inputs` for inputs that select products by attribute instead.","items":{"$ref":"#/components/schemas/BillOfMaterialsProductInput"},"type":"array"}},"required":["costs","dynamic_inputs","product_inputs","name","id"],"title":"BillOfMaterials","type":"object"},"InventoryValuationReportColumn":{"properties":{"key":{"description":"The key used for this column in each data row","type":"string"},"label":{"description":"The human-readable label of the column","type":"string"}},"required":["label","key"],"title":"InventoryValuationReportColumn","type":"object"},"ContactResponse":{"description":"A single contact","properties":{"data":{"$ref":"#/components/schemas/Contact"}},"title":"Contact Response","type":"object"},"SalesByCompanyReport":{"description":"The Sales By Company report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/SalesByCompanyReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/SalesByCompanyReportMeta"}},"required":["meta","data"],"title":"SalesByCompanyReport","type":"object"},"InvoiceHistoryReportRow":{"description":"A single row of the Invoice History report. Companies on a compliance integration and companies with Invoice custom fields will see additional keys.\n","properties":{"charge_summary":{"description":"Human-readable per-charge breakdown of the positive charges. Null when the invoice has none.","type":"string"},"customer":{"description":"The customer's company name","type":"string"},"discount_summary":{"description":"Human-readable per-discount breakdown. Null when the invoice has no discounts.","type":"string"},"due_date":{"description":"The invoice due date as `YYYY-MM-DD` in the API key user's timezone. Null when the invoice has no due date set.","type":"string"},"invoice_date":{"description":"The invoice date as `YYYY-MM-DD` in the API key user's timezone. Null when the invoice has no invoice date set.","type":"string"},"invoice_number":{"description":"The invoice number. Null when the invoice has none.","type":"string"},"line_item_subtotal":{"description":"Sum of the invoice's line items (quantity times unit price); `0` when there are none.","type":"string"},"outstanding":{"description":"The unpaid balance: invoice total minus amount paid. Negative when overpaid.","type":"string"},"owner":{"description":"Full name of the invoice's owner (the assigned user). Null when the invoice has no owner.","type":"string"},"paid":{"description":"The total amount paid on the invoice across its non-voided payments; `0` when nothing has been paid.","type":"string"},"sales_order":{"description":"The order number of the sales order this invoice belongs to","type":"string"},"status":{"description":"The invoice payment status, SCREAMING_CASE: NOT_PAID, PARTIALLY_PAID, FULLY_PAID, OVER_PAID (OVER_PAID only on legacy invoices). Null on the rare invoice with no payment status set.","enum":["NOT_PAID","PARTIALLY_PAID","FULLY_PAID","OVER_PAID"],"type":"string"},"tax_summary":{"description":"Human-readable per-tax breakdown, e.g. `Excise Tax - $12.50, City Tax - $4.00`. Null when the invoice has no taxes.","type":"string"},"total":{"description":"The invoice grand total","type":"string"},"total_charges":{"description":"Sum of the invoice's positive (non-discount, non-tax) charges; `0` when none.","type":"string"},"total_discounts":{"description":"Sum of the invoice's discounts (negative charges), expressed as a negative number; `0` when none.","type":"string"},"total_taxes":{"description":"Sum of the invoice's taxes; `0` when none.","type":"string"}},"required":["total","total_discounts","total_charges","total_taxes","line_item_subtotal","outstanding","paid","customer","sales_order"],"title":"InvoiceHistoryReportRow","type":"object"},"PrimaryTestResult":{"description":"A compact view of the primary [test result](#model-testresult) nested on a package or batch — just its headline potency figures. Fetch the full result from the test results endpoint for the complete analyte breakdown.","properties":{"cbd_mg_per_unit":{"description":"CBD content in milligrams per unit, as a decimal string, or null when not measured","type":"string"},"cbd_mg_per_unit_total":{"description":"Total CBD (including its acid precursor) in milligrams per unit, as a decimal string, or null when not measured","type":"string"},"cbd_percentage":{"description":"CBD content as a percentage by weight, as a decimal string, or null when not measured","type":"string"},"cbd_percentage_total":{"description":"Total CBD (including its acid precursor) as a percentage by weight, as a decimal string, or null when not measured","type":"string"},"coa_url":{"description":"Public URL to view/download this test result's Certificate of Analysis (COA), or null when no file is attached","type":"string"},"mg_per_unit_type":{"description":"The unit that the `*_mg_per_unit` figures are measured against, or null when not set","type":"string"},"name":{"description":"The name of the test result","type":"string"},"thc_mg_per_unit":{"description":"THC content in milligrams per unit, as a decimal string, or null when not measured","type":"string"},"thc_mg_per_unit_total":{"description":"Total THC (including its acid precursor) in milligrams per unit, as a decimal string, or null when not measured","type":"string"},"thc_percentage":{"description":"THC content as a percentage by weight, as a decimal string, or null when not measured","type":"string"},"thc_percentage_total":{"description":"Total THC (including its acid precursor) as a percentage by weight, as a decimal string, or null when not measured","type":"string"}},"required":["name"],"title":"PrimaryTestResult","type":"object"},"Credit":{"description":"Store credit held by a customer that can be applied toward what they owe. Credits can be issued manually or generated automatically (for example from a return or an invoice overpayment).","properties":{"amount":{"description":"The current spendable face value of this credit, as a decimal string (e.g. `\"100.00\"`). Always greater than 0 for a live credit. May differ from `original_amount` if the credit was edited after creation.","type":"string"},"canceled_datetime":{"description":"The ISO8601 UTC datetime at which the credit was canceled (e.g. `2022-07-10T00:00:00Z`). Only set for CANCELED credits; null otherwise.","type":"string"},"company":{"$ref":"#/components/schemas/CompanyCompact"},"creator":{"$ref":"#/components/schemas/User"},"credit_number":{"description":"The human-readable credit number as shown in the Distru UI (e.g. `CR-1001`). Assigned automatically on create.","type":"string"},"credit_uses":{"description":"This credit's applications to invoices, one entry per active application. Each use carries the applied amount, the compact credit, and the invoice payment it was applied to. Empty array when the credit has not been applied anywhere.","items":{"$ref":"#/components/schemas/CreditUse"},"type":"array"},"deleted_in_qbo":{"description":"Whether this credit was pushed to QuickBooks Online and later deleted there. False for credits never synced to QuickBooks Online.","type":"boolean"},"external_note":{"description":"A note on this credit, visible to the customer. Null if unset.","type":"string"},"id":{"description":"ID for this credit","type":"string"},"inserted_datetime":{"description":"The ISO8601 UTC datetime at which the credit was created in Distru (e.g. `2022-07-10T00:00:00Z`).","type":"string"},"internal_note":{"description":"An internal note on this credit, not shown to the customer. Null if unset.","type":"string"},"original_amount":{"description":"The amount this credit was created with, as a decimal string. Frozen at create time and never changes, even when `amount` is later edited — useful for looking a credit up by the value it was first issued for.","type":"string"},"owner":{"$ref":"#/components/schemas/User"},"payment":{"$ref":"#/components/schemas/Payment"},"qb_credit_memo_id":{"description":"The id of the QuickBooks Online credit memo this credit maps to. Null until the credit has been synced to QuickBooks Online as a credit memo, and for credits that back an overpayment payment instead (see `qb_payment_id`).","type":"string"},"qb_payment_id":{"description":"The id of the QuickBooks Online payment this credit maps to (for overpayment credits carried as an unapplied amount on a payment). Null when the credit is not backed by a QuickBooks Online payment. A credit never has both this and `qb_credit_memo_id` set.","type":"string"},"qb_sync_status":{"description":"The credit's QuickBooks Online sync status. Only meaningful when the QuickBooks Online integration is enabled; null otherwise. Values: PENDING (the latest sync covering this credit is still in flight), ERROR (the latest sync covering this credit failed), DELETED_IN_QBO (pushed to QuickBooks Online once, then deleted there), NOT_SYNCED (never pushed to QuickBooks Online), PARTIALLY_SYNCED (the credit is in QuickBooks Online but at least one of its applications has not been synced yet), SYNCED (fully synced).","enum":["PENDING","ERROR","DELETED_IN_QBO","NOT_SYNCED","PARTIALLY_SYNCED","SYNCED"],"type":"string"},"remaining_balance":{"description":"The unused balance still available to apply to invoices, as a decimal string: `amount` minus the sum of its active credit uses. Equals `amount` when nothing has been applied, and `\"0.00\"` when fully used (REDEEMED).","type":"string"},"return":{"$ref":"#/components/schemas/CompactReturn"},"source":{"description":"How this credit was created (SCREAMING_CASE): USER (added by hand — the only kind the API can create or edit), RETURN (from a return), INVOICE_PAYMENT (an invoice overpayment in Distru), QB_PAYMENT / QB_CREDIT_MEMO (originated in QuickBooks Online). Immutable after creation.","enum":["INVOICE_PAYMENT","QB_CREDIT_MEMO","QB_PAYMENT","RETURN","USER"],"type":"string"},"status":{"description":"The credit's live computed status (SCREAMING_CASE), derived from its balance and cancellation state rather than stored. • ACTIVE: has a remaining balance still available to apply. • REDEEMED: fully applied, `remaining_balance` is `\"0.00\"`. • CANCELED: voided; its balance can no longer be applied.","enum":["ACTIVE","CANCELED","REDEEMED"],"type":"string"},"updated_datetime":{"description":"The ISO8601 UTC datetime at which the credit was last updated in Distru.","type":"string"}},"required":["updated_datetime","inserted_datetime","deleted_in_qbo","credit_uses","status","source","credit_number","original_amount","amount","id"],"title":"Credit","type":"object"},"InventoryAssetsReport":{"description":"The Inventory Assets report","properties":{"data":{"description":"The report rows","items":{"$ref":"#/components/schemas/InventoryAssetsReportRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/InventoryAssetsReportMeta"}},"required":["meta","data"],"title":"InventoryAssetsReport","type":"object"},"UpsertAssemblyRequest":{"description":"An assembly to create, update, or delete, with its outputs, inputs, and costs","properties":{"action":{"description":"CREATE, UPDATE, or DELETE. Required. DELETE removes the assembly and all of its outputs, inputs, and costs.","enum":["CREATE","UPDATE","DELETE"],"type":"string"},"custom_data":{"description":"A map of custom field IDs to their values for this assembly. Use GET /public/v1/custom-fields?parent_object=assembly to retrieve the available 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. On update this replaces the whole custom-data map, so send every field you want to keep.","example":{"101":"Some text value","102":"2026-08-18T00:00:00.000-07:00","103":["Option A","Option B"]},"type":"object"},"description":{"description":"A free-text description for this assembly. Editable at any status.","type":"string"},"estimated_start_datetime":{"description":"When this assembly is planned to start, as an ISO 8601 datetime (e.g. `2026-08-19T00:00:00Z`). Optional; omit to leave it unset.","type":"string"},"estimated_work_hours":{"description":"The whole-hours portion of the estimated work time; must be 0 or greater. Combine with `estimated_work_minutes` for the full estimate (e.g. 1 hour 30 minutes is `estimated_work_hours` 1, `estimated_work_minutes` 30). Editable at any status.","type":"integer"},"estimated_work_minutes":{"description":"The minutes portion of the estimated work time; must be 0 or greater. Pairs with `estimated_work_hours` (see above). Editable at any status.","type":"integer"},"id":{"description":"The assembly to update or delete. Required for UPDATE and DELETE; omit for CREATE.","type":"string"},"metrc_processing_job":{"$ref":"#/components/schemas/UpsertAssemblyMetrcProcessingJob"},"outputs":{"description":"The outputs this assembly produces, each with its own inputs and costs. Sparse on update: an output you omit is left untouched; remove one by sending it with `action` `DELETE`.","items":{"$ref":"#/components/schemas/UpsertAssemblyOutput"},"type":"array"},"owner_id":{"description":"The ID of the user that owns this assembly. Optional. Editable at any status.","type":"string"},"status":{"description":"The assembly's lifecycle state, `PENDING` or `COMPLETED` (SCREAMING_CASE). Required when creating. `PENDING` claims/reserves ingredient inventory but consumes nothing; `COMPLETED` consumes the inputs and produces the outputs into inventory, and requires every output to be COMPLETED. Creating directly as `COMPLETED` performs that consumption immediately. Once `COMPLETED` an assembly cannot be moved back to `PENDING` and only a few fields remain editable (see the endpoint description).","enum":["PENDING","COMPLETED"],"type":"string"}},"required":["action"],"title":"Upsert Assembly Request","type":"object"},"OrderFulfillmentReportColumn":{"properties":{"key":{"description":"The key under which this column's value appears in each data row. For a per-order column this is the slugified order number (e.g. `so_1042`).","type":"string"},"label":{"description":"The human-readable column label (e.g. the order number, or `Total Units`).","type":"string"}},"required":["label","key"],"title":"OrderFulfillmentReportColumn","type":"object"},"CompactCredit":{"description":"A compact representation of a credit","properties":{"amount":{"description":"The current spendable face value of this credit, as a decimal string (e.g. `\"100.00\"`).","type":"string"},"credit_number":{"description":"The credit number as shown in the Distru UI (e.g. `CR-1001`).","type":"string"},"id":{"description":"ID for this credit","type":"string"},"source":{"description":"How this credit was created (SCREAMING_CASE): USER, RETURN, INVOICE_PAYMENT, QB_PAYMENT, or QB_CREDIT_MEMO.","enum":["INVOICE_PAYMENT","QB_CREDIT_MEMO","QB_PAYMENT","RETURN","USER"],"type":"string"}},"required":["source","credit_number","amount","id"],"title":"CompactCredit","type":"object"},"Company":{"description":"A business in your network — a customer, a vendor, or both. Holds contact details, locations, licenses, and the terms you deal on.","properties":{"category":{"description":"The kind of cannabis business this company is (Title-Case, not SCREAMING_CASE) — one of Dispensary, Delivery, Cultivator, Manufacturer, Distributor, Microbusiness, Lab, Retail, or Other. Null when no category is set.","enum":["Other","Cultivator","Delivery","Dispensary","Distributor","Lab","Manufacturer","Microbusiness","Retail"],"type":"string"},"custom_data":{"description":"The custom data for this company","items":{"$ref":"#/components/schemas/CustomField"},"type":"array"},"default_email":{"description":"The primary email address for this company, or null when not set","type":"string"},"default_payment_term":{"$ref":"#/components/schemas/PaymentTerm"},"default_purchase_order_notes":{"description":"The default notes automatically added to purchase orders when this company is the supplier, or null when not set","type":"string"},"default_sales_order_notes":{"description":"The default external notes automatically added to sales orders when this company is the customer, or null when not set","type":"string"},"deleted_at":{"description":"ISO 8601 datetime this company was deleted at, or null when it is not deleted","type":"string"},"group":{"$ref":"#/components/schemas/CompanyGroup"},"id":{"description":"ID for this company","example":"3f128a34-cc59-4b49-8883-23bf10e59c6c","type":"string"},"inserted_datetime":{"description":"ISO 8601 datetime this company was created at","type":"string"},"invoice_email":{"description":"The email address where sales order invoices are delivered, or null when not set","type":"string"},"leaflink_brand_id":{"description":"The LeafLink brand ID mapped to this company; only set on self-relationships (your own company), otherwise null","type":"integer"},"leaflink_customer_id":{"description":"The LeafLink customer ID mapped to this company, or null when it is not mapped to a LeafLink customer","type":"integer"},"legal_business_name":{"description":"The legal business name for this company. Empty string when never set.","type":"string"},"licenses":{"description":"The [licenses](#model-license) held by this company. Empty when the company has none.","items":{"$ref":"#/components/schemas/License"},"type":"array"},"locations":{"description":"The [locations](#model-locationcompact) belonging to this company. Empty when the company has none.","items":{"$ref":"#/components/schemas/LocationCompact"},"type":"array"},"name":{"description":"Human readable name for this company","example":"Retailer 1","type":"string"},"order_shipment_email":{"description":"The email address where sales order shipment packing slips are delivered, or null when not set","type":"string"},"outstanding_balance":{"description":"The current outstanding balance for this company, as a decimal string (e.g. \"150.50\"). \"0\" when nothing is outstanding, and can be negative when the company has more unused credit than they owe. Computed as the unpaid total of all invoices on the company's non-canceled orders (invoice total minus non-voided payments), reduced by the company's remaining unused credit (active credits issued minus credit already applied).","type":"string"},"outstanding_balance_threshold":{"description":"The balance above which this company is treated as over its credit limit; when `outstanding_balance` exceeds it, Distru shows a warning banner on the company's page and when selling to them. Null when no threshold is set.","type":"integer"},"owner":{"$ref":"#/components/schemas/User"},"owner_id":{"description":"The ID of the Distru [user](#model-user) who is the account owner (main point of contact) for this company, or null when no owner is assigned","type":"string"},"phone_number":{"description":"The phone number for this company, or null when not set","type":"string"},"purchase_order_email":{"description":"The email address where purchase order slips are delivered, or null when not set","type":"string"},"qb_customer_id":{"description":"The QuickBooks Online customer ID mapped to this company, or null when it is not mapped to a QuickBooks Online customer","type":"string"},"qb_vendor_id":{"description":"The QuickBooks Online vendor ID mapped to this company, or null when it is not mapped to a QuickBooks Online vendor","type":"string"},"relationship_type":{"$ref":"#/components/schemas/RelationshipType"},"sales_order_email":{"description":"The email address where sales order slips are delivered, or null when not set","type":"string"},"updated_datetime":{"description":"ISO 8601 datetime this company was last updated at. This is the later of when the trading relationship or the underlying company record was last changed, so an edit to either updates it.","type":"string"},"website":{"description":"The website for this company, or null when not set","type":"string"}},"required":["custom_data","updated_datetime","name","id"],"title":"Company","type":"object"},"PaymentCreditUse":{"description":"A credit applied towards an invoice payment","properties":{"amount":{"description":"The amount of credit applied toward the payment, as a decimal string (e.g. `\"25.00\"`). Always present.","type":"string"},"credit":{"$ref":"#/components/schemas/PaymentCredit"},"id":{"description":"ID for this credit use. Always present.","type":"string"}},"required":["amount","id"],"title":"PaymentCreditUse","type":"object"},"AssemblyResponse":{"description":"A single assembly","properties":{"data":{"$ref":"#/components/schemas/Assembly"}},"title":"Assembly Response","type":"object"},"PackageFull":{"description":"A specific, compliance-tracked quantity of a product with all its details — its tag, product, dates, quantity, and testing state. This is the physical unit of inventory for package-tracked products.","properties":{"biotrack_net_quantity_per_unit":{"description":"The BioTrack net quantity per unit for this package, as a decimal string. Null for non-BioTrack packages, and may be null for some BioTrack inventory types.","type":"string"},"inserted_datetime":{"description":"The datetime this package was created at (ISO 8601)","type":"string"},"bins":{"description":"The bins this package is stored in. Only present when bin inventory tracking is enabled for the company.","items":{"$ref":"#/components/schemas/BinCompact"},"type":"array"},"metrc_source_harvest_names":{"description":"The Metrc source harvest names for this package. Null when the package is not Metrc-tracked or has no source harvest.","type":"string"},"biotrack_usable_weight":{"description":"The BioTrack usable weight for this package. For weighable (weight/volume) packages this is the package's weight at creation; for count-based packages it is the per-unit amount of cannabis. May be null for some inventory types (and is null for non-BioTrack packages).","type":"string"},"compliance_strain_name":{"description":"The strain name as reported by the compliance system (Metrc or BioTrack), or null when none is reported","type":"string"},"harvest_date":{"description":"The harvest date for this package as an ISO 8601 date (e.g. \"2026-08-20\"), or null when none is set","type":"string"},"creator":{"$ref":"#/components/schemas/User"},"location":{"$ref":"#/components/schemas/LocationCompact"},"metrc_unit_name":{"description":"The Metrc unit of measure name for this package (e.g. \"Grams\"). Null when the package is not Metrc-tracked.","type":"string"},"biotrack_inventory_type_id":{"description":"The BioTrack inventory type ID for this package. Null for non-BioTrack packages.","type":"integer"},"batch_number":{"description":"A free-text batch number set on the package, separate from any compliance tag. Null when none is set.","type":"string"},"finished_datetime":{"description":"The datetime this package was finished in the compliance system (ISO 8601); null if not finished","type":"string"},"compliance_transferred_datetime":{"description":"The datetime this package was transferred out in the compliance system (ISO 8601); null if not transferred out","type":"string"},"product_unit_type":{"$ref":"#/components/schemas/UnitType"},"is_test_sample":{"description":"True when this package is a test sample.","type":"boolean"},"metrc_transfer_id":{"description":"The Metrc transfer ID this package is currently on; null if not in transit","type":"integer"},"compliance_label":{"description":"The unique tag assigned by the state compliance system (e.g. the Metrc package tag). Always present for Metrc-tracked packages; may be null for BioTrack packages, and null when the package is not compliance-tracked.","type":"string"},"biotrack_id":{"description":"The BioTrack inventory ID for this package; null for non-BioTrack packages","type":"integer"},"quantity_assembling":{"description":"The portion of `quantity` currently held for a pending assembly, in the package's `unit_type`, as a decimal string. \"0\" when none is allocated. Never negative.","type":"string"},"inactivated_datetime":{"description":"The datetime this package was inactivated (ISO 8601); null while active","type":"string"},"distru_status":{"description":"The package's inventory lifecycle status. One of:• ACTIVE: on hand and available.• ASSEMBLING: allocated to a pending assembly.• SELLING: reserved on an open sales order.• SOLD: consumed by a completed sale.• RETURNING: on an in-progress return.• TRANSFERRED: sent out on a compliance transfer.• ONHOLD: placed on hold in the compliance system.• FINISHED: finished in the compliance system.• DISCONTINUED: discontinued.• DESTROYED: destroyed.","enum":["ACTIVE","ASSEMBLING","DESTROYED","DISCONTINUED","FINISHED","ONHOLD","RETURNING","SELLING","SOLD","TRANSFERRED"],"type":"string"},"description":{"description":"Free-text description of this package, or null when none was entered","type":"string"},"id":{"description":"ID for this package in Distru","type":"string"},"is_production_batch":{"description":"True when this package is a new production lot (rather than added to existing inventory). Only applies to Metrc-tracked packages; always false otherwise.","type":"boolean"},"metrc_finished_date":{"description":"The date this package was finished in Metrc (ISO 8601 date)","type":"string"},"unit_type":{"$ref":"#/components/schemas/UnitType"},"metrc_id":{"description":"The Metrc package ID for this package; null for non-Metrc packages","type":"integer"},"license_id":{"description":"The ID of the [license](#model-license) this package is held under","type":"string"},"quantity":{"description":"The total on-hand quantity of this package, in the package's `unit_type`, as a decimal string (e.g. \"100\"). Always equals `quantity_active` + `quantity_assembling`. Never negative.","type":"string"},"metrc_archived_date":{"description":"The date this package was archived in Metrc — i.e. the moment it was discontinued in Metrc (ISO 8601 date)","type":"string"},"product_unit_quantity":{"description":"This package's quantity converted into its product's unit type (see `product_unit_type`), as a decimal string rounded to 9 places (e.g. \"100\"). Use this when you need the amount in product units rather than the package's own `unit_type`.","type":"string"},"custom_data":{"description":"The custom data for this package","items":{"$ref":"#/components/schemas/CustomField"},"type":"array"},"owner":{"$ref":"#/components/schemas/User"},"quantity_active":{"description":"The freely usable portion of `quantity` — what can be used as an assembly input, moved to another location, added to a sales order, or adjusted down. Equals `quantity` minus `quantity_assembling`, in the package's `unit_type`, as a decimal string. Never negative.","type":"string"},"metrc_received_datetime":{"description":"The most recent ISO 8601 datetime this package was received via a Metrc transfer, or null when it was never received via a transfer","type":"string"},"packaged_date":{"description":"The compliance packaged date as an ISO 8601 date (e.g. \"2026-08-20\"), or null when none is set","type":"string"},"expiration_datetime":{"description":"ISO 8601 datetime this package expires, or null when none is set","type":"string"},"primary_test_result":{"$ref":"#/components/schemas/PrimaryTestResult"},"license":{"$ref":"#/components/schemas/License"},"location_id":{"description":"The ID of the [location](#model-location) where this package is physically stored","type":"string"},"compliance_type":{"description":"The state compliance system tracking this package: METRC or BIOTRACK, or null when the package is not compliance-tracked. This determines which system-specific fields are populated: METRC packages carry the `metrc_*` fields (BioTrack ones null), BIOTRACK packages carry the `biotrack_*` fields (Metrc ones null).","type":"string"},"is_trade_sample":{"description":"True when this package is a Metrc trade sample. Always false for non-Metrc packages.","type":"boolean"},"total_cost_actual":{"description":"Total actual cost of this package. Distru traces the inputs and components that produced the package and sums the real costs incurred along that chain — for example the price paid when a component was purchased, assembly costs, and costs added by stock adjustments, among others. Returned only by the list endpoint when the request passes `include_costs=true`; the field is absent otherwise. Null when no cost has been traced for this package.","type":"string"},"biotrack_room_id":{"description":"The BioTrack room ID where this package is stored. Null for non-BioTrack packages.","type":"integer"},"product_id":{"description":"The ID of the [product](#model-product) this package holds","type":"string"},"metrc_status":{"description":"The package's Metrc inventory status. Null for BioTrack-synced packages.","enum":["ACTIVE","INACTIVE","ONHOLD"],"type":"string"},"metrc_received_from_manifest_number":{"description":"The Metrc manifest number the package was most recently received from, or null when it was not received via a transfer","type":"string"},"compliance_product_name":{"description":"The product/item name as reported by the compliance system (Metrc or BioTrack), or null when none is reported","type":"string"},"product":{"$ref":"#/components/schemas/Product"},"cost_per_unit_default":{"description":"Default (standard) cost per unit — `total_cost_default` divided by the package quantity. Returned only by the list endpoint when the request passes `include_costs=true`; the field is absent otherwise. Null when no cost has been traced for this package.","type":"string"},"total_cost_default":{"description":"Total default (standard) cost of this package. Traced the same way as `total_cost_actual`, but each input/component is valued at its product's configured unit cost (the product's `unit_cost`) instead of its real cost. Returned only by the list endpoint when the request passes `include_costs=true`; the field is absent otherwise. Null when no cost has been traced for this package.","type":"string"},"lab_testing_state":{"description":"The package's compliance lab-testing state (e.g. Metrc's TestPassed / NotSubmitted; BioTrack uses analogous values). Defaults to NotSubmitted, so this is always present.","type":"string"},"cost_per_unit_actual":{"description":"Actual cost per unit — `total_cost_actual` divided by the package quantity. Returned only by the list endpoint when the request passes `include_costs=true`; the field is absent otherwise. Null when no cost has been traced for this package.","type":"string"},"biotrack_status":{"description":"The package's BioTrack inventory status. Null for non-BioTrack packages.","enum":["ACTIVE","DESTROYED","SCHEDULED_FOR_DESTRUCTION","SCHEDULED_FOR_TRANSPORT","IN_TRANSPORT_BUT_NOT_RECEIVED","RECEIVED"],"type":"string"},"metrc_production_batch_number":{"description":"The Metrc production batch number, set only when this package is a production batch (see `is_production_batch`); otherwise null.","type":"string"}},"required":["custom_data","inserted_datetime","is_production_batch","is_test_sample","quantity_active","quantity_assembling","quantity","product_unit_quantity","distru_status","location_id","license_id","lab_testing_state","is_trade_sample","product_id","product","id"],"title":"PackageFull","type":"object"}}}}